Compare commits
1366 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d79ab5ffeb | ||
|
|
01526a7b37 | ||
|
|
091a02f737 | ||
|
|
5bdd35464b | ||
|
|
0325a62f18 | ||
|
|
3a5538813c | ||
|
|
1f1b4b95ce | ||
|
|
8c3ed57ecc | ||
|
|
dc8a64fa7d | ||
|
|
0d1e72a764 | ||
|
|
9b3fe09508 | ||
|
|
7c0cfb1da2 | ||
|
|
66429ce331 | ||
|
|
bce859569f | ||
|
|
425fb8905b | ||
|
|
4f59c7f77f | ||
|
|
21d1faa793 | ||
|
|
b9f3991d03 | ||
|
|
3794b181d5 | ||
|
|
f09256a24e | ||
|
|
34fca9d6f5 | ||
|
|
433f10ef93 | ||
|
|
3dcc9bc143 | ||
|
|
7311895894 | ||
|
|
a7cab51369 | ||
|
|
fb00b79d19 | ||
|
|
7782aa7379 | ||
|
|
f3ee4a5dac | ||
|
|
a8d6e59a7a | ||
|
|
1d4b1870cf | ||
|
|
f63ad2dd69 | ||
|
|
6903eed4e7 | ||
|
|
2f1fe5468e | ||
|
|
24d15d4274 | ||
|
|
0bc7aa52d8 | ||
|
|
e52603b4a7 | ||
|
|
3b88712402 | ||
|
|
33e9ef2106 | ||
|
|
689fe4ed9a | ||
|
|
b82d026f39 | ||
|
|
009059def4 | ||
|
|
03ff61d113 | ||
|
|
c00914bea2 | ||
|
|
944d1c0a4a | ||
|
|
2cf23e33e3 | ||
|
|
e2a0b42d03 | ||
|
|
894e9818ac | ||
|
|
de18e256ce | ||
|
|
1a3c70ce1b | ||
|
|
bd4a603e16 | ||
|
|
358b80d782 | ||
|
|
824ec42005 | ||
|
|
466935e9a3 | ||
|
|
b52d3e3a7b | ||
|
|
888a6da4a5 | ||
|
|
972ac73dd9 | ||
|
|
d8b238d5f1 | ||
|
|
63206c3da2 | ||
|
|
5713de8966 | ||
|
|
58f293fef3 | ||
|
|
ffbb2c9689 | ||
|
|
9cd3dcdebf | ||
|
|
f2fe58c3c5 | ||
|
|
b78010aa94 | ||
|
|
49035543b9 | ||
|
|
f9ccf635ca | ||
|
|
e8ea294964 | ||
|
|
19ef2be88b | ||
|
|
30e8b8186f | ||
|
|
741643af5f | ||
|
|
6aaf9ba470 | ||
|
|
5957dc72eb | ||
|
|
e32a9777d7 | ||
|
|
84a8f1eb2b | ||
|
|
6810953014 | ||
|
|
398964945a | ||
|
|
5f43c032f2 | ||
|
|
627cf90de0 | ||
|
|
2bedb36d7f | ||
|
|
e93a95d0cb | ||
|
|
3f31666796 | ||
|
|
3fe8031cf3 | ||
|
|
b27c7ce11b | ||
|
|
ed34c2ca68 | ||
|
|
3ca2e953fb | ||
|
|
d8a7328365 | ||
|
|
f33cd625bf | ||
|
|
80530bb13c | ||
|
|
affc12df4b | ||
|
|
4eedf00025 | ||
|
|
e5acbcc0dd | ||
|
|
1b6743ee53 | ||
|
|
b5fb82d95d | ||
|
|
193aa4e1f2 | ||
|
|
ebd34427c7 | ||
|
|
3d75573889 | ||
|
|
c6240ca415 | ||
|
|
2ee8984b44 | ||
|
|
b7ec587e5b | ||
|
|
47c58bce2b | ||
|
|
96e95ac533 | ||
|
|
b013a065f7 | ||
|
|
74b37d11cf | ||
|
|
c6cc013617 | ||
|
|
f4e1d80a87 | ||
|
|
91dad4060f | ||
|
|
e07cb82c15 | ||
|
|
2770cec187 | ||
|
|
5c3928190a | ||
|
|
9f4b04ea0f | ||
|
|
96d20756ca | ||
|
|
b8454c7f5b | ||
|
|
c84f703f92 | ||
|
|
57c2e867d8 | ||
|
|
553f496d84 | ||
|
|
b1d8aca46a | ||
|
|
8e884fd3ea | ||
|
|
76524b7498 | ||
|
|
65914fb2b2 | ||
|
|
a4d0da0085 | ||
|
|
c9d496e9a0 | ||
|
|
88a951ba4f | ||
|
|
403ceb19dc | ||
|
|
835d3c3d18 | ||
|
|
3135b456be | ||
|
|
0be6d3661a | ||
|
|
6f5f5b4711 | ||
|
|
c6c5f85abb | ||
|
|
7b860f7739 | ||
|
|
e28804c03a | ||
|
|
1b9432824b | ||
|
|
3b71a6b5c5 | ||
|
|
7ce8768c19 | ||
|
|
25e0f12976 | ||
|
|
f168682a68 | ||
|
|
d25058a46d | ||
|
|
4d0c092d9f | ||
|
|
15714ef855 | ||
|
|
eb743beaa3 | ||
|
|
0007535a46 | ||
|
|
8391af026c | ||
|
|
800f656dcf | ||
|
|
088c5f49d9 | ||
|
|
d8d98b6143 | ||
|
|
02fb3b9315 | ||
|
|
4f87db784e | ||
|
|
7e6287b925 | ||
|
|
999cdfd997 | ||
|
|
8d6cb087c6 | ||
|
|
2b7417c728 | ||
|
|
3c455cf1c1 | ||
|
|
5135185e31 | ||
|
|
b461f26e5d | ||
|
|
faef5b8570 | ||
|
|
0a20e04c10 | ||
|
|
d19bb2308d | ||
|
|
d8dd07d9ef | ||
|
|
36c56243cd | ||
|
|
23d06b79a6 | ||
|
|
e4c4e923ee | ||
|
|
936d2f1f47 | ||
|
|
07018b5060 | ||
|
|
ac90d6ae5c | ||
|
|
2141f2c4c5 | ||
|
|
81870777a9 | ||
|
|
845092dcad | ||
|
|
dd473d1e1e | ||
|
|
d2869bf4ed | ||
|
|
891a3f4b29 | ||
|
|
6767b50d75 | ||
|
|
d9e4b562a9 | ||
|
|
fb3243f1bc | ||
|
|
5fe1497c92 | ||
|
|
5446592d44 | ||
|
|
40ed9a53c9 | ||
|
|
f7ac8cea90 | ||
|
|
4ef5d1f0cd | ||
|
|
6992615c98 | ||
|
|
43dabb2825 | ||
|
|
05e40e5681 | ||
|
|
2c4536e137 | ||
|
|
3dc81058a0 | ||
|
|
bd84667a2b | ||
|
|
e5b6a12977 | ||
|
|
ca415d5d62 | ||
|
|
99b4fe7278 | ||
|
|
327e164869 | ||
|
|
25bc571f30 | ||
|
|
38c7e8a1d2 | ||
|
|
ca282e28e0 | ||
|
|
5ef59c06df | ||
|
|
8f55d385d6 | ||
|
|
cd2fc25c19 | ||
|
|
709983eea6 | ||
|
|
40e99b1b80 | ||
|
|
488684d960 | ||
|
|
f35034b989 | ||
|
|
9d6f9b1f26 | ||
|
|
6148a608fb | ||
|
|
3fa9e70383 | ||
|
|
16fea6f009 | ||
|
|
df9ed835ca | ||
|
|
e394c8f0f2 | ||
|
|
21974f7288 | ||
|
|
5ef0170d77 | ||
|
|
c21dcf14de | ||
|
|
a8d20d4e1e | ||
|
|
8b307485b0 | ||
|
|
4544afe422 | ||
|
|
9d7eba5f70 | ||
|
|
be0aee95f2 | ||
|
|
3469ed7ab9 | ||
|
|
1f223aa7e6 | ||
|
|
0a431ead5e | ||
|
|
f750796444 | ||
|
|
c82bcd882a | ||
|
|
7d0ec33b54 | ||
|
|
43d48b3feb | ||
|
|
2e406d2687 | ||
|
|
3f30808104 | ||
|
|
ab10217c86 | ||
|
|
00430491ca | ||
|
|
109202329f | ||
|
|
3b1509f307 | ||
|
|
7ad7b08bed | ||
|
|
4650e5e8fb | ||
|
|
af59d4929e | ||
|
|
e34100bab4 | ||
|
|
d9b3a9fb60 | ||
|
|
39eec59c90 | ||
|
|
d651d0d472 | ||
|
|
87a2358a65 | ||
|
|
cef4e313e1 | ||
|
|
7cc1a4eba0 | ||
|
|
c6cc0133b3 | ||
|
|
7748e68440 | ||
|
|
6c2230a076 | ||
|
|
66b233eaea | ||
|
|
fed58f3920 | ||
|
|
815b2be7f7 | ||
|
|
f420c9fb7c | ||
|
|
01bdf10b94 | ||
|
|
ddedc1ee92 | ||
|
|
9e9703183f | ||
|
|
adce9e6220 | ||
|
|
c499133bbe | ||
|
|
8f505c2dcc | ||
|
|
b320064418 | ||
|
|
a643933d16 | ||
|
|
2659ec5887 | ||
|
|
9f8327926d | ||
|
|
7a568dc118 | ||
|
|
c946b06be5 | ||
|
|
c65fd0e477 | ||
|
|
8f8217e928 | ||
|
|
6c9e1799c7 | ||
|
|
decd70eb23 | ||
|
|
a20d40618f | ||
|
|
b4af8ec751 | ||
|
|
feb5eed8a5 | ||
|
|
f4fa39c70e | ||
|
|
7b7165f5d8 | ||
|
|
13897db6d3 | ||
|
|
c4afdb7198 | ||
|
|
0284975f3f | ||
|
|
269e3d1303 | ||
|
|
8c81f7ece9 | ||
|
|
f6e0593774 | ||
|
|
3d80e549cb | ||
|
|
acc7448dc5 | ||
|
|
35d3d3de72 | ||
|
|
0372e07eb0 | ||
|
|
00221e3410 | ||
|
|
9c264611cf | ||
|
|
31d7f70e27 | ||
|
|
04e8b83d45 | ||
|
|
e87bf71f20 | ||
|
|
2dd70c8d62 | ||
|
|
a3886702a3 | ||
|
|
713af133a0 | ||
|
|
057ffffbf2 | ||
|
|
a81d6d124b | ||
|
|
23f07fde5e | ||
|
|
b42b760393 | ||
|
|
bf6f4c48c0 | ||
|
|
6133f04841 | ||
|
|
3c18f79ea4 | ||
|
|
2af8342fea | ||
|
|
fc3db7942d | ||
|
|
164e2b2678 | ||
|
|
b7b28390df | ||
|
|
a6e996d921 | ||
|
|
07e666345d | ||
|
|
007f10d29d | ||
|
|
f9284d20ca | ||
|
|
9050869781 | ||
|
|
54975de0f3 | ||
|
|
a7aead5138 | ||
|
|
6868f66f24 | ||
|
|
3c0b00e42d | ||
|
|
3327388f1f | ||
|
|
04497aec36 | ||
|
|
aa9d596930 | ||
|
|
f96e68cd11 | ||
|
|
013227323d | ||
|
|
19cbb442ee | ||
|
|
c0e7f341cb | ||
|
|
0a1ba7c434 | ||
|
|
b708dabf98 | ||
|
|
899e56e5b8 | ||
|
|
f6d3bd8ccb | ||
|
|
deb5677a57 | ||
|
|
5c464c3f5a | ||
|
|
cceef33fef | ||
|
|
ed8174fe36 | ||
|
|
3c8906494f | ||
|
|
6e745e9882 | ||
|
|
fb4e9c3772 | ||
|
|
2c282f9550 | ||
|
|
d92d41cb05 | ||
|
|
82e7050561 | ||
|
|
44f92d4169 | ||
|
|
2f1fae38dd | ||
|
|
9fe99979fe | ||
|
|
6399de0b51 | ||
|
|
959740a585 | ||
|
|
159b082828 | ||
|
|
8e7c5af16c | ||
|
|
c1645ab7a7 | ||
|
|
2ae2bfdde9 | ||
|
|
3fe93968a6 | ||
|
|
79a2d715b0 | ||
|
|
50b271c868 | ||
|
|
a57f28ac83 | ||
|
|
3f3747a2fe | ||
|
|
d133913c3d | ||
|
|
e049cef00a | ||
|
|
eb8176971c | ||
|
|
5bbfca45fa | ||
|
|
9b500cd867 | ||
|
|
b52cae6575 | ||
|
|
35a0142f9b | ||
|
|
d4f6ef4f1b | ||
|
|
11024deaae | ||
|
|
5a038de1d5 | ||
|
|
903982e896 | ||
|
|
6355c404cc | ||
|
|
92b9cb5d43 | ||
|
|
7580383d26 | ||
|
|
ba0934e41e | ||
|
|
a6a1021521 | ||
|
|
33b4d83c73 | ||
|
|
6cf630c74a | ||
|
|
736fe5b84e | ||
|
|
4241bde6ea | ||
|
|
b4ce14d744 | ||
|
|
10832a2ccc | ||
|
|
91aca44f67 | ||
|
|
96cfbb201a | ||
|
|
b2bc155701 | ||
|
|
a70ef5594d | ||
|
|
6d991586fd | ||
|
|
f8890ca841 | ||
|
|
0752c6b24f | ||
|
|
3ffaf2c0e1 | ||
|
|
a3e0fbd606 | ||
|
|
9c8ceb6b4e | ||
|
|
bebce2c053 | ||
|
|
34c6790762 | ||
|
|
a5fb009b62 | ||
|
|
9671ca5ebf | ||
|
|
5334ea393e | ||
|
|
2aaacc02e3 | ||
|
|
222e929b2d | ||
|
|
6f16d35a92 | ||
|
|
d7a2ccf5ac | ||
|
|
9ce605221a | ||
|
|
1e930fe950 | ||
|
|
4dc158589c | ||
|
|
4525eb457b | ||
|
|
56a2e07dc2 | ||
|
|
9b7fe9ac31 | ||
|
|
c3da07ccf7 | ||
|
|
b691a56d51 | ||
|
|
13e0a1b5bb | ||
|
|
646baddce4 | ||
|
|
02f61c323d | ||
|
|
1e3d2df9e7 | ||
|
|
e43fae86f1 | ||
|
|
c6151e34e0 | ||
|
|
45cb991254 | ||
|
|
839bc99f94 | ||
|
|
0aeb1ca408 | ||
|
|
cd76a906f4 | ||
|
|
e438491938 | ||
|
|
307b35a5bf | ||
|
|
217c9720ea | ||
|
|
778c7dc5f2 | ||
|
|
4c80154437 | ||
|
|
6bd9529a66 | ||
|
|
33ea2b4844 | ||
|
|
5c807f3dc8 | ||
|
|
9063b559c4 | ||
|
|
40f6df7160 | ||
|
|
95165aa92f | ||
|
|
d96fcdb35c | ||
|
|
5efabdcea3 | ||
|
|
2d57dc0565 | ||
|
|
576629f825 | ||
|
|
5badb9d151 | ||
|
|
45dc379d9a | ||
|
|
49c0c9f44c | ||
|
|
ef5fa4d062 | ||
|
|
35b66d5d94 | ||
|
|
d0b749a43c | ||
|
|
bcc4d4e8c6 | ||
|
|
41bff0b293 | ||
|
|
dfc7f35ef1 | ||
|
|
0bbbbdde80 | ||
|
|
5fa5284b58 | ||
|
|
b7ef82cb67 | ||
|
|
1233780265 | ||
|
|
dd095279c8 | ||
|
|
4d5200c50f | ||
|
|
1bcd675ead | ||
|
|
2a3d3de0b2 | ||
|
|
b124836f3a | ||
|
|
93ba95971b | ||
|
|
7b193b3745 | ||
|
|
2b647d2405 | ||
|
|
7714cca599 | ||
|
|
42511aa9cf | ||
|
|
ace2a2f3d1 | ||
|
|
2062fe7a08 | ||
|
|
d4c02c3988 | ||
|
|
4c1496b4a4 | ||
|
|
eec876295d | ||
|
|
3093175f54 | ||
|
|
dd05c4d34a | ||
|
|
57e3a40321 | ||
|
|
9e70152076 | ||
|
|
e1da83a8f6 | ||
|
|
8108198613 | ||
|
|
915849b2ce | ||
|
|
2e96302336 | ||
|
|
051cd744ad | ||
|
|
53fbc165ba | ||
|
|
1862bcf867 | ||
|
|
8909d1d144 | ||
|
|
a2f0f20284 | ||
|
|
1951b52aa5 | ||
|
|
cd7a9345ec | ||
|
|
dba4c33c81 | ||
|
|
153c239c9b | ||
|
|
4034ab4182 | ||
|
|
9c917c3bd3 | ||
|
|
cca0222e1d | ||
|
|
682db9b81f | ||
|
|
3e000f9be1 | ||
|
|
548a552638 | ||
|
|
1d5b5b7d15 | ||
|
|
91aa4586e2 | ||
|
|
6d3bc43ef6 | ||
|
|
0f63e26641 | ||
|
|
ab2ef69c6a | ||
|
|
621350515e | ||
|
|
03ed5c398a | ||
|
|
65d6f8c018 | ||
|
|
79d0673ae6 | ||
|
|
cbd488e19f | ||
|
|
380d869195 | ||
|
|
73893f2a33 | ||
|
|
ad81470d35 | ||
|
|
fc140d04ef | ||
|
|
a0257ed7e7 | ||
|
|
4769487c3b | ||
|
|
29def587ff | ||
|
|
f35d0b2b37 | ||
|
|
283e92d55d | ||
|
|
c82b26d334 | ||
|
|
2753e02cda | ||
|
|
fde733c205 | ||
|
|
f730591f2c | ||
|
|
94eac1e79d | ||
|
|
9f2b6d0ec6 | ||
|
|
7d7d0ea001 | ||
|
|
794101691c | ||
|
|
a443144a5c | ||
|
|
73f0867061 | ||
|
|
f97db93212 | ||
|
|
d36708933c | ||
|
|
14f82ea0a9 | ||
|
|
c41dd6495d | ||
|
|
1005c99e9c | ||
|
|
f4478fc762 | ||
|
|
c5ed308ea5 | ||
|
|
3ab5ba6149 | ||
|
|
9b2fde962c | ||
|
|
571a7dc42d | ||
|
|
3421fffa9b | ||
|
|
c25619fd63 | ||
|
|
76adb13a64 | ||
|
|
33b1eed361 | ||
|
|
c44891a1a8 | ||
|
|
f31f52ff1c | ||
|
|
6ad9a56bd9 | ||
|
|
a5c2fc4f9d | ||
|
|
0a65006bb4 | ||
|
|
3db896c4e2 | ||
|
|
e80322021a | ||
|
|
48316ba60d | ||
|
|
c0f1493473 | ||
|
|
ccbd128fa2 | ||
|
|
46817caa68 | ||
|
|
775c8624d4 | ||
|
|
36eedc987c | ||
|
|
3b8f31c888 | ||
|
|
a34fa74eaa | ||
|
|
d6b2d8dcb5 | ||
|
|
aab0599280 | ||
|
|
dfa8eaf24e | ||
|
|
63d55cb797 | ||
|
|
c642eee0d2 | ||
|
|
5f33d298d7 | ||
|
|
fc39fd7519 | ||
|
|
7f442f7485 | ||
|
|
0ee3203a5a | ||
|
|
43a5df8780 | ||
|
|
0949df014b | ||
|
|
01f4dd8f97 | ||
|
|
8b7599f5d9 | ||
|
|
9bdc320cf8 | ||
|
|
d9c8285806 | ||
|
|
4b8344082f | ||
|
|
e5cf76b460 | ||
|
|
422ca87a12 | ||
|
|
a512ccca28 | ||
|
|
ba215be97c | ||
|
|
ca16050681 | ||
|
|
06e4ed1bb4 | ||
|
|
d4a8ae5743 | ||
|
|
a4f2f811d3 | ||
|
|
ebaba95eb3 | ||
|
|
31f7769199 | ||
|
|
7726be94be | ||
|
|
f2cbcea6d7 | ||
|
|
5d6a28954b | ||
|
|
319f1deceb | ||
|
|
3f14958741 | ||
|
|
42ba4a5c56 | ||
|
|
c804c395ed | ||
|
|
58c8cf1a3a | ||
|
|
76ea8c86b7 | ||
|
|
050378fa72 | ||
|
|
29d858d58c | ||
|
|
dc45920afb | ||
|
|
15fcb57e2f | ||
|
|
91ee85152c | ||
|
|
aa7bf7af1e | ||
|
|
02c1ba39ad | ||
|
|
8e8d9426df | ||
|
|
57f301815d | ||
|
|
dfc9dc713c | ||
|
|
1a0cad7f5f | ||
|
|
3df436f0d8 | ||
|
|
d737fca295 | ||
|
|
da5a3532d7 | ||
|
|
27111e7b29 | ||
|
|
b847bc0aba | ||
|
|
6eb0bc50e2 | ||
|
|
7530f03bf6 | ||
|
|
24a9633edc | ||
|
|
7e1a5ce445 | ||
|
|
2ffdbc7fc0 | ||
|
|
52c7b68cc3 | ||
|
|
ddbcc8e84b | ||
|
|
2bfb195ad6 | ||
|
|
cd2d9517a0 | ||
|
|
19dc312128 | ||
|
|
175659628d | ||
|
|
8fea2b09be | ||
|
|
f77f45b70c | ||
|
|
103a287f11 | ||
|
|
d600ade40c | ||
|
|
a6a7cba121 | ||
|
|
7fff635a3f | ||
|
|
7a749b88c7 | ||
|
|
1ce6a7f4be | ||
|
|
a092910fdd | ||
|
|
bb77838b3e | ||
|
|
1001f1bd36 | ||
|
|
de0e5583a5 | ||
|
|
cbd2a44350 | ||
|
|
c888e461ba | ||
|
|
d135522087 | ||
|
|
ce2b148dd2 | ||
|
|
2d075c4dd6 | ||
|
|
bcd1841f71 | ||
|
|
029cf4ad1f | ||
|
|
ed7fc86d69 | ||
|
|
82a9e43b6f | ||
|
|
9ae2c731ed | ||
|
|
7d1ba466b4 | ||
|
|
4f1d8678ea | ||
|
|
4bd72ebc63 | ||
|
|
e5986e0ae2 | ||
|
|
fae39e4bc9 | ||
|
|
dbe8357dd5 | ||
|
|
3234f0bdd7 | ||
|
|
47a4d58009 | ||
|
|
4ae60da58d | ||
|
|
47f995bda3 | ||
|
|
42721628eb | ||
|
|
f42ab957d4 | ||
|
|
ce9d0d7e82 | ||
|
|
baf79dda21 | ||
|
|
b71a9bc097 | ||
|
|
129632cd6b | ||
|
|
aca8899c4d | ||
|
|
5c3d91e65e | ||
|
|
0205d827f1 | ||
|
|
225c31d583 | ||
|
|
b18d87ddba | ||
|
|
25298c72bb | ||
|
|
3df3d27533 | ||
|
|
cbb0b57018 | ||
|
|
65f205bca8 | ||
|
|
1cc7f80109 | ||
|
|
213a0a18a5 | ||
|
|
1a24d599b3 | ||
|
|
d80be60e2b | ||
|
|
0ffe79d76c | ||
|
|
db36d0a375 | ||
|
|
ff659a0be3 | ||
|
|
8485b12102 | ||
|
|
d889cc3c5a | ||
|
|
7bb65fca4e | ||
|
|
8aaa5951ca | ||
|
|
d58f3b7520 | ||
|
|
e5a636a159 | ||
|
|
51f314e907 | ||
|
|
531fa30b69 | ||
|
|
2b3bb81fae | ||
|
|
80f80cd31f | ||
|
|
79705fbf11 | ||
|
|
191a4e569e | ||
|
|
1cac35be03 | ||
|
|
6d48100f44 | ||
|
|
4627af3e90 | ||
|
|
913952ffe1 | ||
|
|
67bf6afc89 | ||
|
|
06064decd2 | ||
|
|
4cca9f17df | ||
|
|
74a89223c0 | ||
|
|
2954017836 | ||
|
|
a03262fc01 | ||
|
|
d65ce6fc2c | ||
|
|
d27e1eee25 | ||
|
|
b1f00bb708 | ||
|
|
e0f1e79e6a | ||
|
|
d70b7d41e8 | ||
|
|
43af9f3fad | ||
|
|
bc53dd6830 | ||
|
|
263616ef01 | ||
|
|
285da0542e | ||
|
|
17f7e2f892 | ||
|
|
a29d8f1d68 | ||
|
|
8965172603 | ||
|
|
03c2967337 | ||
|
|
5b154a0da4 | ||
|
|
b2c8c326d7 | ||
|
|
96aedaa91f | ||
|
|
a22ad1ec32 | ||
|
|
a4244defb5 | ||
|
|
57328e55f3 | ||
|
|
87c32aeb40 | ||
|
|
2e01e0c30e | ||
|
|
a12b2de74a | ||
|
|
6b01d8f99b | ||
|
|
eac4f6062e | ||
|
|
5583cf0a5f | ||
|
|
57d772fa23 | ||
|
|
1bdc3988a9 | ||
|
|
2af55baa9a | ||
|
|
0452eec11d | ||
|
|
c4f7db6c04 | ||
|
|
3569529a84 | ||
|
|
70942ac0f6 | ||
|
|
dc02e39918 | ||
|
|
73d6bc35ec | ||
|
|
b1d558d700 | ||
|
|
897480265f | ||
|
|
73724f5a33 | ||
|
|
bdbd495a9e | ||
|
|
1fcf009804 | ||
|
|
914c5752a5 | ||
|
|
201b12a886 | ||
|
|
c5f23ad93d | ||
|
|
28d62009a7 | ||
|
|
1a5a436f82 | ||
|
|
1275ac0569 | ||
|
|
5112fb777e | ||
|
|
f571a944c9 | ||
|
|
bc9aff8c60 | ||
|
|
c4c7ab7888 | ||
|
|
d9819a990c | ||
|
|
aea400e26a | ||
|
|
eb4e7735c1 | ||
|
|
4b498ae8cd | ||
|
|
158e2a4ca9 | ||
|
|
b011d48d82 | ||
|
|
8ac3e725f8 | ||
|
|
9a4aef0358 | ||
|
|
7d3146234a | ||
|
|
5d2ca6493d | ||
|
|
4752f9aa37 | ||
|
|
025d3a03d6 | ||
|
|
aec06183e7 | ||
|
|
aa28abd517 | ||
|
|
7430b31697 | ||
|
|
759f72169a | ||
|
|
1f7135be61 | ||
|
|
6942f9c1cf | ||
|
|
d9da75d1c0 | ||
|
|
7ab7372be4 | ||
|
|
3503c98857 | ||
|
|
708c3f1e2a | ||
|
|
6f645e8619 | ||
|
|
bce7ca7ac4 | ||
|
|
350465c25d | ||
|
|
5b9c70ae22 | ||
|
|
9b30afeca9 | ||
|
|
c1b202c119 | ||
|
|
41cfe5d2ca | ||
|
|
05339e184f | ||
|
|
447127d956 | ||
|
|
394334fbea | ||
|
|
9f8cd33d43 | ||
|
|
f066e28c35 | ||
|
|
b349a449bb | ||
|
|
1c5898d396 | ||
|
|
6802967863 | ||
|
|
0462f18680 | ||
|
|
af6699098f | ||
|
|
6b7e7dc124 | ||
|
|
6bae4c6a66 | ||
|
|
46da918dbe | ||
|
|
bb7e5f17b5 | ||
|
|
b9d03114c2 | ||
|
|
436b1ce176 | ||
|
|
50fb5d83f1 | ||
|
|
fda672f806 | ||
|
|
2bf783b04d | ||
|
|
2f72b23a0d | ||
|
|
85336f9777 | ||
|
|
174d964553 | ||
|
|
cf8677248e | ||
|
|
1e6a3163af | ||
|
|
e008919978 | ||
|
|
4814066c67 | ||
|
|
f17f8b48c2 | ||
|
|
ab0aec0ac5 | ||
|
|
b49a641ba5 | ||
|
|
2f50051426 | ||
|
|
43cc32db40 | ||
|
|
b4d6f6b947 | ||
|
|
71ff533623 | ||
|
|
e33a5bbef5 | ||
|
|
6c0112c2be | ||
|
|
15bbf26b93 | ||
|
|
87c97efce0 | ||
|
|
6c4aee1479 | ||
|
|
73549a9044 | ||
|
|
30fdd3e184 | ||
|
|
c97eb5d63f | ||
|
|
5729c7d5e7 | ||
|
|
d77b13efcb | ||
|
|
c43faca7b9 | ||
|
|
892ddd5724 | ||
|
|
a9de779f33 | ||
|
|
1c2f016ba0 | ||
|
|
7b4d9140af | ||
|
|
c1fc87ff4e | ||
|
|
cd5ea5d4e0 | ||
|
|
30c01089f5 | ||
|
|
89825a2b21 | ||
|
|
a743b75bb4 | ||
|
|
f7ebf8dedd | ||
|
|
f6220cab3b | ||
|
|
0c5e1c4138 | ||
|
|
03fe431f1a | ||
|
|
a8e4554fec | ||
|
|
e81b09b9aa | ||
|
|
c6e846e0ae | ||
|
|
03dcfb5c4b | ||
|
|
3e54da03e2 | ||
|
|
c4b3196917 | ||
|
|
0d81e7933e | ||
|
|
b2a2735034 | ||
|
|
f865c5de90 | ||
|
|
4159369e8b | ||
|
|
170693cf0b | ||
|
|
4e7b5d4af8 | ||
|
|
67bf789fcf | ||
|
|
f5cf616c2f | ||
|
|
7975f19817 | ||
|
|
017602056d | ||
|
|
c63f43854b | ||
|
|
5cc71ec2ad | ||
|
|
80e81f8475 | ||
|
|
3685c8e015 | ||
|
|
99e943c365 | ||
|
|
21818e71f5 | ||
|
|
bcc6d25e21 | ||
|
|
7b885ee0d3 | ||
|
|
c10e808a4f | ||
|
|
54e9be0ed8 | ||
|
|
938cdf316a | ||
|
|
27c33911e6 | ||
|
|
e88f8759e7 | ||
|
|
f2992e3165 | ||
|
|
c71fd1ee3b | ||
|
|
fb45b19fdc | ||
|
|
c4ea8d4942 | ||
|
|
646aa131ef | ||
|
|
0adb40bf92 | ||
|
|
17d6014bf1 | ||
|
|
ff57cd4eaf | ||
|
|
74bd7c3744 | ||
|
|
cfbb283f85 | ||
|
|
74a3c4451b | ||
|
|
be3643c962 | ||
|
|
f4aa546af8 | ||
|
|
67b876a7f4 | ||
|
|
94e177c0ef | ||
|
|
1bd83cc9bc | ||
|
|
ecda3f4a7d | ||
|
|
8f972a965d | ||
|
|
0f051fc57c | ||
|
|
c3f8925f46 | ||
|
|
5d0cab2052 | ||
|
|
4d7492f682 | ||
|
|
fc9d99080f | ||
|
|
47ebac0276 | ||
|
|
cb3fca03e9 | ||
|
|
abbbd83729 | ||
|
|
1743ab7812 | ||
|
|
324e3972a6 | ||
|
|
1502dda2ab | ||
|
|
f31b2c4a79 | ||
|
|
89b9b60e0c | ||
|
|
de9ba12779 | ||
|
|
9cc4359c04 | ||
|
|
67eaf120b9 | ||
|
|
b8353c4a33 | ||
|
|
7013033ae4 | ||
|
|
cb8cd03852 | ||
|
|
f63fb62014 | ||
|
|
2e4fb86b86 | ||
|
|
5e776a07dd | ||
|
|
81e637e50e | ||
|
|
0971ad0a80 | ||
|
|
8267ded7ec | ||
|
|
7f36ea55f5 | ||
|
|
72a051f2d3 | ||
|
|
51b197888c | ||
|
|
cd63865d31 | ||
|
|
5be5685a09 | ||
|
|
76b2f25d46 | ||
|
|
58607d4a7f | ||
|
|
c0a5b16a7f | ||
|
|
3a0c69005b | ||
|
|
5c295fb9e3 | ||
|
|
4ee212e7d5 | ||
|
|
70651ce994 | ||
|
|
a778a91106 | ||
|
|
cfc31eead3 | ||
|
|
da0a1bbe9f | ||
|
|
bc66fb33e9 | ||
|
|
b1b6493755 | ||
|
|
1d189f239b | ||
|
|
5b90691bcc | ||
|
|
d1d5972277 | ||
|
|
2c07d77368 | ||
|
|
642cfbf59a | ||
|
|
bb1367cfb9 | ||
|
|
11724aa555 | ||
|
|
4d374712de | ||
|
|
eb9003187d | ||
|
|
caba444962 | ||
|
|
5b6c8c191f | ||
|
|
dd51589f67 | ||
|
|
b02a31d4b9 | ||
|
|
0e7878b406 | ||
|
|
cae91ce0c5 | ||
|
|
67a65a2aa9 | ||
|
|
364b0a7163 | ||
|
|
d6419f2059 | ||
|
|
6f7ad7ef91 | ||
|
|
5ae588833b | ||
|
|
a70dbac0e6 | ||
|
|
4d34a02afe | ||
|
|
4db4f45897 | ||
|
|
2d5280fc95 | ||
|
|
b8d568761e | ||
|
|
29309dac9a | ||
|
|
7f7745071a | ||
|
|
1914032e35 | ||
|
|
f44c8f1205 | ||
|
|
fe2ef4e61c | ||
|
|
fc3eda55c7 | ||
|
|
8adf1cdd02 | ||
|
|
adbbc656d4 | ||
|
|
8e852bce02 | ||
|
|
bb461b009f | ||
|
|
03559a3cc4 | ||
|
|
7bb2fe128a | ||
|
|
2312e17a8e | ||
|
|
9835b382da | ||
|
|
1eacc6fbff | ||
|
|
85187239b6 | ||
|
|
819ff2a902 | ||
|
|
c744104a18 | ||
|
|
c87801f0a9 | ||
|
|
39735594bd | ||
|
|
30964f65e4 | ||
|
|
ee0c7fd8bf | ||
|
|
dfdecef8e7 | ||
|
|
edcdfeb057 | ||
|
|
47f0de9836 | ||
|
|
9ba657797e | ||
|
|
07442a6f84 | ||
|
|
3faf3c84be | ||
|
|
abcacc82f3 | ||
|
|
9544b7d968 | ||
|
|
babbc8bcd6 | ||
|
|
12809ebc74 | ||
|
|
b45a601ad2 | ||
|
|
f099dc6a37 | ||
|
|
803caddbd4 | ||
|
|
4d7b988018 | ||
|
|
c1f88a4e14 | ||
|
|
5d9ec0b208 | ||
|
|
1877cacf9c | ||
|
|
2f4978cfea | ||
|
|
d27a1103fa | ||
|
|
b85bb95082 | ||
|
|
db7f93cff3 | ||
|
|
85e271098f | ||
|
|
17001e2f74 | ||
|
|
c82f4f0d45 | ||
|
|
88247a3af9 | ||
|
|
158578a406 | ||
|
|
19314e7e06 | ||
|
|
8bcbc6d545 | ||
|
|
ef55e6d476 | ||
|
|
295ef3dc1d | ||
|
|
9d125c9e79 | ||
|
|
86363986fc | ||
|
|
0a2dbbc58b | ||
|
|
673a966541 | ||
|
|
db1e69813b | ||
|
|
e60d56f060 | ||
|
|
328e062ae9 | ||
|
|
0523c2ea4b | ||
|
|
c5c7378c63 | ||
|
|
9b2080d036 | ||
|
|
d4b3649640 | ||
|
|
b085993901 | ||
|
|
0d4afad342 | ||
|
|
0da694b845 | ||
|
|
6d5e7d9e81 | ||
|
|
bc08bea284 | ||
|
|
0e5a0661e1 | ||
|
|
a839bd428f | ||
|
|
0277062693 | ||
|
|
7affa5ab69 | ||
|
|
ed22af4e73 | ||
|
|
63ebb6998e | ||
|
|
7914cd47ca | ||
|
|
708dbac70e | ||
|
|
1b62dd5c40 | ||
|
|
4911545843 | ||
|
|
c5cc4b7867 | ||
|
|
eacb614750 | ||
|
|
341e1e7a6d | ||
|
|
a02c820c2d | ||
|
|
2f6890c78a | ||
|
|
516591fe88 | ||
|
|
d2941a9110 | ||
|
|
f7302f710b | ||
|
|
6a02ac7e80 | ||
|
|
d1b86fdef5 | ||
|
|
57ac38ddca | ||
|
|
7a73a92074 | ||
|
|
d1b30f4792 | ||
|
|
16dcf78cab | ||
|
|
d868cfdeb0 | ||
|
|
c074f4d925 | ||
|
|
453024c58d | ||
|
|
fe8340617a | ||
|
|
b024dd913d | ||
|
|
a2a698ab0e | ||
|
|
bb56f92213 | ||
|
|
8dcd998945 | ||
|
|
bcbbbe4046 | ||
|
|
7200a8cb84 | ||
|
|
6925344807 | ||
|
|
60ceeb0ddd | ||
|
|
06caabf333 | ||
|
|
954131bd51 | ||
|
|
855efe7fe8 | ||
|
|
d902a74ab0 | ||
|
|
499e11f730 | ||
|
|
6db59a9c31 | ||
|
|
6465726008 | ||
|
|
3a3b96e0be | ||
|
|
992c91dc0c | ||
|
|
809473c15c | ||
|
|
d79a5ec3d6 | ||
|
|
237469ceaf | ||
|
|
c28d9135d9 | ||
|
|
48a5679087 | ||
|
|
7c938712f2 | ||
|
|
4df12bebc2 | ||
|
|
dfe8987aaa | ||
|
|
02dbe401d8 | ||
|
|
c18f8c92e7 | ||
|
|
857cd718df | ||
|
|
11d4f6499a | ||
|
|
f2c25b4744 | ||
|
|
27b846717f | ||
|
|
9ed138f896 | ||
|
|
1978dc80eb | ||
|
|
fc4b247f4f | ||
|
|
ebf7056f4a | ||
|
|
eb975d7e13 | ||
|
|
a2dd8cb6b9 | ||
|
|
7c254c6136 | ||
|
|
c8a33b83f1 | ||
|
|
1145c72b01 | ||
|
|
7fc45fb711 | ||
|
|
e146262c38 | ||
|
|
6f808bd06e | ||
|
|
0b6ab49325 | ||
|
|
66d9182e50 | ||
|
|
654cca82a9 | ||
|
|
89785da1c5 | ||
|
|
2f9964e46e | ||
|
|
168ecd67b0 | ||
|
|
bcbe740598 | ||
|
|
86c8929d77 | ||
|
|
6738a9433b | ||
|
|
23843ec86e | ||
|
|
f4db0da585 | ||
|
|
9ee3b796cd | ||
|
|
f57569f553 | ||
|
|
fffd0e8990 | ||
|
|
200e52bab5 | ||
|
|
a0ef649dd8 | ||
|
|
0dd01bda01 | ||
|
|
a707598042 | ||
|
|
8a3171308a | ||
|
|
29c887f30b | ||
|
|
661398d891 | ||
|
|
2cd722d751 | ||
|
|
49f5b4fa5c | ||
|
|
67baf465f4 | ||
|
|
ee7666ddea | ||
|
|
02fc41ff1c | ||
|
|
d07a9d2ef8 | ||
|
|
3622ebfabd | ||
|
|
70b320633f | ||
|
|
f30208f345 | ||
|
|
5bcc454678 | ||
|
|
473110568f | ||
|
|
88ca0f8196 | ||
|
|
a171005010 | ||
|
|
f56ad2fa58 | ||
|
|
c9dc441915 | ||
|
|
a0d255369a | ||
|
|
40b0a15b35 | ||
|
|
b98b06ff79 | ||
|
|
a448c9aebf | ||
|
|
b3f462a39d | ||
|
|
7ce34ca019 | ||
|
|
719bb53c3a | ||
|
|
214415969f | ||
|
|
7431b1f123 | ||
|
|
d8ffa843a9 | ||
|
|
a69db231cc | ||
|
|
c17f94422f | ||
|
|
b4777f7f4f | ||
|
|
a57d9a9303 | ||
|
|
5e70e1bcb2 | ||
|
|
0c43787996 | ||
|
|
dc310b99f9 | ||
|
|
e98c5e10bc | ||
|
|
f1b1090263 | ||
|
|
6efd6faa3f | ||
|
|
1e4d48d371 | ||
|
|
93a2adb3e6 | ||
|
|
a66d516777 | ||
|
|
7a97d42338 | ||
|
|
b66cdc8fa0 | ||
|
|
67f43b2aad | ||
|
|
d143e50238 | ||
|
|
e27439be6a | ||
|
|
2ad5ffbda2 | ||
|
|
dae9e662a5 | ||
|
|
f22737d6a4 | ||
|
|
a458d5a176 | ||
|
|
d92ed04538 | ||
|
|
80b3df8953 | ||
|
|
bcf83ec761 | ||
|
|
e44e72bce3 | ||
|
|
35f2781518 | ||
|
|
dc5512e403 | ||
|
|
48ef176e28 | ||
|
|
1aa2b86df3 | ||
|
|
73026047e9 | ||
|
|
6c2c33cac8 | ||
|
|
d593f7e04b | ||
|
|
6c599ef506 | ||
|
|
f48a0b7b7d | ||
|
|
d9f538170b | ||
|
|
1785ced655 | ||
|
|
e155e1fa86 | ||
|
|
e28fab0550 | ||
|
|
fb0dd2c1ca | ||
|
|
6e89e736b7 | ||
|
|
634b874c46 | ||
|
|
9d16364394 | ||
|
|
daeecef59e | ||
|
|
8131f0a752 | ||
|
|
f4ea1ad517 | ||
|
|
f34e8a0ff6 | ||
|
|
4209d61b13 | ||
|
|
fa83fba637 | ||
|
|
af86aee970 | ||
|
|
f26f1a526c | ||
|
|
7cb46d0761 | ||
|
|
0cb4070364 | ||
|
|
bc008c2597 | ||
|
|
a1d142d3a4 | ||
|
|
aa00dc1031 | ||
|
|
592c654916 | ||
|
|
5021b10535 | ||
|
|
43d6e64cfa | ||
|
|
8d21e5f3c1 | ||
|
|
fbe5df84c0 | ||
|
|
caff44c663 | ||
|
|
d6edef98c6 | ||
|
|
e0d2fab3c3 | ||
|
|
9867e918fa | ||
|
|
e6374ab425 | ||
|
|
e116bb9227 | ||
|
|
f1a1aa54d8 | ||
|
|
574f3c23d3 | ||
|
|
c31d6a6898 | ||
|
|
44a2a164c0 | ||
|
|
a7ca9950fc | ||
|
|
e0dd33e6be | ||
|
|
2e718e1130 | ||
|
|
ede9fcfb00 | ||
|
|
a3d43b77ca | ||
|
|
e2b32b4bb3 | ||
|
|
025c16c95d | ||
|
|
000eff73cc | ||
|
|
254efdde79 | ||
|
|
f0d4e76418 | ||
|
|
ba7101ff92 | ||
|
|
a2457df45e | ||
|
|
305540f0fd | ||
|
|
c2928d8a57 | ||
|
|
7451244cd2 | ||
|
|
d935b5764a | ||
|
|
f3af76e38c | ||
|
|
a7631223a3 | ||
|
|
8aae4f0ed0 | ||
|
|
542049f252 | ||
|
|
9f3394dc6d | ||
|
|
06f5dc6ad7 | ||
|
|
dc3b09c218 | ||
|
|
ad15781d8f | ||
|
|
ea53612822 | ||
|
|
c3a065dd33 | ||
|
|
5cb2812231 | ||
|
|
f8904a5504 | ||
|
|
eb1df23e68 | ||
|
|
e5648a4af9 | ||
|
|
a246154961 | ||
|
|
ce44843e27 | ||
|
|
1a54dad643 | ||
|
|
940dfff625 | ||
|
|
c2b15183cb | ||
|
|
27e8aa9c68 | ||
|
|
e1d8c6516a | ||
|
|
eba81e368b | ||
|
|
74a3fd7596 | ||
|
|
eeb5a83e98 | ||
|
|
d47134bbf1 | ||
|
|
ee725354db | ||
|
|
985bfd22de | ||
|
|
0d35e3a3e9 | ||
|
|
d94a191656 | ||
|
|
0eafa4acd8 | ||
|
|
f27a53653b | ||
|
|
3b60adc8da | ||
|
|
626a3369b5 | ||
|
|
4244e7569b | ||
|
|
ef4b32aca7 | ||
|
|
dcd23a0b4d | ||
|
|
5447c6e947 | ||
|
|
f1b97fbc8b | ||
|
|
4c8dfc3fc2 | ||
|
|
ceece5a7e2 | ||
|
|
7e6b035ca2 | ||
|
|
fbc46a52af | ||
|
|
8d2e7b4372 | ||
|
|
e7da9144f5 | ||
|
|
2128e169f3 | ||
|
|
8410d64daa | ||
|
|
b2f78fadd9 | ||
|
|
3656323f25 | ||
|
|
2fe1c20475 | ||
|
|
0fb976a80a | ||
|
|
3cf62de753 | ||
|
|
06119b306d | ||
|
|
0493bbbc76 | ||
|
|
4c9e90732e | ||
|
|
35f084ba76 | ||
|
|
f28f336026 | ||
|
|
122d75f677 | ||
|
|
12f6a3f5a3 | ||
|
|
5d44e1d6ca | ||
|
|
04592c876b | ||
|
|
c0571beec8 | ||
|
|
1302316eb0 | ||
|
|
18d8008b89 | ||
|
|
4670f09a67 | ||
|
|
159ef12ed7 | ||
|
|
7a760f5640 | ||
|
|
2b6c42a56c | ||
|
|
ab4ff99105 | ||
|
|
774895ec8c | ||
|
|
c5ce96c391 | ||
|
|
b4a98a4000 | ||
|
|
5f0d86f509 | ||
|
|
c96a1b00cf | ||
|
|
1eb6436682 | ||
|
|
a84e1f17bb | ||
|
|
3ffc9dffc2 | ||
|
|
048c84ab95 | ||
|
|
a7470360d2 | ||
|
|
50f1ca91d4 | ||
|
|
0d37e1cd98 | ||
|
|
9aa77bb3c9 | ||
|
|
fd11244966 | ||
|
|
d060da094f | ||
|
|
306f9c5ffd | ||
|
|
5ef5611682 | ||
|
|
ebdd2d730c | ||
|
|
1ddf8b3159 | ||
|
|
a6bc870815 | ||
|
|
56cd73823e | ||
|
|
6299015039 | ||
|
|
11b7cfb5ff | ||
|
|
367f49ce1c | ||
|
|
8165131419 | ||
|
|
e402157b4d | ||
|
|
967da7944f | ||
|
|
89f1c21f20 | ||
|
|
7e706190a5 | ||
|
|
36a3770673 | ||
|
|
bc92f78afb | ||
|
|
f7e22d2b8b | ||
|
|
0b1e11ba1f | ||
|
|
10e0b1daec | ||
|
|
731d8fc6be | ||
|
|
f6d0b53ae5 | ||
|
|
0efb90deb6 | ||
|
|
b16eabd2b6 | ||
|
|
f8350409ad | ||
|
|
5b498bd8d6 | ||
|
|
941042d0ba | ||
|
|
9251ce312b | ||
|
|
96a964a183 | ||
|
|
9e513e08ae | ||
|
|
9dfee83e68 | ||
|
|
7cde979736 | ||
|
|
870ff1d4d9 | ||
|
|
52c162a478 | ||
|
|
ddd11c7ed2 | ||
|
|
2c119dea47 | ||
|
|
ebd1561682 | ||
|
|
3ccc495c75 | ||
|
|
0eda7a5a3c | ||
|
|
f2c16452c6 | ||
|
|
a2c429a4a5 | ||
|
|
4a71c5b424 | ||
|
|
268dd80cd0 | ||
|
|
3002e79c98 | ||
|
|
5eab348e82 | ||
|
|
1cdbade761 | ||
|
|
8c9afbd278 | ||
|
|
cd73654683 | ||
|
|
9654fe0d8d | ||
|
|
3d49c33c6a | ||
|
|
e58b3390aa | ||
|
|
92a1f5736b | ||
|
|
00a57f6cea | ||
|
|
1c345edc49 | ||
|
|
7aa1f47378 | ||
|
|
473d5ead7b | ||
|
|
68f760b563 | ||
|
|
9c1cd81adb | ||
|
|
85b81fb12a | ||
|
|
5d7444c115 | ||
|
|
b0c1ec04b5 | ||
|
|
5cfd8909a8 | ||
|
|
6e2d2f33de | ||
|
|
5e65d27832 | ||
|
|
36993097b4 | ||
|
|
2447349383 | ||
|
|
7765f272ac | ||
|
|
13d8dfdb5f | ||
|
|
5e94637adc | ||
|
|
ac6e793bbe | ||
|
|
d0d9c3ea26 | ||
|
|
f7bc58a767 | ||
|
|
bafdf0381a | ||
|
|
3fc5dc8523 | ||
|
|
df4dc3492c | ||
|
|
10731b0fd8 | ||
|
|
cb9166aba4 | ||
|
|
fe62c3aacb | ||
|
|
c60ea40828 | ||
|
|
c59ea26845 | ||
|
|
9bd8b3e9a5 | ||
|
|
5271f3b4a0 | ||
|
|
8a7b619b77 | ||
|
|
88f96b0838 | ||
|
|
1e1e48732a | ||
|
|
3537897fc5 | ||
|
|
3653981416 | ||
|
|
94d1e566c0 | ||
|
|
a692316293 | ||
|
|
e2f3406e89 | ||
|
|
81c7007f80 | ||
|
|
e4f38b5665 | ||
|
|
14b6c471cf | ||
|
|
0d0befe23e | ||
|
|
efad628a87 | ||
|
|
c16e6d74e6 | ||
|
|
80db9e7716 | ||
|
|
7cf2a3e978 | ||
|
|
681b74a41c | ||
|
|
d39d10b9fb | ||
|
|
dff44ef74e | ||
|
|
485047f20b | ||
|
|
6affbbe865 | ||
|
|
e3600ef4de | ||
|
|
d92f992c01 | ||
|
|
20a5d9051d | ||
|
|
782d48594a |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -14,4 +14,7 @@ env/
|
||||
.project
|
||||
.pydevproject
|
||||
tests/test_bugfix.py
|
||||
htmlcov/
|
||||
htmlcov/
|
||||
venv
|
||||
venv3
|
||||
scratchpad
|
||||
|
||||
27
.install_mongodb_on_travis.sh
Normal file
27
.install_mongodb_on_travis.sh
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
|
||||
sudo apt-get remove mongodb-org-server
|
||||
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
|
||||
|
||||
if [ "$MONGODB" = "2.4" ]; then
|
||||
echo "deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen" | sudo tee /etc/apt/sources.list.d/mongodb.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install mongodb-10gen=2.4.14
|
||||
sudo service mongodb start
|
||||
elif [ "$MONGODB" = "2.6" ]; then
|
||||
echo "deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen" | sudo tee /etc/apt/sources.list.d/mongodb.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install mongodb-org-server=2.6.12
|
||||
# service should be started automatically
|
||||
elif [ "$MONGODB" = "3.0" ]; then
|
||||
echo "deb http://repo.mongodb.org/apt/ubuntu trusty/mongodb-org/3.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install mongodb-org-server=3.0.14
|
||||
# service should be started automatically
|
||||
else
|
||||
echo "Invalid MongoDB version, expected 2.4, 2.6, or 3.0."
|
||||
exit 1
|
||||
fi;
|
||||
|
||||
mkdir db
|
||||
1>db/logs mongod --dbpath=db &
|
||||
22
.landscape.yml
Normal file
22
.landscape.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
pylint:
|
||||
disable:
|
||||
# We use this a lot (e.g. via document._meta)
|
||||
- protected-access
|
||||
|
||||
options:
|
||||
additional-builtins:
|
||||
# add xrange and long as valid built-ins. In Python 3, xrange is
|
||||
# translated into range and long is translated into int via 2to3 (see
|
||||
# "use_2to3" in setup.py). This should be removed when we drop Python
|
||||
# 2 support (which probably won't happen any time soon).
|
||||
- xrange
|
||||
- long
|
||||
|
||||
pyflakes:
|
||||
disable:
|
||||
# undefined variables are already covered by pylint (and exclude
|
||||
# xrange & long)
|
||||
- F821
|
||||
|
||||
ignore-paths:
|
||||
- benchmark.py
|
||||
117
.travis.yml
117
.travis.yml
@@ -1,26 +1,107 @@
|
||||
# http://travis-ci.org/#!/MongoEngine/mongoengine
|
||||
# For full coverage, we'd have to test all supported Python, MongoDB, and
|
||||
# PyMongo combinations. However, that would result in an overly long build
|
||||
# with a very large number of jobs, hence we only test a subset of all the
|
||||
# combinations:
|
||||
# * MongoDB v2.4 & v3.0 are only tested against Python v2.7 & v3.5.
|
||||
# * MongoDB v2.4 is tested against PyMongo v2.7 & v3.x.
|
||||
# * MongoDB v3.0 is tested against PyMongo v3.x.
|
||||
# * MongoDB v2.6 is currently the "main" version tested against Python v2.7,
|
||||
# v3.5, PyPy & PyPy3, and PyMongo v2.7, v2.8 & v3.x.
|
||||
#
|
||||
# Reminder: Update README.rst if you change MongoDB versions we test.
|
||||
|
||||
language: python
|
||||
services: mongodb
|
||||
|
||||
python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
- "3.2"
|
||||
- "3.3"
|
||||
- 2.7
|
||||
- 3.5
|
||||
- 3.6
|
||||
- pypy
|
||||
|
||||
env:
|
||||
- PYMONGO=dev
|
||||
- PYMONGO=2.5
|
||||
- PYMONGO=2.4.2
|
||||
- MONGODB=2.6 PYMONGO=2.7
|
||||
- MONGODB=2.6 PYMONGO=2.8
|
||||
- MONGODB=2.6 PYMONGO=3.0
|
||||
|
||||
matrix:
|
||||
# Finish the build as soon as one job fails
|
||||
fast_finish: true
|
||||
|
||||
include:
|
||||
- python: 2.7
|
||||
env: MONGODB=2.4 PYMONGO=2.7
|
||||
- python: 2.7
|
||||
env: MONGODB=2.4 PYMONGO=3.0
|
||||
- python: 2.7
|
||||
env: MONGODB=3.0 PYMONGO=3.0
|
||||
- python: 3.5
|
||||
env: MONGODB=2.4 PYMONGO=2.7
|
||||
- python: 3.5
|
||||
env: MONGODB=2.4 PYMONGO=3.0
|
||||
- python: 3.5
|
||||
env: MONGODB=3.0 PYMONGO=3.0
|
||||
- python: 3.6
|
||||
env: MONGODB=2.4 PYMONGO=3.0
|
||||
- python: 3.6
|
||||
env: MONGODB=3.0 PYMONGO=3.0
|
||||
|
||||
before_install:
|
||||
- bash .install_mongodb_on_travis.sh
|
||||
- sleep 15 # https://docs.travis-ci.com/user/database-setup/#MongoDB-does-not-immediately-accept-connections
|
||||
- mongo --eval 'db.version();'
|
||||
|
||||
install:
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then cp /usr/lib/*/libz.so $VIRTUAL_ENV/lib/; fi
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == '2.'* ]]; then pip install pil --use-mirrors ; true; fi
|
||||
- if [[ $PYMONGO == 'dev' ]]; then pip install https://github.com/mongodb/mongo-python-driver/tarball/master; true; fi
|
||||
- if [[ $PYMONGO != 'dev' ]]; then pip install pymongo==$PYMONGO --use-mirrors; true; fi
|
||||
- python setup.py install
|
||||
- sudo apt-get install python-dev python3-dev libopenjpeg-dev zlib1g-dev libjpeg-turbo8-dev
|
||||
libtiff4-dev libjpeg8-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev
|
||||
python-tk
|
||||
- travis_retry pip install --upgrade pip
|
||||
- travis_retry pip install coveralls
|
||||
- travis_retry pip install flake8 flake8-import-order
|
||||
- travis_retry pip install tox>=1.9
|
||||
- travis_retry pip install "virtualenv<14.0.0" # virtualenv>=14.0.0 has dropped Python 3.2 support (and pypy3 is based on py32)
|
||||
- travis_retry tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO | tr -d . | sed -e 's/pypypy/pypy/') -- -e test
|
||||
|
||||
# Cache dependencies installed via pip
|
||||
cache: pip
|
||||
|
||||
# Run flake8 for py27
|
||||
before_script:
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then flake8 .; else echo "flake8 only runs on py27"; fi
|
||||
|
||||
script:
|
||||
- python setup.py test
|
||||
- tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO | tr -d . | sed -e 's/pypypy/pypy/') -- --with-coverage
|
||||
|
||||
# For now only submit coveralls for Python v2.7. Python v3.x currently shows
|
||||
# 0% coverage. That's caused by 'use_2to3', which builds the py3-compatible
|
||||
# code in a separate dir and runs tests on that.
|
||||
after_success:
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then coveralls --verbose; fi
|
||||
|
||||
notifications:
|
||||
irc: "irc.freenode.org#mongoengine"
|
||||
irc: irc.freenode.org#mongoengine
|
||||
|
||||
# Only run builds on the master branch and GitHub releases (tagged as vX.Y.Z)
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- "0.8"
|
||||
- master
|
||||
- /^v.*$/
|
||||
|
||||
# Whenever a new release is created via GitHub, publish it on PyPI.
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: the_drow
|
||||
password:
|
||||
secure: QMyatmWBnC6ZN3XLW2+fTBDU4LQcp1m/LjR2/0uamyeUzWKdlOoh/Wx5elOgLwt/8N9ppdPeG83ose1jOz69l5G0MUMjv8n/RIcMFSpCT59tGYqn3kh55b0cIZXFT9ar+5cxlif6a5rS72IHm5li7QQyxexJIII6Uxp0kpvUmek=
|
||||
|
||||
# create a source distribution and a pure python wheel for faster installs
|
||||
distributions: "sdist bdist_wheel"
|
||||
|
||||
# only deploy on tagged commits (aka GitHub releases) and only for the
|
||||
# parent repo's builds running Python 2.7 along with PyMongo v3.0 (we run
|
||||
# Travis against many different Python and PyMongo versions and we don't
|
||||
# want the deploy to occur multiple times).
|
||||
on:
|
||||
tags: true
|
||||
repo: MongoEngine/mongoengine
|
||||
condition: "$PYMONGO = 3.0"
|
||||
python: 2.7
|
||||
|
||||
107
AUTHORS
107
AUTHORS
@@ -12,12 +12,10 @@ Laine Herron https://github.com/LaineHerron
|
||||
|
||||
CONTRIBUTORS
|
||||
|
||||
Dervived from the git logs, inevitably incomplete but all of whom and others
|
||||
Derived from the git logs, inevitably incomplete but all of whom and others
|
||||
have submitted patches, reported bugs and generally helped make MongoEngine
|
||||
that much better:
|
||||
|
||||
* Harry Marr
|
||||
* Ross Lawley
|
||||
* blackbrrr
|
||||
* Florian Schlachter
|
||||
* Vincent Driessen
|
||||
@@ -25,7 +23,7 @@ that much better:
|
||||
* flosch
|
||||
* Deepak Thukral
|
||||
* Colin Howe
|
||||
* Wilson Júnior
|
||||
* Wilson Júnior (https://github.com/wpjunior)
|
||||
* Alistair Roche
|
||||
* Dan Crosta
|
||||
* Viktor Kerkez
|
||||
@@ -77,7 +75,7 @@ that much better:
|
||||
* Adam Parrish
|
||||
* jpfarias
|
||||
* jonrscott
|
||||
* Alice Zoë Bevan-McGregor
|
||||
* Alice Zoë Bevan-McGregor (https://github.com/amcgregor/)
|
||||
* Stephen Young
|
||||
* tkloc
|
||||
* aid
|
||||
@@ -121,7 +119,7 @@ that much better:
|
||||
* Anton Kolechkin
|
||||
* Sergey Nikitin
|
||||
* psychogenic
|
||||
* Stefan Wójcik
|
||||
* Stefan Wójcik (https://github.com/wojcikstefan)
|
||||
* dimonb
|
||||
* Garry Polley
|
||||
* James Slagle
|
||||
@@ -136,15 +134,14 @@ that much better:
|
||||
* Paul Swartz
|
||||
* Sundar Raman
|
||||
* Benoit Louy
|
||||
* lraucy
|
||||
* Loic Raucy (https://github.com/lraucy)
|
||||
* hellysmile
|
||||
* Jaepil Jeong
|
||||
* Daniil Sharou
|
||||
* Stefan Wójcik
|
||||
* Pete Campton
|
||||
* Martyn Smith
|
||||
* Marcelo Anton
|
||||
* Aleksey Porfirov
|
||||
* Aleksey Porfirov (https://github.com/lexqt)
|
||||
* Nicolas Trippar
|
||||
* Manuel Hermann
|
||||
* Gustavo Gawryszewski
|
||||
@@ -157,4 +154,94 @@ that much better:
|
||||
* Kenneth Falck
|
||||
* Lukasz Balcerzak
|
||||
* Nicolas Cortot
|
||||
|
||||
* Alex (https://github.com/kelsta)
|
||||
* Jin Zhang
|
||||
* Daniel Axtens
|
||||
* Leo-Naeka
|
||||
* Ryan Witt (https://github.com/ryanwitt)
|
||||
* Jiequan (https://github.com/Jiequan)
|
||||
* hensom (https://github.com/hensom)
|
||||
* zhy0216 (https://github.com/zhy0216)
|
||||
* istinspring (https://github.com/istinspring)
|
||||
* Massimo Santini (https://github.com/mapio)
|
||||
* Nigel McNie (https://github.com/nigelmcnie)
|
||||
* ygbourhis (https://github.com/ygbourhis)
|
||||
* Bob Dickinson (https://github.com/BobDickinson)
|
||||
* Michael Bartnett (https://github.com/michaelbartnett)
|
||||
* Alon Horev (https://github.com/alonho)
|
||||
* Kelvin Hammond (https://github.com/kelvinhammond)
|
||||
* Jatin Chopra (https://github.com/jatin)
|
||||
* Paul Uithol (https://github.com/PaulUithol)
|
||||
* Thom Knowles (https://github.com/fleat)
|
||||
* Paul (https://github.com/squamous)
|
||||
* Olivier Cortès (https://github.com/Karmak23)
|
||||
* crazyzubr (https://github.com/crazyzubr)
|
||||
* FrankSomething (https://github.com/FrankSomething)
|
||||
* Alexandr Morozov (https://github.com/LK4D4)
|
||||
* mishudark (https://github.com/mishudark)
|
||||
* Joe Friedl (https://github.com/grampajoe)
|
||||
* Daniel Ward (https://github.com/danielward)
|
||||
* Aniket Deshpande (https://github.com/anicake)
|
||||
* rfkrocktk (https://github.com/rfkrocktk)
|
||||
* Gustavo Andrés Angulo (https://github.com/woakas)
|
||||
* Dmytro Popovych (https://github.com/drudim)
|
||||
* Tom (https://github.com/tomprimozic)
|
||||
* j0hnsmith (https://github.com/j0hnsmith)
|
||||
* Damien Churchill (https://github.com/damoxc)
|
||||
* Jonathan Simon Prates (https://github.com/jonathansp)
|
||||
* Thiago Papageorgiou (https://github.com/tmpapageorgiou)
|
||||
* Omer Katz (https://github.com/thedrow)
|
||||
* Falcon Dai (https://github.com/falcondai)
|
||||
* Polyrabbit (https://github.com/polyrabbit)
|
||||
* Sagiv Malihi (https://github.com/sagivmalihi)
|
||||
* Dmitry Konishchev (https://github.com/KonishchevDmitry)
|
||||
* Martyn Smith (https://github.com/martynsmith)
|
||||
* Andrei Zbikowski (https://github.com/b1naryth1ef)
|
||||
* Ronald van Rij (https://github.com/ronaldvanrij)
|
||||
* François Schmidts (https://github.com/jaesivsm)
|
||||
* Eric Plumb (https://github.com/professorplumb)
|
||||
* Damien Churchill (https://github.com/damoxc)
|
||||
* Aleksandr Sorokoumov (https://github.com/Gerrrr)
|
||||
* Clay McClure (https://github.com/claymation)
|
||||
* Bruno Rocha (https://github.com/rochacbruno)
|
||||
* Norberto Leite (https://github.com/nleite)
|
||||
* Bob Cribbs (https://github.com/bocribbz)
|
||||
* Jay Shirley (https://github.com/jshirley)
|
||||
* David Bordeynik (https://github.com/DavidBord)
|
||||
* Axel Haustant (https://github.com/noirbizarre)
|
||||
* David Czarnecki (https://github.com/czarneckid)
|
||||
* Vyacheslav Murashkin (https://github.com/a4tunado)
|
||||
* André Ericson https://github.com/aericson)
|
||||
* Mikhail Moshnogorsky (https://github.com/mikhailmoshnogorsky)
|
||||
* Diego Berrocal (https://github.com/cestdiego)
|
||||
* Matthew Ellison (https://github.com/seglberg)
|
||||
* Jimmy Shen (https://github.com/jimmyshen)
|
||||
* J. Fernando Sánchez (https://github.com/balkian)
|
||||
* Michael Chase (https://github.com/rxsegrxup)
|
||||
* Eremeev Danil (https://github.com/elephanter)
|
||||
* Catstyle Lee (https://github.com/Catstyle)
|
||||
* Kiryl Yermakou (https://github.com/rma4ok)
|
||||
* Matthieu Rigal (https://github.com/MRigal)
|
||||
* Charanpal Dhanjal (https://github.com/charanpald)
|
||||
* Emmanuel Leblond (https://github.com/touilleMan)
|
||||
* Breeze.Kay (https://github.com/9nix00)
|
||||
* Vicki Donchenko (https://github.com/kivistein)
|
||||
* Emile Caron (https://github.com/emilecaron)
|
||||
* Amit Lichtenberg (https://github.com/amitlicht)
|
||||
* Gang Li (https://github.com/iici-gli)
|
||||
* Lars Butler (https://github.com/larsbutler)
|
||||
* George Macon (https://github.com/gmacon)
|
||||
* Ashley Whetter (https://github.com/AWhetter)
|
||||
* Paul-Armand Verhaegen (https://github.com/paularmand)
|
||||
* Steven Rossiter (https://github.com/BeardedSteve)
|
||||
* Luo Peng (https://github.com/RussellLuo)
|
||||
* Bryan Bennett (https://github.com/bbenne10)
|
||||
* Gilb's Gilb's (https://github.com/gilbsgilbs)
|
||||
* Joshua Nedrud (https://github.com/Neurostack)
|
||||
* Shu Shen (https://github.com/shushen)
|
||||
* xiaost7 (https://github.com/xiaost7)
|
||||
* Victor Varvaryuk
|
||||
* Stanislav Kaledin (https://github.com/sallyruthstruik)
|
||||
* Dmitry Yantsen (https://github.com/mrTable)
|
||||
* Renjianxin (https://github.com/Davidrjx)
|
||||
* Erdenezul Batmunkh (https://github.com/erdenezul)
|
||||
@@ -14,13 +14,13 @@ Before starting to write code, look for existing `tickets
|
||||
<https://github.com/MongoEngine/mongoengine/issues?state=open>`_ or `create one
|
||||
<https://github.com/MongoEngine/mongoengine/issues>`_ for your specific
|
||||
issue or feature request. That way you avoid working on something
|
||||
that might not be of interest or that has already been addressed. If in doubt
|
||||
that might not be of interest or that has already been addressed. If in doubt
|
||||
post to the `user group <http://groups.google.com/group/mongoengine-users>`
|
||||
|
||||
Supported Interpreters
|
||||
----------------------
|
||||
|
||||
PyMongo supports CPython 2.5 and newer. Language
|
||||
MongoEngine supports CPython 2.7 and newer. Language
|
||||
features not supported by all interpreters can not be used.
|
||||
Please also ensure that your code is properly converted by
|
||||
`2to3 <http://docs.python.org/library/2to3.html>`_ for Python 3 support.
|
||||
@@ -29,24 +29,40 @@ Style Guide
|
||||
-----------
|
||||
|
||||
MongoEngine aims to follow `PEP8 <http://www.python.org/dev/peps/pep-0008/>`_
|
||||
including 4 space indents and 79 character line limits.
|
||||
including 4 space indents. When possible we try to stick to 79 character line
|
||||
limits. However, screens got bigger and an ORM has a strong focus on
|
||||
readability and if it can help, we accept 119 as maximum line length, in a
|
||||
similar way as `django does
|
||||
<https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/#python-style>`_
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
All tests are run on `Travis <http://travis-ci.org/MongoEngine/mongoengine>`_
|
||||
and any pull requests are automatically tested by Travis. Any pull requests
|
||||
without tests will take longer to be integrated and might be refused.
|
||||
and any pull requests are automatically tested. Any pull requests without
|
||||
tests will take longer to be integrated and might be refused.
|
||||
|
||||
You may also submit a simple failing test as a pull request if you don't know
|
||||
how to fix it, it will be easier for other people to work on it and it may get
|
||||
fixed faster.
|
||||
|
||||
General Guidelines
|
||||
------------------
|
||||
|
||||
- Avoid backward breaking changes if at all possible.
|
||||
- If you *have* to introduce a breaking change, make it very clear in your
|
||||
pull request's description. Also, describe how users of this package
|
||||
should adapt to the breaking change in docs/upgrade.rst.
|
||||
- Write inline documentation for new classes and methods.
|
||||
- Write tests and make sure they pass (make sure you have a mongod
|
||||
running on the default port, then execute ``python setup.py test``
|
||||
running on the default port, then execute ``python setup.py nosetests``
|
||||
from the cmd line to run the test suite).
|
||||
- Add yourself to AUTHORS.rst :)
|
||||
- Ensure tests pass on all supported Python, PyMongo, and MongoDB versions.
|
||||
You can test various Python and PyMongo versions locally by executing
|
||||
``tox``. For different MongoDB versions, you can rely on our automated
|
||||
Travis tests.
|
||||
- Add enhancements or problematic bug fixes to docs/changelog.rst.
|
||||
- Add yourself to AUTHORS :)
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
@@ -59,3 +75,8 @@ just make your changes to the inline documentation of the appropriate
|
||||
branch and submit a `pull request <https://help.github.com/articles/using-pull-requests>`_.
|
||||
You might also use the github `Edit <https://github.com/blog/844-forking-with-the-edit-button>`_
|
||||
button.
|
||||
|
||||
If you want to test your documentation changes locally, you need to install
|
||||
the ``sphinx`` and ``sphinx_rtd_theme`` packages. Once these are installed,
|
||||
go to the ``docs`` directory, run ``make html`` and inspect the updated docs
|
||||
by running ``open _build/html/index.html``.
|
||||
|
||||
99
README.rst
99
README.rst
@@ -4,39 +4,72 @@ MongoEngine
|
||||
:Info: MongoEngine is an ORM-like layer on top of PyMongo.
|
||||
:Repository: https://github.com/MongoEngine/mongoengine
|
||||
:Author: Harry Marr (http://github.com/hmarr)
|
||||
:Maintainer: Ross Lawley (http://github.com/rozza)
|
||||
:Maintainer: Stefan Wójcik (http://github.com/wojcikstefan)
|
||||
|
||||
.. image:: https://secure.travis-ci.org/MongoEngine/mongoengine.png?branch=master
|
||||
:target: http://travis-ci.org/MongoEngine/mongoengine
|
||||
.. image:: https://travis-ci.org/MongoEngine/mongoengine.svg?branch=master
|
||||
:target: https://travis-ci.org/MongoEngine/mongoengine
|
||||
|
||||
.. image:: https://coveralls.io/repos/github/MongoEngine/mongoengine/badge.svg?branch=master
|
||||
:target: https://coveralls.io/github/MongoEngine/mongoengine?branch=master
|
||||
|
||||
.. image:: https://landscape.io/github/MongoEngine/mongoengine/master/landscape.svg?style=flat
|
||||
:target: https://landscape.io/github/MongoEngine/mongoengine/master
|
||||
:alt: Code Health
|
||||
|
||||
About
|
||||
=====
|
||||
MongoEngine is a Python Object-Document Mapper for working with MongoDB.
|
||||
Documentation available at http://mongoengine-odm.rtfd.org - there is currently
|
||||
a `tutorial <http://readthedocs.org/docs/mongoengine-odm/en/latest/tutorial.html>`_, a `user guide
|
||||
<https://mongoengine-odm.readthedocs.org/en/latest/guide/index.html>`_ and an `API reference
|
||||
<http://readthedocs.org/docs/mongoengine-odm/en/latest/apireference.html>`_.
|
||||
Documentation is available at https://mongoengine-odm.readthedocs.io - there
|
||||
is currently a `tutorial <https://mongoengine-odm.readthedocs.io/tutorial.html>`_,
|
||||
a `user guide <https://mongoengine-odm.readthedocs.io/guide/index.html>`_, and
|
||||
an `API reference <https://mongoengine-odm.readthedocs.io/apireference.html>`_.
|
||||
|
||||
Supported MongoDB Versions
|
||||
==========================
|
||||
MongoEngine is currently tested against MongoDB v2.4, v2.6, and v3.0. Future
|
||||
versions should be supported as well, but aren't actively tested at the moment.
|
||||
Make sure to open an issue or submit a pull request if you experience any
|
||||
problems with MongoDB v3.2+.
|
||||
|
||||
Installation
|
||||
============
|
||||
If you have `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_
|
||||
you can use ``easy_install -U mongoengine``. Otherwise, you can download the
|
||||
We recommend the use of `virtualenv <https://virtualenv.pypa.io/>`_ and of
|
||||
`pip <https://pip.pypa.io/>`_. You can then use ``pip install -U mongoengine``.
|
||||
You may also have `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_
|
||||
and thus you can use ``easy_install -U mongoengine``. Otherwise, you can download the
|
||||
source from `GitHub <http://github.com/MongoEngine/mongoengine>`_ and run ``python
|
||||
setup.py install``.
|
||||
|
||||
Dependencies
|
||||
============
|
||||
- pymongo 2.1.1+
|
||||
- sphinx (optional - for documentation generation)
|
||||
All of the dependencies can easily be installed via `pip <https://pip.pypa.io/>`_.
|
||||
At the very least, you'll need these two packages to use MongoEngine:
|
||||
|
||||
- pymongo>=2.7.1
|
||||
- six>=1.10.0
|
||||
|
||||
If you utilize a ``DateTimeField``, you might also use a more flexible date parser:
|
||||
|
||||
- dateutil>=2.1.0
|
||||
|
||||
If you need to use an ``ImageField`` or ``ImageGridFsProxy``:
|
||||
|
||||
- Pillow>=2.0.0
|
||||
|
||||
Examples
|
||||
========
|
||||
Some simple examples of what MongoEngine code looks like::
|
||||
Some simple examples of what MongoEngine code looks like:
|
||||
|
||||
.. code :: python
|
||||
|
||||
from mongoengine import *
|
||||
connect('mydb')
|
||||
|
||||
class BlogPost(Document):
|
||||
title = StringField(required=True, max_length=200)
|
||||
posted = DateTimeField(default=datetime.datetime.now)
|
||||
posted = DateTimeField(default=datetime.datetime.utcnow)
|
||||
tags = ListField(StringField(max_length=50))
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
class TextPost(BlogPost):
|
||||
content = StringField(required=True)
|
||||
@@ -64,23 +97,46 @@ Some simple examples of what MongoEngine code looks like::
|
||||
... print
|
||||
...
|
||||
|
||||
>>> len(BlogPost.objects)
|
||||
# Count all blog posts and its subtypes
|
||||
>>> BlogPost.objects.count()
|
||||
2
|
||||
>>> len(HtmlPost.objects)
|
||||
>>> TextPost.objects.count()
|
||||
1
|
||||
>>> len(LinkPost.objects)
|
||||
>>> LinkPost.objects.count()
|
||||
1
|
||||
|
||||
# Find tagged posts
|
||||
>>> len(BlogPost.objects(tags='mongoengine'))
|
||||
# Count tagged posts
|
||||
>>> BlogPost.objects(tags='mongoengine').count()
|
||||
2
|
||||
>>> len(BlogPost.objects(tags='mongodb'))
|
||||
>>> BlogPost.objects(tags='mongodb').count()
|
||||
1
|
||||
|
||||
Tests
|
||||
=====
|
||||
To run the test suite, ensure you are running a local instance of MongoDB on
|
||||
the standard port, and run: ``python setup.py test``.
|
||||
the standard port and have ``nose`` installed. Then, run ``python setup.py nosetests``.
|
||||
|
||||
To run the test suite on every supported Python and PyMongo version, you can
|
||||
use ``tox``. You'll need to make sure you have each supported Python version
|
||||
installed in your environment and then:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
# Install tox
|
||||
$ pip install tox
|
||||
# Run the test suites
|
||||
$ tox
|
||||
|
||||
If you wish to run a subset of tests, use the nosetests convention:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
# Run all the tests in a particular test file
|
||||
$ python setup.py nosetests --tests tests/fields/fields.py
|
||||
# Run only particular test class in that file
|
||||
$ python setup.py nosetests --tests tests/fields/fields.py:FieldTest
|
||||
# Use the -s option if you want to print some debug statements or use pdb
|
||||
$ python setup.py nosetests --tests tests/fields/fields.py:FieldTest -s
|
||||
|
||||
Community
|
||||
=========
|
||||
@@ -88,8 +144,7 @@ Community
|
||||
<http://groups.google.com/group/mongoengine-users>`_
|
||||
- `MongoEngine Developers mailing list
|
||||
<http://groups.google.com/group/mongoengine-dev>`_
|
||||
- `#mongoengine IRC channel <http://webchat.freenode.net/?channels=mongoengine>`_
|
||||
|
||||
Contributing
|
||||
============
|
||||
We welcome contributions! see the`Contribution guidelines <https://github.com/MongoEngine/mongoengine/blob/master/CONTRIBUTING.rst>`_
|
||||
We welcome contributions! See the `Contribution guidelines <https://github.com/MongoEngine/mongoengine/blob/master/CONTRIBUTING.rst>`_
|
||||
|
||||
244
benchmark.py
244
benchmark.py
@@ -1,137 +1,111 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Simple benchmark comparing PyMongo and MongoEngine.
|
||||
|
||||
Sample run on a mid 2015 MacBook Pro (commit b282511):
|
||||
|
||||
Benchmarking...
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - Pymongo
|
||||
2.58979988098
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - Pymongo write_concern={"w": 0}
|
||||
1.26657605171
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine
|
||||
8.4351580143
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries without continual assign - MongoEngine
|
||||
7.20191693306
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine - write_concern={"w": 0}, cascade = True
|
||||
6.31104588509
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False, cascade=True
|
||||
6.07083487511
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False
|
||||
5.97704291344
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine, force_insert=True, write_concern={"w": 0}, validate=False
|
||||
5.9111430645
|
||||
"""
|
||||
|
||||
import timeit
|
||||
|
||||
|
||||
def cprofile_main():
|
||||
from pymongo import Connection
|
||||
connection = Connection()
|
||||
connection.drop_database('timeit_test')
|
||||
connection.disconnect()
|
||||
|
||||
from mongoengine import Document, DictField, connect
|
||||
connect("timeit_test")
|
||||
|
||||
class Noddy(Document):
|
||||
fields = DictField()
|
||||
|
||||
for i in xrange(1):
|
||||
noddy = Noddy()
|
||||
for j in range(20):
|
||||
noddy.fields["key" + str(j)] = "value " + str(j)
|
||||
noddy.save()
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
0.4 Performance Figures ...
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - Pymongo
|
||||
3.86744189262
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine
|
||||
6.23374891281
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False
|
||||
5.33027005196
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False, cascade=False
|
||||
pass - No Cascade
|
||||
|
||||
0.5.X
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - Pymongo
|
||||
3.89597702026
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine
|
||||
21.7735359669
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False
|
||||
19.8670389652
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False, cascade=False
|
||||
pass - No Cascade
|
||||
|
||||
0.6.X
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - Pymongo
|
||||
3.81559205055
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine
|
||||
10.0446798801
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False
|
||||
9.51354718208
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False, cascade=False
|
||||
9.02567505836
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine, force=True
|
||||
8.44933390617
|
||||
|
||||
0.7.X
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - Pymongo
|
||||
3.78801012039
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine
|
||||
9.73050498962
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False
|
||||
8.33456707001
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False, cascade=False
|
||||
8.37778115273
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine, force=True
|
||||
8.36906409264
|
||||
"""
|
||||
print("Benchmarking...")
|
||||
|
||||
setup = """
|
||||
from pymongo import Connection
|
||||
connection = Connection()
|
||||
from pymongo import MongoClient
|
||||
connection = MongoClient()
|
||||
connection.drop_database('timeit_test')
|
||||
"""
|
||||
|
||||
stmt = """
|
||||
from pymongo import Connection
|
||||
connection = Connection()
|
||||
from pymongo import MongoClient
|
||||
connection = MongoClient()
|
||||
|
||||
db = connection.timeit_test
|
||||
noddy = db.noddy
|
||||
|
||||
for i in xrange(10000):
|
||||
for i in range(10000):
|
||||
example = {'fields': {}}
|
||||
for j in range(20):
|
||||
example['fields']["key"+str(j)] = "value "+str(j)
|
||||
example['fields']['key' + str(j)] = 'value ' + str(j)
|
||||
|
||||
noddy.insert(example)
|
||||
noddy.save(example)
|
||||
|
||||
myNoddys = noddy.find()
|
||||
[n for n in myNoddys] # iterate
|
||||
"""
|
||||
|
||||
print "-" * 100
|
||||
print """Creating 10000 dictionaries - Pymongo"""
|
||||
print("-" * 100)
|
||||
print("""Creating 10000 dictionaries - Pymongo""")
|
||||
t = timeit.Timer(stmt=stmt, setup=setup)
|
||||
print t.timeit(1)
|
||||
print(t.timeit(1))
|
||||
|
||||
stmt = """
|
||||
from pymongo import MongoClient
|
||||
from pymongo.write_concern import WriteConcern
|
||||
connection = MongoClient()
|
||||
|
||||
db = connection.get_database('timeit_test', write_concern=WriteConcern(w=0))
|
||||
noddy = db.noddy
|
||||
|
||||
for i in range(10000):
|
||||
example = {'fields': {}}
|
||||
for j in range(20):
|
||||
example['fields']["key"+str(j)] = "value "+str(j)
|
||||
|
||||
noddy.save(example)
|
||||
|
||||
myNoddys = noddy.find()
|
||||
[n for n in myNoddys] # iterate
|
||||
"""
|
||||
|
||||
print("-" * 100)
|
||||
print("""Creating 10000 dictionaries - Pymongo write_concern={"w": 0}""")
|
||||
t = timeit.Timer(stmt=stmt, setup=setup)
|
||||
print(t.timeit(1))
|
||||
|
||||
setup = """
|
||||
from pymongo import Connection
|
||||
connection = Connection()
|
||||
from pymongo import MongoClient
|
||||
connection = MongoClient()
|
||||
connection.drop_database('timeit_test')
|
||||
connection.disconnect()
|
||||
connection.close()
|
||||
|
||||
from mongoengine import Document, DictField, connect
|
||||
connect("timeit_test")
|
||||
connect('timeit_test')
|
||||
|
||||
class Noddy(Document):
|
||||
fields = DictField()
|
||||
"""
|
||||
|
||||
stmt = """
|
||||
for i in xrange(10000):
|
||||
for i in range(10000):
|
||||
noddy = Noddy()
|
||||
for j in range(20):
|
||||
noddy.fields["key"+str(j)] = "value "+str(j)
|
||||
@@ -141,59 +115,93 @@ myNoddys = Noddy.objects()
|
||||
[n for n in myNoddys] # iterate
|
||||
"""
|
||||
|
||||
print "-" * 100
|
||||
print """Creating 10000 dictionaries - MongoEngine"""
|
||||
print("-" * 100)
|
||||
print("""Creating 10000 dictionaries - MongoEngine""")
|
||||
t = timeit.Timer(stmt=stmt, setup=setup)
|
||||
print t.timeit(1)
|
||||
print(t.timeit(1))
|
||||
|
||||
stmt = """
|
||||
for i in xrange(10000):
|
||||
for i in range(10000):
|
||||
noddy = Noddy()
|
||||
fields = {}
|
||||
for j in range(20):
|
||||
noddy.fields["key"+str(j)] = "value "+str(j)
|
||||
noddy.save(safe=False, validate=False)
|
||||
fields["key"+str(j)] = "value "+str(j)
|
||||
noddy.fields = fields
|
||||
noddy.save()
|
||||
|
||||
myNoddys = Noddy.objects()
|
||||
[n for n in myNoddys] # iterate
|
||||
"""
|
||||
|
||||
print "-" * 100
|
||||
print """Creating 10000 dictionaries - MongoEngine, safe=False, validate=False"""
|
||||
print("-" * 100)
|
||||
print("""Creating 10000 dictionaries without continual assign - MongoEngine""")
|
||||
t = timeit.Timer(stmt=stmt, setup=setup)
|
||||
print t.timeit(1)
|
||||
|
||||
print(t.timeit(1))
|
||||
|
||||
stmt = """
|
||||
for i in xrange(10000):
|
||||
for i in range(10000):
|
||||
noddy = Noddy()
|
||||
for j in range(20):
|
||||
noddy.fields["key"+str(j)] = "value "+str(j)
|
||||
noddy.save(safe=False, validate=False, cascade=False)
|
||||
noddy.save(write_concern={"w": 0}, cascade=True)
|
||||
|
||||
myNoddys = Noddy.objects()
|
||||
[n for n in myNoddys] # iterate
|
||||
"""
|
||||
|
||||
print "-" * 100
|
||||
print """Creating 10000 dictionaries - MongoEngine, safe=False, validate=False, cascade=False"""
|
||||
print("-" * 100)
|
||||
print("""Creating 10000 dictionaries - MongoEngine - write_concern={"w": 0}, cascade = True""")
|
||||
t = timeit.Timer(stmt=stmt, setup=setup)
|
||||
print t.timeit(1)
|
||||
print(t.timeit(1))
|
||||
|
||||
stmt = """
|
||||
for i in xrange(10000):
|
||||
for i in range(10000):
|
||||
noddy = Noddy()
|
||||
for j in range(20):
|
||||
noddy.fields["key"+str(j)] = "value "+str(j)
|
||||
noddy.save(force_insert=True, safe=False, validate=False, cascade=False)
|
||||
noddy.save(write_concern={"w": 0}, validate=False, cascade=True)
|
||||
|
||||
myNoddys = Noddy.objects()
|
||||
[n for n in myNoddys] # iterate
|
||||
"""
|
||||
|
||||
print "-" * 100
|
||||
print """Creating 10000 dictionaries - MongoEngine, force=True"""
|
||||
print("-" * 100)
|
||||
print("""Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False, cascade=True""")
|
||||
t = timeit.Timer(stmt=stmt, setup=setup)
|
||||
print t.timeit(1)
|
||||
print(t.timeit(1))
|
||||
|
||||
stmt = """
|
||||
for i in range(10000):
|
||||
noddy = Noddy()
|
||||
for j in range(20):
|
||||
noddy.fields["key"+str(j)] = "value "+str(j)
|
||||
noddy.save(validate=False, write_concern={"w": 0})
|
||||
|
||||
myNoddys = Noddy.objects()
|
||||
[n for n in myNoddys] # iterate
|
||||
"""
|
||||
|
||||
print("-" * 100)
|
||||
print("""Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False""")
|
||||
t = timeit.Timer(stmt=stmt, setup=setup)
|
||||
print(t.timeit(1))
|
||||
|
||||
stmt = """
|
||||
for i in range(10000):
|
||||
noddy = Noddy()
|
||||
for j in range(20):
|
||||
noddy.fields["key"+str(j)] = "value "+str(j)
|
||||
noddy.save(force_insert=True, write_concern={"w": 0}, validate=False)
|
||||
|
||||
myNoddys = Noddy.objects()
|
||||
[n for n in myNoddys] # iterate
|
||||
"""
|
||||
|
||||
print("-" * 100)
|
||||
print("""Creating 10000 dictionaries - MongoEngine, force_insert=True, write_concern={"w": 0}, validate=False""")
|
||||
t = timeit.Timer(stmt=stmt, setup=setup)
|
||||
print(t.timeit(1))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
229
docs/_themes/nature/static/nature.css_t
vendored
229
docs/_themes/nature/static/nature.css_t
vendored
@@ -1,229 +0,0 @@
|
||||
/**
|
||||
* Sphinx stylesheet -- default theme
|
||||
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
*/
|
||||
|
||||
@import url("basic.css");
|
||||
|
||||
/* -- page layout ----------------------------------------------------------- */
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 100%;
|
||||
background-color: #111;
|
||||
color: #555;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.documentwrapper {
|
||||
float: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.bodywrapper {
|
||||
margin: 0 0 0 230px;
|
||||
}
|
||||
|
||||
hr{
|
||||
border: 1px solid #B1B4B6;
|
||||
}
|
||||
|
||||
div.document {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
div.body {
|
||||
background-color: #ffffff;
|
||||
color: #3E4349;
|
||||
padding: 0 30px 30px 30px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
div.footer {
|
||||
color: #555;
|
||||
width: 100%;
|
||||
padding: 13px 0;
|
||||
text-align: center;
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
div.footer a {
|
||||
color: #444;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
div.related {
|
||||
background-color: #6BA81E;
|
||||
line-height: 32px;
|
||||
color: #fff;
|
||||
text-shadow: 0px 1px 0 #444;
|
||||
font-size: 0.80em;
|
||||
}
|
||||
|
||||
div.related a {
|
||||
color: #E2F3CC;
|
||||
}
|
||||
|
||||
div.sphinxsidebar {
|
||||
font-size: 0.75em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper{
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3,
|
||||
div.sphinxsidebar h4 {
|
||||
font-family: Arial, sans-serif;
|
||||
color: #222;
|
||||
font-size: 1.2em;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
padding: 5px 10px;
|
||||
background-color: #ddd;
|
||||
text-shadow: 1px 1px 0 white
|
||||
}
|
||||
|
||||
div.sphinxsidebar h4{
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3 a {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
|
||||
div.sphinxsidebar p {
|
||||
color: #888;
|
||||
padding: 5px 20px;
|
||||
}
|
||||
|
||||
div.sphinxsidebar p.topless {
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul {
|
||||
margin: 10px 20px;
|
||||
padding: 0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
div.sphinxsidebar a {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
div.sphinxsidebar input {
|
||||
border: 1px solid #ccc;
|
||||
font-family: sans-serif;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
div.sphinxsidebar input[type=text]{
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
/* -- body styles ----------------------------------------------------------- */
|
||||
|
||||
a {
|
||||
color: #005B81;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #E32E00;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
div.body h1,
|
||||
div.body h2,
|
||||
div.body h3,
|
||||
div.body h4,
|
||||
div.body h5,
|
||||
div.body h6 {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #BED4EB;
|
||||
font-weight: normal;
|
||||
color: #212224;
|
||||
margin: 30px 0px 10px 0px;
|
||||
padding: 5px 0 5px 10px;
|
||||
text-shadow: 0px 1px 0 white
|
||||
}
|
||||
|
||||
div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; }
|
||||
div.body h2 { font-size: 150%; background-color: #C8D5E3; }
|
||||
div.body h3 { font-size: 120%; background-color: #D8DEE3; }
|
||||
div.body h4 { font-size: 110%; background-color: #D8DEE3; }
|
||||
div.body h5 { font-size: 100%; background-color: #D8DEE3; }
|
||||
div.body h6 { font-size: 100%; background-color: #D8DEE3; }
|
||||
|
||||
a.headerlink {
|
||||
color: #c60f0f;
|
||||
font-size: 0.8em;
|
||||
padding: 0 4px 0 4px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.headerlink:hover {
|
||||
background-color: #c60f0f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
div.body p, div.body dd, div.body li {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
div.admonition p.admonition-title + p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
div.highlight{
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
div.note {
|
||||
background-color: #eee;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
div.seealso {
|
||||
background-color: #ffc;
|
||||
border: 1px solid #ff6;
|
||||
}
|
||||
|
||||
div.topic {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
div.warning {
|
||||
background-color: #ffe4e4;
|
||||
border: 1px solid #f66;
|
||||
}
|
||||
|
||||
p.admonition-title {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
p.admonition-title:after {
|
||||
content: ":";
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 10px;
|
||||
background-color: White;
|
||||
color: #222;
|
||||
line-height: 1.2em;
|
||||
border: 1px solid #C6C9CB;
|
||||
font-size: 1.2em;
|
||||
margin: 1.5em 0 1.5em 0;
|
||||
-webkit-box-shadow: 1px 1px 1px #d8d8d8;
|
||||
-moz-box-shadow: 1px 1px 1px #d8d8d8;
|
||||
}
|
||||
|
||||
tt {
|
||||
background-color: #ecf0f3;
|
||||
color: #222;
|
||||
padding: 1px 2px;
|
||||
font-size: 1.2em;
|
||||
font-family: monospace;
|
||||
}
|
||||
54
docs/_themes/nature/static/pygments.css
vendored
54
docs/_themes/nature/static/pygments.css
vendored
@@ -1,54 +0,0 @@
|
||||
.c { color: #999988; font-style: italic } /* Comment */
|
||||
.k { font-weight: bold } /* Keyword */
|
||||
.o { font-weight: bold } /* Operator */
|
||||
.cm { color: #999988; font-style: italic } /* Comment.Multiline */
|
||||
.cp { color: #999999; font-weight: bold } /* Comment.preproc */
|
||||
.c1 { color: #999988; font-style: italic } /* Comment.Single */
|
||||
.gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
|
||||
.ge { font-style: italic } /* Generic.Emph */
|
||||
.gr { color: #aa0000 } /* Generic.Error */
|
||||
.gh { color: #999999 } /* Generic.Heading */
|
||||
.gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
|
||||
.go { color: #111 } /* Generic.Output */
|
||||
.gp { color: #555555 } /* Generic.Prompt */
|
||||
.gs { font-weight: bold } /* Generic.Strong */
|
||||
.gu { color: #aaaaaa } /* Generic.Subheading */
|
||||
.gt { color: #aa0000 } /* Generic.Traceback */
|
||||
.kc { font-weight: bold } /* Keyword.Constant */
|
||||
.kd { font-weight: bold } /* Keyword.Declaration */
|
||||
.kp { font-weight: bold } /* Keyword.Pseudo */
|
||||
.kr { font-weight: bold } /* Keyword.Reserved */
|
||||
.kt { color: #445588; font-weight: bold } /* Keyword.Type */
|
||||
.m { color: #009999 } /* Literal.Number */
|
||||
.s { color: #bb8844 } /* Literal.String */
|
||||
.na { color: #008080 } /* Name.Attribute */
|
||||
.nb { color: #999999 } /* Name.Builtin */
|
||||
.nc { color: #445588; font-weight: bold } /* Name.Class */
|
||||
.no { color: #ff99ff } /* Name.Constant */
|
||||
.ni { color: #800080 } /* Name.Entity */
|
||||
.ne { color: #990000; font-weight: bold } /* Name.Exception */
|
||||
.nf { color: #990000; font-weight: bold } /* Name.Function */
|
||||
.nn { color: #555555 } /* Name.Namespace */
|
||||
.nt { color: #000080 } /* Name.Tag */
|
||||
.nv { color: purple } /* Name.Variable */
|
||||
.ow { font-weight: bold } /* Operator.Word */
|
||||
.mf { color: #009999 } /* Literal.Number.Float */
|
||||
.mh { color: #009999 } /* Literal.Number.Hex */
|
||||
.mi { color: #009999 } /* Literal.Number.Integer */
|
||||
.mo { color: #009999 } /* Literal.Number.Oct */
|
||||
.sb { color: #bb8844 } /* Literal.String.Backtick */
|
||||
.sc { color: #bb8844 } /* Literal.String.Char */
|
||||
.sd { color: #bb8844 } /* Literal.String.Doc */
|
||||
.s2 { color: #bb8844 } /* Literal.String.Double */
|
||||
.se { color: #bb8844 } /* Literal.String.Escape */
|
||||
.sh { color: #bb8844 } /* Literal.String.Heredoc */
|
||||
.si { color: #bb8844 } /* Literal.String.Interpol */
|
||||
.sx { color: #bb8844 } /* Literal.String.Other */
|
||||
.sr { color: #808000 } /* Literal.String.Regex */
|
||||
.s1 { color: #bb8844 } /* Literal.String.Single */
|
||||
.ss { color: #bb8844 } /* Literal.String.Symbol */
|
||||
.bp { color: #999999 } /* Name.Builtin.Pseudo */
|
||||
.vc { color: #ff99ff } /* Name.Variable.Class */
|
||||
.vg { color: #ff99ff } /* Name.Variable.Global */
|
||||
.vi { color: #ff99ff } /* Name.Variable.Instance */
|
||||
.il { color: #009999 } /* Literal.Number.Integer.Long */
|
||||
4
docs/_themes/nature/theme.conf
vendored
4
docs/_themes/nature/theme.conf
vendored
@@ -1,4 +0,0 @@
|
||||
[theme]
|
||||
inherit = basic
|
||||
stylesheet = nature.css
|
||||
pygments_style = tango
|
||||
@@ -34,48 +34,95 @@ Documents
|
||||
.. autoclass:: mongoengine.ValidationError
|
||||
:members:
|
||||
|
||||
.. autoclass:: mongoengine.FieldDoesNotExist
|
||||
|
||||
|
||||
Context Managers
|
||||
================
|
||||
|
||||
.. autoclass:: mongoengine.context_managers.switch_db
|
||||
.. autoclass:: mongoengine.context_managers.switch_collection
|
||||
.. autoclass:: mongoengine.context_managers.no_dereference
|
||||
.. autoclass:: mongoengine.context_managers.query_counter
|
||||
|
||||
Querying
|
||||
========
|
||||
|
||||
.. autoclass:: mongoengine.queryset.QuerySet
|
||||
:members:
|
||||
.. automodule:: mongoengine.queryset
|
||||
:synopsis: Queryset level operations
|
||||
|
||||
.. automethod:: mongoengine.queryset.QuerySet.__call__
|
||||
.. autoclass:: mongoengine.queryset.QuerySet
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
.. autofunction:: mongoengine.queryset.queryset_manager
|
||||
.. automethod:: QuerySet.__call__
|
||||
|
||||
.. autoclass:: mongoengine.queryset.QuerySetNoCache
|
||||
:members:
|
||||
|
||||
.. automethod:: mongoengine.queryset.QuerySetNoCache.__call__
|
||||
|
||||
.. autofunction:: mongoengine.queryset.queryset_manager
|
||||
|
||||
Fields
|
||||
======
|
||||
|
||||
.. autoclass:: mongoengine.BinaryField
|
||||
.. autoclass:: mongoengine.BooleanField
|
||||
.. autoclass:: mongoengine.ComplexDateTimeField
|
||||
.. autoclass:: mongoengine.DateTimeField
|
||||
.. autoclass:: mongoengine.DecimalField
|
||||
.. autoclass:: mongoengine.DictField
|
||||
.. autoclass:: mongoengine.DynamicField
|
||||
.. autoclass:: mongoengine.EmailField
|
||||
.. autoclass:: mongoengine.EmbeddedDocumentField
|
||||
.. autoclass:: mongoengine.FileField
|
||||
.. autoclass:: mongoengine.FloatField
|
||||
.. autoclass:: mongoengine.GenericEmbeddedDocumentField
|
||||
.. autoclass:: mongoengine.GenericReferenceField
|
||||
.. autoclass:: mongoengine.GeoPointField
|
||||
.. autoclass:: mongoengine.ImageField
|
||||
.. autoclass:: mongoengine.IntField
|
||||
.. autoclass:: mongoengine.ListField
|
||||
.. autoclass:: mongoengine.MapField
|
||||
.. autoclass:: mongoengine.ObjectIdField
|
||||
.. autoclass:: mongoengine.ReferenceField
|
||||
.. autoclass:: mongoengine.SequenceField
|
||||
.. autoclass:: mongoengine.SortedListField
|
||||
.. autoclass:: mongoengine.StringField
|
||||
.. autoclass:: mongoengine.URLField
|
||||
.. autoclass:: mongoengine.UUIDField
|
||||
.. autoclass:: mongoengine.base.fields.BaseField
|
||||
.. autoclass:: mongoengine.fields.StringField
|
||||
.. autoclass:: mongoengine.fields.URLField
|
||||
.. autoclass:: mongoengine.fields.EmailField
|
||||
.. autoclass:: mongoengine.fields.IntField
|
||||
.. autoclass:: mongoengine.fields.LongField
|
||||
.. autoclass:: mongoengine.fields.FloatField
|
||||
.. autoclass:: mongoengine.fields.DecimalField
|
||||
.. autoclass:: mongoengine.fields.BooleanField
|
||||
.. autoclass:: mongoengine.fields.DateTimeField
|
||||
.. autoclass:: mongoengine.fields.ComplexDateTimeField
|
||||
.. autoclass:: mongoengine.fields.EmbeddedDocumentField
|
||||
.. autoclass:: mongoengine.fields.GenericEmbeddedDocumentField
|
||||
.. autoclass:: mongoengine.fields.DynamicField
|
||||
.. autoclass:: mongoengine.fields.ListField
|
||||
.. autoclass:: mongoengine.fields.EmbeddedDocumentListField
|
||||
.. autoclass:: mongoengine.fields.SortedListField
|
||||
.. autoclass:: mongoengine.fields.DictField
|
||||
.. autoclass:: mongoengine.fields.MapField
|
||||
.. autoclass:: mongoengine.fields.ReferenceField
|
||||
.. autoclass:: mongoengine.fields.GenericReferenceField
|
||||
.. autoclass:: mongoengine.fields.CachedReferenceField
|
||||
.. autoclass:: mongoengine.fields.BinaryField
|
||||
.. autoclass:: mongoengine.fields.FileField
|
||||
.. autoclass:: mongoengine.fields.ImageField
|
||||
.. autoclass:: mongoengine.fields.SequenceField
|
||||
.. autoclass:: mongoengine.fields.ObjectIdField
|
||||
.. autoclass:: mongoengine.fields.UUIDField
|
||||
.. autoclass:: mongoengine.fields.GeoPointField
|
||||
.. autoclass:: mongoengine.fields.PointField
|
||||
.. autoclass:: mongoengine.fields.LineStringField
|
||||
.. autoclass:: mongoengine.fields.PolygonField
|
||||
.. autoclass:: mongoengine.fields.MultiPointField
|
||||
.. autoclass:: mongoengine.fields.MultiLineStringField
|
||||
.. autoclass:: mongoengine.fields.MultiPolygonField
|
||||
.. autoclass:: mongoengine.fields.GridFSError
|
||||
.. autoclass:: mongoengine.fields.GridFSProxy
|
||||
.. autoclass:: mongoengine.fields.ImageGridFsProxy
|
||||
.. autoclass:: mongoengine.fields.ImproperlyConfigured
|
||||
|
||||
Embedded Document Querying
|
||||
==========================
|
||||
|
||||
.. versionadded:: 0.9
|
||||
|
||||
Additional queries for Embedded Documents are available when using the
|
||||
:class:`~mongoengine.EmbeddedDocumentListField` to store a list of embedded
|
||||
documents.
|
||||
|
||||
A list of embedded documents is returned as a special list with the
|
||||
following methods:
|
||||
|
||||
.. autoclass:: mongoengine.base.datastructures.EmbeddedDocumentList
|
||||
:members:
|
||||
|
||||
Misc
|
||||
====
|
||||
|
||||
.. autofunction:: mongoengine.common._import_class
|
||||
|
||||
@@ -2,8 +2,368 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
Changes in 0.8.X
|
||||
Development
|
||||
===========
|
||||
- (Fill this out as you fix issues and develop your features).
|
||||
|
||||
Changes in 0.14.1
|
||||
=================
|
||||
- Removed SemiStrictDict and started using a regular dict for `BaseDocument._data` #1630
|
||||
- Added support for the `$position` param in the `$push` operator #1566
|
||||
- Fixed `DateTimeField` interpreting an empty string as today #1533
|
||||
- Added a missing `__ne__` method to the `GridFSProxy` class #1632
|
||||
- Fixed `BaseQuerySet._fields_to_db_fields` #1553
|
||||
|
||||
Changes in 0.14.0
|
||||
=================
|
||||
- BREAKING CHANGE: Removed the `coerce_types` param from `QuerySet.as_pymongo` #1549
|
||||
- POTENTIAL BREAKING CHANGE: Made EmbeddedDocument not hashable by default #1528
|
||||
- Improved code quality #1531, #1540, #1541, #1547
|
||||
|
||||
Changes in 0.13.0
|
||||
=================
|
||||
- POTENTIAL BREAKING CHANGE: Added Unicode support to the `EmailField`, see
|
||||
docs/upgrade.rst for details.
|
||||
|
||||
Changes in 0.12.0
|
||||
=================
|
||||
- POTENTIAL BREAKING CHANGE: Fixed limit/skip/hint/batch_size chaining #1476
|
||||
- POTENTIAL BREAKING CHANGE: Changed a public `QuerySet.clone_into` method to a private `QuerySet._clone_into` #1476
|
||||
- Fixed the way `Document.objects.create` works with duplicate IDs #1485
|
||||
- Fixed connecting to a replica set with PyMongo 2.x #1436
|
||||
- Fixed using sets in field choices #1481
|
||||
- Fixed deleting items from a `ListField` #1318
|
||||
- Fixed an obscure error message when filtering by `field__in=non_iterable`. #1237
|
||||
- Fixed behavior of a `dec` update operator #1450
|
||||
- Added a `rename` update operator #1454
|
||||
- Added validation for the `db_field` parameter #1448
|
||||
- Fixed the error message displayed when querying an `EmbeddedDocumentField` by an invalid value #1440
|
||||
- Fixed the error message displayed when validating unicode URLs #1486
|
||||
- Raise an error when trying to save an abstract document #1449
|
||||
|
||||
Changes in 0.11.0
|
||||
=================
|
||||
- BREAKING CHANGE: Renamed `ConnectionError` to `MongoEngineConnectionError` since the former is a built-in exception name in Python v3.x. #1428
|
||||
- BREAKING CHANGE: Dropped Python 2.6 support. #1428
|
||||
- BREAKING CHANGE: `from mongoengine.base import ErrorClass` won't work anymore for any error from `mongoengine.errors` (e.g. `ValidationError`). Use `from mongoengine.errors import ErrorClass instead`. #1428
|
||||
- BREAKING CHANGE: Accessing a broken reference will raise a `DoesNotExist` error. In the past it used to return `None`. #1334
|
||||
- Fixed absent rounding for DecimalField when `force_string` is set. #1103
|
||||
|
||||
Changes in 0.10.8
|
||||
=================
|
||||
- Added support for QuerySet.batch_size (#1426)
|
||||
- Fixed query set iteration within iteration #1427
|
||||
- Fixed an issue where specifying a MongoDB URI host would override more information than it should #1421
|
||||
- Added ability to filter the generic reference field by ObjectId and DBRef #1425
|
||||
- Fixed delete cascade for models with a custom primary key field #1247
|
||||
- Added ability to specify an authentication mechanism (e.g. X.509) #1333
|
||||
- Added support for falsey primary keys (e.g. doc.pk = 0) #1354
|
||||
- Fixed QuerySet#sum/average for fields w/ explicit db_field #1417
|
||||
- Fixed filtering by embedded_doc=None #1422
|
||||
- Added support for cursor.comment #1420
|
||||
- Fixed doc.get_<field>_display #1419
|
||||
- Fixed __repr__ method of the StrictDict #1424
|
||||
- Added a deprecation warning for Python 2.6
|
||||
|
||||
Changes in 0.10.7
|
||||
=================
|
||||
- Dropped Python 3.2 support #1390
|
||||
- Fixed the bug where dynamic doc has index inside a dict field #1278
|
||||
- Fixed: ListField minus index assignment does not work #1128
|
||||
- Fixed cascade delete mixing among collections #1224
|
||||
- Add `signal_kwargs` argument to `Document.save`, `Document.delete` and `BaseQuerySet.insert` to be passed to signals calls #1206
|
||||
- Raise `OperationError` when trying to do a `drop_collection` on document with no collection set.
|
||||
- count on ListField of EmbeddedDocumentField fails. #1187
|
||||
- Fixed long fields stored as int32 in Python 3. #1253
|
||||
- MapField now handles unicodes keys correctly. #1267
|
||||
- ListField now handles negative indicies correctly. #1270
|
||||
- Fixed AttributeError when initializing EmbeddedDocument with positional args. #681
|
||||
- Fixed no_cursor_timeout error with pymongo 3.0+ #1304
|
||||
- Replaced map-reduce based QuerySet.sum/average with aggregation-based implementations #1336
|
||||
- Fixed support for `__` to escape field names that match operators names in `update` #1351
|
||||
- Fixed BaseDocument#_mark_as_changed #1369
|
||||
- Added support for pickling QuerySet instances. #1397
|
||||
- Fixed connecting to a list of hosts #1389
|
||||
- Fixed a bug where accessing broken references wouldn't raise a DoesNotExist error #1334
|
||||
- Fixed not being able to specify use_db_field=False on ListField(EmbeddedDocumentField) instances #1218
|
||||
- Improvements to the dictionary fields docs #1383
|
||||
|
||||
Changes in 0.10.6
|
||||
=================
|
||||
- Add support for mocking MongoEngine based on mongomock. #1151
|
||||
- Fixed not being able to run tests on Windows. #1153
|
||||
- Allow creation of sparse compound indexes. #1114
|
||||
- count on ListField of EmbeddedDocumentField fails. #1187
|
||||
|
||||
Changes in 0.10.5
|
||||
=================
|
||||
- Fix for reloading of strict with special fields. #1156
|
||||
|
||||
Changes in 0.10.4
|
||||
=================
|
||||
- SaveConditionError is now importable from the top level package. #1165
|
||||
- upsert_one method added. #1157
|
||||
|
||||
Changes in 0.10.3
|
||||
=================
|
||||
- Fix `read_preference` (it had chaining issues with PyMongo 2.x and it didn't work at all with PyMongo 3.x) #1042
|
||||
|
||||
Changes in 0.10.2
|
||||
=================
|
||||
- Allow shard key to point to a field in an embedded document. #551
|
||||
- Allow arbirary metadata in fields. #1129
|
||||
- ReferenceFields now support abstract document types. #837
|
||||
|
||||
Changes in 0.10.1
|
||||
=================
|
||||
- Fix infinite recursion with CASCADE delete rules under specific conditions. #1046
|
||||
- Fix CachedReferenceField bug when loading cached docs as DBRef but failing to save them. #1047
|
||||
- Fix ignored chained options #842
|
||||
- Document save's save_condition error raises `SaveConditionError` exception #1070
|
||||
- Fix Document.reload for DynamicDocument. #1050
|
||||
- StrictDict & SemiStrictDict are shadowed at init time. #1105
|
||||
- Fix ListField minus index assignment does not work. #1119
|
||||
- Remove code that marks field as changed when the field has default but not existed in database #1126
|
||||
- Remove test dependencies (nose and rednose) from install dependencies list. #1079
|
||||
- Recursively build query when using elemMatch operator. #1130
|
||||
- Fix instance back references for lists of embedded documents. #1131
|
||||
|
||||
Changes in 0.10.0
|
||||
=================
|
||||
- Django support was removed and will be available as a separate extension. #958
|
||||
- Allow to load undeclared field with meta attribute 'strict': False #957
|
||||
- Support for PyMongo 3+ #946
|
||||
- Removed get_or_create() deprecated since 0.8.0. #300
|
||||
- Improve Document._created status when switch collection and db #1020
|
||||
- Queryset update doesn't go through field validation #453
|
||||
- Added support for specifying authentication source as option `authSource` in URI. #967
|
||||
- Fixed mark_as_changed to handle higher/lower level fields changed. #927
|
||||
- ListField of embedded docs doesn't set the _instance attribute when iterating over it #914
|
||||
- Support += and *= for ListField #595
|
||||
- Use sets for populating dbrefs to dereference
|
||||
- Fixed unpickled documents replacing the global field's list. #888
|
||||
- Fixed storage of microseconds in ComplexDateTimeField and unused separator option. #910
|
||||
- Don't send a "cls" option to ensureIndex (related to https://jira.mongodb.org/browse/SERVER-769)
|
||||
- Fix for updating sorting in SortedListField. #978
|
||||
- Added __ support to escape field name in fields lookup keywords that match operators names #949
|
||||
- Fix for issue where FileField deletion did not free space in GridFS.
|
||||
- No_dereference() not respected on embedded docs containing reference. #517
|
||||
- Document save raise an exception if save_condition fails #1005
|
||||
- Fixes some internal _id handling issue. #961
|
||||
- Updated URL and Email Field regex validators, added schemes argument to URLField validation. #652
|
||||
- Capped collection multiple of 256. #1011
|
||||
- Added `BaseQuerySet.aggregate_sum` and `BaseQuerySet.aggregate_average` methods.
|
||||
- Fix for delete with write_concern {'w': 0}. #1008
|
||||
- Allow dynamic lookup for more than two parts. #882
|
||||
- Added support for min_distance on geo queries. #831
|
||||
- Allow to add custom metadata to fields #705
|
||||
|
||||
Changes in 0.9.0
|
||||
================
|
||||
- Update FileField when creating a new file #714
|
||||
- Added `EmbeddedDocumentListField` for Lists of Embedded Documents. #826
|
||||
- ComplexDateTimeField should fall back to None when null=True #864
|
||||
- Request Support for $min, $max Field update operators #863
|
||||
- `BaseDict` does not follow `setdefault` #866
|
||||
- Add support for $type operator # 766
|
||||
- Fix tests for pymongo 2.8+ #877
|
||||
- No module named 'django.utils.importlib' (Django dev) #872
|
||||
- Field Choices Now Accept Subclasses of Documents
|
||||
- Ensure Indexes before Each Save #812
|
||||
- Generate Unique Indices for Lists of EmbeddedDocuments #358
|
||||
- Sparse fields #515
|
||||
- write_concern not in params of Collection#remove #801
|
||||
- Better BaseDocument equality check when not saved #798
|
||||
- OperationError: Shard Keys are immutable. Tried to update id even though the document is not yet saved #771
|
||||
- with_limit_and_skip for count should default like in pymongo #759
|
||||
- Fix storing value of precision attribute in DecimalField #787
|
||||
- Set attribute to None does not work (at least for fields with default values) #734
|
||||
- Querying by a field defined in a subclass raises InvalidQueryError #744
|
||||
- Add Support For MongoDB 2.6.X's maxTimeMS #778
|
||||
- abstract shouldn't be inherited in EmbeddedDocument # 789
|
||||
- Allow specifying the '_cls' as a field for indexes #397
|
||||
- Stop ensure_indexes running on a secondaries unless connection is through mongos #746
|
||||
- Not overriding default values when loading a subset of fields #399
|
||||
- Saving document doesn't create new fields in existing collection #620
|
||||
- Added `Queryset.aggregate` wrapper to aggregation framework #703
|
||||
- Added support to show original model fields on to_json calls instead of db_field #697
|
||||
- Added Queryset.search_text to Text indexes searchs #700
|
||||
- Fixed tests for Django 1.7 #696
|
||||
- Follow ReferenceFields in EmbeddedDocuments with select_related #690
|
||||
- Added preliminary support for text indexes #680
|
||||
- Added `elemMatch` operator as well - `match` is too obscure #653
|
||||
- Added support for progressive JPEG #486 #548
|
||||
- Allow strings to be used in index creation #675
|
||||
- Fixed EmbeddedDoc weakref proxy issue #592
|
||||
- Fixed nested reference field distinct error #583
|
||||
- Fixed change tracking on nested MapFields #539
|
||||
- Dynamic fields in embedded documents now visible to queryset.only() / qs.exclude() #425 #507
|
||||
- Add authentication_source option to register_connection #178 #464 #573 #580 #590
|
||||
- Implemented equality between Documents and DBRefs #597
|
||||
- Fixed ReferenceField inside nested ListFields dereferencing problem #368
|
||||
- Added the ability to reload specific document fields #100
|
||||
- Added db_alias support and fixes for custom map/reduce output #586
|
||||
- post_save signal now has access to delta information about field changes #594 #589
|
||||
- Don't query with $orderby for qs.get() #600
|
||||
- Fix id shard key save issue #636
|
||||
- Fixes issue with recursive embedded document errors #557
|
||||
- Fix clear_changed_fields() clearing unsaved documents bug #602
|
||||
- Removing support for Django 1.4.x, pymongo 2.5.x, pymongo 2.6.x.
|
||||
- Removing support for Python < 2.6.6
|
||||
- Fixed $maxDistance location for geoJSON $near queries with MongoDB 2.6+ #664
|
||||
- QuerySet.modify() and Document.modify() methods to provide find_and_modify() like behaviour #677 #773
|
||||
- Added support for the using() method on a queryset #676
|
||||
- PYPY support #673
|
||||
- Connection pooling #674
|
||||
- Avoid to open all documents from cursors in an if stmt #655
|
||||
- Ability to clear the ordering #657
|
||||
- Raise NotUniqueError in Document.update() on pymongo.errors.DuplicateKeyError #626
|
||||
- Slots - memory improvements #625
|
||||
- Fixed incorrectly split a query key when it ends with "_" #619
|
||||
- Geo docs updates #613
|
||||
- Workaround a dateutil bug #608
|
||||
- Conditional save for atomic-style operations #511
|
||||
- Allow dynamic dictionary-style field access #559
|
||||
- Increase email field length to accommodate new TLDs #726
|
||||
- index_cls is ignored when deciding to set _cls as index prefix #733
|
||||
- Make 'db' argument to connection optional #737
|
||||
- Allow atomic update for the entire `DictField` #742
|
||||
- Added MultiPointField, MultiLineField, MultiPolygonField
|
||||
- Fix multiple connections aliases being rewritten #748
|
||||
- Fixed a few instances where reverse_delete_rule was written as reverse_delete_rules. #791
|
||||
- Make `in_bulk()` respect `no_dereference()` #775
|
||||
- Handle None from model __str__; Fixes #753 #754
|
||||
- _get_changed_fields fix for embedded documents with id field. #925
|
||||
|
||||
Changes in 0.8.7
|
||||
================
|
||||
- Calling reload on deleted / nonexistent documents raises DoesNotExist (#538)
|
||||
- Stop ensure_indexes running on a secondaries (#555)
|
||||
- Fix circular import issue with django auth (#531) (#545)
|
||||
|
||||
Changes in 0.8.6
|
||||
================
|
||||
- Fix django auth import (#531)
|
||||
|
||||
Changes in 0.8.5
|
||||
================
|
||||
- Fix multi level nested fields getting marked as changed (#523)
|
||||
- Django 1.6 login fix (#522) (#527)
|
||||
- Django 1.6 session fix (#509)
|
||||
- EmbeddedDocument._instance is now set when setting the attribute (#506)
|
||||
- Fixed EmbeddedDocument with ReferenceField equality issue (#502)
|
||||
- Fixed GenericReferenceField serialization order (#499)
|
||||
- Fixed count and none bug (#498)
|
||||
- Fixed bug with .only() and DictField with digit keys (#496)
|
||||
- Added user_permissions to Django User object (#491, #492)
|
||||
- Fix updating Geo Location fields (#488)
|
||||
- Fix handling invalid dict field value (#485)
|
||||
- Added app_label to MongoUser (#484)
|
||||
- Use defaults when host and port are passed as None (#483)
|
||||
- Fixed distinct casting issue with ListField of EmbeddedDocuments (#470)
|
||||
- Fixed Django 1.6 sessions (#454, #480)
|
||||
|
||||
Changes in 0.8.4
|
||||
================
|
||||
- Remove database name necessity in uri connection schema (#452)
|
||||
- Fixed "$pull" semantics for nested ListFields (#447)
|
||||
- Allow fields to be named the same as query operators (#445)
|
||||
- Updated field filter logic - can now exclude subclass fields (#443)
|
||||
- Fixed dereference issue with embedded listfield referencefields (#439)
|
||||
- Fixed slice when using inheritance causing fields to be excluded (#437)
|
||||
- Fixed ._get_db() attribute after a Document.switch_db() (#441)
|
||||
- Dynamic Fields store and recompose Embedded Documents / Documents correctly (#449)
|
||||
- Handle dynamic fieldnames that look like digits (#434)
|
||||
- Added get_user_document and improve mongo_auth module (#423)
|
||||
- Added str representation of GridFSProxy (#424)
|
||||
- Update transform to handle docs erroneously passed to unset (#416)
|
||||
- Fixed indexing - turn off _cls (#414)
|
||||
- Fixed dereference threading issue in ComplexField.__get__ (#412)
|
||||
- Fixed QuerySetNoCache.count() caching (#410)
|
||||
- Don't follow references in _get_changed_fields (#422, #417)
|
||||
- Allow args and kwargs to be passed through to_json (#420)
|
||||
|
||||
Changes in 0.8.3
|
||||
================
|
||||
- Fixed EmbeddedDocuments with `id` also storing `_id` (#402)
|
||||
- Added get_proxy_object helper to filefields (#391)
|
||||
- Added QuerySetNoCache and QuerySet.no_cache() for lower memory consumption (#365)
|
||||
- Fixed sum and average mapreduce dot notation support (#375, #376, #393)
|
||||
- Fixed as_pymongo to return the id (#386)
|
||||
- Document.select_related() now respects `db_alias` (#377)
|
||||
- Reload uses shard_key if applicable (#384)
|
||||
- Dynamic fields are ordered based on creation and stored in _fields_ordered (#396)
|
||||
|
||||
**Potential breaking change:** http://docs.mongoengine.org/en/latest/upgrade.html#to-0-8-3
|
||||
|
||||
- Fixed pickling dynamic documents `_dynamic_fields` (#387)
|
||||
- Fixed ListField setslice and delslice dirty tracking (#390)
|
||||
- Added Django 1.5 PY3 support (#392)
|
||||
- Added match ($elemMatch) support for EmbeddedDocuments (#379)
|
||||
- Fixed weakref being valid after reload (#374)
|
||||
- Fixed queryset.get() respecting no_dereference (#373)
|
||||
- Added full_result kwarg to update (#380)
|
||||
|
||||
|
||||
|
||||
Changes in 0.8.2
|
||||
================
|
||||
- Added compare_indexes helper (#361)
|
||||
- Fixed cascading saves which weren't turned off as planned (#291)
|
||||
- Fixed Datastructures so instances are a Document or EmbeddedDocument (#363)
|
||||
- Improved cascading saves write performance (#361)
|
||||
- Fixed ambiguity and differing behaviour regarding field defaults (#349)
|
||||
- ImageFields now include PIL error messages if invalid error (#353)
|
||||
- Added lock when calling doc.Delete() for when signals have no sender (#350)
|
||||
- Reload forces read preference to be PRIMARY (#355)
|
||||
- Querysets are now lest restrictive when querying duplicate fields (#332, #333)
|
||||
- FileField now honouring db_alias (#341)
|
||||
- Removed customised __set__ change tracking in ComplexBaseField (#344)
|
||||
- Removed unused var in _get_changed_fields (#347)
|
||||
- Added pre_save_post_validation signal (#345)
|
||||
- DateTimeField now auto converts valid datetime isostrings into dates (#343)
|
||||
- DateTimeField now uses dateutil for parsing if available (#343)
|
||||
- Fixed Doc.objects(read_preference=X) not setting read preference (#352)
|
||||
- Django session ttl index expiry fixed (#329)
|
||||
- Fixed pickle.loads (#342)
|
||||
- Documentation fixes
|
||||
|
||||
Changes in 0.8.1
|
||||
================
|
||||
- Fixed Python 2.6 django auth importlib issue (#326)
|
||||
- Fixed pickle unsaved document regression (#327)
|
||||
|
||||
Changes in 0.8.0
|
||||
================
|
||||
- Fixed querying ReferenceField custom_id (#317)
|
||||
- Fixed pickle issues with collections (#316)
|
||||
- Added `get_next_value` preview for SequenceFields (#319)
|
||||
- Added no_sub_classes context manager and queryset helper (#312)
|
||||
- Querysets now utilises a local cache
|
||||
- Changed __len__ behaviour in the queryset (#247, #311)
|
||||
- Fixed querying string versions of ObjectIds issue with ReferenceField (#307)
|
||||
- Added $setOnInsert support for upserts (#308)
|
||||
- Upserts now possible with just query parameters (#309)
|
||||
- Upserting is the only way to ensure docs are saved correctly (#306)
|
||||
- Fixed register_delete_rule inheritance issue
|
||||
- Fix cloning of sliced querysets (#303)
|
||||
- Fixed update_one write concern (#302)
|
||||
- Updated minimum requirement for pymongo to 2.5
|
||||
- Add support for new geojson fields, indexes and queries (#299)
|
||||
- If values cant be compared mark as changed (#287)
|
||||
- Ensure as_pymongo() and to_json honour only() and exclude() (#293)
|
||||
- Document serialization uses field order to ensure a strict order is set (#296)
|
||||
- DecimalField now stores as float not string (#289)
|
||||
- UUIDField now stores as a binary by default (#292)
|
||||
- Added Custom User Model for Django 1.5 (#285)
|
||||
- Cascading saves now default to off (#291)
|
||||
- ReferenceField now store ObjectId's by default rather than DBRef (#290)
|
||||
- Added ImageField support for inline replacements (#86)
|
||||
- Added SequenceField.set_next_value(value) helper (#159)
|
||||
- Updated .only() behaviour - now like exclude it is chainable (#202)
|
||||
- Added with_limit_and_skip support to count() (#235)
|
||||
- Objects queryset manager now inherited (#256)
|
||||
- Updated connection to use MongoClient (#262, #274)
|
||||
- Fixed db_alias and inherited Documents (#143)
|
||||
- Documentation update for document errors (#124)
|
||||
- Deprecated `get_or_create` (#35)
|
||||
@@ -31,7 +391,7 @@ Changes in 0.8.X
|
||||
- Uses getlasterror to test created on updated saves (#163)
|
||||
- Fixed inheritance and unique index creation (#140)
|
||||
- Fixed reverse delete rule with inheritance (#197)
|
||||
- Fixed validation for GenericReferences which havent been dereferenced
|
||||
- Fixed validation for GenericReferences which haven't been dereferenced
|
||||
- Added switch_db context manager (#106)
|
||||
- Added switch_db method to document instances (#106)
|
||||
- Added no_dereference context manager (#82) (#61)
|
||||
@@ -113,11 +473,11 @@ Changes in 0.7.2
|
||||
- Update index spec generation so its not destructive (#113)
|
||||
|
||||
Changes in 0.7.1
|
||||
=================
|
||||
================
|
||||
- Fixed index spec inheritance (#111)
|
||||
|
||||
Changes in 0.7.0
|
||||
=================
|
||||
================
|
||||
- Updated queryset.delete so you can use with skip / limit (#107)
|
||||
- Updated index creation allows kwargs to be passed through refs (#104)
|
||||
- Fixed Q object merge edge case (#109)
|
||||
@@ -198,7 +558,7 @@ Changes in 0.6.12
|
||||
- Fixes error with _delta handling DBRefs
|
||||
|
||||
Changes in 0.6.11
|
||||
==================
|
||||
=================
|
||||
- Fixed inconsistency handling None values field attrs
|
||||
- Fixed map_field embedded db_field issue
|
||||
- Fixed .save() _delta issue with DbRefs
|
||||
@@ -278,7 +638,7 @@ Changes in 0.6.1
|
||||
- Fix for replicaSet connections
|
||||
|
||||
Changes in 0.6
|
||||
================
|
||||
==============
|
||||
|
||||
- Added FutureWarning to inherited classes not declaring 'allow_inheritance' as the default will change in 0.7
|
||||
- Added support for covered indexes when inheritance is off
|
||||
@@ -366,8 +726,8 @@ Changes in v0.5
|
||||
- Updated default collection naming convention
|
||||
- Added Document Mixin support
|
||||
- Fixed queryet __repr__ mid iteration
|
||||
- Added hint() support, so cantell Mongo the proper index to use for the query
|
||||
- Fixed issue with inconsitent setting of _cls breaking inherited referencing
|
||||
- Added hint() support, so can tell Mongo the proper index to use for the query
|
||||
- Fixed issue with inconsistent setting of _cls breaking inherited referencing
|
||||
- Added help_text and verbose_name to fields to help with some form libs
|
||||
- Updated item_frequencies to handle embedded document lookups
|
||||
- Added delta tracking now only sets / unsets explicitly changed fields
|
||||
|
||||
@@ -17,6 +17,10 @@ class Post(Document):
|
||||
tags = ListField(StringField(max_length=30))
|
||||
comments = ListField(EmbeddedDocumentField(Comment))
|
||||
|
||||
# bugfix
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
|
||||
class TextPost(Post):
|
||||
content = StringField()
|
||||
|
||||
@@ -45,7 +49,8 @@ print 'ALL POSTS'
|
||||
print
|
||||
for post in Post.objects:
|
||||
print post.title
|
||||
print '=' * len(post.title)
|
||||
#print '=' * post.title.count()
|
||||
print "=" * 20
|
||||
|
||||
if isinstance(post, TextPost):
|
||||
print post.content
|
||||
|
||||
27
docs/conf.py
27
docs/conf.py
@@ -13,6 +13,10 @@
|
||||
|
||||
import sys, os
|
||||
|
||||
import sphinx_rtd_theme
|
||||
|
||||
import mongoengine
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
@@ -44,7 +48,6 @@ copyright = u'2009, MongoEngine Authors'
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
import mongoengine
|
||||
# The short X.Y version.
|
||||
version = mongoengine.get_version()
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
@@ -92,15 +95,17 @@ pygments_style = 'sphinx'
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
||||
# Sphinx are currently 'default' and 'sphinxdoc'.
|
||||
html_theme = 'nature'
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
html_theme_options = {
|
||||
'canonical_url': 'http://docs.mongoengine.org/en/latest/'
|
||||
}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
html_theme_path = ['_themes']
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
@@ -116,7 +121,7 @@ html_theme_path = ['_themes']
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
html_favicon = "favicon.ico"
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
@@ -132,7 +137,11 @@ html_theme_path = ['_themes']
|
||||
html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
html_sidebars = {
|
||||
'index': ['globaltoc.html', 'searchbox.html'],
|
||||
'**': ['localtoc.html', 'relations.html', 'searchbox.html']
|
||||
}
|
||||
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
@@ -173,8 +182,8 @@ latex_paper_size = 'a4'
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'MongoEngine.tex', u'MongoEngine Documentation',
|
||||
u'Harry Marr', 'manual'),
|
||||
('index', 'MongoEngine.tex', 'MongoEngine Documentation',
|
||||
'Ross Lawley', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
@@ -193,3 +202,5 @@ latex_documents = [
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_use_modindex = True
|
||||
|
||||
autoclass_content = 'both'
|
||||
|
||||
110
docs/django.rst
110
docs/django.rst
@@ -1,103 +1,19 @@
|
||||
=============================
|
||||
Using MongoEngine with Django
|
||||
=============================
|
||||
|
||||
.. note:: Updated to support Django 1.4
|
||||
|
||||
Connecting
|
||||
==========
|
||||
In your **settings.py** file, ignore the standard database settings (unless you
|
||||
also plan to use the ORM in your project), and instead call
|
||||
:func:`~mongoengine.connect` somewhere in the settings module.
|
||||
|
||||
.. note ::
|
||||
If you are not using another Database backend you may need to add a dummy
|
||||
database backend to ``settings.py`` eg::
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.dummy'
|
||||
}
|
||||
}
|
||||
|
||||
Authentication
|
||||
==============
|
||||
MongoEngine includes a Django authentication backend, which uses MongoDB. The
|
||||
:class:`~mongoengine.django.auth.User` model is a MongoEngine
|
||||
:class:`~mongoengine.Document`, but implements most of the methods and
|
||||
attributes that the standard Django :class:`User` model does - so the two are
|
||||
moderately compatible. Using this backend will allow you to store users in
|
||||
MongoDB but still use many of the Django authentication infrastucture (such as
|
||||
the :func:`login_required` decorator and the :func:`authenticate` function). To
|
||||
enable the MongoEngine auth backend, add the following to you **settings.py**
|
||||
file::
|
||||
Django Support
|
||||
==============
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'mongoengine.django.auth.MongoEngineBackend',
|
||||
)
|
||||
.. note:: Django support has been split from the main MongoEngine
|
||||
repository. The *legacy* Django extension may be found bundled with the
|
||||
0.9 release of MongoEngine.
|
||||
|
||||
The :mod:`~mongoengine.django.auth` module also contains a
|
||||
:func:`~mongoengine.django.auth.get_user` helper function, that takes a user's
|
||||
:attr:`id` and returns a :class:`~mongoengine.django.auth.User` object.
|
||||
|
||||
.. versionadded:: 0.1.3
|
||||
|
||||
Sessions
|
||||
========
|
||||
Django allows the use of different backend stores for its sessions. MongoEngine
|
||||
provides a MongoDB-based session backend for Django, which allows you to use
|
||||
sessions in you Django application with just MongoDB. To enable the MongoEngine
|
||||
session backend, ensure that your settings module has
|
||||
``'django.contrib.sessions.middleware.SessionMiddleware'`` in the
|
||||
``MIDDLEWARE_CLASSES`` field and ``'django.contrib.sessions'`` in your
|
||||
``INSTALLED_APPS``. From there, all you need to do is add the following line
|
||||
into you settings module::
|
||||
Help Wanted!
|
||||
------------
|
||||
|
||||
SESSION_ENGINE = 'mongoengine.django.sessions'
|
||||
|
||||
Django provides session cookie, which expires after ```SESSION_COOKIE_AGE``` seconds, but doesnt delete cookie at sessions backend, so ``'mongoengine.django.sessions'`` supports `mongodb TTL
|
||||
<http://docs.mongodb.org/manual/tutorial/expire-data/>`_.
|
||||
|
||||
.. versionadded:: 0.2.1
|
||||
|
||||
Storage
|
||||
=======
|
||||
With MongoEngine's support for GridFS via the :class:`~mongoengine.FileField`,
|
||||
it is useful to have a Django file storage backend that wraps this. The new
|
||||
storage module is called :class:`~mongoengine.django.storage.GridFSStorage`.
|
||||
Using it is very similar to using the default FileSystemStorage.::
|
||||
|
||||
from mongoengine.django.storage import GridFSStorage
|
||||
fs = GridFSStorage()
|
||||
|
||||
filename = fs.save('hello.txt', 'Hello, World!')
|
||||
|
||||
All of the `Django Storage API methods
|
||||
<http://docs.djangoproject.com/en/dev/ref/files/storage/>`_ have been
|
||||
implemented except :func:`path`. If the filename provided already exists, an
|
||||
underscore and a number (before # the file extension, if one exists) will be
|
||||
appended to the filename until the generated filename doesn't exist. The
|
||||
:func:`save` method will return the new filename.::
|
||||
|
||||
>>> fs.exists('hello.txt')
|
||||
True
|
||||
>>> fs.open('hello.txt').read()
|
||||
'Hello, World!'
|
||||
>>> fs.size('hello.txt')
|
||||
13
|
||||
>>> fs.url('hello.txt')
|
||||
'http://your_media_url/hello.txt'
|
||||
>>> fs.open('hello.txt').name
|
||||
'hello.txt'
|
||||
>>> fs.listdir()
|
||||
([], [u'hello.txt'])
|
||||
|
||||
All files will be saved and retrieved in GridFS via the :class::`FileDocument`
|
||||
document, allowing easy access to the files without the GridFSStorage
|
||||
backend.::
|
||||
|
||||
>>> from mongoengine.django.storage import FileDocument
|
||||
>>> FileDocument.objects()
|
||||
[<FileDocument: FileDocument object>]
|
||||
|
||||
.. versionadded:: 0.4
|
||||
The MongoEngine team is looking for help contributing and maintaining a new
|
||||
Django extension for MongoEngine! If you have Django experience and would like
|
||||
to help contribute to the project, please get in touch on the
|
||||
`mailing list <http://groups.google.com/group/mongoengine-users>`_ or by
|
||||
simply contributing on
|
||||
`GitHub <https://github.com/MongoEngine/django-mongoengine>`_.
|
||||
|
||||
@@ -6,34 +6,56 @@ Connecting to MongoDB
|
||||
|
||||
To connect to a running instance of :program:`mongod`, use the
|
||||
:func:`~mongoengine.connect` function. The first argument is the name of the
|
||||
database to connect to. If the database does not exist, it will be created. If
|
||||
the database requires authentication, :attr:`username` and :attr:`password`
|
||||
arguments may be provided::
|
||||
database to connect to::
|
||||
|
||||
from mongoengine import connect
|
||||
connect('project1', username='webapp', password='pwd123')
|
||||
connect('project1')
|
||||
|
||||
By default, MongoEngine assumes that the :program:`mongod` instance is running
|
||||
on **localhost** on port **27017**. If MongoDB is running elsewhere, you may
|
||||
provide :attr:`host` and :attr:`port` arguments to
|
||||
on **localhost** on port **27017**. If MongoDB is running elsewhere, you should
|
||||
provide the :attr:`host` and :attr:`port` arguments to
|
||||
:func:`~mongoengine.connect`::
|
||||
|
||||
connect('project1', host='192.168.1.35', port=12345)
|
||||
|
||||
Uri style connections are also supported as long as you include the database
|
||||
name - just supply the uri as the :attr:`host` to
|
||||
If the database requires authentication, :attr:`username` and :attr:`password`
|
||||
arguments should be provided::
|
||||
|
||||
connect('project1', username='webapp', password='pwd123')
|
||||
|
||||
URI style connections are also supported -- just supply the URI as
|
||||
the :attr:`host` to
|
||||
:func:`~mongoengine.connect`::
|
||||
|
||||
connect('project1', host='mongodb://localhost/database_name')
|
||||
|
||||
ReplicaSets
|
||||
===========
|
||||
.. note:: Database, username and password from URI string overrides
|
||||
corresponding parameters in :func:`~mongoengine.connect`: ::
|
||||
|
||||
MongoEngine now supports :func:`~pymongo.replica_set_connection.ReplicaSetConnection`
|
||||
to use them please use a URI style connection and provide the `replicaSet` name in the
|
||||
connection kwargs.
|
||||
connect(
|
||||
db='test',
|
||||
username='user',
|
||||
password='12345',
|
||||
host='mongodb://admin:qwerty@localhost/production'
|
||||
)
|
||||
|
||||
Read preferences are supported throught the connection or via individual
|
||||
will establish connection to ``production`` database using
|
||||
``admin`` username and ``qwerty`` password.
|
||||
|
||||
Replica Sets
|
||||
============
|
||||
|
||||
MongoEngine supports connecting to replica sets::
|
||||
|
||||
from mongoengine import connect
|
||||
|
||||
# Regular connect
|
||||
connect('dbname', replicaset='rs-name')
|
||||
|
||||
# MongoDB URI-style connect
|
||||
connect(host='mongodb://localhost/dbname?replicaSet=rs-name')
|
||||
|
||||
Read preferences are supported through the connection or via individual
|
||||
queries by passing the read_preference ::
|
||||
|
||||
Bar.objects().read_preference(ReadPreference.PRIMARY)
|
||||
@@ -42,48 +64,76 @@ queries by passing the read_preference ::
|
||||
Multiple Databases
|
||||
==================
|
||||
|
||||
Multiple database support was added in MongoEngine 0.6. To use multiple
|
||||
databases you can use :func:`~mongoengine.connect` and provide an `alias` name
|
||||
for the connection - if no `alias` is provided then "default" is used.
|
||||
To use multiple databases you can use :func:`~mongoengine.connect` and provide
|
||||
an `alias` name for the connection - if no `alias` is provided then "default"
|
||||
is used.
|
||||
|
||||
In the background this uses :func:`~mongoengine.register_connection` to
|
||||
store the data and you can register all aliases up front if required.
|
||||
|
||||
Individual documents can also support multiple databases by providing a
|
||||
`db_alias` in their meta data. This allows :class:`~pymongo.dbref.DBRef` objects
|
||||
to point across databases and collections. Below is an example schema, using
|
||||
3 different databases to store data::
|
||||
`db_alias` in their meta data. This allows :class:`~pymongo.dbref.DBRef`
|
||||
objects to point across databases and collections. Below is an example schema,
|
||||
using 3 different databases to store data::
|
||||
|
||||
class User(Document):
|
||||
name = StringField()
|
||||
|
||||
meta = {"db_alias": "user-db"}
|
||||
meta = {'db_alias': 'user-db'}
|
||||
|
||||
class Book(Document):
|
||||
name = StringField()
|
||||
|
||||
meta = {"db_alias": "book-db"}
|
||||
meta = {'db_alias': 'book-db'}
|
||||
|
||||
class AuthorBooks(Document):
|
||||
author = ReferenceField(User)
|
||||
book = ReferenceField(Book)
|
||||
|
||||
meta = {"db_alias": "users-books-db"}
|
||||
meta = {'db_alias': 'users-books-db'}
|
||||
|
||||
|
||||
Switch Database Context Manager
|
||||
===============================
|
||||
Context Managers
|
||||
================
|
||||
Sometimes you may want to switch the database or collection to query against.
|
||||
For example, archiving older data into a separate database for performance
|
||||
reasons or writing functions that dynamically choose collections to write
|
||||
a document to.
|
||||
|
||||
Sometimes you might want to switch the database to query against for a class.
|
||||
Switch Database
|
||||
---------------
|
||||
The :class:`~mongoengine.context_managers.switch_db` context manager allows
|
||||
you to change the database alias for a class eg ::
|
||||
you to change the database alias for a given class allowing quick and easy
|
||||
access to the same User document across databases::
|
||||
|
||||
from mongoengine.context_managers import switch_db
|
||||
from mongoengine.context_managers import switch_db
|
||||
|
||||
class User(Document):
|
||||
class User(Document):
|
||||
name = StringField()
|
||||
|
||||
meta = {'db_alias': 'user-db'}
|
||||
|
||||
with switch_db(User, 'archive-user-db') as User:
|
||||
User(name='Ross').save() # Saves the 'archive-user-db'
|
||||
|
||||
|
||||
Switch Collection
|
||||
-----------------
|
||||
The :class:`~mongoengine.context_managers.switch_collection` context manager
|
||||
allows you to change the collection for a given class allowing quick and easy
|
||||
access to the same Group document across collection::
|
||||
|
||||
from mongoengine.context_managers import switch_collection
|
||||
|
||||
class Group(Document):
|
||||
name = StringField()
|
||||
|
||||
meta = {"db_alias": "user-db"}
|
||||
Group(name='test').save() # Saves in the default db
|
||||
|
||||
with switch_db(User, 'archive-user-db') as User:
|
||||
User(name="Ross").save() # Saves the 'archive-user-db'
|
||||
with switch_collection(Group, 'group2000') as Group:
|
||||
Group(name='hello Group 2000 collection!').save() # Saves in group2000 collection
|
||||
|
||||
|
||||
.. note:: Make sure any aliases have been registered with
|
||||
:func:`~mongoengine.register_connection` or :func:`~mongoengine.connect`
|
||||
before using the context manager.
|
||||
|
||||
@@ -4,7 +4,7 @@ Defining documents
|
||||
In MongoDB, a **document** is roughly equivalent to a **row** in an RDBMS. When
|
||||
working with relational databases, rows are stored in **tables**, which have a
|
||||
strict **schema** that the rows follow. MongoDB stores documents in
|
||||
**collections** rather than tables - the principle difference is that no schema
|
||||
**collections** rather than tables --- the principal difference is that no schema
|
||||
is enforced at a database level.
|
||||
|
||||
Defining a document's schema
|
||||
@@ -24,9 +24,12 @@ objects** as class attributes to the document class::
|
||||
title = StringField(max_length=200, required=True)
|
||||
date_modified = DateTimeField(default=datetime.datetime.now)
|
||||
|
||||
As BSON (the binary format for storing data in mongodb) is order dependent,
|
||||
documents are serialized based on their field order.
|
||||
|
||||
Dynamic document schemas
|
||||
========================
|
||||
One of the benefits of MongoDb is dynamic schemas for a collection, whilst data
|
||||
One of the benefits of MongoDB is dynamic schemas for a collection, whilst data
|
||||
should be planned and organised (after all explicit is better than implicit!)
|
||||
there are scenarios where having dynamic / expando style documents is desirable.
|
||||
|
||||
@@ -51,6 +54,7 @@ be saved ::
|
||||
|
||||
There is one caveat on Dynamic Documents: fields cannot start with `_`
|
||||
|
||||
Dynamic fields are stored in creation order *after* any declared fields.
|
||||
|
||||
Fields
|
||||
======
|
||||
@@ -62,31 +66,38 @@ not provided. Default values may optionally be a callable, which will be called
|
||||
to retrieve the value (such as in the above example). The field types available
|
||||
are as follows:
|
||||
|
||||
* :class:`~mongoengine.BinaryField`
|
||||
* :class:`~mongoengine.BooleanField`
|
||||
* :class:`~mongoengine.ComplexDateTimeField`
|
||||
* :class:`~mongoengine.DateTimeField`
|
||||
* :class:`~mongoengine.DecimalField`
|
||||
* :class:`~mongoengine.DictField`
|
||||
* :class:`~mongoengine.DynamicField`
|
||||
* :class:`~mongoengine.EmailField`
|
||||
* :class:`~mongoengine.EmbeddedDocumentField`
|
||||
* :class:`~mongoengine.FileField`
|
||||
* :class:`~mongoengine.FloatField`
|
||||
* :class:`~mongoengine.GenericEmbeddedDocumentField`
|
||||
* :class:`~mongoengine.GenericReferenceField`
|
||||
* :class:`~mongoengine.GeoPointField`
|
||||
* :class:`~mongoengine.ImageField`
|
||||
* :class:`~mongoengine.IntField`
|
||||
* :class:`~mongoengine.ListField`
|
||||
* :class:`~mongoengine.MapField`
|
||||
* :class:`~mongoengine.ObjectIdField`
|
||||
* :class:`~mongoengine.ReferenceField`
|
||||
* :class:`~mongoengine.SequenceField`
|
||||
* :class:`~mongoengine.SortedListField`
|
||||
* :class:`~mongoengine.StringField`
|
||||
* :class:`~mongoengine.URLField`
|
||||
* :class:`~mongoengine.UUIDField`
|
||||
* :class:`~mongoengine.fields.BinaryField`
|
||||
* :class:`~mongoengine.fields.BooleanField`
|
||||
* :class:`~mongoengine.fields.ComplexDateTimeField`
|
||||
* :class:`~mongoengine.fields.DateTimeField`
|
||||
* :class:`~mongoengine.fields.DecimalField`
|
||||
* :class:`~mongoengine.fields.DictField`
|
||||
* :class:`~mongoengine.fields.DynamicField`
|
||||
* :class:`~mongoengine.fields.EmailField`
|
||||
* :class:`~mongoengine.fields.EmbeddedDocumentField`
|
||||
* :class:`~mongoengine.fields.EmbeddedDocumentListField`
|
||||
* :class:`~mongoengine.fields.FileField`
|
||||
* :class:`~mongoengine.fields.FloatField`
|
||||
* :class:`~mongoengine.fields.GenericEmbeddedDocumentField`
|
||||
* :class:`~mongoengine.fields.GenericReferenceField`
|
||||
* :class:`~mongoengine.fields.GeoPointField`
|
||||
* :class:`~mongoengine.fields.ImageField`
|
||||
* :class:`~mongoengine.fields.IntField`
|
||||
* :class:`~mongoengine.fields.ListField`
|
||||
* :class:`~mongoengine.fields.MapField`
|
||||
* :class:`~mongoengine.fields.ObjectIdField`
|
||||
* :class:`~mongoengine.fields.ReferenceField`
|
||||
* :class:`~mongoengine.fields.SequenceField`
|
||||
* :class:`~mongoengine.fields.SortedListField`
|
||||
* :class:`~mongoengine.fields.StringField`
|
||||
* :class:`~mongoengine.fields.URLField`
|
||||
* :class:`~mongoengine.fields.UUIDField`
|
||||
* :class:`~mongoengine.fields.PointField`
|
||||
* :class:`~mongoengine.fields.LineStringField`
|
||||
* :class:`~mongoengine.fields.PolygonField`
|
||||
* :class:`~mongoengine.fields.MultiPointField`
|
||||
* :class:`~mongoengine.fields.MultiLineStringField`
|
||||
* :class:`~mongoengine.fields.MultiPolygonField`
|
||||
|
||||
Field arguments
|
||||
---------------
|
||||
@@ -96,9 +107,6 @@ arguments can be set on all fields:
|
||||
:attr:`db_field` (Default: None)
|
||||
The MongoDB field name.
|
||||
|
||||
:attr:`name` (Default: None)
|
||||
The mongoengine field name.
|
||||
|
||||
:attr:`required` (Default: False)
|
||||
If set to True and the field is not set on the document instance, a
|
||||
:class:`~mongoengine.ValidationError` will be raised when the document is
|
||||
@@ -107,10 +115,10 @@ arguments can be set on all fields:
|
||||
:attr:`default` (Default: None)
|
||||
A value to use when no value is set for this field.
|
||||
|
||||
The definion of default parameters follow `the general rules on Python
|
||||
The definition of default parameters follow `the general rules on Python
|
||||
<http://docs.python.org/reference/compound_stmts.html#function-definitions>`__,
|
||||
which means that some care should be taken when dealing with default mutable objects
|
||||
(like in :class:`~mongoengine.ListField` or :class:`~mongoengine.DictField`)::
|
||||
(like in :class:`~mongoengine.fields.ListField` or :class:`~mongoengine.fields.DictField`)::
|
||||
|
||||
class ExampleFirst(Document):
|
||||
# Default an empty list
|
||||
@@ -125,6 +133,7 @@ arguments can be set on all fields:
|
||||
# instead to just an object
|
||||
values = ListField(IntField(), default=[1,2,3])
|
||||
|
||||
.. note:: Unsetting a field with a default value will revert back to the default.
|
||||
|
||||
:attr:`unique` (Default: False)
|
||||
When True, no documents in the collection will have the same value for this
|
||||
@@ -138,8 +147,10 @@ arguments can be set on all fields:
|
||||
When True, use this field as a primary key for the collection. `DictField`
|
||||
and `EmbeddedDocuments` both support being the primary key for a document.
|
||||
|
||||
.. note:: If set, this field is also accessible through the `pk` field.
|
||||
|
||||
:attr:`choices` (Default: None)
|
||||
An iterable (e.g. a list or tuple) of choices to which the value of this
|
||||
An iterable (e.g. list, tuple or set) of choices to which the value of this
|
||||
field should be limited.
|
||||
|
||||
Can be either be a nested tuples of value (stored in mongo) and a
|
||||
@@ -162,18 +173,18 @@ arguments can be set on all fields:
|
||||
class Shirt(Document):
|
||||
size = StringField(max_length=3, choices=SIZE)
|
||||
|
||||
:attr:`help_text` (Default: None)
|
||||
Optional help text to output with the field - used by form libraries
|
||||
|
||||
:attr:`verbose_name` (Default: None)
|
||||
Optional human-readable name for the field - used by form libraries
|
||||
:attr:`**kwargs` (Optional)
|
||||
You can supply additional metadata as arbitrary additional keyword
|
||||
arguments. You can not override existing attributes, however. Common
|
||||
choices include `help_text` and `verbose_name`, commonly used by form and
|
||||
widget libraries.
|
||||
|
||||
|
||||
List fields
|
||||
-----------
|
||||
MongoDB allows the storage of lists of items. To add a list of items to a
|
||||
:class:`~mongoengine.Document`, use the :class:`~mongoengine.ListField` field
|
||||
type. :class:`~mongoengine.ListField` takes another field object as its first
|
||||
MongoDB allows storing lists of items. To add a list of items to a
|
||||
:class:`~mongoengine.Document`, use the :class:`~mongoengine.fields.ListField` field
|
||||
type. :class:`~mongoengine.fields.ListField` takes another field object as its first
|
||||
argument, which specifies which type elements may be stored within the list::
|
||||
|
||||
class Page(Document):
|
||||
@@ -191,7 +202,7 @@ inherit from :class:`~mongoengine.EmbeddedDocument` rather than
|
||||
content = StringField()
|
||||
|
||||
To embed the document within another document, use the
|
||||
:class:`~mongoengine.EmbeddedDocumentField` field type, providing the embedded
|
||||
:class:`~mongoengine.fields.EmbeddedDocumentField` field type, providing the embedded
|
||||
document class as the first argument::
|
||||
|
||||
class Page(Document):
|
||||
@@ -203,10 +214,10 @@ document class as the first argument::
|
||||
|
||||
Dictionary Fields
|
||||
-----------------
|
||||
Often, an embedded document may be used instead of a dictionary -- generally
|
||||
this is recommended as dictionaries don't support validation or custom field
|
||||
types. However, sometimes you will not know the structure of what you want to
|
||||
store; in this situation a :class:`~mongoengine.DictField` is appropriate::
|
||||
Often, an embedded document may be used instead of a dictionary – generally
|
||||
embedded documents are recommended as dictionaries don’t support validation
|
||||
or custom field types. However, sometimes you will not know the structure of what you want to
|
||||
store; in this situation a :class:`~mongoengine.fields.DictField` is appropriate::
|
||||
|
||||
class SurveyResponse(Document):
|
||||
date = DateTimeField()
|
||||
@@ -224,7 +235,7 @@ other objects, so are the most flexible field type available.
|
||||
Reference fields
|
||||
----------------
|
||||
References may be stored to other documents in the database using the
|
||||
:class:`~mongoengine.ReferenceField`. Pass in another document class as the
|
||||
:class:`~mongoengine.fields.ReferenceField`. Pass in another document class as the
|
||||
first argument to the constructor, then simply assign document objects to the
|
||||
field::
|
||||
|
||||
@@ -245,9 +256,9 @@ field::
|
||||
The :class:`User` object is automatically turned into a reference behind the
|
||||
scenes, and dereferenced when the :class:`Page` object is retrieved.
|
||||
|
||||
To add a :class:`~mongoengine.ReferenceField` that references the document
|
||||
To add a :class:`~mongoengine.fields.ReferenceField` that references the document
|
||||
being defined, use the string ``'self'`` in place of the document class as the
|
||||
argument to :class:`~mongoengine.ReferenceField`'s constructor. To reference a
|
||||
argument to :class:`~mongoengine.fields.ReferenceField`'s constructor. To reference a
|
||||
document that has not yet been defined, use the name of the undefined document
|
||||
as the constructor's argument::
|
||||
|
||||
@@ -288,6 +299,12 @@ instance of the object to the query::
|
||||
# Find all pages that both Bob and John have authored
|
||||
Page.objects(authors__all=[bob, john])
|
||||
|
||||
# Remove Bob from the authors for a page.
|
||||
Page.objects(id='...').update_one(pull__authors=bob)
|
||||
|
||||
# Add John to the authors for a page.
|
||||
Page.objects(id='...').update_one(push__authors=john)
|
||||
|
||||
|
||||
Dealing with deletion of referred documents
|
||||
'''''''''''''''''''''''''''''''''''''''''''
|
||||
@@ -299,12 +316,12 @@ reference with a delete rule specification. A delete rule is specified by
|
||||
supplying the :attr:`reverse_delete_rule` attributes on the
|
||||
:class:`ReferenceField` definition, like this::
|
||||
|
||||
class Employee(Document):
|
||||
class ProfilePage(Document):
|
||||
...
|
||||
profile_page = ReferenceField('ProfilePage', reverse_delete_rule=mongoengine.NULLIFY)
|
||||
employee = ReferenceField('Employee', reverse_delete_rule=mongoengine.CASCADE)
|
||||
|
||||
The declaration in this example means that when an :class:`Employee` object is
|
||||
removed, the :class:`ProfilePage` that belongs to that employee is removed as
|
||||
removed, the :class:`ProfilePage` that references that employee is removed as
|
||||
well. If a whole batch of employees is removed, all profile pages that are
|
||||
linked are removed as well.
|
||||
|
||||
@@ -320,12 +337,12 @@ Its value can take any of the following constants:
|
||||
Any object's fields still referring to the object being deleted are removed
|
||||
(using MongoDB's "unset" operation), effectively nullifying the relationship.
|
||||
:const:`mongoengine.CASCADE`
|
||||
Any object containing fields that are refererring to the object being deleted
|
||||
Any object containing fields that are referring to the object being deleted
|
||||
are deleted first.
|
||||
:const:`mongoengine.PULL`
|
||||
Removes the reference to the object (using MongoDB's "pull" operation)
|
||||
from any object's fields of
|
||||
:class:`~mongoengine.ListField` (:class:`~mongoengine.ReferenceField`).
|
||||
:class:`~mongoengine.fields.ListField` (:class:`~mongoengine.fields.ReferenceField`).
|
||||
|
||||
|
||||
.. warning::
|
||||
@@ -344,15 +361,10 @@ Its value can take any of the following constants:
|
||||
In Django, be sure to put all apps that have such delete rule declarations in
|
||||
their :file:`models.py` in the :const:`INSTALLED_APPS` tuple.
|
||||
|
||||
|
||||
.. warning::
|
||||
Signals are not triggered when doing cascading updates / deletes - if this
|
||||
is required you must manually handle the update / delete.
|
||||
|
||||
Generic reference fields
|
||||
''''''''''''''''''''''''
|
||||
A second kind of reference field also exists,
|
||||
:class:`~mongoengine.GenericReferenceField`. This allows you to reference any
|
||||
:class:`~mongoengine.fields.GenericReferenceField`. This allows you to reference any
|
||||
kind of :class:`~mongoengine.Document`, and hence doesn't take a
|
||||
:class:`~mongoengine.Document` subclass as a constructor argument::
|
||||
|
||||
@@ -376,18 +388,18 @@ kind of :class:`~mongoengine.Document`, and hence doesn't take a
|
||||
|
||||
.. note::
|
||||
|
||||
Using :class:`~mongoengine.GenericReferenceField`\ s is slightly less
|
||||
efficient than the standard :class:`~mongoengine.ReferenceField`\ s, so if
|
||||
Using :class:`~mongoengine.fields.GenericReferenceField`\ s is slightly less
|
||||
efficient than the standard :class:`~mongoengine.fields.ReferenceField`\ s, so if
|
||||
you will only be referencing one document type, prefer the standard
|
||||
:class:`~mongoengine.ReferenceField`.
|
||||
:class:`~mongoengine.fields.ReferenceField`.
|
||||
|
||||
Uniqueness constraints
|
||||
----------------------
|
||||
MongoEngine allows you to specify that a field should be unique across a
|
||||
collection by providing ``unique=True`` to a :class:`~mongoengine.Field`\ 's
|
||||
collection by providing ``unique=True`` to a :class:`~mongoengine.fields.Field`\ 's
|
||||
constructor. If you try to save a document that has the same value for a unique
|
||||
field as a document that is already in the database, a
|
||||
:class:`~mongoengine.OperationError` will be raised. You may also specify
|
||||
:class:`~mongoengine.NotUniqueError` will be raised. You may also specify
|
||||
multi-field uniqueness constraints by using :attr:`unique_with`, which may be
|
||||
either a single field name, or a list or tuple of field names::
|
||||
|
||||
@@ -399,7 +411,7 @@ either a single field name, or a list or tuple of field names::
|
||||
Skipping Document validation on save
|
||||
------------------------------------
|
||||
You can also skip the whole document validation process by setting
|
||||
``validate=False`` when caling the :meth:`~mongoengine.document.Document.save`
|
||||
``validate=False`` when calling the :meth:`~mongoengine.document.Document.save`
|
||||
method::
|
||||
|
||||
class Recipient(Document):
|
||||
@@ -414,7 +426,7 @@ Document collections
|
||||
====================
|
||||
Document classes that inherit **directly** from :class:`~mongoengine.Document`
|
||||
will have their own **collection** in the database. The name of the collection
|
||||
is by default the name of the class, coverted to lowercase (so in the example
|
||||
is by default the name of the class, converted to lowercase (so in the example
|
||||
above, the collection would be called `page`). If you need to change the name
|
||||
of the collection (e.g. to use MongoEngine with an existing database), then
|
||||
create a class dictionary attribute called :attr:`meta` on your document, and
|
||||
@@ -431,8 +443,10 @@ A :class:`~mongoengine.Document` may use a **Capped Collection** by specifying
|
||||
:attr:`max_documents` and :attr:`max_size` in the :attr:`meta` dictionary.
|
||||
:attr:`max_documents` is the maximum number of documents that is allowed to be
|
||||
stored in the collection, and :attr:`max_size` is the maximum size of the
|
||||
collection in bytes. If :attr:`max_size` is not specified and
|
||||
:attr:`max_documents` is, :attr:`max_size` defaults to 10000000 bytes (10MB).
|
||||
collection in bytes. :attr:`max_size` is rounded up to the next multiple of 256
|
||||
by MongoDB internally and mongoengine before. Use also a multiple of 256 to
|
||||
avoid confusions. If :attr:`max_size` is not specified and
|
||||
:attr:`max_documents` is, :attr:`max_size` defaults to 10485760 bytes (10MB).
|
||||
The following example shows a :class:`Log` document that will be limited to
|
||||
1000 entries and 2MB of disk space::
|
||||
|
||||
@@ -440,6 +454,8 @@ The following example shows a :class:`Log` document that will be limited to
|
||||
ip_address = StringField()
|
||||
meta = {'max_documents': 1000, 'max_size': 2000000}
|
||||
|
||||
.. defining-indexes_
|
||||
|
||||
Indexes
|
||||
=======
|
||||
|
||||
@@ -447,15 +463,31 @@ You can specify indexes on collections to make querying faster. This is done
|
||||
by creating a list of index specifications called :attr:`indexes` in the
|
||||
:attr:`~mongoengine.Document.meta` dictionary, where an index specification may
|
||||
either be a single field name, a tuple containing multiple field names, or a
|
||||
dictionary containing a full index definition. A direction may be specified on
|
||||
fields by prefixing the field name with a **+** or a **-** sign. Note that
|
||||
direction only matters on multi-field indexes. ::
|
||||
dictionary containing a full index definition.
|
||||
|
||||
A direction may be specified on fields by prefixing the field name with a
|
||||
**+** (for ascending) or a **-** sign (for descending). Note that direction
|
||||
only matters on multi-field indexes. Text indexes may be specified by prefixing
|
||||
the field name with a **$**. Hashed indexes may be specified by prefixing
|
||||
the field name with a **#**::
|
||||
|
||||
class Page(Document):
|
||||
category = IntField()
|
||||
title = StringField()
|
||||
rating = StringField()
|
||||
created = DateTimeField()
|
||||
meta = {
|
||||
'indexes': ['title', ('title', '-rating')]
|
||||
'indexes': [
|
||||
'title',
|
||||
'$title', # text index
|
||||
'#title', # hashed index
|
||||
('title', '-rating'),
|
||||
('category', '_cls'),
|
||||
{
|
||||
'fields': ['created'],
|
||||
'expireAfterSeconds': 3600
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
If a dictionary is passed then the following options are available:
|
||||
@@ -475,28 +507,97 @@ If a dictionary is passed then the following options are available:
|
||||
:attr:`unique` (Default: False)
|
||||
Whether the index should be unique.
|
||||
|
||||
:attr:`expireAfterSeconds` (Optional)
|
||||
Allows you to automatically expire data from a collection by setting the
|
||||
time in seconds to expire the a field.
|
||||
|
||||
.. note::
|
||||
|
||||
Inheritance adds extra fields indices see: :ref:`document-inheritance`.
|
||||
|
||||
Global index default options
|
||||
----------------------------
|
||||
|
||||
There are a few top level defaults for all indexes that can be set::
|
||||
|
||||
class Page(Document):
|
||||
title = StringField()
|
||||
rating = StringField()
|
||||
meta = {
|
||||
'index_options': {},
|
||||
'index_background': True,
|
||||
'index_drop_dups': True,
|
||||
'index_cls': False
|
||||
}
|
||||
|
||||
|
||||
:attr:`index_options` (Optional)
|
||||
Set any default index options - see the `full options list <http://docs.mongodb.org/manual/reference/method/db.collection.ensureIndex/#db.collection.ensureIndex>`_
|
||||
|
||||
:attr:`index_background` (Optional)
|
||||
Set the default value for if an index should be indexed in the background
|
||||
|
||||
:attr:`index_cls` (Optional)
|
||||
A way to turn off a specific index for _cls.
|
||||
|
||||
:attr:`index_drop_dups` (Optional)
|
||||
Set the default value for if an index should drop duplicates
|
||||
|
||||
.. note:: Since MongoDB 3.0 drop_dups is not supported anymore. Raises a Warning
|
||||
and has no effect
|
||||
|
||||
|
||||
Compound Indexes and Indexing sub documents
|
||||
-------------------------------------------
|
||||
|
||||
Compound indexes can be created by adding the Embedded field or dictionary
|
||||
field name to the index definition.
|
||||
|
||||
Sometimes its more efficient to index parts of Embeedded / dictionary fields,
|
||||
Sometimes its more efficient to index parts of Embedded / dictionary fields,
|
||||
in this case use 'dot' notation to identify the value to index eg: `rank.title`
|
||||
|
||||
.. _geospatial-indexes:
|
||||
|
||||
Geospatial indexes
|
||||
------------------
|
||||
|
||||
The best geo index for mongodb is the new "2dsphere", which has an improved
|
||||
spherical model and provides better performance and more options when querying.
|
||||
The following fields will explicitly add a "2dsphere" index:
|
||||
|
||||
- :class:`~mongoengine.fields.PointField`
|
||||
- :class:`~mongoengine.fields.LineStringField`
|
||||
- :class:`~mongoengine.fields.PolygonField`
|
||||
- :class:`~mongoengine.fields.MultiPointField`
|
||||
- :class:`~mongoengine.fields.MultiLineStringField`
|
||||
- :class:`~mongoengine.fields.MultiPolygonField`
|
||||
|
||||
As "2dsphere" indexes can be part of a compound index, you may not want the
|
||||
automatic index but would prefer a compound index. In this example we turn off
|
||||
auto indexing and explicitly declare a compound index on ``location`` and ``datetime``::
|
||||
|
||||
class Log(Document):
|
||||
location = PointField(auto_index=False)
|
||||
datetime = DateTimeField()
|
||||
|
||||
meta = {
|
||||
'indexes': [[("location", "2dsphere"), ("datetime", 1)]]
|
||||
}
|
||||
|
||||
|
||||
Pre MongoDB 2.4 Geo
|
||||
'''''''''''''''''''
|
||||
|
||||
.. note:: For MongoDB < 2.4 this is still current, however the new 2dsphere
|
||||
index is a big improvement over the previous 2D model - so upgrading is
|
||||
advised.
|
||||
|
||||
Geospatial indexes will be automatically created for all
|
||||
:class:`~mongoengine.GeoPointField`\ s
|
||||
:class:`~mongoengine.fields.GeoPointField`\ s
|
||||
|
||||
It is also possible to explicitly define geospatial indexes. This is
|
||||
useful if you need to define a geospatial index on a subfield of a
|
||||
:class:`~mongoengine.DictField` or a custom field that contains a
|
||||
:class:`~mongoengine.fields.DictField` or a custom field that contains a
|
||||
point. To create a geospatial index you must prefix the field with the
|
||||
***** sign. ::
|
||||
|
||||
@@ -508,6 +609,35 @@ point. To create a geospatial index you must prefix the field with the
|
||||
],
|
||||
}
|
||||
|
||||
Time To Live indexes
|
||||
--------------------
|
||||
|
||||
A special index type that allows you to automatically expire data from a
|
||||
collection after a given period. See the official
|
||||
`ttl <http://docs.mongodb.org/manual/tutorial/expire-data/#expire-data-from-collections-by-setting-ttl>`_
|
||||
documentation for more information. A common usecase might be session data::
|
||||
|
||||
class Session(Document):
|
||||
created = DateTimeField(default=datetime.now)
|
||||
meta = {
|
||||
'indexes': [
|
||||
{'fields': ['created'], 'expireAfterSeconds': 3600}
|
||||
]
|
||||
}
|
||||
|
||||
.. warning:: TTL indexes happen on the MongoDB server and not in the application
|
||||
code, therefore no signals will be fired on document deletion.
|
||||
If you need signals to be fired on deletion, then you must handle the
|
||||
deletion of Documents in your application code.
|
||||
|
||||
Comparing Indexes
|
||||
-----------------
|
||||
|
||||
Use :func:`mongoengine.Document.compare_indexes` to compare actual indexes in
|
||||
the database to those that your document definitions define. This is useful
|
||||
for maintenance purposes and ensuring you have the correct indexes for your
|
||||
schema.
|
||||
|
||||
Ordering
|
||||
========
|
||||
A default ordering can be specified for your
|
||||
@@ -552,11 +682,11 @@ Shard keys
|
||||
==========
|
||||
|
||||
If your collection is sharded, then you need to specify the shard key as a tuple,
|
||||
using the :attr:`shard_key` attribute of :attr:`-mongoengine.Document.meta`.
|
||||
using the :attr:`shard_key` attribute of :attr:`~mongoengine.Document.meta`.
|
||||
This ensures that the shard key is sent with the query when calling the
|
||||
:meth:`~mongoengine.document.Document.save` or
|
||||
:meth:`~mongoengine.document.Document.update` method on an existing
|
||||
:class:`-mongoengine.Document` instance::
|
||||
:class:`~mongoengine.Document` instance::
|
||||
|
||||
class LogEntry(Document):
|
||||
machine = StringField()
|
||||
@@ -578,7 +708,7 @@ defined, you may subclass it and add any extra fields or methods you may need.
|
||||
As this is new class is not a direct subclass of
|
||||
:class:`~mongoengine.Document`, it will not be stored in its own collection; it
|
||||
will use the same collection as its superclass uses. This allows for more
|
||||
convenient and efficient retrieval of related documents - all you need do is
|
||||
convenient and efficient retrieval of related documents -- all you need do is
|
||||
set :attr:`allow_inheritance` to True in the :attr:`meta` data for a
|
||||
document.::
|
||||
|
||||
@@ -592,13 +722,12 @@ document.::
|
||||
class DatedPage(Page):
|
||||
date = DateTimeField()
|
||||
|
||||
.. note:: From 0.8 onwards you must declare :attr:`allow_inheritance` defaults
|
||||
.. note:: From 0.8 onwards :attr:`allow_inheritance` defaults
|
||||
to False, meaning you must set it to True to use inheritance.
|
||||
|
||||
|
||||
Working with existing data
|
||||
--------------------------
|
||||
As MongoEngine no longer defaults to needing :attr:`_cls` you can quickly and
|
||||
As MongoEngine no longer defaults to needing :attr:`_cls`, you can quickly and
|
||||
easily get working with existing data. Just define the document to match
|
||||
the expected schema in your database ::
|
||||
|
||||
@@ -615,3 +744,25 @@ defining all possible field types.
|
||||
|
||||
If you use :class:`~mongoengine.Document` and the database contains data that
|
||||
isn't defined then that data will be stored in the `document._data` dictionary.
|
||||
|
||||
Abstract classes
|
||||
================
|
||||
|
||||
If you want to add some extra functionality to a group of Document classes but
|
||||
you don't need or want the overhead of inheritance you can use the
|
||||
:attr:`abstract` attribute of :attr:`~mongoengine.Document.meta`.
|
||||
This won't turn on :ref:`document-inheritance` but will allow you to keep your
|
||||
code DRY::
|
||||
|
||||
class BaseDocument(Document):
|
||||
meta = {
|
||||
'abstract': True,
|
||||
}
|
||||
def check_permissions(self):
|
||||
...
|
||||
|
||||
class User(BaseDocument):
|
||||
...
|
||||
|
||||
Now the User class will have access to the inherited `check_permissions` method
|
||||
and won't store any of the extra `_cls` information.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Documents instances
|
||||
===================
|
||||
To create a new document object, create an instance of the relevant document
|
||||
class, providing values for its fields as its constructor keyword arguments.
|
||||
class, providing values for its fields as constructor keyword arguments.
|
||||
You may provide values for any of the fields on the document::
|
||||
|
||||
>>> page = Page(title="Test Page")
|
||||
@@ -30,10 +30,13 @@ already exist, then any changes will be updated atomically. For example::
|
||||
|
||||
.. note::
|
||||
|
||||
Changes to documents are tracked and on the whole perform `set` operations.
|
||||
Changes to documents are tracked and on the whole perform ``set`` operations.
|
||||
|
||||
* ``list_field.pop(0)`` - *sets* the resulting list
|
||||
* ``del(list_field)`` - *unsets* whole list
|
||||
* ``list_field.push(0)`` --- *sets* the resulting list
|
||||
* ``del(list_field)`` --- *unsets* whole list
|
||||
|
||||
With lists its preferable to use ``Doc.update(push__list_field=0)`` as
|
||||
this stops the whole list being updated --- stopping any race conditions.
|
||||
|
||||
.. seealso::
|
||||
:ref:`guide-atomic-updates`
|
||||
@@ -68,11 +71,12 @@ document values for example::
|
||||
|
||||
Cascading Saves
|
||||
---------------
|
||||
If your document contains :class:`~mongoengine.ReferenceField` or
|
||||
:class:`~mongoengine.GenericReferenceField` objects, then by default the
|
||||
:meth:`~mongoengine.Document.save` method will automatically save any changes to
|
||||
those objects as well. If this is not desired passing :attr:`cascade` as False
|
||||
to the save method turns this feature off.
|
||||
If your document contains :class:`~mongoengine.fields.ReferenceField` or
|
||||
:class:`~mongoengine.fields.GenericReferenceField` objects, then by default the
|
||||
:meth:`~mongoengine.Document.save` method will not save any changes to
|
||||
those objects. If you want all references to be saved also, noting each
|
||||
save is a separate query, then passing :attr:`cascade` as True
|
||||
to the save method will cascade any saves.
|
||||
|
||||
Deleting documents
|
||||
------------------
|
||||
@@ -109,12 +113,13 @@ you may still use :attr:`id` to access the primary key if you want::
|
||||
>>> bob.id == bob.email == 'bob@example.com'
|
||||
True
|
||||
|
||||
You can also access the document's "primary key" using the :attr:`pk` field; in
|
||||
is an alias to :attr:`id`::
|
||||
You can also access the document's "primary key" using the :attr:`pk` field,
|
||||
it's an alias to :attr:`id`::
|
||||
|
||||
>>> page = Page(title="Another Test Page")
|
||||
>>> page.save()
|
||||
>>> page.id == page.pk
|
||||
True
|
||||
|
||||
.. note::
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ GridFS
|
||||
Writing
|
||||
-------
|
||||
|
||||
GridFS support comes in the form of the :class:`~mongoengine.FileField` field
|
||||
GridFS support comes in the form of the :class:`~mongoengine.fields.FileField` field
|
||||
object. This field acts as a file-like object and provides a couple of
|
||||
different ways of inserting and retrieving data. Arbitrary metadata such as
|
||||
content type can also be stored alongside the files. In the following example,
|
||||
@@ -20,14 +20,14 @@ a document is created to store details about animals, including a photo::
|
||||
|
||||
marmot = Animal(genus='Marmota', family='Sciuridae')
|
||||
|
||||
marmot_photo = open('marmot.jpg', 'r')
|
||||
marmot_photo = open('marmot.jpg', 'rb')
|
||||
marmot.photo.put(marmot_photo, content_type = 'image/jpeg')
|
||||
marmot.save()
|
||||
|
||||
Retrieval
|
||||
---------
|
||||
|
||||
So using the :class:`~mongoengine.FileField` is just like using any other
|
||||
So using the :class:`~mongoengine.fields.FileField` is just like using any other
|
||||
field. The file can also be retrieved just as easily::
|
||||
|
||||
marmot = Animal.objects(genus='Marmota').first()
|
||||
@@ -37,7 +37,7 @@ field. The file can also be retrieved just as easily::
|
||||
Streaming
|
||||
---------
|
||||
|
||||
Streaming data into a :class:`~mongoengine.FileField` is achieved in a
|
||||
Streaming data into a :class:`~mongoengine.fields.FileField` is achieved in a
|
||||
slightly different manner. First, a new file must be created by calling the
|
||||
:func:`new_file` method. Data can then be written using :func:`write`::
|
||||
|
||||
@@ -46,7 +46,7 @@ slightly different manner. First, a new file must be created by calling the
|
||||
marmot.photo.write('some_more_image_data')
|
||||
marmot.photo.close()
|
||||
|
||||
marmot.photo.save()
|
||||
marmot.save()
|
||||
|
||||
Deletion
|
||||
--------
|
||||
@@ -70,5 +70,5 @@ Replacing files
|
||||
Files can be replaced with the :func:`replace` method. This works just like
|
||||
the :func:`put` method so even metadata can (and should) be replaced::
|
||||
|
||||
another_marmot = open('another_marmot.png', 'r')
|
||||
another_marmot = open('another_marmot.png', 'rb')
|
||||
marmot.photo.replace(another_marmot, content_type='image/png')
|
||||
|
||||
@@ -12,3 +12,5 @@ User Guide
|
||||
querying
|
||||
gridfs
|
||||
signals
|
||||
text-indexes
|
||||
mongomock
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
Installing MongoEngine
|
||||
======================
|
||||
|
||||
To use MongoEngine, you will need to download `MongoDB <http://mongodb.org/>`_
|
||||
To use MongoEngine, you will need to download `MongoDB <http://mongodb.com/>`_
|
||||
and ensure it is running in an accessible location. You will also need
|
||||
`PyMongo <http://api.mongodb.org/python>`_ to use MongoEngine, but if you
|
||||
install MongoEngine using setuptools, then the dependencies will be handled for
|
||||
you.
|
||||
|
||||
MongoEngine is available on PyPI, so to use it you can use :program:`pip`:
|
||||
MongoEngine is available on PyPI, so you can use :program:`pip`:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
@@ -22,10 +22,10 @@ Alternatively, if you don't have setuptools installed, `download it from PyPi
|
||||
$ python setup.py install
|
||||
|
||||
To use the bleeding-edge version of MongoEngine, you can get the source from
|
||||
`GitHub <http://github.com/hmarr/mongoengine/>`_ and install it as above:
|
||||
`GitHub <http://github.com/mongoengine/mongoengine/>`_ and install it as above:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ git clone git://github.com/hmarr/mongoengine
|
||||
$ git clone git://github.com/mongoengine/mongoengine
|
||||
$ cd mongoengine
|
||||
$ python setup.py install
|
||||
|
||||
21
docs/guide/mongomock.rst
Normal file
21
docs/guide/mongomock.rst
Normal file
@@ -0,0 +1,21 @@
|
||||
==============================
|
||||
Use mongomock for testing
|
||||
==============================
|
||||
|
||||
`mongomock <https://github.com/vmalloc/mongomock/>`_ is a package to do just
|
||||
what the name implies, mocking a mongo database.
|
||||
|
||||
To use with mongoengine, simply specify mongomock when connecting with
|
||||
mongoengine:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
connect('mongoenginetest', host='mongomock://localhost')
|
||||
conn = get_connection()
|
||||
|
||||
or with an alias:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
connect('mongoenginetest', host='mongomock://localhost', alias='testdb')
|
||||
conn = get_connection('testdb')
|
||||
@@ -15,11 +15,10 @@ fetch documents from the database::
|
||||
|
||||
.. note::
|
||||
|
||||
Once the iteration finishes (when :class:`StopIteration` is raised),
|
||||
:meth:`~mongoengine.queryset.QuerySet.rewind` will be called so that the
|
||||
:class:`~mongoengine.queryset.QuerySet` may be iterated over again. The
|
||||
results of the first iteration are *not* cached, so the database will be hit
|
||||
each time the :class:`~mongoengine.queryset.QuerySet` is iterated over.
|
||||
As of MongoEngine 0.8 the querysets utilise a local cache. So iterating
|
||||
it multiple times will only cause a single query. If this is not the
|
||||
desired behaviour you can call :class:`~mongoengine.QuerySet.no_cache`
|
||||
(version **0.8.3+**) to return a non-caching queryset.
|
||||
|
||||
Filtering queries
|
||||
=================
|
||||
@@ -40,10 +39,18 @@ syntax::
|
||||
# been written by a user whose 'country' field is set to 'uk'
|
||||
uk_pages = Page.objects(author__country='uk')
|
||||
|
||||
.. note::
|
||||
|
||||
(version **0.9.1+**) if your field name is like mongodb operator name (for example
|
||||
type, lte, lt...) and you want to place it at the end of lookup keyword
|
||||
mongoengine automatically prepend $ to it. To avoid this use __ at the end of
|
||||
your lookup keyword. For example if your field name is ``type`` and you want to
|
||||
query by this field you must use ``.objects(user__type__="admin")`` instead of
|
||||
``.objects(user__type="admin")``
|
||||
|
||||
Query operators
|
||||
===============
|
||||
Operators other than equality may also be used in queries; just attach the
|
||||
Operators other than equality may also be used in queries --- just attach the
|
||||
operator name to a key with a double-underscore::
|
||||
|
||||
# Only find users whose age is 18 or less
|
||||
@@ -65,6 +72,9 @@ Available operators are as follows:
|
||||
* ``size`` -- the size of the array is
|
||||
* ``exists`` -- value for field exists
|
||||
|
||||
String queries
|
||||
--------------
|
||||
|
||||
The following operators are available as shortcuts to querying with regular
|
||||
expressions:
|
||||
|
||||
@@ -78,12 +88,75 @@ expressions:
|
||||
* ``iendswith`` -- string field ends with value (case insensitive)
|
||||
* ``match`` -- performs an $elemMatch so you can match an entire document within an array
|
||||
|
||||
There are a few special operators for performing geographical queries, that
|
||||
may used with :class:`~mongoengine.GeoPointField`\ s:
|
||||
|
||||
Geo queries
|
||||
-----------
|
||||
|
||||
There are a few special operators for performing geographical queries.
|
||||
The following were added in MongoEngine 0.8 for
|
||||
:class:`~mongoengine.fields.PointField`,
|
||||
:class:`~mongoengine.fields.LineStringField` and
|
||||
:class:`~mongoengine.fields.PolygonField`:
|
||||
|
||||
* ``geo_within`` -- check if a geometry is within a polygon. For ease of use
|
||||
it accepts either a geojson geometry or just the polygon coordinates eg::
|
||||
|
||||
loc.objects(point__geo_within=[[[40, 5], [40, 6], [41, 6], [40, 5]]])
|
||||
loc.objects(point__geo_within={"type": "Polygon",
|
||||
"coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]})
|
||||
|
||||
* ``geo_within_box`` -- simplified geo_within searching with a box eg::
|
||||
|
||||
loc.objects(point__geo_within_box=[(-125.0, 35.0), (-100.0, 40.0)])
|
||||
loc.objects(point__geo_within_box=[<bottom left coordinates>, <upper right coordinates>])
|
||||
|
||||
* ``geo_within_polygon`` -- simplified geo_within searching within a simple polygon eg::
|
||||
|
||||
loc.objects(point__geo_within_polygon=[[40, 5], [40, 6], [41, 6], [40, 5]])
|
||||
loc.objects(point__geo_within_polygon=[ [ <x1> , <y1> ] ,
|
||||
[ <x2> , <y2> ] ,
|
||||
[ <x3> , <y3> ] ])
|
||||
|
||||
* ``geo_within_center`` -- simplified geo_within the flat circle radius of a point eg::
|
||||
|
||||
loc.objects(point__geo_within_center=[(-125.0, 35.0), 1])
|
||||
loc.objects(point__geo_within_center=[ [ <x>, <y> ] , <radius> ])
|
||||
|
||||
* ``geo_within_sphere`` -- simplified geo_within the spherical circle radius of a point eg::
|
||||
|
||||
loc.objects(point__geo_within_sphere=[(-125.0, 35.0), 1])
|
||||
loc.objects(point__geo_within_sphere=[ [ <x>, <y> ] , <radius> ])
|
||||
|
||||
* ``geo_intersects`` -- selects all locations that intersect with a geometry eg::
|
||||
|
||||
# Inferred from provided points lists:
|
||||
loc.objects(poly__geo_intersects=[40, 6])
|
||||
loc.objects(poly__geo_intersects=[[40, 5], [40, 6]])
|
||||
loc.objects(poly__geo_intersects=[[[40, 5], [40, 6], [41, 6], [41, 5], [40, 5]]])
|
||||
|
||||
# With geoJson style objects
|
||||
loc.objects(poly__geo_intersects={"type": "Point", "coordinates": [40, 6]})
|
||||
loc.objects(poly__geo_intersects={"type": "LineString",
|
||||
"coordinates": [[40, 5], [40, 6]]})
|
||||
loc.objects(poly__geo_intersects={"type": "Polygon",
|
||||
"coordinates": [[[40, 5], [40, 6], [41, 6], [41, 5], [40, 5]]]})
|
||||
|
||||
* ``near`` -- find all the locations near a given point::
|
||||
|
||||
loc.objects(point__near=[40, 5])
|
||||
loc.objects(point__near={"type": "Point", "coordinates": [40, 5]})
|
||||
|
||||
You can also set the maximum and/or the minimum distance in meters as well::
|
||||
|
||||
loc.objects(point__near=[40, 5], point__max_distance=1000)
|
||||
loc.objects(point__near=[40, 5], point__min_distance=100)
|
||||
|
||||
The older 2D indexes are still supported with the
|
||||
:class:`~mongoengine.fields.GeoPointField`:
|
||||
|
||||
* ``within_distance`` -- provide a list containing a point and a maximum
|
||||
distance (e.g. [(41.342, -87.653), 5])
|
||||
* ``within_spherical_distance`` -- Same as above but using the spherical geo model
|
||||
* ``within_spherical_distance`` -- same as above but using the spherical geo model
|
||||
(e.g. [(41.342, -87.653), 5/earth_radius])
|
||||
* ``near`` -- order the documents by how close they are to a given point
|
||||
* ``near_sphere`` -- Same as above but using the spherical geo model
|
||||
@@ -91,16 +164,19 @@ may used with :class:`~mongoengine.GeoPointField`\ s:
|
||||
[(35.0, -125.0), (40.0, -100.0)])
|
||||
* ``within_polygon`` -- filter documents to those within a given polygon (e.g.
|
||||
[(41.91,-87.69), (41.92,-87.68), (41.91,-87.65), (41.89,-87.65)]).
|
||||
|
||||
.. note:: Requires Mongo Server 2.0
|
||||
|
||||
* ``max_distance`` -- can be added to your location queries to set a maximum
|
||||
distance.
|
||||
|
||||
* ``min_distance`` -- can be added to your location queries to set a minimum
|
||||
distance.
|
||||
|
||||
Querying lists
|
||||
--------------
|
||||
On most fields, this syntax will look up documents where the field specified
|
||||
matches the given value exactly, but when the field refers to a
|
||||
:class:`~mongoengine.ListField`, a single item may be provided, in which case
|
||||
:class:`~mongoengine.fields.ListField`, a single item may be provided, in which case
|
||||
lists that contain that item will be matched::
|
||||
|
||||
class Page(Document):
|
||||
@@ -131,12 +207,14 @@ However, this doesn't map well to the syntax so you can also use a capital S ins
|
||||
|
||||
Post.objects(comments__by="joe").update(inc__comments__S__votes=1)
|
||||
|
||||
.. note:: Due to Mongo currently the $ operator only applies to the first matched item in the query.
|
||||
.. note::
|
||||
Due to :program:`Mongo`, currently the $ operator only applies to the
|
||||
first matched item in the query.
|
||||
|
||||
|
||||
Raw queries
|
||||
-----------
|
||||
It is possible to provide a raw PyMongo query as a query parameter, which will
|
||||
It is possible to provide a raw :mod:`PyMongo` query as a query parameter, which will
|
||||
be integrated directly into the query. This is done using the ``__raw__``
|
||||
keyword argument::
|
||||
|
||||
@@ -146,12 +224,12 @@ keyword argument::
|
||||
|
||||
Limiting and skipping results
|
||||
=============================
|
||||
Just as with traditional ORMs, you may limit the number of results returned, or
|
||||
Just as with traditional ORMs, you may limit the number of results returned or
|
||||
skip a number or results in you query.
|
||||
:meth:`~mongoengine.queryset.QuerySet.limit` and
|
||||
:meth:`~mongoengine.queryset.QuerySet.skip` and methods are available on
|
||||
:class:`~mongoengine.queryset.QuerySet` objects, but the prefered syntax for
|
||||
achieving this is using array-slicing syntax::
|
||||
:class:`~mongoengine.queryset.QuerySet` objects, but the `array-slicing` syntax
|
||||
is preferred for achieving this::
|
||||
|
||||
# Only the first 5 people
|
||||
users = User.objects[:5]
|
||||
@@ -159,7 +237,7 @@ achieving this is using array-slicing syntax::
|
||||
# All except for the first 5 people
|
||||
users = User.objects[5:]
|
||||
|
||||
# 5 users, starting from the 10th user found
|
||||
# 5 users, starting from the 11th user found
|
||||
users = User.objects[10:15]
|
||||
|
||||
You may also index the query to retrieve a single result. If an item at that
|
||||
@@ -185,23 +263,17 @@ To retrieve a result that should be unique in the collection, use
|
||||
no document matches the query, and
|
||||
:class:`~mongoengine.queryset.MultipleObjectsReturned`
|
||||
if more than one document matched the query. These exceptions are merged into
|
||||
your document defintions eg: `MyDoc.DoesNotExist`
|
||||
your document definitions eg: `MyDoc.DoesNotExist`
|
||||
|
||||
A variation of this method exists,
|
||||
:meth:`~mongoengine.queryset.Queryset.get_or_create`, that will create a new
|
||||
document with the query arguments if no documents match the query. An
|
||||
additional keyword argument, :attr:`defaults` may be provided, which will be
|
||||
used as default values for the new document, in the case that it should need
|
||||
to be created::
|
||||
|
||||
>>> a, created = User.objects.get_or_create(name='User A', defaults={'age': 30})
|
||||
>>> b, created = User.objects.get_or_create(name='User A', defaults={'age': 40})
|
||||
>>> a.name == b.name and a.age == b.age
|
||||
True
|
||||
A variation of this method, get_or_create() existed, but it was unsafe. It
|
||||
could not be made safe, because there are no transactions in mongoDB. Other
|
||||
approaches should be investigated, to ensure you don't accidentally duplicate
|
||||
data when using something similar to this method. Therefore it was deprecated
|
||||
in 0.8 and removed in 0.10.
|
||||
|
||||
Default Document queries
|
||||
========================
|
||||
By default, the objects :attr:`~mongoengine.Document.objects` attribute on a
|
||||
By default, the objects :attr:`~Document.objects` attribute on a
|
||||
document returns a :class:`~mongoengine.queryset.QuerySet` that doesn't filter
|
||||
the collection -- it returns all objects. This may be changed by defining a
|
||||
method on a document that modifies a queryset. The method should accept two
|
||||
@@ -244,7 +316,7 @@ Should you want to add custom methods for interacting with or filtering
|
||||
documents, extending the :class:`~mongoengine.queryset.QuerySet` class may be
|
||||
the way to go. To use a custom :class:`~mongoengine.queryset.QuerySet` class on
|
||||
a document, set ``queryset_class`` to the custom class in a
|
||||
:class:`~mongoengine.Document`\ s ``meta`` dictionary::
|
||||
:class:`~mongoengine.Document`'s ``meta`` dictionary::
|
||||
|
||||
class AwesomerQuerySet(QuerySet):
|
||||
|
||||
@@ -268,12 +340,19 @@ Javascript code that is executed on the database server.
|
||||
|
||||
Counting results
|
||||
----------------
|
||||
Just as with limiting and skipping results, there is a method on
|
||||
:class:`~mongoengine.queryset.QuerySet` objects --
|
||||
:meth:`~mongoengine.queryset.QuerySet.count`, but there is also a more Pythonic
|
||||
way of achieving this::
|
||||
Just as with limiting and skipping results, there is a method on a
|
||||
:class:`~mongoengine.queryset.QuerySet` object --
|
||||
:meth:`~mongoengine.queryset.QuerySet.count`::
|
||||
|
||||
num_users = len(User.objects)
|
||||
num_users = User.objects.count()
|
||||
|
||||
You could technically use ``len(User.objects)`` to get the same result, but it
|
||||
would be significantly slower than :meth:`~mongoengine.queryset.QuerySet.count`.
|
||||
When you execute a server-side count query, you let MongoDB do the heavy
|
||||
lifting and you receive a single integer over the wire. Meanwhile, len()
|
||||
retrieves all the results, places them in a local cache, and finally counts
|
||||
them. If we compare the performance of the two operations, len() is much slower
|
||||
than :meth:`~mongoengine.queryset.QuerySet.count`.
|
||||
|
||||
Further aggregation
|
||||
-------------------
|
||||
@@ -319,7 +398,7 @@ Retrieving a subset of fields
|
||||
Sometimes a subset of fields on a :class:`~mongoengine.Document` is required,
|
||||
and for efficiency only these should be retrieved from the database. This issue
|
||||
is especially important for MongoDB, as fields may often be extremely large
|
||||
(e.g. a :class:`~mongoengine.ListField` of
|
||||
(e.g. a :class:`~mongoengine.fields.ListField` of
|
||||
:class:`~mongoengine.EmbeddedDocument`\ s, which represent the comments on a
|
||||
blog post. To select only a subset of fields, use
|
||||
:meth:`~mongoengine.queryset.QuerySet.only`, specifying the fields you want to
|
||||
@@ -351,14 +430,14 @@ If you later need the missing fields, just call
|
||||
Getting related data
|
||||
--------------------
|
||||
|
||||
When iterating the results of :class:`~mongoengine.ListField` or
|
||||
:class:`~mongoengine.DictField` we automatically dereference any
|
||||
When iterating the results of :class:`~mongoengine.fields.ListField` or
|
||||
:class:`~mongoengine.fields.DictField` we automatically dereference any
|
||||
:class:`~pymongo.dbref.DBRef` objects as efficiently as possible, reducing the
|
||||
number the queries to mongo.
|
||||
|
||||
There are times when that efficiency is not enough, documents that have
|
||||
:class:`~mongoengine.ReferenceField` objects or
|
||||
:class:`~mongoengine.GenericReferenceField` objects at the top level are
|
||||
:class:`~mongoengine.fields.ReferenceField` objects or
|
||||
:class:`~mongoengine.fields.GenericReferenceField` objects at the top level are
|
||||
expensive as the number of queries to MongoDB can quickly rise.
|
||||
|
||||
To limit the number of queries use
|
||||
@@ -392,6 +471,7 @@ You can also turn off all dereferencing for a fixed period by using the
|
||||
|
||||
Advanced queries
|
||||
================
|
||||
|
||||
Sometimes calling a :class:`~mongoengine.queryset.QuerySet` object with keyword
|
||||
arguments can't fully express the query you want to use -- for example if you
|
||||
need to combine a number of constraints using *and* and *or*. This is made
|
||||
@@ -404,34 +484,46 @@ operators. To use a :class:`~mongoengine.queryset.Q` object, pass it in as the
|
||||
first positional argument to :attr:`Document.objects` when you filter it by
|
||||
calling it with keyword arguments::
|
||||
|
||||
from mongoengine.queryset.visitor import Q
|
||||
|
||||
# Get published posts
|
||||
Post.objects(Q(published=True) | Q(publish_date__lte=datetime.now()))
|
||||
|
||||
# Get top posts
|
||||
Post.objects((Q(featured=True) & Q(hits__gte=1000)) | Q(hits__gte=5000))
|
||||
|
||||
.. warning:: You have to use bitwise operators. You cannot use ``or``, ``and``
|
||||
to combine queries as ``Q(a=a) or Q(b=b)`` is not the same as
|
||||
``Q(a=a) | Q(b=b)``. As ``Q(a=a)`` equates to true ``Q(a=a) or Q(b=b)`` is
|
||||
the same as ``Q(a=a)``.
|
||||
|
||||
.. _guide-atomic-updates:
|
||||
|
||||
Atomic updates
|
||||
==============
|
||||
Documents may be updated atomically by using the
|
||||
:meth:`~mongoengine.queryset.QuerySet.update_one` and
|
||||
:meth:`~mongoengine.queryset.QuerySet.update` methods on a
|
||||
:meth:`~mongoengine.queryset.QuerySet`. There are several different "modifiers"
|
||||
that you may use with these methods:
|
||||
:meth:`~mongoengine.queryset.QuerySet.update_one`,
|
||||
:meth:`~mongoengine.queryset.QuerySet.update` and
|
||||
:meth:`~mongoengine.queryset.QuerySet.modify` methods on a
|
||||
:class:`~mongoengine.queryset.QuerySet` or
|
||||
:meth:`~mongoengine.Document.modify` and
|
||||
:meth:`~mongoengine.Document.save` (with :attr:`save_condition` argument) on a
|
||||
:class:`~mongoengine.Document`.
|
||||
There are several different "modifiers" that you may use with these methods:
|
||||
|
||||
* ``set`` -- set a particular value
|
||||
* ``unset`` -- delete a particular value (since MongoDB v1.3+)
|
||||
* ``unset`` -- delete a particular value (since MongoDB v1.3)
|
||||
* ``inc`` -- increment a value by a given amount
|
||||
* ``dec`` -- decrement a value by a given amount
|
||||
* ``pop`` -- remove the last item from a list
|
||||
* ``push`` -- append a value to a list
|
||||
* ``push_all`` -- append several values to a list
|
||||
* ``pop`` -- remove the first or last element of a list
|
||||
* ``pop`` -- remove the first or last element of a list `depending on the value`_
|
||||
* ``pull`` -- remove a value from a list
|
||||
* ``pull_all`` -- remove several values from a list
|
||||
* ``add_to_set`` -- add value to a list only if its not in the list already
|
||||
|
||||
.. _depending on the value: http://docs.mongodb.org/manual/reference/operator/update/pop/
|
||||
|
||||
The syntax for atomic updates is similar to the querying syntax, but the
|
||||
modifier comes before the field, not after it::
|
||||
|
||||
@@ -450,7 +542,14 @@ modifier comes before the field, not after it::
|
||||
>>> post.tags
|
||||
['database', 'nosql']
|
||||
|
||||
.. note ::
|
||||
.. note::
|
||||
|
||||
If no modifier operator is specified the default will be ``$set``. So the following sentences are identical::
|
||||
|
||||
>>> BlogPost.objects(id=post.id).update(title='Example Post')
|
||||
>>> BlogPost.objects(id=post.id).update(set__title='Example Post')
|
||||
|
||||
.. note::
|
||||
|
||||
In version 0.5 the :meth:`~mongoengine.Document.save` runs atomic updates
|
||||
on changed documents by tracking changes to that document.
|
||||
@@ -466,7 +565,16 @@ cannot use the `$` syntax in keyword arguments it has been mapped to `S`::
|
||||
>>> post.tags
|
||||
['database', 'mongodb']
|
||||
|
||||
.. note ::
|
||||
From MongoDB version 2.6, push operator supports $position value which allows
|
||||
to push values with index.
|
||||
>>> post = BlogPost(title="Test", tags=["mongo"])
|
||||
>>> post.save()
|
||||
>>> post.update(push__tags__0=["database", "code"])
|
||||
>>> post.reload()
|
||||
>>> post.tags
|
||||
['database', 'code', 'mongo']
|
||||
|
||||
.. note::
|
||||
Currently only top level lists are handled, future versions of mongodb /
|
||||
pymongo plan to support nested positional operators. See `The $ positional
|
||||
operator <http://www.mongodb.org/display/DOCS/Updating#Updating-The%24positionaloperator>`_.
|
||||
@@ -508,7 +616,7 @@ Some variables are made available in the scope of the Javascript function:
|
||||
|
||||
The following example demonstrates the intended usage of
|
||||
:meth:`~mongoengine.queryset.QuerySet.exec_js` by defining a function that sums
|
||||
over a field on a document (this functionality is already available throught
|
||||
over a field on a document (this functionality is already available through
|
||||
:meth:`~mongoengine.queryset.QuerySet.sum` but is shown here for sake of
|
||||
example)::
|
||||
|
||||
@@ -535,7 +643,7 @@ Javascript code. When accessing a field on a collection object, use
|
||||
square-bracket notation, and prefix the MongoEngine field name with a tilde.
|
||||
The field name that follows the tilde will be translated to the name used in
|
||||
the database. Note that when referring to fields on embedded documents,
|
||||
the name of the :class:`~mongoengine.EmbeddedDocumentField`, followed by a dot,
|
||||
the name of the :class:`~mongoengine.fields.EmbeddedDocumentField`, followed by a dot,
|
||||
should be used before the name of the field on the embedded document. The
|
||||
following example shows how the substitutions are made::
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.. _signals:
|
||||
|
||||
=======
|
||||
Signals
|
||||
=======
|
||||
|
||||
@@ -7,32 +8,95 @@ Signals
|
||||
|
||||
.. note::
|
||||
|
||||
Signal support is provided by the excellent `blinker`_ library and
|
||||
will gracefully fall back if it is not available.
|
||||
Signal support is provided by the excellent `blinker`_ library. If you wish
|
||||
to enable signal support this library must be installed, though it is not
|
||||
required for MongoEngine to function.
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
The following document signals exist in MongoEngine and are pretty self-explanatory:
|
||||
Signals are found within the `mongoengine.signals` module. Unless
|
||||
specified signals receive no additional arguments beyond the `sender` class and
|
||||
`document` instance. Post-signals are only called if there were no exceptions
|
||||
raised during the processing of their related function.
|
||||
|
||||
* `mongoengine.signals.pre_init`
|
||||
* `mongoengine.signals.post_init`
|
||||
* `mongoengine.signals.pre_save`
|
||||
* `mongoengine.signals.post_save`
|
||||
* `mongoengine.signals.pre_delete`
|
||||
* `mongoengine.signals.post_delete`
|
||||
* `mongoengine.signals.pre_bulk_insert`
|
||||
* `mongoengine.signals.post_bulk_insert`
|
||||
Available signals include:
|
||||
|
||||
Example usage::
|
||||
`pre_init`
|
||||
Called during the creation of a new :class:`~mongoengine.Document` or
|
||||
:class:`~mongoengine.EmbeddedDocument` instance, after the constructor
|
||||
arguments have been collected but before any additional processing has been
|
||||
done to them. (I.e. assignment of default values.) Handlers for this signal
|
||||
are passed the dictionary of arguments using the `values` keyword argument
|
||||
and may modify this dictionary prior to returning.
|
||||
|
||||
`post_init`
|
||||
Called after all processing of a new :class:`~mongoengine.Document` or
|
||||
:class:`~mongoengine.EmbeddedDocument` instance has been completed.
|
||||
|
||||
`pre_save`
|
||||
Called within :meth:`~mongoengine.Document.save` prior to performing
|
||||
any actions.
|
||||
|
||||
`pre_save_post_validation`
|
||||
Called within :meth:`~mongoengine.Document.save` after validation
|
||||
has taken place but before saving.
|
||||
|
||||
`post_save`
|
||||
Called within :meth:`~mongoengine.Document.save` after all actions
|
||||
(validation, insert/update, cascades, clearing dirty flags) have completed
|
||||
successfully. Passed the additional boolean keyword argument `created` to
|
||||
indicate if the save was an insert or an update.
|
||||
|
||||
`pre_delete`
|
||||
Called within :meth:`~mongoengine.Document.delete` prior to
|
||||
attempting the delete operation.
|
||||
|
||||
`post_delete`
|
||||
Called within :meth:`~mongoengine.Document.delete` upon successful
|
||||
deletion of the record.
|
||||
|
||||
`pre_bulk_insert`
|
||||
Called after validation of the documents to insert, but prior to any data
|
||||
being written. In this case, the `document` argument is replaced by a
|
||||
`documents` argument representing the list of documents being inserted.
|
||||
|
||||
`post_bulk_insert`
|
||||
Called after a successful bulk insert operation. As per `pre_bulk_insert`,
|
||||
the `document` argument is omitted and replaced with a `documents` argument.
|
||||
An additional boolean argument, `loaded`, identifies the contents of
|
||||
`documents` as either :class:`~mongoengine.Document` instances when `True` or
|
||||
simply a list of primary key values for the inserted records if `False`.
|
||||
|
||||
Attaching Events
|
||||
----------------
|
||||
|
||||
After writing a handler function like the following::
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine import signals
|
||||
|
||||
def update_modified(sender, document):
|
||||
document.modified = datetime.utcnow()
|
||||
|
||||
You attach the event handler to your :class:`~mongoengine.Document` or
|
||||
:class:`~mongoengine.EmbeddedDocument` subclass::
|
||||
|
||||
class Record(Document):
|
||||
modified = DateTimeField()
|
||||
|
||||
signals.pre_save.connect(update_modified)
|
||||
|
||||
While this is not the most elaborate document model, it does demonstrate the
|
||||
concepts involved. As a more complete demonstration you can also define your
|
||||
handlers within your subclass::
|
||||
|
||||
class Author(Document):
|
||||
name = StringField()
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def pre_save(cls, sender, document, **kwargs):
|
||||
logging.debug("Pre Save: %s" % document.name)
|
||||
@@ -49,12 +113,33 @@ Example usage::
|
||||
signals.pre_save.connect(Author.pre_save, sender=Author)
|
||||
signals.post_save.connect(Author.post_save, sender=Author)
|
||||
|
||||
Finally, you can also use this small decorator to quickly create a number of
|
||||
signals and attach them to your :class:`~mongoengine.Document` or
|
||||
:class:`~mongoengine.EmbeddedDocument` subclasses as class decorators::
|
||||
|
||||
ReferenceFields and signals
|
||||
---------------------------
|
||||
def handler(event):
|
||||
"""Signal decorator to allow use of callback functions as class decorators."""
|
||||
|
||||
def decorator(fn):
|
||||
def apply(cls):
|
||||
event.connect(fn, sender=cls)
|
||||
return cls
|
||||
|
||||
fn.apply = apply
|
||||
return fn
|
||||
|
||||
return decorator
|
||||
|
||||
Using the first example of updating a modification time the code is now much
|
||||
cleaner looking while still allowing manual execution of the callback::
|
||||
|
||||
@handler(signals.pre_save)
|
||||
def update_modified(sender, document):
|
||||
document.modified = datetime.utcnow()
|
||||
|
||||
@update_modified.apply
|
||||
class Record(Document):
|
||||
modified = DateTimeField()
|
||||
|
||||
Currently `reverse_delete_rules` do not trigger signals on the other part of
|
||||
the relationship. If this is required you must manually handled the
|
||||
reverse deletion.
|
||||
|
||||
.. _blinker: http://pypi.python.org/pypi/blinker
|
||||
|
||||
51
docs/guide/text-indexes.rst
Normal file
51
docs/guide/text-indexes.rst
Normal file
@@ -0,0 +1,51 @@
|
||||
===========
|
||||
Text Search
|
||||
===========
|
||||
|
||||
After MongoDB 2.4 version, supports search documents by text indexes.
|
||||
|
||||
|
||||
Defining a Document with text index
|
||||
===================================
|
||||
Use the *$* prefix to set a text index, Look the declaration::
|
||||
|
||||
class News(Document):
|
||||
title = StringField()
|
||||
content = StringField()
|
||||
is_active = BooleanField()
|
||||
|
||||
meta = {'indexes': [
|
||||
{'fields': ['$title', "$content"],
|
||||
'default_language': 'english',
|
||||
'weights': {'title': 10, 'content': 2}
|
||||
}
|
||||
]}
|
||||
|
||||
|
||||
|
||||
Querying
|
||||
========
|
||||
|
||||
Saving a document::
|
||||
|
||||
News(title="Using mongodb text search",
|
||||
content="Testing text search").save()
|
||||
|
||||
News(title="MongoEngine 0.9 released",
|
||||
content="Various improvements").save()
|
||||
|
||||
Next, start a text search using :attr:`QuerySet.search_text` method::
|
||||
|
||||
document = News.objects.search_text('testing').first()
|
||||
document.title # may be: "Using mongodb text search"
|
||||
|
||||
document = News.objects.search_text('released').first()
|
||||
document.title # may be: "MongoEngine 0.9 released"
|
||||
|
||||
|
||||
Ordering by text score
|
||||
======================
|
||||
|
||||
::
|
||||
|
||||
objects = News.objects.search('mongo').order_by('$text_score')
|
||||
@@ -7,16 +7,18 @@ MongoDB. To install it, simply run
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# pip install -U mongoengine
|
||||
$ pip install -U mongoengine
|
||||
|
||||
:doc:`tutorial`
|
||||
Start here for a quick overview.
|
||||
A quick tutorial building a tumblelog to get you up and running with
|
||||
MongoEngine.
|
||||
|
||||
:doc:`guide/index`
|
||||
The Full guide to MongoEngine
|
||||
The Full guide to MongoEngine --- from modeling documents to storing files,
|
||||
from querying for data to firing signals and *everything* between.
|
||||
|
||||
:doc:`apireference`
|
||||
The complete API documentation.
|
||||
The complete API documentation --- the innards of documents, querysets and fields.
|
||||
|
||||
:doc:`upgrade`
|
||||
How to upgrade MongoEngine.
|
||||
@@ -28,35 +30,50 @@ Community
|
||||
---------
|
||||
|
||||
To get help with using MongoEngine, use the `MongoEngine Users mailing list
|
||||
<http://groups.google.com/group/mongoengine-users>`_ or come chat on the
|
||||
`#mongoengine IRC channel <irc://irc.freenode.net/mongoengine>`_.
|
||||
<http://groups.google.com/group/mongoengine-users>`_ or the ever popular
|
||||
`stackoverflow <http://www.stackoverflow.com>`_.
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
The source is available on `GitHub <http://github.com/MongoEngine/mongoengine>`_ and
|
||||
contributions are always encouraged. Contributions can be as simple as
|
||||
minor tweaks to this documentation. To contribute, fork the project on
|
||||
**Yes please!** We are always looking for contributions, additions and improvements.
|
||||
|
||||
The source is available on `GitHub <http://github.com/MongoEngine/mongoengine>`_
|
||||
and contributions are always encouraged. Contributions can be as simple as
|
||||
minor tweaks to this documentation, the website or the core.
|
||||
|
||||
To contribute, fork the project on
|
||||
`GitHub <http://github.com/MongoEngine/mongoengine>`_ and send a
|
||||
pull request.
|
||||
|
||||
Also, you can join the developers' `mailing list
|
||||
<http://groups.google.com/group/mongoengine-dev>`_.
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
See the :doc:`changelog` for a full list of changes to MongoEngine and
|
||||
:doc:`upgrade` for upgrade information.
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
.. note:: Always read and test the `upgrade <upgrade>`_ documentation before
|
||||
putting updates live in production **;)**
|
||||
|
||||
tutorial
|
||||
guide/index
|
||||
apireference
|
||||
django
|
||||
changelog
|
||||
upgrade
|
||||
Offline Reading
|
||||
---------------
|
||||
|
||||
Download the docs in `pdf <https://media.readthedocs.org/pdf/mongoengine-odm/latest/mongoengine-odm.pdf>`_
|
||||
or `epub <https://media.readthedocs.org/epub/mongoengine-odm/latest/mongoengine-odm.epub>`_
|
||||
formats for offline reading.
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:numbered:
|
||||
:hidden:
|
||||
|
||||
tutorial
|
||||
guide/index
|
||||
apireference
|
||||
changelog
|
||||
upgrade
|
||||
django
|
||||
|
||||
Indices and tables
|
||||
------------------
|
||||
|
||||
@@ -1,73 +1,83 @@
|
||||
========
|
||||
Tutorial
|
||||
========
|
||||
|
||||
This tutorial introduces **MongoEngine** by means of example --- we will walk
|
||||
through how to create a simple **Tumblelog** application. A Tumblelog is a type
|
||||
of blog where posts are not constrained to being conventional text-based posts.
|
||||
As well as text-based entries, users may post images, links, videos, etc. For
|
||||
simplicity's sake, we'll stick to text, image and link entries in our
|
||||
application. As the purpose of this tutorial is to introduce MongoEngine, we'll
|
||||
through how to create a simple **Tumblelog** application. A tumblelog is a
|
||||
blog that supports mixed media content, including text, images, links, video,
|
||||
audio, etc. For simplicity's sake, we'll stick to text, image, and link
|
||||
entries. As the purpose of this tutorial is to introduce MongoEngine, we'll
|
||||
focus on the data-modelling side of the application, leaving out a user
|
||||
interface.
|
||||
|
||||
Getting started
|
||||
===============
|
||||
|
||||
Before we start, make sure that a copy of MongoDB is running in an accessible
|
||||
location --- running it locally will be easier, but if that is not an option
|
||||
then it may be run on a remote server.
|
||||
then it may be run on a remote server. If you haven't installed MongoEngine,
|
||||
simply use pip to install it like so::
|
||||
|
||||
$ pip install mongoengine
|
||||
|
||||
Before we can start using MongoEngine, we need to tell it how to connect to our
|
||||
instance of :program:`mongod`. For this we use the :func:`~mongoengine.connect`
|
||||
function. The only argument we need to provide is the name of the MongoDB
|
||||
database to use::
|
||||
function. If running locally, the only argument we need to provide is the name
|
||||
of the MongoDB database to use::
|
||||
|
||||
from mongoengine import *
|
||||
|
||||
connect('tumblelog')
|
||||
|
||||
For more information about connecting to MongoDB see :ref:`guide-connecting`.
|
||||
There are lots of options for connecting to MongoDB, for more information about
|
||||
them see the :ref:`guide-connecting` guide.
|
||||
|
||||
Defining our documents
|
||||
======================
|
||||
|
||||
MongoDB is *schemaless*, which means that no schema is enforced by the database
|
||||
--- we may add and remove fields however we want and MongoDB won't complain.
|
||||
This makes life a lot easier in many regards, especially when there is a change
|
||||
to the data model. However, defining schemata for our documents can help to
|
||||
iron out bugs involving incorrect types or missing fields, and also allow us to
|
||||
to the data model. However, defining schemas for our documents can help to iron
|
||||
out bugs involving incorrect types or missing fields, and also allow us to
|
||||
define utility methods on our documents in the same way that traditional
|
||||
:abbr:`ORMs (Object-Relational Mappers)` do.
|
||||
|
||||
In our Tumblelog application we need to store several different types of
|
||||
information. We will need to have a collection of **users**, so that we may
|
||||
link posts to an individual. We also need to store our different types
|
||||
**posts** (text, image and link) in the database. To aid navigation of our
|
||||
information. We will need to have a collection of **users**, so that we may
|
||||
link posts to an individual. We also need to store our different types of
|
||||
**posts** (eg: text, image and link) in the database. To aid navigation of our
|
||||
Tumblelog, posts may have **tags** associated with them, so that the list of
|
||||
posts shown to the user may be limited to posts that have been assigned a
|
||||
specified tag. Finally, it would be nice if **comments** could be added to
|
||||
posts. We'll start with **users**, as the others are slightly more involved.
|
||||
specific tag. Finally, it would be nice if **comments** could be added to
|
||||
posts. We'll start with **users**, as the other document models are slightly
|
||||
more involved.
|
||||
|
||||
Users
|
||||
-----
|
||||
|
||||
Just as if we were using a relational database with an ORM, we need to define
|
||||
which fields a :class:`User` may have, and what their types will be::
|
||||
which fields a :class:`User` may have, and what types of data they might store::
|
||||
|
||||
class User(Document):
|
||||
email = StringField(required=True)
|
||||
first_name = StringField(max_length=50)
|
||||
last_name = StringField(max_length=50)
|
||||
|
||||
This looks similar to how a the structure of a table would be defined in a
|
||||
This looks similar to how the structure of a table would be defined in a
|
||||
regular ORM. The key difference is that this schema will never be passed on to
|
||||
MongoDB --- this will only be enforced at the application level. Also, the User
|
||||
documents will be stored in a MongoDB *collection* rather than a table.
|
||||
MongoDB --- this will only be enforced at the application level, making future
|
||||
changes easy to manage. Also, the User documents will be stored in a
|
||||
MongoDB *collection* rather than a table.
|
||||
|
||||
Posts, Comments and Tags
|
||||
------------------------
|
||||
|
||||
Now we'll think about how to store the rest of the information. If we were
|
||||
using a relational database, we would most likely have a table of **posts**, a
|
||||
table of **comments** and a table of **tags**. To associate the comments with
|
||||
individual posts, we would put a column in the comments table that contained a
|
||||
foreign key to the posts table. We'd also need a link table to provide the
|
||||
foreign key to the posts table. We'd also need a link table to provide the
|
||||
many-to-many relationship between posts and tags. Then we'd need to address the
|
||||
problem of storing the specialised post-types (text, image and link). There are
|
||||
several ways we can achieve this, but each of them have their problems --- none
|
||||
@@ -75,16 +85,17 @@ of them stand out as particularly intuitive solutions.
|
||||
|
||||
Posts
|
||||
^^^^^
|
||||
But MongoDB *isn't* a relational database, so we're not going to do it that
|
||||
|
||||
Happily mongoDB *isn't* a relational database, so we're not going to do it that
|
||||
way. As it turns out, we can use MongoDB's schemaless nature to provide us with
|
||||
a much nicer solution. We will store all of the posts in *one collection* ---
|
||||
each post type will just have the fields it needs. If we later want to add
|
||||
a much nicer solution. We will store all of the posts in *one collection* and
|
||||
each post type will only store the fields it needs. If we later want to add
|
||||
video posts, we don't have to modify the collection at all, we just *start
|
||||
using* the new fields we need to support video posts. This fits with the
|
||||
Object-Oriented principle of *inheritance* nicely. We can think of
|
||||
:class:`Post` as a base class, and :class:`TextPost`, :class:`ImagePost` and
|
||||
:class:`LinkPost` as subclasses of :class:`Post`. In fact, MongoEngine supports
|
||||
this kind of modelling out of the box - all you need do is turn on inheritance
|
||||
this kind of modeling out of the box --- all you need do is turn on inheritance
|
||||
by setting :attr:`allow_inheritance` to True in the :attr:`meta`::
|
||||
|
||||
class Post(Document):
|
||||
@@ -103,20 +114,21 @@ by setting :attr:`allow_inheritance` to True in the :attr:`meta`::
|
||||
link_url = StringField()
|
||||
|
||||
We are storing a reference to the author of the posts using a
|
||||
:class:`~mongoengine.ReferenceField` object. These are similar to foreign key
|
||||
:class:`~mongoengine.fields.ReferenceField` object. These are similar to foreign key
|
||||
fields in traditional ORMs, and are automatically translated into references
|
||||
when they are saved, and dereferenced when they are loaded.
|
||||
|
||||
Tags
|
||||
^^^^
|
||||
|
||||
Now that we have our Post models figured out, how will we attach tags to them?
|
||||
MongoDB allows us to store lists of items natively, so rather than having a
|
||||
link table, we can just store a list of tags in each post. So, for both
|
||||
efficiency and simplicity's sake, we'll store the tags as strings directly
|
||||
within the post, rather than storing references to tags in a separate
|
||||
collection. Especially as tags are generally very short (often even shorter
|
||||
than a document's id), this denormalisation won't impact very strongly on the
|
||||
size of our database. So let's take a look that the code our modified
|
||||
than a document's id), this denormalization won't impact the size of the
|
||||
database very strongly. Let's take a look at the code of our modified
|
||||
:class:`Post` class::
|
||||
|
||||
class Post(Document):
|
||||
@@ -124,16 +136,19 @@ size of our database. So let's take a look that the code our modified
|
||||
author = ReferenceField(User)
|
||||
tags = ListField(StringField(max_length=30))
|
||||
|
||||
The :class:`~mongoengine.ListField` object that is used to define a Post's tags
|
||||
The :class:`~mongoengine.fields.ListField` object that is used to define a Post's tags
|
||||
takes a field object as its first argument --- this means that you can have
|
||||
lists of any type of field (including lists). Note that we don't need to
|
||||
modify the specialised post types as they all inherit from :class:`Post`.
|
||||
lists of any type of field (including lists).
|
||||
|
||||
.. note:: We don't need to modify the specialized post types as they all
|
||||
inherit from :class:`Post`.
|
||||
|
||||
Comments
|
||||
^^^^^^^^
|
||||
|
||||
A comment is typically associated with *one* post. In a relational database, to
|
||||
display a post with its comments, we would have to retrieve the post from the
|
||||
database, then query the database again for the comments associated with the
|
||||
database and then query the database again for the comments associated with the
|
||||
post. This works, but there is no real reason to be storing the comments
|
||||
separately from their associated posts, other than to work around the
|
||||
relational model. Using MongoDB we can store the comments as a list of
|
||||
@@ -158,7 +173,7 @@ We can then store a list of comment documents in our post document::
|
||||
Handling deletions of references
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The :class:`~mongoengine.ReferenceField` object takes a keyword
|
||||
The :class:`~mongoengine.fields.ReferenceField` object takes a keyword
|
||||
`reverse_delete_rule` for handling deletion rules if the reference is deleted.
|
||||
To delete all the posts if a user is deleted set the rule::
|
||||
|
||||
@@ -168,7 +183,7 @@ To delete all the posts if a user is deleted set the rule::
|
||||
tags = ListField(StringField(max_length=30))
|
||||
comments = ListField(EmbeddedDocumentField(Comment))
|
||||
|
||||
See :class:`~mongoengine.ReferenceField` for more information.
|
||||
See :class:`~mongoengine.fields.ReferenceField` for more information.
|
||||
|
||||
.. note::
|
||||
MapFields and DictFields currently don't support automatic handling of
|
||||
@@ -181,33 +196,37 @@ Now that we've defined how our documents will be structured, let's start adding
|
||||
some documents to the database. Firstly, we'll need to create a :class:`User`
|
||||
object::
|
||||
|
||||
john = User(email='jdoe@example.com', first_name='John', last_name='Doe')
|
||||
john.save()
|
||||
ross = User(email='ross@example.com', first_name='Ross', last_name='Lawley').save()
|
||||
|
||||
Note that we could have also defined our user using attribute syntax::
|
||||
.. note::
|
||||
We could have also defined our user using attribute syntax::
|
||||
|
||||
john = User(email='jdoe@example.com')
|
||||
john.first_name = 'John'
|
||||
john.last_name = 'Doe'
|
||||
john.save()
|
||||
ross = User(email='ross@example.com')
|
||||
ross.first_name = 'Ross'
|
||||
ross.last_name = 'Lawley'
|
||||
ross.save()
|
||||
|
||||
Now that we've got our user in the database, let's add a couple of posts::
|
||||
Assign another user to a variable called ``john``, just like we did above with
|
||||
``ross``.
|
||||
|
||||
Now that we've got our users in the database, let's add a couple of posts::
|
||||
|
||||
post1 = TextPost(title='Fun with MongoEngine', author=john)
|
||||
post1.content = 'Took a look at MongoEngine today, looks pretty cool.'
|
||||
post1.tags = ['mongodb', 'mongoengine']
|
||||
post1.save()
|
||||
|
||||
post2 = LinkPost(title='MongoEngine Documentation', author=john)
|
||||
post2.link_url = 'http://tractiondigital.com/labs/mongoengine/docs'
|
||||
post2 = LinkPost(title='MongoEngine Documentation', author=ross)
|
||||
post2.link_url = 'http://docs.mongoengine.com/'
|
||||
post2.tags = ['mongoengine']
|
||||
post2.save()
|
||||
|
||||
Note that if you change a field on a object that has already been saved, then
|
||||
call :meth:`save` again, the document will be updated.
|
||||
.. note:: If you change a field on an object that has already been saved and
|
||||
then call :meth:`save` again, the document will be updated.
|
||||
|
||||
Accessing our data
|
||||
==================
|
||||
|
||||
So now we've got a couple of posts in our database, how do we display them?
|
||||
Each document class (i.e. any class that inherits either directly or indirectly
|
||||
from :class:`~mongoengine.Document`) has an :attr:`objects` attribute, which is
|
||||
@@ -215,16 +234,17 @@ used to access the documents in the database collection associated with that
|
||||
class. So let's see how we can get our posts' titles::
|
||||
|
||||
for post in Post.objects:
|
||||
print post.title
|
||||
print(post.title)
|
||||
|
||||
Retrieving type-specific information
|
||||
------------------------------------
|
||||
This will print the titles of our posts, one on each line. But What if we want
|
||||
|
||||
This will print the titles of our posts, one on each line. But what if we want
|
||||
to access the type-specific data (link_url, content, etc.)? One way is simply
|
||||
to use the :attr:`objects` attribute of a subclass of :class:`Post`::
|
||||
|
||||
for post in TextPost.objects:
|
||||
print post.content
|
||||
print(post.content)
|
||||
|
||||
Using TextPost's :attr:`objects` attribute only returns documents that were
|
||||
created using :class:`TextPost`. Actually, there is a more general rule here:
|
||||
@@ -241,22 +261,21 @@ instances of :class:`Post` --- they were instances of the subclass of
|
||||
practice::
|
||||
|
||||
for post in Post.objects:
|
||||
print post.title
|
||||
print '=' * len(post.title)
|
||||
print(post.title)
|
||||
print('=' * len(post.title))
|
||||
|
||||
if isinstance(post, TextPost):
|
||||
print post.content
|
||||
print(post.content)
|
||||
|
||||
if isinstance(post, LinkPost):
|
||||
print 'Link:', post.link_url
|
||||
|
||||
print
|
||||
print('Link: {}'.format(post.link_url))
|
||||
|
||||
This would print the title of each post, followed by the content if it was a
|
||||
text post, and "Link: <url>" if it was a link post.
|
||||
|
||||
Searching our posts by tag
|
||||
--------------------------
|
||||
|
||||
The :attr:`objects` attribute of a :class:`~mongoengine.Document` is actually a
|
||||
:class:`~mongoengine.queryset.QuerySet` object. This lazily queries the
|
||||
database only when you need the data. It may also be filtered to narrow down
|
||||
@@ -264,7 +283,7 @@ your query. Let's adjust our query so that only posts with the tag "mongodb"
|
||||
are returned::
|
||||
|
||||
for post in Post.objects(tags='mongodb'):
|
||||
print post.title
|
||||
print(post.title)
|
||||
|
||||
There are also methods available on :class:`~mongoengine.queryset.QuerySet`
|
||||
objects that allow different results to be returned, for example, calling
|
||||
@@ -273,5 +292,11 @@ the first matched by the query you provide. Aggregation functions may also be
|
||||
used on :class:`~mongoengine.queryset.QuerySet` objects::
|
||||
|
||||
num_posts = Post.objects(tags='mongodb').count()
|
||||
print 'Found %d posts with tag "mongodb"' % num_posts
|
||||
print('Found {} posts with tag "mongodb"'.format(num_posts))
|
||||
|
||||
Learning more about MongoEngine
|
||||
-------------------------------
|
||||
|
||||
If you got this far you've made a great start, so well done! The next step on
|
||||
your MongoEngine journey is the `full user guide <guide/index.html>`_, where
|
||||
you can learn in-depth about how to use MongoEngine and MongoDB.
|
||||
|
||||
375
docs/upgrade.rst
375
docs/upgrade.rst
@@ -1,16 +1,130 @@
|
||||
=========
|
||||
#########
|
||||
Upgrading
|
||||
=========
|
||||
#########
|
||||
|
||||
Development
|
||||
***********
|
||||
(Fill this out whenever you introduce breaking changes to MongoEngine)
|
||||
|
||||
0.14.0
|
||||
******
|
||||
This release includes a few bug fixes and a significant code cleanup. The most
|
||||
important change is that `QuerySet.as_pymongo` no longer supports a
|
||||
`coerce_types` mode. If you used it in the past, a) please let us know of your
|
||||
use case, b) you'll need to override `as_pymongo` to get the desired outcome.
|
||||
|
||||
This release also makes the EmbeddedDocument not hashable by default. If you
|
||||
use embedded documents in sets or dictionaries, you might have to override
|
||||
`__hash__` and implement a hashing logic specific to your use case. See #1528
|
||||
for the reason behind this change.
|
||||
|
||||
0.13.0
|
||||
******
|
||||
This release adds Unicode support to the `EmailField` and changes its
|
||||
structure significantly. Previously, email addresses containing Unicode
|
||||
characters didn't work at all. Starting with v0.13.0, domains with Unicode
|
||||
characters are supported out of the box, meaning some emails that previously
|
||||
didn't pass validation now do. Make sure the rest of your application can
|
||||
accept such email addresses. Additionally, if you subclassed the `EmailField`
|
||||
in your application and overrode `EmailField.EMAIL_REGEX`, you will have to
|
||||
adjust your code to override `EmailField.USER_REGEX`, `EmailField.DOMAIN_REGEX`,
|
||||
and potentially `EmailField.UTF8_USER_REGEX`.
|
||||
|
||||
0.12.0
|
||||
******
|
||||
This release includes various fixes for the `BaseQuerySet` methods and how they
|
||||
are chained together. Since version 0.10.1 applying limit/skip/hint/batch_size
|
||||
to an already-existing queryset wouldn't modify the underlying PyMongo cursor.
|
||||
This has been fixed now, so you'll need to make sure that your code didn't rely
|
||||
on the broken implementation.
|
||||
|
||||
Additionally, a public `BaseQuerySet.clone_into` has been renamed to a private
|
||||
`_clone_into`. If you directly used that method in your code, you'll need to
|
||||
rename its occurrences.
|
||||
|
||||
0.11.0
|
||||
******
|
||||
This release includes a major rehaul of MongoEngine's code quality and
|
||||
introduces a few breaking changes. It also touches many different parts of
|
||||
the package and although all the changes have been tested and scrutinized,
|
||||
you're encouraged to thorougly test the upgrade.
|
||||
|
||||
First breaking change involves renaming `ConnectionError` to `MongoEngineConnectionError`.
|
||||
If you import or catch this exception, you'll need to rename it in your code.
|
||||
|
||||
Second breaking change drops Python v2.6 support. If you run MongoEngine on
|
||||
that Python version, you'll need to upgrade it first.
|
||||
|
||||
Third breaking change drops an old backward compatibility measure where
|
||||
`from mongoengine.base import ErrorClass` would work on top of
|
||||
`from mongoengine.errors import ErrorClass` (where `ErrorClass` is e.g.
|
||||
`ValidationError`). If you import any exceptions from `mongoengine.base`,
|
||||
change it to `mongoengine.errors`.
|
||||
|
||||
0.10.8
|
||||
******
|
||||
This version fixed an issue where specifying a MongoDB URI host would override
|
||||
more information than it should. These changes are minor, but they still
|
||||
subtly modify the connection logic and thus you're encouraged to test your
|
||||
MongoDB connection before shipping v0.10.8 in production.
|
||||
|
||||
0.10.7
|
||||
******
|
||||
|
||||
`QuerySet.aggregate_sum` and `QuerySet.aggregate_average` are dropped. Use
|
||||
`QuerySet.sum` and `QuerySet.average` instead which use the aggreation framework
|
||||
by default from now on.
|
||||
|
||||
0.9.0
|
||||
*****
|
||||
|
||||
The 0.8.7 package on pypi was corrupted. If upgrading from 0.8.7 to 0.9.0 please follow: ::
|
||||
|
||||
pip uninstall pymongo
|
||||
pip uninstall mongoengine
|
||||
pip install pymongo==2.8
|
||||
pip install mongoengine
|
||||
|
||||
0.8.7
|
||||
*****
|
||||
|
||||
Calling reload on deleted / nonexistent documents now raises a DoesNotExist
|
||||
exception.
|
||||
|
||||
|
||||
0.8.2 to 0.8.3
|
||||
**************
|
||||
|
||||
Minor change that may impact users:
|
||||
|
||||
DynamicDocument fields are now stored in creation order after any declared
|
||||
fields. Previously they were stored alphabetically.
|
||||
|
||||
|
||||
0.7 to 0.8
|
||||
**********
|
||||
|
||||
There have been numerous backwards breaking changes in 0.8. The reasons for
|
||||
these are to ensure that MongoEngine has sane defaults going forward and that it
|
||||
performs the best it can out of the box. Where possible there have been
|
||||
FutureWarnings to help get you ready for the change, but that hasn't been
|
||||
possible for the whole of the release.
|
||||
|
||||
.. warning:: Breaking changes - test upgrading on a test system before putting
|
||||
live. There maybe multiple manual steps in migrating and these are best honed
|
||||
on a staging / test system.
|
||||
|
||||
Python and PyMongo
|
||||
==================
|
||||
|
||||
MongoEngine requires python 2.6 (or above) and pymongo 2.5 (or above)
|
||||
|
||||
Data Model
|
||||
==========
|
||||
|
||||
Inheritance
|
||||
-----------
|
||||
|
||||
Data Model
|
||||
~~~~~~~~~~
|
||||
|
||||
The inheritance model has changed, we no longer need to store an array of
|
||||
:attr:`types` with the model we can just use the classname in :attr:`_cls`.
|
||||
This means that you will have to update your indexes for each of your
|
||||
@@ -44,9 +158,9 @@ inherited classes like so: ::
|
||||
|
||||
|
||||
Document Definition
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
-------------------
|
||||
|
||||
The default for inheritance has changed - its now off by default and
|
||||
The default for inheritance has changed - it is now off by default and
|
||||
:attr:`_cls` will not be stored automatically with the class. So if you extend
|
||||
your :class:`~mongoengine.Document` or :class:`~mongoengine.EmbeddedDocuments`
|
||||
you will need to declare :attr:`allow_inheritance` in the meta data like so: ::
|
||||
@@ -56,7 +170,7 @@ you will need to declare :attr:`allow_inheritance` in the meta data like so: ::
|
||||
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
Previously, if you had data the database that wasn't defined in the Document
|
||||
Previously, if you had data in the database that wasn't defined in the Document
|
||||
definition, it would set it as an attribute on the document. This is no longer
|
||||
the case and the data is set only in the ``document._data`` dictionary: ::
|
||||
|
||||
@@ -76,8 +190,141 @@ the case and the data is set only in the ``document._data`` dictionary: ::
|
||||
File "<stdin>", line 1, in <module>
|
||||
AttributeError: 'Animal' object has no attribute 'size'
|
||||
|
||||
The Document class has introduced a reserved function `clean()`, which will be
|
||||
called before saving the document. If your document class happens to have a method
|
||||
with the same name, please try to rename it.
|
||||
|
||||
def clean(self):
|
||||
pass
|
||||
|
||||
ReferenceField
|
||||
--------------
|
||||
|
||||
ReferenceFields now store ObjectIds by default - this is more efficient than
|
||||
DBRefs as we already know what Document types they reference::
|
||||
|
||||
# Old code
|
||||
class Animal(Document):
|
||||
name = ReferenceField('self')
|
||||
|
||||
# New code to keep dbrefs
|
||||
class Animal(Document):
|
||||
name = ReferenceField('self', dbref=True)
|
||||
|
||||
To migrate all the references you need to touch each object and mark it as dirty
|
||||
eg::
|
||||
|
||||
# Doc definition
|
||||
class Person(Document):
|
||||
name = StringField()
|
||||
parent = ReferenceField('self')
|
||||
friends = ListField(ReferenceField('self'))
|
||||
|
||||
# Mark all ReferenceFields as dirty and save
|
||||
for p in Person.objects:
|
||||
p._mark_as_changed('parent')
|
||||
p._mark_as_changed('friends')
|
||||
p.save()
|
||||
|
||||
`An example test migration for ReferenceFields is available on github
|
||||
<https://github.com/MongoEngine/mongoengine/blob/master/tests/migration/refrencefield_dbref_to_object_id.py>`_.
|
||||
|
||||
.. Note:: Internally mongoengine handles ReferenceFields the same, so they are
|
||||
converted to DBRef on loading and ObjectIds or DBRefs depending on settings
|
||||
on storage.
|
||||
|
||||
UUIDField
|
||||
---------
|
||||
|
||||
UUIDFields now default to storing binary values::
|
||||
|
||||
# Old code
|
||||
class Animal(Document):
|
||||
uuid = UUIDField()
|
||||
|
||||
# New code
|
||||
class Animal(Document):
|
||||
uuid = UUIDField(binary=False)
|
||||
|
||||
To migrate all the uuids you need to touch each object and mark it as dirty
|
||||
eg::
|
||||
|
||||
# Doc definition
|
||||
class Animal(Document):
|
||||
uuid = UUIDField()
|
||||
|
||||
# Mark all UUIDFields as dirty and save
|
||||
for a in Animal.objects:
|
||||
a._mark_as_changed('uuid')
|
||||
a.save()
|
||||
|
||||
`An example test migration for UUIDFields is available on github
|
||||
<https://github.com/MongoEngine/mongoengine/blob/master/tests/migration/uuidfield_to_binary.py>`_.
|
||||
|
||||
DecimalField
|
||||
------------
|
||||
|
||||
DecimalFields now store floats - previously it was storing strings and that
|
||||
made it impossible to do comparisons when querying correctly.::
|
||||
|
||||
# Old code
|
||||
class Person(Document):
|
||||
balance = DecimalField()
|
||||
|
||||
# New code
|
||||
class Person(Document):
|
||||
balance = DecimalField(force_string=True)
|
||||
|
||||
To migrate all the DecimalFields you need to touch each object and mark it as dirty
|
||||
eg::
|
||||
|
||||
# Doc definition
|
||||
class Person(Document):
|
||||
balance = DecimalField()
|
||||
|
||||
# Mark all DecimalField's as dirty and save
|
||||
for p in Person.objects:
|
||||
p._mark_as_changed('balance')
|
||||
p.save()
|
||||
|
||||
.. note:: DecimalFields have also been improved with the addition of precision
|
||||
and rounding. See :class:`~mongoengine.fields.DecimalField` for more information.
|
||||
|
||||
`An example test migration for DecimalFields is available on github
|
||||
<https://github.com/MongoEngine/mongoengine/blob/master/tests/migration/decimalfield_as_float.py>`_.
|
||||
|
||||
Cascading Saves
|
||||
---------------
|
||||
To improve performance document saves will no longer automatically cascade.
|
||||
Any changes to a Document's references will either have to be saved manually or
|
||||
you will have to explicitly tell it to cascade on save::
|
||||
|
||||
# At the class level:
|
||||
class Person(Document):
|
||||
meta = {'cascade': True}
|
||||
|
||||
# Or on save:
|
||||
my_document.save(cascade=True)
|
||||
|
||||
Storage
|
||||
-------
|
||||
|
||||
Document and Embedded Documents are now serialized based on declared field order.
|
||||
Previously, the data was passed to mongodb as a dictionary and which meant that
|
||||
order wasn't guaranteed - so things like ``$addToSet`` operations on
|
||||
:class:`~mongoengine.EmbeddedDocument` could potentially fail in unexpected
|
||||
ways.
|
||||
|
||||
If this impacts you, you may want to rewrite the objects using the
|
||||
``doc.mark_as_dirty('field')`` pattern described above. If you are using a
|
||||
compound primary key then you will need to ensure the order is fixed and match
|
||||
your EmbeddedDocument to that order.
|
||||
|
||||
Querysets
|
||||
~~~~~~~~~
|
||||
=========
|
||||
|
||||
Attack of the clones
|
||||
--------------------
|
||||
|
||||
Querysets now return clones and should no longer be considered editable in
|
||||
place. This brings us in line with how Django's querysets work and removes a
|
||||
@@ -91,15 +338,87 @@ update your code like so: ::
|
||||
|
||||
# Update example a) assign queryset after a change:
|
||||
mammals = Animal.objects(type="mammal")
|
||||
carnivores = mammals.filter(order="Carnivora") # Reassign the new queryset so fitler can be applied
|
||||
carnivores = mammals.filter(order="Carnivora") # Reassign the new queryset so filter can be applied
|
||||
[m for m in carnivores] # This will return all carnivores
|
||||
|
||||
# Update example b) chain the queryset:
|
||||
mammals = Animal.objects(type="mammal").filter(order="Carnivora") # The final queryset is assgined to mammals
|
||||
[m for m in mammals] # This will return all carnivores
|
||||
|
||||
Len iterates the queryset
|
||||
-------------------------
|
||||
|
||||
If you ever did `len(queryset)` it previously did a `count()` under the covers,
|
||||
this caused some unusual issues. As `len(queryset)` is most often used by
|
||||
`list(queryset)` we now cache the queryset results and use that for the length.
|
||||
|
||||
This isn't as performant as a `count()` and if you aren't iterating the
|
||||
queryset you should upgrade to use count::
|
||||
|
||||
# Old code
|
||||
len(Animal.objects(type="mammal"))
|
||||
|
||||
# New code
|
||||
Animal.objects(type="mammal").count()
|
||||
|
||||
|
||||
.only() now inline with .exclude()
|
||||
----------------------------------
|
||||
|
||||
The behaviour of `.only()` was highly ambiguous, now it works in mirror fashion
|
||||
to `.exclude()`. Chaining `.only()` calls will increase the fields required::
|
||||
|
||||
# Old code
|
||||
Animal.objects().only(['type', 'name']).only('name', 'order') # Would have returned just `name`
|
||||
|
||||
# New code
|
||||
Animal.objects().only('name')
|
||||
|
||||
# Note:
|
||||
Animal.objects().only(['name']).only('order') # Now returns `name` *and* `order`
|
||||
|
||||
|
||||
Client
|
||||
======
|
||||
PyMongo 2.4 came with a new connection client; MongoClient_ and started the
|
||||
depreciation of the old :class:`~pymongo.connection.Connection`. MongoEngine
|
||||
now uses the latest `MongoClient` for connections. By default operations were
|
||||
`safe` but if you turned them off or used the connection directly this will
|
||||
impact your queries.
|
||||
|
||||
Querysets
|
||||
---------
|
||||
|
||||
Safe
|
||||
^^^^
|
||||
|
||||
`safe` has been depreciated in the new MongoClient connection. Please use
|
||||
`write_concern` instead. As `safe` always defaulted as `True` normally no code
|
||||
change is required. To disable confirmation of the write just pass `{"w": 0}`
|
||||
eg: ::
|
||||
|
||||
# Old
|
||||
Animal(name="Dinasour").save(safe=False)
|
||||
|
||||
# new code:
|
||||
Animal(name="Dinasour").save(write_concern={"w": 0})
|
||||
|
||||
Write Concern
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
`write_options` has been replaced with `write_concern` to bring it inline with
|
||||
pymongo. To upgrade simply rename any instances where you used the `write_option`
|
||||
keyword to `write_concern` like so::
|
||||
|
||||
# Old code:
|
||||
Animal(name="Dinasour").save(write_options={"w": 2})
|
||||
|
||||
# new code:
|
||||
Animal(name="Dinasour").save(write_concern={"w": 2})
|
||||
|
||||
|
||||
Indexes
|
||||
-------
|
||||
=======
|
||||
|
||||
Index methods are no longer tied to querysets but rather to the document class.
|
||||
Although `QuerySet._ensure_indexes` and `QuerySet.ensure_index` still exist.
|
||||
@@ -107,17 +426,19 @@ They should be replaced with :func:`~mongoengine.Document.ensure_indexes` /
|
||||
:func:`~mongoengine.Document.ensure_index`.
|
||||
|
||||
SequenceFields
|
||||
--------------
|
||||
==============
|
||||
|
||||
:class:`~mongoengine.fields.SequenceField` now inherits from `BaseField` to
|
||||
allow flexible storage of the calculated value. As such MIN and MAX settings
|
||||
are no longer handled.
|
||||
|
||||
.. _MongoClient: http://blog.mongodb.org/post/36666163412/introducing-mongoclient
|
||||
|
||||
0.6 to 0.7
|
||||
==========
|
||||
**********
|
||||
|
||||
Cascade saves
|
||||
-------------
|
||||
=============
|
||||
|
||||
Saves will raise a `FutureWarning` if they cascade and cascade hasn't been set
|
||||
to True. This is because in 0.8 it will default to False. If you require
|
||||
@@ -131,11 +452,11 @@ via `save` eg ::
|
||||
# Or in code:
|
||||
my_document.save(cascade=True)
|
||||
|
||||
.. note ::
|
||||
.. note::
|
||||
Remember: cascading saves **do not** cascade through lists.
|
||||
|
||||
ReferenceFields
|
||||
---------------
|
||||
===============
|
||||
|
||||
ReferenceFields now can store references as ObjectId strings instead of DBRefs.
|
||||
This will become the default in 0.8 and if `dbref` is not set a `FutureWarning`
|
||||
@@ -164,7 +485,7 @@ migrate ::
|
||||
|
||||
|
||||
item_frequencies
|
||||
----------------
|
||||
================
|
||||
|
||||
In the 0.6 series we added support for null / zero / false values in
|
||||
item_frequencies. A side effect was to return keys in the value they are
|
||||
@@ -173,14 +494,14 @@ updated to handle native types rather than strings keys for the results of
|
||||
item frequency queries.
|
||||
|
||||
BinaryFields
|
||||
------------
|
||||
============
|
||||
|
||||
Binary fields have been updated so that they are native binary types. If you
|
||||
previously were doing `str` comparisons with binary field values you will have
|
||||
to update and wrap the value in a `str`.
|
||||
|
||||
0.5 to 0.6
|
||||
==========
|
||||
**********
|
||||
|
||||
Embedded Documents - if you had a `pk` field you will have to rename it from
|
||||
`_id` to `pk` as pk is no longer a property of Embedded Documents.
|
||||
@@ -200,21 +521,21 @@ don't define :attr:`allow_inheritance` in their meta.
|
||||
You may need to update pyMongo to 2.0 for use with Sharding.
|
||||
|
||||
0.4 to 0.5
|
||||
===========
|
||||
**********
|
||||
|
||||
There have been the following backwards incompatibilities from 0.4 to 0.5. The
|
||||
main areas of changed are: choices in fields, map_reduce and collection names.
|
||||
|
||||
Choice options:
|
||||
---------------
|
||||
===============
|
||||
|
||||
Are now expected to be an iterable of tuples, with the first element in each
|
||||
Are now expected to be an iterable of tuples, with the first element in each
|
||||
tuple being the actual value to be stored. The second element is the
|
||||
human-readable name for the option.
|
||||
|
||||
|
||||
PyMongo / MongoDB
|
||||
-----------------
|
||||
=================
|
||||
|
||||
map reduce now requires pymongo 1.11+- The pymongo `merge_output` and
|
||||
`reduce_output` parameters, have been depreciated.
|
||||
@@ -228,10 +549,10 @@ such the following have been changed:
|
||||
|
||||
|
||||
Default collection naming
|
||||
-------------------------
|
||||
=========================
|
||||
|
||||
Previously it was just lowercase, its now much more pythonic and readable as
|
||||
its lowercase and underscores, previously ::
|
||||
Previously it was just lowercase, it's now much more pythonic and readable as
|
||||
it's lowercase and underscores, previously ::
|
||||
|
||||
class MyAceDocument(Document):
|
||||
pass
|
||||
@@ -298,5 +619,5 @@ Alternatively, you can rename your collections eg ::
|
||||
mongodb 1.8 > 2.0 +
|
||||
===================
|
||||
|
||||
Its been reported that indexes may need to be recreated to the newer version of indexes.
|
||||
It's been reported that indexes may need to be recreated to the newer version of indexes.
|
||||
To do this drop indexes and call ``ensure_indexes`` on each model.
|
||||
|
||||
@@ -1,24 +1,36 @@
|
||||
import document
|
||||
from document import *
|
||||
import fields
|
||||
from fields import *
|
||||
import connection
|
||||
from connection import *
|
||||
import queryset
|
||||
from queryset import *
|
||||
import signals
|
||||
from signals import *
|
||||
import django
|
||||
# Import submodules so that we can expose their __all__
|
||||
from mongoengine import connection
|
||||
from mongoengine import document
|
||||
from mongoengine import errors
|
||||
from mongoengine import fields
|
||||
from mongoengine import queryset
|
||||
from mongoengine import signals
|
||||
|
||||
__all__ = (list(document.__all__) + fields.__all__ + connection.__all__ +
|
||||
list(queryset.__all__) + signals.__all__)
|
||||
# Import everything from each submodule so that it can be accessed via
|
||||
# mongoengine, e.g. instead of `from mongoengine.connection import connect`,
|
||||
# users can simply use `from mongoengine import connect`, or even
|
||||
# `from mongoengine import *` and then `connect('testdb')`.
|
||||
from mongoengine.connection import *
|
||||
from mongoengine.document import *
|
||||
from mongoengine.errors import *
|
||||
from mongoengine.fields import *
|
||||
from mongoengine.queryset import *
|
||||
from mongoengine.signals import *
|
||||
|
||||
VERSION = (0, 8, 0, '+')
|
||||
|
||||
__all__ = (list(document.__all__) + list(fields.__all__) +
|
||||
list(connection.__all__) + list(queryset.__all__) +
|
||||
list(signals.__all__) + list(errors.__all__))
|
||||
|
||||
|
||||
VERSION = (0, 14, 1)
|
||||
|
||||
|
||||
def get_version():
|
||||
if isinstance(VERSION[-1], basestring):
|
||||
return '.'.join(map(str, VERSION[:-1])) + VERSION[-1]
|
||||
"""Return the VERSION as a string, e.g. for VERSION == (0, 10, 7),
|
||||
return '0.10.7'.
|
||||
"""
|
||||
return '.'.join(map(str, VERSION))
|
||||
|
||||
|
||||
__version__ = get_version()
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# Base module is split into several files for convenience. Files inside of
|
||||
# this module should import from a specific submodule (e.g.
|
||||
# `from mongoengine.base.document import BaseDocument`), but all of the
|
||||
# other modules should import directly from the top-level module (e.g.
|
||||
# `from mongoengine.base import BaseDocument`). This approach is cleaner and
|
||||
# also helps with cyclical import errors.
|
||||
from mongoengine.base.common import *
|
||||
from mongoengine.base.datastructures import *
|
||||
from mongoengine.base.document import *
|
||||
from mongoengine.base.fields import *
|
||||
from mongoengine.base.metaclasses import *
|
||||
|
||||
__all__ = (
|
||||
# common
|
||||
'UPDATE_OPERATORS', '_document_registry', 'get_document',
|
||||
|
||||
# datastructures
|
||||
'BaseDict', 'BaseList', 'EmbeddedDocumentList',
|
||||
|
||||
# document
|
||||
'BaseDocument',
|
||||
|
||||
# fields
|
||||
'BaseField', 'ComplexBaseField', 'ObjectIdField', 'GeoJsonBaseField',
|
||||
|
||||
# metaclasses
|
||||
'DocumentMetaclass', 'TopLevelDocumentMetaclass'
|
||||
)
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
from mongoengine.errors import NotRegistered
|
||||
|
||||
__all__ = ('ALLOW_INHERITANCE', 'get_document', '_document_registry')
|
||||
__all__ = ('UPDATE_OPERATORS', 'get_document', '_document_registry')
|
||||
|
||||
|
||||
UPDATE_OPERATORS = set(['set', 'unset', 'inc', 'dec', 'pop', 'push',
|
||||
'push_all', 'pull', 'pull_all', 'add_to_set',
|
||||
'set_on_insert', 'min', 'max', 'rename'])
|
||||
|
||||
ALLOW_INHERITANCE = False
|
||||
|
||||
_document_registry = {}
|
||||
|
||||
|
||||
def get_document(name):
|
||||
"""Get a document class by name."""
|
||||
doc = _document_registry.get(name, None)
|
||||
if not doc:
|
||||
# Possible old style name
|
||||
|
||||
@@ -1,45 +1,61 @@
|
||||
import itertools
|
||||
import weakref
|
||||
from mongoengine.common import _import_class
|
||||
|
||||
__all__ = ("BaseDict", "BaseList")
|
||||
import six
|
||||
|
||||
from mongoengine.common import _import_class
|
||||
from mongoengine.errors import DoesNotExist, MultipleObjectsReturned
|
||||
|
||||
__all__ = ('BaseDict', 'BaseList', 'EmbeddedDocumentList')
|
||||
|
||||
|
||||
class BaseDict(dict):
|
||||
"""A special dict so we can watch any changes
|
||||
"""
|
||||
"""A special dict so we can watch any changes."""
|
||||
|
||||
_dereferenced = False
|
||||
_instance = None
|
||||
_name = None
|
||||
|
||||
def __init__(self, dict_items, instance, name):
|
||||
self._instance = weakref.proxy(instance)
|
||||
self._name = name
|
||||
return super(BaseDict, self).__init__(dict_items)
|
||||
Document = _import_class('Document')
|
||||
EmbeddedDocument = _import_class('EmbeddedDocument')
|
||||
|
||||
def __getitem__(self, *args, **kwargs):
|
||||
value = super(BaseDict, self).__getitem__(*args, **kwargs)
|
||||
if isinstance(instance, (Document, EmbeddedDocument)):
|
||||
self._instance = weakref.proxy(instance)
|
||||
self._name = name
|
||||
super(BaseDict, self).__init__(dict_items)
|
||||
|
||||
def __getitem__(self, key, *args, **kwargs):
|
||||
value = super(BaseDict, self).__getitem__(key)
|
||||
|
||||
EmbeddedDocument = _import_class('EmbeddedDocument')
|
||||
if isinstance(value, EmbeddedDocument) and value._instance is None:
|
||||
value._instance = self._instance
|
||||
elif not isinstance(value, BaseDict) and isinstance(value, dict):
|
||||
value = BaseDict(value, None, '%s.%s' % (self._name, key))
|
||||
super(BaseDict, self).__setitem__(key, value)
|
||||
value._instance = self._instance
|
||||
elif not isinstance(value, BaseList) and isinstance(value, list):
|
||||
value = BaseList(value, None, '%s.%s' % (self._name, key))
|
||||
super(BaseDict, self).__setitem__(key, value)
|
||||
value._instance = self._instance
|
||||
return value
|
||||
|
||||
def __setitem__(self, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
return super(BaseDict, self).__setitem__(*args, **kwargs)
|
||||
def __setitem__(self, key, value, *args, **kwargs):
|
||||
self._mark_as_changed(key)
|
||||
return super(BaseDict, self).__setitem__(key, value)
|
||||
|
||||
def __delete__(self, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
return super(BaseDict, self).__delete__(*args, **kwargs)
|
||||
|
||||
def __delitem__(self, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
return super(BaseDict, self).__delitem__(*args, **kwargs)
|
||||
def __delitem__(self, key, *args, **kwargs):
|
||||
self._mark_as_changed(key)
|
||||
return super(BaseDict, self).__delitem__(key)
|
||||
|
||||
def __delattr__(self, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
return super(BaseDict, self).__delattr__(*args, **kwargs)
|
||||
def __delattr__(self, key, *args, **kwargs):
|
||||
self._mark_as_changed(key)
|
||||
return super(BaseDict, self).__delattr__(key)
|
||||
|
||||
def __getstate__(self):
|
||||
self.instance = None
|
||||
@@ -52,7 +68,7 @@ class BaseDict(dict):
|
||||
|
||||
def clear(self, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
return super(BaseDict, self).clear(*args, **kwargs)
|
||||
return super(BaseDict, self).clear()
|
||||
|
||||
def pop(self, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
@@ -60,45 +76,78 @@ class BaseDict(dict):
|
||||
|
||||
def popitem(self, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
return super(BaseDict, self).popitem(*args, **kwargs)
|
||||
return super(BaseDict, self).popitem()
|
||||
|
||||
def setdefault(self, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
return super(BaseDict, self).setdefault(*args, **kwargs)
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
return super(BaseDict, self).update(*args, **kwargs)
|
||||
|
||||
def _mark_as_changed(self):
|
||||
def _mark_as_changed(self, key=None):
|
||||
if hasattr(self._instance, '_mark_as_changed'):
|
||||
self._instance._mark_as_changed(self._name)
|
||||
if key:
|
||||
self._instance._mark_as_changed('%s.%s' % (self._name, key))
|
||||
else:
|
||||
self._instance._mark_as_changed(self._name)
|
||||
|
||||
|
||||
class BaseList(list):
|
||||
"""A special list so we can watch any changes
|
||||
"""
|
||||
"""A special list so we can watch any changes."""
|
||||
|
||||
_dereferenced = False
|
||||
_instance = None
|
||||
_name = None
|
||||
|
||||
def __init__(self, list_items, instance, name):
|
||||
self._instance = weakref.proxy(instance)
|
||||
self._name = name
|
||||
return super(BaseList, self).__init__(list_items)
|
||||
Document = _import_class('Document')
|
||||
EmbeddedDocument = _import_class('EmbeddedDocument')
|
||||
|
||||
def __getitem__(self, *args, **kwargs):
|
||||
value = super(BaseList, self).__getitem__(*args, **kwargs)
|
||||
if isinstance(instance, (Document, EmbeddedDocument)):
|
||||
self._instance = weakref.proxy(instance)
|
||||
self._name = name
|
||||
super(BaseList, self).__init__(list_items)
|
||||
|
||||
def __getitem__(self, key, *args, **kwargs):
|
||||
value = super(BaseList, self).__getitem__(key)
|
||||
|
||||
EmbeddedDocument = _import_class('EmbeddedDocument')
|
||||
if isinstance(value, EmbeddedDocument) and value._instance is None:
|
||||
value._instance = self._instance
|
||||
elif not isinstance(value, BaseDict) and isinstance(value, dict):
|
||||
value = BaseDict(value, None, '%s.%s' % (self._name, key))
|
||||
super(BaseList, self).__setitem__(key, value)
|
||||
value._instance = self._instance
|
||||
elif not isinstance(value, BaseList) and isinstance(value, list):
|
||||
value = BaseList(value, None, '%s.%s' % (self._name, key))
|
||||
super(BaseList, self).__setitem__(key, value)
|
||||
value._instance = self._instance
|
||||
return value
|
||||
|
||||
def __setitem__(self, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
return super(BaseList, self).__setitem__(*args, **kwargs)
|
||||
def __iter__(self):
|
||||
for i in six.moves.range(self.__len__()):
|
||||
yield self[i]
|
||||
|
||||
def __delitem__(self, *args, **kwargs):
|
||||
def __setitem__(self, key, value, *args, **kwargs):
|
||||
if isinstance(key, slice):
|
||||
self._mark_as_changed()
|
||||
else:
|
||||
self._mark_as_changed(key)
|
||||
return super(BaseList, self).__setitem__(key, value)
|
||||
|
||||
def __delitem__(self, key, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
return super(BaseList, self).__delitem__(*args, **kwargs)
|
||||
return super(BaseList, self).__delitem__(key)
|
||||
|
||||
def __setslice__(self, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
return super(BaseList, self).__setslice__(*args, **kwargs)
|
||||
|
||||
def __delslice__(self, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
return super(BaseList, self).__delslice__(*args, **kwargs)
|
||||
|
||||
def __getstate__(self):
|
||||
self.instance = None
|
||||
@@ -109,6 +158,14 @@ class BaseList(list):
|
||||
self = state
|
||||
return self
|
||||
|
||||
def __iadd__(self, other):
|
||||
self._mark_as_changed()
|
||||
return super(BaseList, self).__iadd__(other)
|
||||
|
||||
def __imul__(self, other):
|
||||
self._mark_as_changed()
|
||||
return super(BaseList, self).__imul__(other)
|
||||
|
||||
def append(self, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
return super(BaseList, self).append(*args, **kwargs)
|
||||
@@ -131,12 +188,260 @@ class BaseList(list):
|
||||
|
||||
def reverse(self, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
return super(BaseList, self).reverse(*args, **kwargs)
|
||||
return super(BaseList, self).reverse()
|
||||
|
||||
def sort(self, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
return super(BaseList, self).sort(*args, **kwargs)
|
||||
|
||||
def _mark_as_changed(self):
|
||||
def _mark_as_changed(self, key=None):
|
||||
if hasattr(self._instance, '_mark_as_changed'):
|
||||
self._instance._mark_as_changed(self._name)
|
||||
if key:
|
||||
self._instance._mark_as_changed(
|
||||
'%s.%s' % (self._name, key % len(self))
|
||||
)
|
||||
else:
|
||||
self._instance._mark_as_changed(self._name)
|
||||
|
||||
|
||||
class EmbeddedDocumentList(BaseList):
|
||||
|
||||
@classmethod
|
||||
def __match_all(cls, embedded_doc, kwargs):
|
||||
"""Return True if a given embedded doc matches all the filter
|
||||
kwargs. If it doesn't return False.
|
||||
"""
|
||||
for key, expected_value in kwargs.items():
|
||||
doc_val = getattr(embedded_doc, key)
|
||||
if doc_val != expected_value and six.text_type(doc_val) != expected_value:
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def __only_matches(cls, embedded_docs, kwargs):
|
||||
"""Return embedded docs that match the filter kwargs."""
|
||||
if not kwargs:
|
||||
return embedded_docs
|
||||
return [doc for doc in embedded_docs if cls.__match_all(doc, kwargs)]
|
||||
|
||||
def __init__(self, list_items, instance, name):
|
||||
super(EmbeddedDocumentList, self).__init__(list_items, instance, name)
|
||||
self._instance = instance
|
||||
|
||||
def filter(self, **kwargs):
|
||||
"""
|
||||
Filters the list by only including embedded documents with the
|
||||
given keyword arguments.
|
||||
|
||||
:param kwargs: The keyword arguments corresponding to the fields to
|
||||
filter on. *Multiple arguments are treated as if they are ANDed
|
||||
together.*
|
||||
:return: A new ``EmbeddedDocumentList`` containing the matching
|
||||
embedded documents.
|
||||
|
||||
Raises ``AttributeError`` if a given keyword is not a valid field for
|
||||
the embedded document class.
|
||||
"""
|
||||
values = self.__only_matches(self, kwargs)
|
||||
return EmbeddedDocumentList(values, self._instance, self._name)
|
||||
|
||||
def exclude(self, **kwargs):
|
||||
"""
|
||||
Filters the list by excluding embedded documents with the given
|
||||
keyword arguments.
|
||||
|
||||
:param kwargs: The keyword arguments corresponding to the fields to
|
||||
exclude on. *Multiple arguments are treated as if they are ANDed
|
||||
together.*
|
||||
:return: A new ``EmbeddedDocumentList`` containing the non-matching
|
||||
embedded documents.
|
||||
|
||||
Raises ``AttributeError`` if a given keyword is not a valid field for
|
||||
the embedded document class.
|
||||
"""
|
||||
exclude = self.__only_matches(self, kwargs)
|
||||
values = [item for item in self if item not in exclude]
|
||||
return EmbeddedDocumentList(values, self._instance, self._name)
|
||||
|
||||
def count(self):
|
||||
"""
|
||||
The number of embedded documents in the list.
|
||||
|
||||
:return: The length of the list, equivalent to the result of ``len()``.
|
||||
"""
|
||||
return len(self)
|
||||
|
||||
def get(self, **kwargs):
|
||||
"""
|
||||
Retrieves an embedded document determined by the given keyword
|
||||
arguments.
|
||||
|
||||
:param kwargs: The keyword arguments corresponding to the fields to
|
||||
search on. *Multiple arguments are treated as if they are ANDed
|
||||
together.*
|
||||
:return: The embedded document matched by the given keyword arguments.
|
||||
|
||||
Raises ``DoesNotExist`` if the arguments used to query an embedded
|
||||
document returns no results. ``MultipleObjectsReturned`` if more
|
||||
than one result is returned.
|
||||
"""
|
||||
values = self.__only_matches(self, kwargs)
|
||||
if len(values) == 0:
|
||||
raise DoesNotExist(
|
||||
'%s matching query does not exist.' % self._name
|
||||
)
|
||||
elif len(values) > 1:
|
||||
raise MultipleObjectsReturned(
|
||||
'%d items returned, instead of 1' % len(values)
|
||||
)
|
||||
|
||||
return values[0]
|
||||
|
||||
def first(self):
|
||||
"""Return the first embedded document in the list, or ``None``
|
||||
if empty.
|
||||
"""
|
||||
if len(self) > 0:
|
||||
return self[0]
|
||||
|
||||
def create(self, **values):
|
||||
"""
|
||||
Creates a new embedded document and saves it to the database.
|
||||
|
||||
.. note::
|
||||
The embedded document changes are not automatically saved
|
||||
to the database after calling this method.
|
||||
|
||||
:param values: A dictionary of values for the embedded document.
|
||||
:return: The new embedded document instance.
|
||||
"""
|
||||
name = self._name
|
||||
EmbeddedClass = self._instance._fields[name].field.document_type_obj
|
||||
self._instance[self._name].append(EmbeddedClass(**values))
|
||||
|
||||
return self._instance[self._name][-1]
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Saves the ancestor document.
|
||||
|
||||
:param args: Arguments passed up to the ancestor Document's save
|
||||
method.
|
||||
:param kwargs: Keyword arguments passed up to the ancestor Document's
|
||||
save method.
|
||||
"""
|
||||
self._instance.save(*args, **kwargs)
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Deletes the embedded documents from the database.
|
||||
|
||||
.. note::
|
||||
The embedded document changes are not automatically saved
|
||||
to the database after calling this method.
|
||||
|
||||
:return: The number of entries deleted.
|
||||
"""
|
||||
values = list(self)
|
||||
for item in values:
|
||||
self._instance[self._name].remove(item)
|
||||
|
||||
return len(values)
|
||||
|
||||
def update(self, **update):
|
||||
"""
|
||||
Updates the embedded documents with the given update values.
|
||||
|
||||
.. note::
|
||||
The embedded document changes are not automatically saved
|
||||
to the database after calling this method.
|
||||
|
||||
:param update: A dictionary of update values to apply to each
|
||||
embedded document.
|
||||
:return: The number of entries updated.
|
||||
"""
|
||||
if len(update) == 0:
|
||||
return 0
|
||||
values = list(self)
|
||||
for item in values:
|
||||
for k, v in update.items():
|
||||
setattr(item, k, v)
|
||||
|
||||
return len(values)
|
||||
|
||||
|
||||
class StrictDict(object):
|
||||
__slots__ = ()
|
||||
_special_fields = set(['get', 'pop', 'iteritems', 'items', 'keys', 'create'])
|
||||
_classes = {}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for k, v in kwargs.iteritems():
|
||||
setattr(self, k, v)
|
||||
|
||||
def __getitem__(self, key):
|
||||
key = '_reserved_' + key if key in self._special_fields else key
|
||||
try:
|
||||
return getattr(self, key)
|
||||
except AttributeError:
|
||||
raise KeyError(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
key = '_reserved_' + key if key in self._special_fields else key
|
||||
return setattr(self, key, value)
|
||||
|
||||
def __contains__(self, key):
|
||||
return hasattr(self, key)
|
||||
|
||||
def get(self, key, default=None):
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def pop(self, key, default=None):
|
||||
v = self.get(key, default)
|
||||
try:
|
||||
delattr(self, key)
|
||||
except AttributeError:
|
||||
pass
|
||||
return v
|
||||
|
||||
def iteritems(self):
|
||||
for key in self:
|
||||
yield key, self[key]
|
||||
|
||||
def items(self):
|
||||
return [(k, self[k]) for k in iter(self)]
|
||||
|
||||
def iterkeys(self):
|
||||
return iter(self)
|
||||
|
||||
def keys(self):
|
||||
return list(iter(self))
|
||||
|
||||
def __iter__(self):
|
||||
return (key for key in self.__slots__ if hasattr(self, key))
|
||||
|
||||
def __len__(self):
|
||||
return len(list(self.iteritems()))
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.items() == other.items()
|
||||
|
||||
def __ne__(self, other):
|
||||
return self.items() != other.items()
|
||||
|
||||
@classmethod
|
||||
def create(cls, allowed_keys):
|
||||
allowed_keys_tuple = tuple(('_reserved_' + k if k in cls._special_fields else k) for k in allowed_keys)
|
||||
allowed_keys = frozenset(allowed_keys_tuple)
|
||||
if allowed_keys not in cls._classes:
|
||||
class SpecificStrictDict(cls):
|
||||
__slots__ = allowed_keys_tuple
|
||||
|
||||
def __repr__(self):
|
||||
return '{%s}' % ', '.join('"{0!s}": {1!r}'.format(k, v) for k, v in self.items())
|
||||
|
||||
cls._classes[allowed_keys] = SpecificStrictDict
|
||||
return cls._classes[allowed_keys]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,15 +2,19 @@ import operator
|
||||
import warnings
|
||||
import weakref
|
||||
|
||||
from bson import DBRef, ObjectId
|
||||
from bson import DBRef, ObjectId, SON
|
||||
import pymongo
|
||||
import six
|
||||
|
||||
from mongoengine.base.common import UPDATE_OPERATORS
|
||||
from mongoengine.base.datastructures import (BaseDict, BaseList,
|
||||
EmbeddedDocumentList)
|
||||
from mongoengine.common import _import_class
|
||||
from mongoengine.errors import ValidationError
|
||||
|
||||
from mongoengine.base.common import ALLOW_INHERITANCE
|
||||
from mongoengine.base.datastructures import BaseDict, BaseList
|
||||
|
||||
__all__ = ("BaseField", "ComplexBaseField", "ObjectIdField")
|
||||
__all__ = ('BaseField', 'ComplexBaseField', 'ObjectIdField',
|
||||
'GeoJsonBaseField')
|
||||
|
||||
|
||||
class BaseField(object):
|
||||
@@ -19,7 +23,6 @@ class BaseField(object):
|
||||
|
||||
.. versionchanged:: 0.5 - added verbose and help text
|
||||
"""
|
||||
|
||||
name = None
|
||||
_geo_index = False
|
||||
_auto_gen = False # Call `generate` to generate a value
|
||||
@@ -33,11 +36,39 @@ class BaseField(object):
|
||||
|
||||
def __init__(self, db_field=None, name=None, required=False, default=None,
|
||||
unique=False, unique_with=None, primary_key=False,
|
||||
validation=None, choices=None, verbose_name=None,
|
||||
help_text=None):
|
||||
validation=None, choices=None, null=False, sparse=False,
|
||||
**kwargs):
|
||||
"""
|
||||
:param db_field: The database field to store this field in
|
||||
(defaults to the name of the field)
|
||||
:param name: Deprecated - use db_field
|
||||
:param required: If the field is required. Whether it has to have a
|
||||
value or not. Defaults to False.
|
||||
:param default: (optional) The default value for this field if no value
|
||||
has been set (or if the value has been unset). It can be a
|
||||
callable.
|
||||
:param unique: Is the field value unique or not. Defaults to False.
|
||||
:param unique_with: (optional) The other field this field should be
|
||||
unique with.
|
||||
:param primary_key: Mark this field as the primary key. Defaults to False.
|
||||
:param validation: (optional) A callable to validate the value of the
|
||||
field. Generally this is deprecated in favour of the
|
||||
`FIELD.validate` method
|
||||
:param choices: (optional) The valid choices
|
||||
:param null: (optional) Is the field value can be null. If no and there is a default value
|
||||
then the default value is set
|
||||
:param sparse: (optional) `sparse=True` combined with `unique=True` and `required=False`
|
||||
means that uniqueness won't be enforced for `None` values
|
||||
:param **kwargs: (optional) Arbitrary indirection-free metadata for
|
||||
this field can be supplied as additional keyword arguments and
|
||||
accessed as attributes of the field. Must not conflict with any
|
||||
existing attributes. Common metadata includes `verbose_name` and
|
||||
`help_text`.
|
||||
"""
|
||||
self.db_field = (db_field or name) if not primary_key else '_id'
|
||||
|
||||
if name:
|
||||
msg = "Fields' 'name' attribute deprecated in favour of 'db_field'"
|
||||
msg = 'Field\'s "name" attribute deprecated in favour of "db_field"'
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
self.required = required or primary_key
|
||||
self.default = default
|
||||
@@ -46,8 +77,37 @@ class BaseField(object):
|
||||
self.primary_key = primary_key
|
||||
self.validation = validation
|
||||
self.choices = choices
|
||||
self.verbose_name = verbose_name
|
||||
self.help_text = help_text
|
||||
self.null = null
|
||||
self.sparse = sparse
|
||||
self._owner_document = None
|
||||
|
||||
# Make sure db_field is a string (if it's explicitly defined).
|
||||
if (
|
||||
self.db_field is not None and
|
||||
not isinstance(self.db_field, six.string_types)
|
||||
):
|
||||
raise TypeError('db_field should be a string.')
|
||||
|
||||
# Make sure db_field doesn't contain any forbidden characters.
|
||||
if isinstance(self.db_field, six.string_types) and (
|
||||
'.' in self.db_field or
|
||||
'\0' in self.db_field or
|
||||
self.db_field.startswith('$')
|
||||
):
|
||||
raise ValueError(
|
||||
'field names cannot contain dots (".") or null characters '
|
||||
'("\\0"), and they must not start with a dollar sign ("$").'
|
||||
)
|
||||
|
||||
# Detect and report conflicts between metadata and base properties.
|
||||
conflicts = set(dir(self)) & set(kwargs)
|
||||
if conflicts:
|
||||
raise TypeError('%s already has attribute(s): %s' % (
|
||||
self.__class__.__name__, ', '.join(conflicts)))
|
||||
|
||||
# Assign metadata to the instance
|
||||
# This efficient method is available because no __slots__ are defined.
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
# Adjust the appropriate creation counter, and save our local copy.
|
||||
if self.db_field == '_id':
|
||||
@@ -58,81 +118,108 @@ class BaseField(object):
|
||||
BaseField.creation_counter += 1
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
"""Descriptor for retrieving a value from a field in a document. Do
|
||||
any necessary conversion between Python and MongoDB types.
|
||||
"""Descriptor for retrieving a value from a field in a document.
|
||||
"""
|
||||
if instance is None:
|
||||
# Document class being used rather than a document object
|
||||
return self
|
||||
# Get value from document instance if available, if not use default
|
||||
value = instance._data.get(self.name)
|
||||
|
||||
if value is None:
|
||||
value = self.default
|
||||
# Allow callable default values
|
||||
if callable(value):
|
||||
value = value()
|
||||
|
||||
EmbeddedDocument = _import_class('EmbeddedDocument')
|
||||
if isinstance(value, EmbeddedDocument) and value._instance is None:
|
||||
value._instance = weakref.proxy(instance)
|
||||
return value
|
||||
# Get value from document instance if available
|
||||
return instance._data.get(self.name)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
"""Descriptor for assigning a value to a field in a document.
|
||||
"""
|
||||
changed = False
|
||||
if (self.name not in instance._data or
|
||||
instance._data[self.name] != value):
|
||||
changed = True
|
||||
instance._data[self.name] = value
|
||||
if changed and instance._initialised:
|
||||
instance._mark_as_changed(self.name)
|
||||
|
||||
def error(self, message="", errors=None, field_name=None):
|
||||
"""Raises a ValidationError.
|
||||
"""
|
||||
# If setting to None and there is a default
|
||||
# Then set the value to the default value
|
||||
if value is None:
|
||||
if self.null:
|
||||
value = None
|
||||
elif self.default is not None:
|
||||
value = self.default
|
||||
if callable(value):
|
||||
value = value()
|
||||
|
||||
if instance._initialised:
|
||||
try:
|
||||
if (self.name not in instance._data or
|
||||
instance._data[self.name] != value):
|
||||
instance._mark_as_changed(self.name)
|
||||
except Exception:
|
||||
# Values cant be compared eg: naive and tz datetimes
|
||||
# So mark it as changed
|
||||
instance._mark_as_changed(self.name)
|
||||
|
||||
EmbeddedDocument = _import_class('EmbeddedDocument')
|
||||
if isinstance(value, EmbeddedDocument):
|
||||
value._instance = weakref.proxy(instance)
|
||||
elif isinstance(value, (list, tuple)):
|
||||
for v in value:
|
||||
if isinstance(v, EmbeddedDocument):
|
||||
v._instance = weakref.proxy(instance)
|
||||
instance._data[self.name] = value
|
||||
|
||||
def error(self, message='', errors=None, field_name=None):
|
||||
"""Raise a ValidationError."""
|
||||
field_name = field_name if field_name else self.name
|
||||
raise ValidationError(message, errors=errors, field_name=field_name)
|
||||
|
||||
def to_python(self, value):
|
||||
"""Convert a MongoDB-compatible type to a Python type.
|
||||
"""
|
||||
"""Convert a MongoDB-compatible type to a Python type."""
|
||||
return value
|
||||
|
||||
def to_mongo(self, value):
|
||||
"""Convert a Python type to a MongoDB-compatible type.
|
||||
"""
|
||||
"""Convert a Python type to a MongoDB-compatible type."""
|
||||
return self.to_python(value)
|
||||
|
||||
def _to_mongo_safe_call(self, value, use_db_field=True, fields=None):
|
||||
"""Helper method to call to_mongo with proper inputs."""
|
||||
f_inputs = self.to_mongo.__code__.co_varnames
|
||||
ex_vars = {}
|
||||
if 'fields' in f_inputs:
|
||||
ex_vars['fields'] = fields
|
||||
|
||||
if 'use_db_field' in f_inputs:
|
||||
ex_vars['use_db_field'] = use_db_field
|
||||
|
||||
return self.to_mongo(value, **ex_vars)
|
||||
|
||||
def prepare_query_value(self, op, value):
|
||||
"""Prepare a value that is being used in a query for PyMongo.
|
||||
"""
|
||||
"""Prepare a value that is being used in a query for PyMongo."""
|
||||
if op in UPDATE_OPERATORS:
|
||||
self.validate(value)
|
||||
return value
|
||||
|
||||
def validate(self, value, clean=True):
|
||||
"""Perform validation on a value.
|
||||
"""
|
||||
"""Perform validation on a value."""
|
||||
pass
|
||||
|
||||
def _validate(self, value, **kwargs):
|
||||
def _validate_choices(self, value):
|
||||
Document = _import_class('Document')
|
||||
EmbeddedDocument = _import_class('EmbeddedDocument')
|
||||
# check choices
|
||||
|
||||
choice_list = self.choices
|
||||
if isinstance(next(iter(choice_list)), (list, tuple)):
|
||||
# next(iter) is useful for sets
|
||||
choice_list = [k for k, _ in choice_list]
|
||||
|
||||
# Choices which are other types of Documents
|
||||
if isinstance(value, (Document, EmbeddedDocument)):
|
||||
if not any(isinstance(value, c) for c in choice_list):
|
||||
self.error(
|
||||
'Value must be an instance of %s' % (
|
||||
six.text_type(choice_list)
|
||||
)
|
||||
)
|
||||
# Choices which are types other than Documents
|
||||
elif value not in choice_list:
|
||||
self.error('Value must be one of %s' % six.text_type(choice_list))
|
||||
|
||||
def _validate(self, value, **kwargs):
|
||||
# Check the Choices Constraint
|
||||
if self.choices:
|
||||
is_cls = isinstance(value, (Document, EmbeddedDocument))
|
||||
value_to_check = value.__class__ if is_cls else value
|
||||
err_msg = 'an instance' if is_cls else 'one'
|
||||
if isinstance(self.choices[0], (list, tuple)):
|
||||
option_keys = [k for k, v in self.choices]
|
||||
if value_to_check not in option_keys:
|
||||
msg = ('Value must be %s of %s' %
|
||||
(err_msg, unicode(option_keys)))
|
||||
self.error(msg)
|
||||
elif value_to_check not in self.choices:
|
||||
msg = ('Value must be %s of %s' %
|
||||
(err_msg, unicode(self.choices)))
|
||||
self.error(msg)
|
||||
self._validate_choices(value)
|
||||
|
||||
# check validation argument
|
||||
if self.validation is not None:
|
||||
@@ -145,6 +232,17 @@ class BaseField(object):
|
||||
|
||||
self.validate(value, **kwargs)
|
||||
|
||||
@property
|
||||
def owner_document(self):
|
||||
return self._owner_document
|
||||
|
||||
def _set_owner_document(self, owner_document):
|
||||
self._owner_document = owner_document
|
||||
|
||||
@owner_document.setter
|
||||
def owner_document(self, owner_document):
|
||||
self._set_owner_document(owner_document)
|
||||
|
||||
|
||||
class ComplexBaseField(BaseField):
|
||||
"""Handles complex fields, such as lists / dictionaries.
|
||||
@@ -157,24 +255,25 @@ class ComplexBaseField(BaseField):
|
||||
"""
|
||||
|
||||
field = None
|
||||
__dereference = False
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
"""Descriptor to automatically dereference references.
|
||||
"""
|
||||
"""Descriptor to automatically dereference references."""
|
||||
if instance is None:
|
||||
# Document class being used rather than a document object
|
||||
return self
|
||||
|
||||
ReferenceField = _import_class('ReferenceField')
|
||||
GenericReferenceField = _import_class('GenericReferenceField')
|
||||
EmbeddedDocumentListField = _import_class('EmbeddedDocumentListField')
|
||||
dereference = (self._auto_dereference and
|
||||
(self.field is None or isinstance(self.field,
|
||||
(GenericReferenceField, ReferenceField))))
|
||||
(GenericReferenceField, ReferenceField))))
|
||||
|
||||
_dereference = _import_class('DeReference')()
|
||||
|
||||
self._auto_dereference = instance._fields[self.name]._auto_dereference
|
||||
if not self.__dereference and instance._initialised and dereference:
|
||||
instance._data[self.name] = self._dereference(
|
||||
if instance._initialised and dereference and instance._data.get(self.name):
|
||||
instance._data[self.name] = _dereference(
|
||||
instance._data.get(self.name), max_depth=1, instance=instance,
|
||||
name=self.name
|
||||
)
|
||||
@@ -182,18 +281,21 @@ class ComplexBaseField(BaseField):
|
||||
value = super(ComplexBaseField, self).__get__(instance, owner)
|
||||
|
||||
# Convert lists / values so we can watch for any changes on them
|
||||
if (isinstance(value, (list, tuple)) and
|
||||
not isinstance(value, BaseList)):
|
||||
value = BaseList(value, instance, self.name)
|
||||
if isinstance(value, (list, tuple)):
|
||||
if (issubclass(type(self), EmbeddedDocumentListField) and
|
||||
not isinstance(value, EmbeddedDocumentList)):
|
||||
value = EmbeddedDocumentList(value, instance, self.name)
|
||||
elif not isinstance(value, BaseList):
|
||||
value = BaseList(value, instance, self.name)
|
||||
instance._data[self.name] = value
|
||||
elif isinstance(value, dict) and not isinstance(value, BaseDict):
|
||||
value = BaseDict(value, instance, self.name)
|
||||
instance._data[self.name] = value
|
||||
|
||||
if (self._auto_dereference and instance._initialised and
|
||||
isinstance(value, (BaseList, BaseDict))
|
||||
and not value._dereferenced):
|
||||
value = self._dereference(
|
||||
isinstance(value, (BaseList, BaseDict)) and
|
||||
not value._dereferenced):
|
||||
value = _dereference(
|
||||
value, max_depth=1, instance=instance, name=self.name
|
||||
)
|
||||
value._dereferenced = True
|
||||
@@ -201,18 +303,9 @@ class ComplexBaseField(BaseField):
|
||||
|
||||
return value
|
||||
|
||||
def __set__(self, instance, value):
|
||||
"""Descriptor for assigning a value to a field in a document.
|
||||
"""
|
||||
instance._data[self.name] = value
|
||||
instance._mark_as_changed(self.name)
|
||||
|
||||
def to_python(self, value):
|
||||
"""Convert a MongoDB-compatible type to a Python type.
|
||||
"""
|
||||
Document = _import_class('Document')
|
||||
|
||||
if isinstance(value, basestring):
|
||||
"""Convert a MongoDB-compatible type to a Python type."""
|
||||
if isinstance(value, six.string_types):
|
||||
return value
|
||||
|
||||
if hasattr(value, 'to_python'):
|
||||
@@ -222,14 +315,16 @@ class ComplexBaseField(BaseField):
|
||||
if not hasattr(value, 'items'):
|
||||
try:
|
||||
is_list = True
|
||||
value = dict([(k, v) for k, v in enumerate(value)])
|
||||
value = {k: v for k, v in enumerate(value)}
|
||||
except TypeError: # Not iterable return the value
|
||||
return value
|
||||
|
||||
if self.field:
|
||||
value_dict = dict([(key, self.field.to_python(item))
|
||||
for key, item in value.items()])
|
||||
self.field._auto_dereference = self._auto_dereference
|
||||
value_dict = {key: self.field.to_python(item)
|
||||
for key, item in value.items()}
|
||||
else:
|
||||
Document = _import_class('Document')
|
||||
value_dict = {}
|
||||
for k, v in value.items():
|
||||
if isinstance(v, Document):
|
||||
@@ -245,27 +340,26 @@ class ComplexBaseField(BaseField):
|
||||
value_dict[k] = self.to_python(v)
|
||||
|
||||
if is_list: # Convert back to a list
|
||||
return [v for k, v in sorted(value_dict.items(),
|
||||
return [v for _, v in sorted(value_dict.items(),
|
||||
key=operator.itemgetter(0))]
|
||||
return value_dict
|
||||
|
||||
def to_mongo(self, value):
|
||||
"""Convert a Python type to a MongoDB-compatible type.
|
||||
"""
|
||||
Document = _import_class("Document")
|
||||
EmbeddedDocument = _import_class("EmbeddedDocument")
|
||||
GenericReferenceField = _import_class("GenericReferenceField")
|
||||
def to_mongo(self, value, use_db_field=True, fields=None):
|
||||
"""Convert a Python type to a MongoDB-compatible type."""
|
||||
Document = _import_class('Document')
|
||||
EmbeddedDocument = _import_class('EmbeddedDocument')
|
||||
GenericReferenceField = _import_class('GenericReferenceField')
|
||||
|
||||
if isinstance(value, basestring):
|
||||
if isinstance(value, six.string_types):
|
||||
return value
|
||||
|
||||
if hasattr(value, 'to_mongo'):
|
||||
if isinstance(value, Document):
|
||||
return GenericReferenceField().to_mongo(value)
|
||||
cls = value.__class__
|
||||
val = value.to_mongo()
|
||||
# If we its a document thats not inherited add _cls
|
||||
if (isinstance(value, EmbeddedDocument)):
|
||||
val = value.to_mongo(use_db_field, fields)
|
||||
# If it's a document that is not inherited add _cls
|
||||
if isinstance(value, EmbeddedDocument):
|
||||
val['_cls'] = cls.__name__
|
||||
return val
|
||||
|
||||
@@ -273,13 +367,15 @@ class ComplexBaseField(BaseField):
|
||||
if not hasattr(value, 'items'):
|
||||
try:
|
||||
is_list = True
|
||||
value = dict([(k, v) for k, v in enumerate(value)])
|
||||
value = {k: v for k, v in enumerate(value)}
|
||||
except TypeError: # Not iterable return the value
|
||||
return value
|
||||
|
||||
if self.field:
|
||||
value_dict = dict([(key, self.field.to_mongo(item))
|
||||
for key, item in value.iteritems()])
|
||||
value_dict = {
|
||||
key: self.field._to_mongo_safe_call(item, use_db_field, fields)
|
||||
for key, item in value.iteritems()
|
||||
}
|
||||
else:
|
||||
value_dict = {}
|
||||
for k, v in value.iteritems():
|
||||
@@ -293,9 +389,7 @@ class ComplexBaseField(BaseField):
|
||||
# any _cls data so make it a generic reference allows
|
||||
# us to dereference
|
||||
meta = getattr(v, '_meta', {})
|
||||
allow_inheritance = (
|
||||
meta.get('allow_inheritance', ALLOW_INHERITANCE)
|
||||
== True)
|
||||
allow_inheritance = meta.get('allow_inheritance')
|
||||
if not allow_inheritance and not self.field:
|
||||
value_dict[k] = GenericReferenceField().to_mongo(v)
|
||||
else:
|
||||
@@ -303,22 +397,21 @@ class ComplexBaseField(BaseField):
|
||||
value_dict[k] = DBRef(collection, v.pk)
|
||||
elif hasattr(v, 'to_mongo'):
|
||||
cls = v.__class__
|
||||
val = v.to_mongo()
|
||||
# If we its a document thats not inherited add _cls
|
||||
if (isinstance(v, (Document, EmbeddedDocument))):
|
||||
val = v.to_mongo(use_db_field, fields)
|
||||
# If it's a document that is not inherited add _cls
|
||||
if isinstance(v, (Document, EmbeddedDocument)):
|
||||
val['_cls'] = cls.__name__
|
||||
value_dict[k] = val
|
||||
else:
|
||||
value_dict[k] = self.to_mongo(v)
|
||||
value_dict[k] = self.to_mongo(v, use_db_field, fields)
|
||||
|
||||
if is_list: # Convert back to a list
|
||||
return [v for k, v in sorted(value_dict.items(),
|
||||
return [v for _, v in sorted(value_dict.items(),
|
||||
key=operator.itemgetter(0))]
|
||||
return value_dict
|
||||
|
||||
def validate(self, value):
|
||||
"""If field is provided ensure the value is valid.
|
||||
"""
|
||||
"""If field is provided ensure the value is valid."""
|
||||
errors = {}
|
||||
if self.field:
|
||||
if hasattr(value, 'iteritems') or hasattr(value, 'items'):
|
||||
@@ -328,9 +421,9 @@ class ComplexBaseField(BaseField):
|
||||
for k, v in sequence:
|
||||
try:
|
||||
self.field._validate(v)
|
||||
except ValidationError, error:
|
||||
except ValidationError as error:
|
||||
errors[k] = error.errors or error
|
||||
except (ValueError, AssertionError), error:
|
||||
except (ValueError, AssertionError) as error:
|
||||
errors[k] = error
|
||||
|
||||
if errors:
|
||||
@@ -354,35 +447,25 @@ class ComplexBaseField(BaseField):
|
||||
self.field.owner_document = owner_document
|
||||
self._owner_document = owner_document
|
||||
|
||||
def _get_owner_document(self, owner_document):
|
||||
self._owner_document = owner_document
|
||||
|
||||
owner_document = property(_get_owner_document, _set_owner_document)
|
||||
|
||||
@property
|
||||
def _dereference(self,):
|
||||
if not self.__dereference:
|
||||
DeReference = _import_class("DeReference")
|
||||
self.__dereference = DeReference() # Cached
|
||||
return self.__dereference
|
||||
|
||||
|
||||
class ObjectIdField(BaseField):
|
||||
"""A field wrapper around MongoDB's ObjectIds.
|
||||
"""
|
||||
"""A field wrapper around MongoDB's ObjectIds."""
|
||||
|
||||
def to_python(self, value):
|
||||
if not isinstance(value, ObjectId):
|
||||
value = ObjectId(value)
|
||||
try:
|
||||
if not isinstance(value, ObjectId):
|
||||
value = ObjectId(value)
|
||||
except Exception:
|
||||
pass
|
||||
return value
|
||||
|
||||
def to_mongo(self, value):
|
||||
if not isinstance(value, ObjectId):
|
||||
try:
|
||||
return ObjectId(unicode(value))
|
||||
except Exception, e:
|
||||
return ObjectId(six.text_type(value))
|
||||
except Exception as e:
|
||||
# e.message attribute has been deprecated since Python 2.6
|
||||
self.error(unicode(e))
|
||||
self.error(six.text_type(e))
|
||||
return value
|
||||
|
||||
def prepare_query_value(self, op, value):
|
||||
@@ -390,6 +473,167 @@ class ObjectIdField(BaseField):
|
||||
|
||||
def validate(self, value):
|
||||
try:
|
||||
ObjectId(unicode(value))
|
||||
except:
|
||||
ObjectId(six.text_type(value))
|
||||
except Exception:
|
||||
self.error('Invalid Object ID')
|
||||
|
||||
|
||||
class GeoJsonBaseField(BaseField):
|
||||
"""A geo json field storing a geojson style object.
|
||||
|
||||
.. versionadded:: 0.8
|
||||
"""
|
||||
|
||||
_geo_index = pymongo.GEOSPHERE
|
||||
_type = 'GeoBase'
|
||||
|
||||
def __init__(self, auto_index=True, *args, **kwargs):
|
||||
"""
|
||||
:param bool auto_index: Automatically create a '2dsphere' index.\
|
||||
Defaults to `True`.
|
||||
"""
|
||||
self._name = '%sField' % self._type
|
||||
if not auto_index:
|
||||
self._geo_index = False
|
||||
super(GeoJsonBaseField, self).__init__(*args, **kwargs)
|
||||
|
||||
def validate(self, value):
|
||||
"""Validate the GeoJson object based on its type."""
|
||||
if isinstance(value, dict):
|
||||
if set(value.keys()) == set(['type', 'coordinates']):
|
||||
if value['type'] != self._type:
|
||||
self.error('%s type must be "%s"' %
|
||||
(self._name, self._type))
|
||||
return self.validate(value['coordinates'])
|
||||
else:
|
||||
self.error('%s can only accept a valid GeoJson dictionary'
|
||||
' or lists of (x, y)' % self._name)
|
||||
return
|
||||
elif not isinstance(value, (list, tuple)):
|
||||
self.error('%s can only accept lists of [x, y]' % self._name)
|
||||
return
|
||||
|
||||
validate = getattr(self, '_validate_%s' % self._type.lower())
|
||||
error = validate(value)
|
||||
if error:
|
||||
self.error(error)
|
||||
|
||||
def _validate_polygon(self, value, top_level=True):
|
||||
if not isinstance(value, (list, tuple)):
|
||||
return 'Polygons must contain list of linestrings'
|
||||
|
||||
# Quick and dirty validator
|
||||
try:
|
||||
value[0][0][0]
|
||||
except (TypeError, IndexError):
|
||||
return 'Invalid Polygon must contain at least one valid linestring'
|
||||
|
||||
errors = []
|
||||
for val in value:
|
||||
error = self._validate_linestring(val, False)
|
||||
if not error and val[0] != val[-1]:
|
||||
error = 'LineStrings must start and end at the same point'
|
||||
if error and error not in errors:
|
||||
errors.append(error)
|
||||
if errors:
|
||||
if top_level:
|
||||
return 'Invalid Polygon:\n%s' % ', '.join(errors)
|
||||
else:
|
||||
return '%s' % ', '.join(errors)
|
||||
|
||||
def _validate_linestring(self, value, top_level=True):
|
||||
"""Validate a linestring."""
|
||||
if not isinstance(value, (list, tuple)):
|
||||
return 'LineStrings must contain list of coordinate pairs'
|
||||
|
||||
# Quick and dirty validator
|
||||
try:
|
||||
value[0][0]
|
||||
except (TypeError, IndexError):
|
||||
return 'Invalid LineString must contain at least one valid point'
|
||||
|
||||
errors = []
|
||||
for val in value:
|
||||
error = self._validate_point(val)
|
||||
if error and error not in errors:
|
||||
errors.append(error)
|
||||
if errors:
|
||||
if top_level:
|
||||
return 'Invalid LineString:\n%s' % ', '.join(errors)
|
||||
else:
|
||||
return '%s' % ', '.join(errors)
|
||||
|
||||
def _validate_point(self, value):
|
||||
"""Validate each set of coords"""
|
||||
if not isinstance(value, (list, tuple)):
|
||||
return 'Points must be a list of coordinate pairs'
|
||||
elif not len(value) == 2:
|
||||
return 'Value (%s) must be a two-dimensional point' % repr(value)
|
||||
elif (not isinstance(value[0], (float, int)) or
|
||||
not isinstance(value[1], (float, int))):
|
||||
return 'Both values (%s) in point must be float or int' % repr(value)
|
||||
|
||||
def _validate_multipoint(self, value):
|
||||
if not isinstance(value, (list, tuple)):
|
||||
return 'MultiPoint must be a list of Point'
|
||||
|
||||
# Quick and dirty validator
|
||||
try:
|
||||
value[0][0]
|
||||
except (TypeError, IndexError):
|
||||
return 'Invalid MultiPoint must contain at least one valid point'
|
||||
|
||||
errors = []
|
||||
for point in value:
|
||||
error = self._validate_point(point)
|
||||
if error and error not in errors:
|
||||
errors.append(error)
|
||||
|
||||
if errors:
|
||||
return '%s' % ', '.join(errors)
|
||||
|
||||
def _validate_multilinestring(self, value, top_level=True):
|
||||
if not isinstance(value, (list, tuple)):
|
||||
return 'MultiLineString must be a list of LineString'
|
||||
|
||||
# Quick and dirty validator
|
||||
try:
|
||||
value[0][0][0]
|
||||
except (TypeError, IndexError):
|
||||
return 'Invalid MultiLineString must contain at least one valid linestring'
|
||||
|
||||
errors = []
|
||||
for linestring in value:
|
||||
error = self._validate_linestring(linestring, False)
|
||||
if error and error not in errors:
|
||||
errors.append(error)
|
||||
|
||||
if errors:
|
||||
if top_level:
|
||||
return 'Invalid MultiLineString:\n%s' % ', '.join(errors)
|
||||
else:
|
||||
return '%s' % ', '.join(errors)
|
||||
|
||||
def _validate_multipolygon(self, value):
|
||||
if not isinstance(value, (list, tuple)):
|
||||
return 'MultiPolygon must be a list of Polygon'
|
||||
|
||||
# Quick and dirty validator
|
||||
try:
|
||||
value[0][0][0][0]
|
||||
except (TypeError, IndexError):
|
||||
return 'Invalid MultiPolygon must contain at least one valid Polygon'
|
||||
|
||||
errors = []
|
||||
for polygon in value:
|
||||
error = self._validate_polygon(polygon, False)
|
||||
if error and error not in errors:
|
||||
errors.append(error)
|
||||
|
||||
if errors:
|
||||
return 'Invalid MultiPolygon:\n%s' % ', '.join(errors)
|
||||
|
||||
def to_mongo(self, value):
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
return SON([('type', self._type), ('coordinates', value)])
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
import warnings
|
||||
|
||||
import pymongo
|
||||
import six
|
||||
|
||||
from mongoengine.base.common import _document_registry
|
||||
from mongoengine.base.fields import BaseField, ComplexBaseField, ObjectIdField
|
||||
from mongoengine.common import _import_class
|
||||
from mongoengine.errors import InvalidDocumentError
|
||||
from mongoengine.python_support import PY3
|
||||
from mongoengine.queryset import (DO_NOTHING, DoesNotExist,
|
||||
MultipleObjectsReturned,
|
||||
QuerySet, QuerySetManager)
|
||||
QuerySetManager)
|
||||
|
||||
from mongoengine.base.common import _document_registry, ALLOW_INHERITANCE
|
||||
from mongoengine.base.fields import BaseField, ComplexBaseField, ObjectIdField
|
||||
|
||||
__all__ = ('DocumentMetaclass', 'TopLevelDocumentMetaclass')
|
||||
|
||||
|
||||
class DocumentMetaclass(type):
|
||||
"""Metaclass for all documents.
|
||||
"""
|
||||
"""Metaclass for all documents."""
|
||||
|
||||
# TODO lower complexity of this method
|
||||
def __new__(cls, name, bases, attrs):
|
||||
flattened_bases = cls._get_bases(bases)
|
||||
super_new = super(DocumentMetaclass, cls).__new__
|
||||
@@ -29,6 +28,7 @@ class DocumentMetaclass(type):
|
||||
return super_new(cls, name, bases, attrs)
|
||||
|
||||
attrs['_is_document'] = attrs.get('_is_document', False)
|
||||
attrs['_cached_reference_fields'] = []
|
||||
|
||||
# EmbeddedDocuments could have meta data for inheritance
|
||||
if 'meta' in attrs:
|
||||
@@ -44,6 +44,12 @@ class DocumentMetaclass(type):
|
||||
elif hasattr(base, '_meta'):
|
||||
meta.merge(base._meta)
|
||||
attrs['_meta'] = meta
|
||||
attrs['_meta']['abstract'] = False # 789: EmbeddedDocument shouldn't inherit abstract
|
||||
|
||||
# If allow_inheritance is True, add a "_cls" string field to the attrs
|
||||
if attrs['_meta'].get('allow_inheritance'):
|
||||
StringField = _import_class('StringField')
|
||||
attrs['_cls'] = StringField()
|
||||
|
||||
# Handle document Fields
|
||||
|
||||
@@ -83,19 +89,21 @@ class DocumentMetaclass(type):
|
||||
# Ensure no duplicate db_fields
|
||||
duplicate_db_fields = [k for k, v in field_names.items() if v > 1]
|
||||
if duplicate_db_fields:
|
||||
msg = ("Multiple db_fields defined for: %s " %
|
||||
", ".join(duplicate_db_fields))
|
||||
msg = ('Multiple db_fields defined for: %s ' %
|
||||
', '.join(duplicate_db_fields))
|
||||
raise InvalidDocumentError(msg)
|
||||
|
||||
# Set _fields and db_field maps
|
||||
attrs['_fields'] = doc_fields
|
||||
attrs['_db_field_map'] = dict([(k, getattr(v, 'db_field', k))
|
||||
for k, v in doc_fields.iteritems()])
|
||||
attrs['_db_field_map'] = {k: getattr(v, 'db_field', k)
|
||||
for k, v in doc_fields.items()}
|
||||
attrs['_reverse_db_field_map'] = {
|
||||
v: k for k, v in attrs['_db_field_map'].items()
|
||||
}
|
||||
|
||||
attrs['_fields_ordered'] = tuple(i[1] for i in sorted(
|
||||
(v.creation_counter, v.name)
|
||||
for v in doc_fields.itervalues()))
|
||||
attrs['_reverse_db_field_map'] = dict(
|
||||
(v, k) for k, v in attrs['_db_field_map'].iteritems())
|
||||
|
||||
#
|
||||
# Set document hierarchy
|
||||
@@ -104,17 +112,15 @@ class DocumentMetaclass(type):
|
||||
class_name = [name]
|
||||
for base in flattened_bases:
|
||||
if (not getattr(base, '_is_base_cls', True) and
|
||||
not getattr(base, '_meta', {}).get('abstract', True)):
|
||||
# Collate heirarchy for _cls and _subclasses
|
||||
not getattr(base, '_meta', {}).get('abstract', True)):
|
||||
# Collate hierarchy for _cls and _subclasses
|
||||
class_name.append(base.__name__)
|
||||
|
||||
if hasattr(base, '_meta'):
|
||||
# Warn if allow_inheritance isn't set and prevent
|
||||
# inheritance of classes where inheritance is set to False
|
||||
allow_inheritance = base._meta.get('allow_inheritance',
|
||||
ALLOW_INHERITANCE)
|
||||
if (allow_inheritance is not True and
|
||||
not base._meta.get('abstract')):
|
||||
allow_inheritance = base._meta.get('allow_inheritance')
|
||||
if not allow_inheritance and not base._meta.get('abstract'):
|
||||
raise ValueError('Document %s may not be subclassed' %
|
||||
base.__name__)
|
||||
|
||||
@@ -138,39 +144,10 @@ class DocumentMetaclass(type):
|
||||
for base in document_bases:
|
||||
if _cls not in base._subclasses:
|
||||
base._subclasses += (_cls,)
|
||||
base._types = base._subclasses # TODO depreciate _types
|
||||
base._types = base._subclasses # TODO depreciate _types
|
||||
|
||||
# Handle delete rules
|
||||
Document, EmbeddedDocument, DictField = cls._import_classes()
|
||||
for field in new_class._fields.itervalues():
|
||||
f = field
|
||||
f.owner_document = new_class
|
||||
delete_rule = getattr(f, 'reverse_delete_rule', DO_NOTHING)
|
||||
if isinstance(f, ComplexBaseField) and hasattr(f, 'field'):
|
||||
delete_rule = getattr(f.field,
|
||||
'reverse_delete_rule',
|
||||
DO_NOTHING)
|
||||
if isinstance(f, DictField) and delete_rule != DO_NOTHING:
|
||||
msg = ("Reverse delete rules are not supported "
|
||||
"for %s (field: %s)" %
|
||||
(field.__class__.__name__, field.name))
|
||||
raise InvalidDocumentError(msg)
|
||||
|
||||
f = field.field
|
||||
|
||||
if delete_rule != DO_NOTHING:
|
||||
if issubclass(new_class, EmbeddedDocument):
|
||||
msg = ("Reverse delete rules are not supported for "
|
||||
"EmbeddedDocuments (field: %s)" % field.name)
|
||||
raise InvalidDocumentError(msg)
|
||||
f.document_type.register_delete_rule(new_class,
|
||||
field.name, delete_rule)
|
||||
|
||||
if (field.name and hasattr(Document, field.name) and
|
||||
EmbeddedDocument not in new_class.mro()):
|
||||
msg = ("%s is a document method and not a valid "
|
||||
"field name" % field.name)
|
||||
raise InvalidDocumentError(msg)
|
||||
(Document, EmbeddedDocument, DictField,
|
||||
CachedReferenceField) = cls._import_classes()
|
||||
|
||||
if issubclass(new_class, Document):
|
||||
new_class._collection = None
|
||||
@@ -185,8 +162,8 @@ class DocumentMetaclass(type):
|
||||
# module continues to use im_func and im_self, so the code below
|
||||
# copies __func__ into im_func and __self__ into im_self for
|
||||
# classmethod objects in Document derived classes.
|
||||
if PY3:
|
||||
for key, val in new_class.__dict__.items():
|
||||
if six.PY3:
|
||||
for val in new_class.__dict__.values():
|
||||
if isinstance(val, classmethod):
|
||||
f = val.__get__(new_class)
|
||||
if hasattr(f, '__func__') and not hasattr(f, 'im_func'):
|
||||
@@ -194,6 +171,52 @@ class DocumentMetaclass(type):
|
||||
if hasattr(f, '__self__') and not hasattr(f, 'im_self'):
|
||||
f.__dict__.update({'im_self': getattr(f, '__self__')})
|
||||
|
||||
# Handle delete rules
|
||||
for field in new_class._fields.itervalues():
|
||||
f = field
|
||||
if f.owner_document is None:
|
||||
f.owner_document = new_class
|
||||
delete_rule = getattr(f, 'reverse_delete_rule', DO_NOTHING)
|
||||
if isinstance(f, CachedReferenceField):
|
||||
|
||||
if issubclass(new_class, EmbeddedDocument):
|
||||
raise InvalidDocumentError('CachedReferenceFields is not '
|
||||
'allowed in EmbeddedDocuments')
|
||||
if not f.document_type:
|
||||
raise InvalidDocumentError(
|
||||
'Document is not available to sync')
|
||||
|
||||
if f.auto_sync:
|
||||
f.start_listener()
|
||||
|
||||
f.document_type._cached_reference_fields.append(f)
|
||||
|
||||
if isinstance(f, ComplexBaseField) and hasattr(f, 'field'):
|
||||
delete_rule = getattr(f.field,
|
||||
'reverse_delete_rule',
|
||||
DO_NOTHING)
|
||||
if isinstance(f, DictField) and delete_rule != DO_NOTHING:
|
||||
msg = ('Reverse delete rules are not supported '
|
||||
'for %s (field: %s)' %
|
||||
(field.__class__.__name__, field.name))
|
||||
raise InvalidDocumentError(msg)
|
||||
|
||||
f = field.field
|
||||
|
||||
if delete_rule != DO_NOTHING:
|
||||
if issubclass(new_class, EmbeddedDocument):
|
||||
msg = ('Reverse delete rules are not supported for '
|
||||
'EmbeddedDocuments (field: %s)' % field.name)
|
||||
raise InvalidDocumentError(msg)
|
||||
f.document_type.register_delete_rule(new_class,
|
||||
field.name, delete_rule)
|
||||
|
||||
if (field.name and hasattr(Document, field.name) and
|
||||
EmbeddedDocument not in new_class.mro()):
|
||||
msg = ('%s is a document method and not a valid '
|
||||
'field name' % field.name)
|
||||
raise InvalidDocumentError(msg)
|
||||
|
||||
return new_class
|
||||
|
||||
def add_to_class(self, name, value):
|
||||
@@ -222,7 +245,8 @@ class DocumentMetaclass(type):
|
||||
Document = _import_class('Document')
|
||||
EmbeddedDocument = _import_class('EmbeddedDocument')
|
||||
DictField = _import_class('DictField')
|
||||
return (Document, EmbeddedDocument, DictField)
|
||||
CachedReferenceField = _import_class('CachedReferenceField')
|
||||
return Document, EmbeddedDocument, DictField, CachedReferenceField
|
||||
|
||||
|
||||
class TopLevelDocumentMetaclass(DocumentMetaclass):
|
||||
@@ -235,7 +259,7 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
||||
super_new = super(TopLevelDocumentMetaclass, cls).__new__
|
||||
|
||||
# Set default _meta data if base class, otherwise get user defined meta
|
||||
if (attrs.get('my_metaclass') == TopLevelDocumentMetaclass):
|
||||
if attrs.get('my_metaclass') == TopLevelDocumentMetaclass:
|
||||
# defaults
|
||||
attrs['_meta'] = {
|
||||
'abstract': True,
|
||||
@@ -248,13 +272,18 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
||||
'index_drop_dups': False,
|
||||
'index_opts': None,
|
||||
'delete_rules': None,
|
||||
|
||||
# allow_inheritance can be True, False, and None. True means
|
||||
# "allow inheritance", False means "don't allow inheritance",
|
||||
# None means "do whatever your parent does, or don't allow
|
||||
# inheritance if you're a top-level class".
|
||||
'allow_inheritance': None,
|
||||
}
|
||||
attrs['_is_base_cls'] = True
|
||||
attrs['_meta'].update(attrs.get('meta', {}))
|
||||
else:
|
||||
attrs['_meta'] = attrs.get('meta', {})
|
||||
# Explictly set abstract to false unless set
|
||||
# Explicitly set abstract to false unless set
|
||||
attrs['_meta']['abstract'] = attrs['_meta'].get('abstract', False)
|
||||
attrs['_is_base_cls'] = False
|
||||
|
||||
@@ -269,26 +298,26 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
||||
|
||||
# Clean up top level meta
|
||||
if 'meta' in attrs:
|
||||
del(attrs['meta'])
|
||||
del attrs['meta']
|
||||
|
||||
# Find the parent document class
|
||||
parent_doc_cls = [b for b in flattened_bases
|
||||
if b.__class__ == TopLevelDocumentMetaclass]
|
||||
if b.__class__ == TopLevelDocumentMetaclass]
|
||||
parent_doc_cls = None if not parent_doc_cls else parent_doc_cls[0]
|
||||
|
||||
# Prevent classes setting collection different to their parents
|
||||
# If parent wasn't an abstract class
|
||||
if (parent_doc_cls and 'collection' in attrs.get('_meta', {})
|
||||
and not parent_doc_cls._meta.get('abstract', True)):
|
||||
msg = "Trying to set a collection on a subclass (%s)" % name
|
||||
warnings.warn(msg, SyntaxWarning)
|
||||
del(attrs['_meta']['collection'])
|
||||
if (parent_doc_cls and 'collection' in attrs.get('_meta', {}) and
|
||||
not parent_doc_cls._meta.get('abstract', True)):
|
||||
msg = 'Trying to set a collection on a subclass (%s)' % name
|
||||
warnings.warn(msg, SyntaxWarning)
|
||||
del attrs['_meta']['collection']
|
||||
|
||||
# Ensure abstract documents have abstract bases
|
||||
if attrs.get('_is_base_cls') or attrs['_meta'].get('abstract'):
|
||||
if (parent_doc_cls and
|
||||
not parent_doc_cls._meta.get('abstract', False)):
|
||||
msg = "Abstract document cannot have non-abstract base"
|
||||
not parent_doc_cls._meta.get('abstract', False)):
|
||||
msg = 'Abstract document cannot have non-abstract base'
|
||||
raise ValueError(msg)
|
||||
return super_new(cls, name, bases, attrs)
|
||||
|
||||
@@ -304,19 +333,23 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
||||
|
||||
# Set collection in the meta if its callable
|
||||
if (getattr(base, '_is_document', False) and
|
||||
not base._meta.get('abstract')):
|
||||
not base._meta.get('abstract')):
|
||||
collection = meta.get('collection', None)
|
||||
if callable(collection):
|
||||
meta['collection'] = collection(base)
|
||||
|
||||
meta.merge(attrs.get('_meta', {})) # Top level meta
|
||||
|
||||
# Only simple classes (direct subclasses of Document)
|
||||
# may set allow_inheritance to False
|
||||
# Only simple classes (i.e. direct subclasses of Document) may set
|
||||
# allow_inheritance to False. If the base Document allows inheritance,
|
||||
# none of its subclasses can override allow_inheritance to False.
|
||||
simple_class = all([b._meta.get('abstract')
|
||||
for b in flattened_bases if hasattr(b, '_meta')])
|
||||
if (not simple_class and meta['allow_inheritance'] == False and
|
||||
not meta['abstract']):
|
||||
if (
|
||||
not simple_class and
|
||||
meta['allow_inheritance'] is False and
|
||||
not meta['abstract']
|
||||
):
|
||||
raise ValueError('Only direct subclasses of Document may set '
|
||||
'"allow_inheritance" to False')
|
||||
|
||||
@@ -339,9 +372,9 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
||||
if callable(collection):
|
||||
new_class._meta['collection'] = collection(new_class)
|
||||
|
||||
# Provide a default queryset unless one has been set
|
||||
manager = attrs.get('objects', QuerySetManager())
|
||||
new_class.objects = manager
|
||||
# Provide a default queryset unless exists or one has been set
|
||||
if 'objects' not in dir(new_class):
|
||||
new_class.objects = QuerySetManager()
|
||||
|
||||
# Validate the fields and set primary key if needed
|
||||
for field_name, field in new_class._fields.iteritems():
|
||||
@@ -357,11 +390,20 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
||||
new_class.id = field
|
||||
|
||||
# Set primary key if not defined by the document
|
||||
new_class._auto_id_field = getattr(parent_doc_cls,
|
||||
'_auto_id_field', False)
|
||||
if not new_class._meta.get('id_field'):
|
||||
new_class._meta['id_field'] = 'id'
|
||||
new_class._fields['id'] = ObjectIdField(db_field='_id')
|
||||
new_class._fields['id'].name = 'id'
|
||||
new_class.id = new_class._fields['id']
|
||||
# After 0.10, find not existing names, instead of overwriting
|
||||
id_name, id_db_name = cls.get_auto_id_names(new_class)
|
||||
new_class._auto_id_field = True
|
||||
new_class._meta['id_field'] = id_name
|
||||
new_class._fields[id_name] = ObjectIdField(db_field=id_db_name)
|
||||
new_class._fields[id_name].name = id_name
|
||||
new_class.id = new_class._fields[id_name]
|
||||
new_class._db_field_map[id_name] = id_db_name
|
||||
new_class._reverse_db_field_map[id_db_name] = id_name
|
||||
# Prepend id field to _fields_ordered
|
||||
new_class._fields_ordered = (id_name, ) + new_class._fields_ordered
|
||||
|
||||
# Merge in exceptions with parent hierarchy
|
||||
exceptions_to_merge = (DoesNotExist, MultipleObjectsReturned)
|
||||
@@ -369,13 +411,27 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
||||
for exc in exceptions_to_merge:
|
||||
name = exc.__name__
|
||||
parents = tuple(getattr(base, name) for base in flattened_bases
|
||||
if hasattr(base, name)) or (exc,)
|
||||
if hasattr(base, name)) or (exc,)
|
||||
# Create new exception and set to new_class
|
||||
exception = type(name, parents, {'__module__': module})
|
||||
setattr(new_class, name, exception)
|
||||
|
||||
return new_class
|
||||
|
||||
@classmethod
|
||||
def get_auto_id_names(cls, new_class):
|
||||
id_name, id_db_name = ('id', '_id')
|
||||
if id_name not in new_class._fields and \
|
||||
id_db_name not in (v.db_field for v in new_class._fields.values()):
|
||||
return id_name, id_db_name
|
||||
id_basename, id_db_basename, i = 'auto_id', '_auto_id', 0
|
||||
while id_name in new_class._fields or \
|
||||
id_db_name in (v.db_field for v in new_class._fields.values()):
|
||||
id_name = '{0}_{1}'.format(id_basename, i)
|
||||
id_db_name = '{0}_{1}'.format(id_db_basename, i)
|
||||
i += 1
|
||||
return id_name, id_db_name
|
||||
|
||||
|
||||
class MetaDict(dict):
|
||||
"""Custom dictionary for meta classes.
|
||||
|
||||
@@ -1,21 +1,43 @@
|
||||
_class_registry_cache = {}
|
||||
_field_list_cache = []
|
||||
|
||||
|
||||
def _import_class(cls_name):
|
||||
"""Cached mechanism for imports"""
|
||||
"""Cache mechanism for imports.
|
||||
|
||||
Due to complications of circular imports mongoengine needs to do lots of
|
||||
inline imports in functions. This is inefficient as classes are
|
||||
imported repeated throughout the mongoengine code. This is
|
||||
compounded by some recursive functions requiring inline imports.
|
||||
|
||||
:mod:`mongoengine.common` provides a single point to import all these
|
||||
classes. Circular imports aren't an issue as it dynamically imports the
|
||||
class when first needed. Subsequent calls to the
|
||||
:func:`~mongoengine.common._import_class` can then directly retrieve the
|
||||
class from the :data:`mongoengine.common._class_registry_cache`.
|
||||
"""
|
||||
if cls_name in _class_registry_cache:
|
||||
return _class_registry_cache.get(cls_name)
|
||||
|
||||
doc_classes = ('Document', 'DynamicEmbeddedDocument', 'EmbeddedDocument',
|
||||
'MapReduceDocument')
|
||||
field_classes = ('DictField', 'DynamicField', 'EmbeddedDocumentField',
|
||||
'FileField', 'GenericReferenceField',
|
||||
'GenericEmbeddedDocumentField', 'GeoPointField',
|
||||
'ReferenceField', 'StringField', 'ComplexBaseField')
|
||||
|
||||
# Field Classes
|
||||
if not _field_list_cache:
|
||||
from mongoengine.fields import __all__ as fields
|
||||
_field_list_cache.extend(fields)
|
||||
from mongoengine.base.fields import __all__ as fields
|
||||
_field_list_cache.extend(fields)
|
||||
|
||||
field_classes = _field_list_cache
|
||||
|
||||
queryset_classes = ('OperationError',)
|
||||
deref_classes = ('DeReference',)
|
||||
|
||||
if cls_name in doc_classes:
|
||||
if cls_name == 'BaseDocument':
|
||||
from mongoengine.base import document as module
|
||||
import_classes = ['BaseDocument']
|
||||
elif cls_name in doc_classes:
|
||||
from mongoengine import document as module
|
||||
import_classes = doc_classes
|
||||
elif cls_name in field_classes:
|
||||
@@ -33,4 +55,4 @@ def _import_class(cls_name):
|
||||
for cls in import_classes:
|
||||
_class_registry_cache[cls] = getattr(module, cls)
|
||||
|
||||
return _class_registry_cache.get(cls_name)
|
||||
return _class_registry_cache.get(cls_name)
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import pymongo
|
||||
from pymongo import Connection, ReplicaSetConnection, uri_parser
|
||||
from pymongo import MongoClient, ReadPreference, uri_parser
|
||||
import six
|
||||
|
||||
from mongoengine.python_support import IS_PYMONGO_3
|
||||
|
||||
__all__ = ['ConnectionError', 'connect', 'register_connection',
|
||||
__all__ = ['MongoEngineConnectionError', 'connect', 'register_connection',
|
||||
'DEFAULT_CONNECTION_NAME']
|
||||
|
||||
|
||||
DEFAULT_CONNECTION_NAME = 'default'
|
||||
|
||||
if IS_PYMONGO_3:
|
||||
READ_PREFERENCE = ReadPreference.PRIMARY
|
||||
else:
|
||||
from pymongo import MongoReplicaSetClient
|
||||
READ_PREFERENCE = False
|
||||
|
||||
class ConnectionError(Exception):
|
||||
|
||||
class MongoEngineConnectionError(Exception):
|
||||
"""Error raised when the database connection can't be established or
|
||||
when a connection with a requested alias can't be retrieved.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@@ -18,9 +28,12 @@ _connections = {}
|
||||
_dbs = {}
|
||||
|
||||
|
||||
def register_connection(alias, name, host='localhost', port=27017,
|
||||
is_slave=False, read_preference=False, slaves=None,
|
||||
username=None, password=None, **kwargs):
|
||||
def register_connection(alias, name=None, host=None, port=None,
|
||||
read_preference=READ_PREFERENCE,
|
||||
username=None, password=None,
|
||||
authentication_source=None,
|
||||
authentication_mechanism=None,
|
||||
**kwargs):
|
||||
"""Add a connection.
|
||||
|
||||
:param alias: the name that will be used to refer to this connection
|
||||
@@ -28,136 +41,217 @@ def register_connection(alias, name, host='localhost', port=27017,
|
||||
:param name: the name of the specific database to use
|
||||
:param host: the host name of the :program:`mongod` instance to connect to
|
||||
:param port: the port that the :program:`mongod` instance is running on
|
||||
:param is_slave: whether the connection can act as a slave
|
||||
** Depreciated pymongo 2.0.1+
|
||||
:param read_preference: The read preference for the collection
|
||||
** Added pymongo 2.1
|
||||
:param slaves: a list of aliases of slave connections; each of these must
|
||||
be a registered connection that has :attr:`is_slave` set to ``True``
|
||||
:param username: username to authenticate with
|
||||
:param password: password to authenticate with
|
||||
:param kwargs: allow ad-hoc parameters to be passed into the pymongo driver
|
||||
:param authentication_source: database to authenticate against
|
||||
:param authentication_mechanism: database authentication mechanisms.
|
||||
By default, use SCRAM-SHA-1 with MongoDB 3.0 and later,
|
||||
MONGODB-CR (MongoDB Challenge Response protocol) for older servers.
|
||||
:param is_mock: explicitly use mongomock for this connection
|
||||
(can also be done by using `mongomock://` as db host prefix)
|
||||
:param kwargs: ad-hoc parameters to be passed into the pymongo driver,
|
||||
for example maxpoolsize, tz_aware, etc. See the documentation
|
||||
for pymongo's `MongoClient` for a full list.
|
||||
|
||||
.. versionchanged:: 0.10.6 - added mongomock support
|
||||
"""
|
||||
global _connection_settings
|
||||
|
||||
conn_settings = {
|
||||
'name': name,
|
||||
'host': host,
|
||||
'port': port,
|
||||
'is_slave': is_slave,
|
||||
'slaves': slaves or [],
|
||||
'name': name or 'test',
|
||||
'host': host or 'localhost',
|
||||
'port': port or 27017,
|
||||
'read_preference': read_preference,
|
||||
'username': username,
|
||||
'password': password,
|
||||
'read_preference': read_preference
|
||||
'authentication_source': authentication_source,
|
||||
'authentication_mechanism': authentication_mechanism
|
||||
}
|
||||
|
||||
# Handle uri style connections
|
||||
if "://" in host:
|
||||
uri_dict = uri_parser.parse_uri(host)
|
||||
if uri_dict.get('database') is None:
|
||||
raise ConnectionError("If using URI style connection include "\
|
||||
"database name in string")
|
||||
conn_settings.update({
|
||||
'host': host,
|
||||
'name': uri_dict.get('database'),
|
||||
'username': uri_dict.get('username'),
|
||||
'password': uri_dict.get('password'),
|
||||
'read_preference': read_preference,
|
||||
})
|
||||
if "replicaSet" in host:
|
||||
conn_settings['replicaSet'] = True
|
||||
conn_host = conn_settings['host']
|
||||
|
||||
# Host can be a list or a string, so if string, force to a list.
|
||||
if isinstance(conn_host, six.string_types):
|
||||
conn_host = [conn_host]
|
||||
|
||||
resolved_hosts = []
|
||||
for entity in conn_host:
|
||||
|
||||
# Handle Mongomock
|
||||
if entity.startswith('mongomock://'):
|
||||
conn_settings['is_mock'] = True
|
||||
# `mongomock://` is not a valid url prefix and must be replaced by `mongodb://`
|
||||
resolved_hosts.append(entity.replace('mongomock://', 'mongodb://', 1))
|
||||
|
||||
# Handle URI style connections, only updating connection params which
|
||||
# were explicitly specified in the URI.
|
||||
elif '://' in entity:
|
||||
uri_dict = uri_parser.parse_uri(entity)
|
||||
resolved_hosts.append(entity)
|
||||
|
||||
if uri_dict.get('database'):
|
||||
conn_settings['name'] = uri_dict.get('database')
|
||||
|
||||
for param in ('read_preference', 'username', 'password'):
|
||||
if uri_dict.get(param):
|
||||
conn_settings[param] = uri_dict[param]
|
||||
|
||||
uri_options = uri_dict['options']
|
||||
if 'replicaset' in uri_options:
|
||||
conn_settings['replicaSet'] = uri_options['replicaset']
|
||||
if 'authsource' in uri_options:
|
||||
conn_settings['authentication_source'] = uri_options['authsource']
|
||||
if 'authmechanism' in uri_options:
|
||||
conn_settings['authentication_mechanism'] = uri_options['authmechanism']
|
||||
else:
|
||||
resolved_hosts.append(entity)
|
||||
conn_settings['host'] = resolved_hosts
|
||||
|
||||
# Deprecated parameters that should not be passed on
|
||||
kwargs.pop('slaves', None)
|
||||
kwargs.pop('is_slave', None)
|
||||
|
||||
conn_settings.update(kwargs)
|
||||
_connection_settings[alias] = conn_settings
|
||||
|
||||
|
||||
def disconnect(alias=DEFAULT_CONNECTION_NAME):
|
||||
global _connections
|
||||
global _dbs
|
||||
|
||||
"""Close the connection with a given alias."""
|
||||
if alias in _connections:
|
||||
get_connection(alias=alias).disconnect()
|
||||
get_connection(alias=alias).close()
|
||||
del _connections[alias]
|
||||
if alias in _dbs:
|
||||
del _dbs[alias]
|
||||
|
||||
|
||||
def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
|
||||
global _connections
|
||||
"""Return a connection with a given alias."""
|
||||
|
||||
# Connect to the database if not already connected
|
||||
if reconnect:
|
||||
disconnect(alias)
|
||||
|
||||
if alias not in _connections:
|
||||
if alias not in _connection_settings:
|
||||
msg = 'Connection with alias "%s" has not been defined' % alias
|
||||
if alias == DEFAULT_CONNECTION_NAME:
|
||||
msg = 'You have not defined a default connection'
|
||||
raise ConnectionError(msg)
|
||||
conn_settings = _connection_settings[alias].copy()
|
||||
# If the requested alias already exists in the _connections list, return
|
||||
# it immediately.
|
||||
if alias in _connections:
|
||||
return _connections[alias]
|
||||
|
||||
if hasattr(pymongo, 'version_tuple'): # Support for 2.1+
|
||||
conn_settings.pop('name', None)
|
||||
conn_settings.pop('slaves', None)
|
||||
conn_settings.pop('is_slave', None)
|
||||
conn_settings.pop('username', None)
|
||||
conn_settings.pop('password', None)
|
||||
# Validate that the requested alias exists in the _connection_settings.
|
||||
# Raise MongoEngineConnectionError if it doesn't.
|
||||
if alias not in _connection_settings:
|
||||
if alias == DEFAULT_CONNECTION_NAME:
|
||||
msg = 'You have not defined a default connection'
|
||||
else:
|
||||
# Get all the slave connections
|
||||
if 'slaves' in conn_settings:
|
||||
slaves = []
|
||||
for slave_alias in conn_settings['slaves']:
|
||||
slaves.append(get_connection(slave_alias))
|
||||
conn_settings['slaves'] = slaves
|
||||
conn_settings.pop('read_preference', None)
|
||||
msg = 'Connection with alias "%s" has not been defined' % alias
|
||||
raise MongoEngineConnectionError(msg)
|
||||
|
||||
connection_class = Connection
|
||||
if 'replicaSet' in conn_settings:
|
||||
def _clean_settings(settings_dict):
|
||||
# set literal more efficient than calling set function
|
||||
irrelevant_fields_set = {
|
||||
'name', 'username', 'password',
|
||||
'authentication_source', 'authentication_mechanism'
|
||||
}
|
||||
return {
|
||||
k: v for k, v in settings_dict.items()
|
||||
if k not in irrelevant_fields_set
|
||||
}
|
||||
|
||||
# Retrieve a copy of the connection settings associated with the requested
|
||||
# alias and remove the database name and authentication info (we don't
|
||||
# care about them at this point).
|
||||
conn_settings = _clean_settings(_connection_settings[alias].copy())
|
||||
|
||||
# Determine if we should use PyMongo's or mongomock's MongoClient.
|
||||
is_mock = conn_settings.pop('is_mock', False)
|
||||
if is_mock:
|
||||
try:
|
||||
import mongomock
|
||||
except ImportError:
|
||||
raise RuntimeError('You need mongomock installed to mock '
|
||||
'MongoEngine.')
|
||||
connection_class = mongomock.MongoClient
|
||||
else:
|
||||
connection_class = MongoClient
|
||||
|
||||
# For replica set connections with PyMongo 2.x, use
|
||||
# MongoReplicaSetClient.
|
||||
# TODO remove this once we stop supporting PyMongo 2.x.
|
||||
if 'replicaSet' in conn_settings and not IS_PYMONGO_3:
|
||||
connection_class = MongoReplicaSetClient
|
||||
conn_settings['hosts_or_uri'] = conn_settings.pop('host', None)
|
||||
# Discard port since it can't be used on ReplicaSetConnection
|
||||
conn_settings.pop('port', None)
|
||||
# Discard replicaSet if not base string
|
||||
if not isinstance(conn_settings['replicaSet'], basestring):
|
||||
conn_settings.pop('replicaSet', None)
|
||||
connection_class = ReplicaSetConnection
|
||||
|
||||
# hosts_or_uri has to be a string, so if 'host' was provided
|
||||
# as a list, join its parts and separate them by ','
|
||||
if isinstance(conn_settings['hosts_or_uri'], list):
|
||||
conn_settings['hosts_or_uri'] = ','.join(
|
||||
conn_settings['hosts_or_uri'])
|
||||
|
||||
# Discard port since it can't be used on MongoReplicaSetClient
|
||||
conn_settings.pop('port', None)
|
||||
|
||||
# Iterate over all of the connection settings and if a connection with
|
||||
# the same parameters is already established, use it instead of creating
|
||||
# a new one.
|
||||
existing_connection = None
|
||||
connection_settings_iterator = (
|
||||
(db_alias, settings.copy())
|
||||
for db_alias, settings in _connection_settings.items()
|
||||
)
|
||||
for db_alias, connection_settings in connection_settings_iterator:
|
||||
connection_settings = _clean_settings(connection_settings)
|
||||
if conn_settings == connection_settings and _connections.get(db_alias):
|
||||
existing_connection = _connections[db_alias]
|
||||
break
|
||||
|
||||
# If an existing connection was found, assign it to the new alias
|
||||
if existing_connection:
|
||||
_connections[alias] = existing_connection
|
||||
else:
|
||||
# Otherwise, create the new connection for this alias. Raise
|
||||
# MongoEngineConnectionError if it can't be established.
|
||||
try:
|
||||
_connections[alias] = connection_class(**conn_settings)
|
||||
except Exception, e:
|
||||
raise ConnectionError("Cannot connect to database %s :\n%s" % (alias, e))
|
||||
except Exception as e:
|
||||
raise MongoEngineConnectionError(
|
||||
'Cannot connect to database %s :\n%s' % (alias, e))
|
||||
|
||||
return _connections[alias]
|
||||
|
||||
|
||||
def get_db(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
|
||||
global _dbs
|
||||
if reconnect:
|
||||
disconnect(alias)
|
||||
|
||||
if alias not in _dbs:
|
||||
conn = get_connection(alias)
|
||||
conn_settings = _connection_settings[alias]
|
||||
_dbs[alias] = conn[conn_settings['name']]
|
||||
db = conn[conn_settings['name']]
|
||||
auth_kwargs = {'source': conn_settings['authentication_source']}
|
||||
if conn_settings['authentication_mechanism'] is not None:
|
||||
auth_kwargs['mechanism'] = conn_settings['authentication_mechanism']
|
||||
# Authenticate if necessary
|
||||
if conn_settings['username'] and conn_settings['password']:
|
||||
_dbs[alias].authenticate(conn_settings['username'],
|
||||
conn_settings['password'])
|
||||
if conn_settings['username'] and (conn_settings['password'] or
|
||||
conn_settings['authentication_mechanism'] == 'MONGODB-X509'):
|
||||
db.authenticate(conn_settings['username'], conn_settings['password'], **auth_kwargs)
|
||||
_dbs[alias] = db
|
||||
return _dbs[alias]
|
||||
|
||||
|
||||
def connect(db, alias=DEFAULT_CONNECTION_NAME, **kwargs):
|
||||
def connect(db=None, alias=DEFAULT_CONNECTION_NAME, **kwargs):
|
||||
"""Connect to the database specified by the 'db' argument.
|
||||
|
||||
Connection settings may be provided here as well if the database is not
|
||||
running on the default port on localhost. If authentication is needed,
|
||||
provide username and password arguments as well.
|
||||
|
||||
Multiple databases are supported by using aliases. Provide a separate
|
||||
Multiple databases are supported by using aliases. Provide a separate
|
||||
`alias` to connect to a different instance of :program:`mongod`.
|
||||
|
||||
See the docstring for `register_connection` for more details about all
|
||||
supported kwargs.
|
||||
|
||||
.. versionchanged:: 0.6 - added multiple database support.
|
||||
"""
|
||||
global _connections
|
||||
if alias not in _connections:
|
||||
register_connection(alias, db, **kwargs)
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from mongoengine.common import _import_class
|
||||
from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db
|
||||
from mongoengine.queryset import OperationError, QuerySet
|
||||
|
||||
__all__ = ("switch_db", "switch_collection", "no_dereference", "query_counter")
|
||||
|
||||
__all__ = ('switch_db', 'switch_collection', 'no_dereference',
|
||||
'no_sub_classes', 'query_counter')
|
||||
|
||||
|
||||
class switch_db(object):
|
||||
""" switch_db alias context manager.
|
||||
"""switch_db alias context manager.
|
||||
|
||||
Example ::
|
||||
|
||||
@@ -17,15 +18,14 @@ class switch_db(object):
|
||||
class Group(Document):
|
||||
name = StringField()
|
||||
|
||||
Group(name="test").save() # Saves in the default db
|
||||
Group(name='test').save() # Saves in the default db
|
||||
|
||||
with switch_db(Group, 'testdb-1') as Group:
|
||||
Group(name="hello testdb!").save() # Saves in testdb-1
|
||||
|
||||
Group(name='hello testdb!').save() # Saves in testdb-1
|
||||
"""
|
||||
|
||||
def __init__(self, cls, db_alias):
|
||||
""" Construct the switch_db context manager
|
||||
"""Construct the switch_db context manager
|
||||
|
||||
:param cls: the class to change the registered db
|
||||
:param db_alias: the name of the specific database to use
|
||||
@@ -33,37 +33,36 @@ class switch_db(object):
|
||||
self.cls = cls
|
||||
self.collection = cls._get_collection()
|
||||
self.db_alias = db_alias
|
||||
self.ori_db_alias = cls._meta.get("db_alias", DEFAULT_CONNECTION_NAME)
|
||||
self.ori_db_alias = cls._meta.get('db_alias', DEFAULT_CONNECTION_NAME)
|
||||
|
||||
def __enter__(self):
|
||||
""" change the db_alias and clear the cached collection """
|
||||
self.cls._meta["db_alias"] = self.db_alias
|
||||
"""Change the db_alias and clear the cached collection."""
|
||||
self.cls._meta['db_alias'] = self.db_alias
|
||||
self.cls._collection = None
|
||||
return self.cls
|
||||
|
||||
def __exit__(self, t, value, traceback):
|
||||
""" Reset the db_alias and collection """
|
||||
self.cls._meta["db_alias"] = self.ori_db_alias
|
||||
"""Reset the db_alias and collection."""
|
||||
self.cls._meta['db_alias'] = self.ori_db_alias
|
||||
self.cls._collection = self.collection
|
||||
|
||||
|
||||
class switch_collection(object):
|
||||
""" switch_collection alias context manager.
|
||||
"""switch_collection alias context manager.
|
||||
|
||||
Example ::
|
||||
|
||||
class Group(Document):
|
||||
name = StringField()
|
||||
|
||||
Group(name="test").save() # Saves in the default db
|
||||
Group(name='test').save() # Saves in the default db
|
||||
|
||||
with switch_collection(Group, 'group1') as Group:
|
||||
Group(name="hello testdb!").save() # Saves in group1 collection
|
||||
|
||||
Group(name='hello testdb!').save() # Saves in group1 collection
|
||||
"""
|
||||
|
||||
def __init__(self, cls, collection_name):
|
||||
""" Construct the switch_collection context manager
|
||||
"""Construct the switch_collection context manager.
|
||||
|
||||
:param cls: the class to change the registered db
|
||||
:param collection_name: the name of the collection to use
|
||||
@@ -74,7 +73,7 @@ class switch_collection(object):
|
||||
self.collection_name = collection_name
|
||||
|
||||
def __enter__(self):
|
||||
""" change the _get_collection_name and clear the cached collection """
|
||||
"""Change the _get_collection_name and clear the cached collection."""
|
||||
|
||||
@classmethod
|
||||
def _get_collection_name(cls):
|
||||
@@ -85,24 +84,23 @@ class switch_collection(object):
|
||||
return self.cls
|
||||
|
||||
def __exit__(self, t, value, traceback):
|
||||
""" Reset the collection """
|
||||
"""Reset the collection."""
|
||||
self.cls._collection = self.ori_collection
|
||||
self.cls._get_collection_name = self.ori_get_collection_name
|
||||
|
||||
|
||||
class no_dereference(object):
|
||||
""" no_dereference context manager.
|
||||
"""no_dereference context manager.
|
||||
|
||||
Turns off all dereferencing in Documents for the duration of the context
|
||||
manager::
|
||||
|
||||
with no_dereference(Group) as Group:
|
||||
Group.objects.find()
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, cls):
|
||||
""" Construct the no_dereference context manager.
|
||||
"""Construct the no_dereference context manager.
|
||||
|
||||
:param cls: the class to turn dereferencing off on
|
||||
"""
|
||||
@@ -118,77 +116,102 @@ class no_dereference(object):
|
||||
ComplexBaseField))]
|
||||
|
||||
def __enter__(self):
|
||||
""" change the objects default and _auto_dereference values"""
|
||||
"""Change the objects default and _auto_dereference values."""
|
||||
for field in self.deref_fields:
|
||||
self.cls._fields[field]._auto_dereference = False
|
||||
return self.cls
|
||||
|
||||
def __exit__(self, t, value, traceback):
|
||||
""" Reset the default and _auto_dereference values"""
|
||||
"""Reset the default and _auto_dereference values."""
|
||||
for field in self.deref_fields:
|
||||
self.cls._fields[field]._auto_dereference = True
|
||||
return self.cls
|
||||
|
||||
|
||||
class QuerySetNoDeRef(QuerySet):
|
||||
"""Special no_dereference QuerySet"""
|
||||
def __dereference(items, max_depth=1, instance=None, name=None):
|
||||
return items
|
||||
class no_sub_classes(object):
|
||||
"""no_sub_classes context manager.
|
||||
|
||||
Only returns instances of this class and no sub (inherited) classes::
|
||||
|
||||
with no_sub_classes(Group) as Group:
|
||||
Group.objects.find()
|
||||
"""
|
||||
|
||||
def __init__(self, cls):
|
||||
"""Construct the no_sub_classes context manager.
|
||||
|
||||
:param cls: the class to turn querying sub classes on
|
||||
"""
|
||||
self.cls = cls
|
||||
|
||||
def __enter__(self):
|
||||
"""Change the objects default and _auto_dereference values."""
|
||||
self.cls._all_subclasses = self.cls._subclasses
|
||||
self.cls._subclasses = (self.cls,)
|
||||
return self.cls
|
||||
|
||||
def __exit__(self, t, value, traceback):
|
||||
"""Reset the default and _auto_dereference values."""
|
||||
self.cls._subclasses = self.cls._all_subclasses
|
||||
delattr(self.cls, '_all_subclasses')
|
||||
return self.cls
|
||||
|
||||
|
||||
class query_counter(object):
|
||||
""" Query_counter context manager to get the number of queries. """
|
||||
"""Query_counter context manager to get the number of queries."""
|
||||
|
||||
def __init__(self):
|
||||
""" Construct the query_counter. """
|
||||
"""Construct the query_counter."""
|
||||
self.counter = 0
|
||||
self.db = get_db()
|
||||
|
||||
def __enter__(self):
|
||||
""" On every with block we need to drop the profile collection. """
|
||||
"""On every with block we need to drop the profile collection."""
|
||||
self.db.set_profiling_level(0)
|
||||
self.db.system.profile.drop()
|
||||
self.db.set_profiling_level(2)
|
||||
return self
|
||||
|
||||
def __exit__(self, t, value, traceback):
|
||||
""" Reset the profiling level. """
|
||||
"""Reset the profiling level."""
|
||||
self.db.set_profiling_level(0)
|
||||
|
||||
def __eq__(self, value):
|
||||
""" == Compare querycounter. """
|
||||
return value == self._get_count()
|
||||
"""== Compare querycounter."""
|
||||
counter = self._get_count()
|
||||
return value == counter
|
||||
|
||||
def __ne__(self, value):
|
||||
""" != Compare querycounter. """
|
||||
"""!= Compare querycounter."""
|
||||
return not self.__eq__(value)
|
||||
|
||||
def __lt__(self, value):
|
||||
""" < Compare querycounter. """
|
||||
"""< Compare querycounter."""
|
||||
return self._get_count() < value
|
||||
|
||||
def __le__(self, value):
|
||||
""" <= Compare querycounter. """
|
||||
"""<= Compare querycounter."""
|
||||
return self._get_count() <= value
|
||||
|
||||
def __gt__(self, value):
|
||||
""" > Compare querycounter. """
|
||||
"""> Compare querycounter."""
|
||||
return self._get_count() > value
|
||||
|
||||
def __ge__(self, value):
|
||||
""" >= Compare querycounter. """
|
||||
""">= Compare querycounter."""
|
||||
return self._get_count() >= value
|
||||
|
||||
def __int__(self):
|
||||
""" int representation. """
|
||||
"""int representation."""
|
||||
return self._get_count()
|
||||
|
||||
def __repr__(self):
|
||||
""" repr query_counter as the number of queries. """
|
||||
"""repr query_counter as the number of queries."""
|
||||
return u"%s" % self._get_count()
|
||||
|
||||
def _get_count(self):
|
||||
""" Get the number of queries. """
|
||||
count = self.db.system.profile.find().count() - self.counter
|
||||
"""Get the number of queries."""
|
||||
ignore_query = {'ns': {'$ne': '%s.system.indexes' % self.db.name}}
|
||||
count = self.db.system.profile.find(ignore_query).count() - self.counter
|
||||
self.counter += 1
|
||||
return count
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
from bson import DBRef, SON
|
||||
import six
|
||||
|
||||
from base import (BaseDict, BaseList, TopLevelDocumentMetaclass, get_document)
|
||||
from fields import (ReferenceField, ListField, DictField, MapField)
|
||||
from connection import get_db
|
||||
from queryset import QuerySet
|
||||
from document import Document
|
||||
from mongoengine.base import (BaseDict, BaseList, EmbeddedDocumentList,
|
||||
TopLevelDocumentMetaclass, get_document)
|
||||
from mongoengine.connection import get_db
|
||||
from mongoengine.document import Document, EmbeddedDocument
|
||||
from mongoengine.fields import DictField, ListField, MapField, ReferenceField
|
||||
from mongoengine.queryset import QuerySet
|
||||
|
||||
|
||||
class DeReference(object):
|
||||
|
||||
def __call__(self, items, max_depth=1, instance=None, name=None):
|
||||
"""
|
||||
Cheaply dereferences the items to a set depth.
|
||||
Also handles the convertion of complex data types.
|
||||
Also handles the conversion of complex data types.
|
||||
|
||||
:param items: The iterable (dict, list, queryset) to be dereferenced.
|
||||
:param max_depth: The maximum depth to recurse to
|
||||
@@ -22,7 +23,7 @@ class DeReference(object):
|
||||
:class:`~mongoengine.base.ComplexBaseField`
|
||||
:param get: A boolean determining if being called by __get__
|
||||
"""
|
||||
if items is None or isinstance(items, basestring):
|
||||
if items is None or isinstance(items, six.string_types):
|
||||
return items
|
||||
|
||||
# cheapest way to convert a queryset to a list
|
||||
@@ -33,9 +34,10 @@ class DeReference(object):
|
||||
self.max_depth = max_depth
|
||||
doc_type = None
|
||||
|
||||
if instance and isinstance(instance, (Document, TopLevelDocumentMetaclass)):
|
||||
if instance and isinstance(instance, (Document, EmbeddedDocument,
|
||||
TopLevelDocumentMetaclass)):
|
||||
doc_type = instance._fields.get(name)
|
||||
if hasattr(doc_type, 'field'):
|
||||
while hasattr(doc_type, 'field'):
|
||||
doc_type = doc_type.field
|
||||
|
||||
if isinstance(doc_type, ReferenceField):
|
||||
@@ -45,20 +47,30 @@ class DeReference(object):
|
||||
|
||||
if is_list and all([i.__class__ == doc_type for i in items]):
|
||||
return items
|
||||
elif not is_list and all([i.__class__ == doc_type
|
||||
for i in items.values()]):
|
||||
elif not is_list and all(
|
||||
[i.__class__ == doc_type for i in items.values()]):
|
||||
return items
|
||||
elif not field.dbref:
|
||||
if not hasattr(items, 'items'):
|
||||
items = [field.to_python(v)
|
||||
if not isinstance(v, (DBRef, Document)) else v
|
||||
for v in items]
|
||||
|
||||
def _get_items(items):
|
||||
new_items = []
|
||||
for v in items:
|
||||
if isinstance(v, list):
|
||||
new_items.append(_get_items(v))
|
||||
elif not isinstance(v, (DBRef, Document)):
|
||||
new_items.append(field.to_python(v))
|
||||
else:
|
||||
new_items.append(v)
|
||||
return new_items
|
||||
|
||||
items = _get_items(items)
|
||||
else:
|
||||
items = dict([
|
||||
(k, field.to_python(v))
|
||||
if not isinstance(v, (DBRef, Document)) else (k, v)
|
||||
for k, v in items.iteritems()]
|
||||
)
|
||||
items = {
|
||||
k: (v if isinstance(v, (DBRef, Document))
|
||||
else field.to_python(v))
|
||||
for k, v in items.iteritems()
|
||||
}
|
||||
|
||||
self.reference_map = self._find_references(items)
|
||||
self.object_map = self._fetch_objects(doc_type=doc_type)
|
||||
@@ -76,36 +88,36 @@ class DeReference(object):
|
||||
return reference_map
|
||||
|
||||
# Determine the iterator to use
|
||||
if not hasattr(items, 'items'):
|
||||
iterator = enumerate(items)
|
||||
if isinstance(items, dict):
|
||||
iterator = items.values()
|
||||
else:
|
||||
iterator = items.iteritems()
|
||||
iterator = items
|
||||
|
||||
# Recursively find dbreferences
|
||||
depth += 1
|
||||
for k, item in iterator:
|
||||
if isinstance(item, Document):
|
||||
for item in iterator:
|
||||
if isinstance(item, (Document, EmbeddedDocument)):
|
||||
for field_name, field in item._fields.iteritems():
|
||||
v = item._data.get(field_name, None)
|
||||
if isinstance(v, (DBRef)):
|
||||
reference_map.setdefault(field.document_type, []).append(v.id)
|
||||
if isinstance(v, DBRef):
|
||||
reference_map.setdefault(field.document_type, set()).add(v.id)
|
||||
elif isinstance(v, (dict, SON)) and '_ref' in v:
|
||||
reference_map.setdefault(get_document(v['_cls']), []).append(v['_ref'].id)
|
||||
reference_map.setdefault(get_document(v['_cls']), set()).add(v['_ref'].id)
|
||||
elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth:
|
||||
field_cls = getattr(getattr(field, 'field', None), 'document_type', None)
|
||||
references = self._find_references(v, depth)
|
||||
for key, refs in references.iteritems():
|
||||
if isinstance(field_cls, (Document, TopLevelDocumentMetaclass)):
|
||||
key = field_cls
|
||||
reference_map.setdefault(key, []).extend(refs)
|
||||
elif isinstance(item, (DBRef)):
|
||||
reference_map.setdefault(item.collection, []).append(item.id)
|
||||
reference_map.setdefault(key, set()).update(refs)
|
||||
elif isinstance(item, DBRef):
|
||||
reference_map.setdefault(item.collection, set()).add(item.id)
|
||||
elif isinstance(item, (dict, SON)) and '_ref' in item:
|
||||
reference_map.setdefault(get_document(item['_cls']), []).append(item['_ref'].id)
|
||||
reference_map.setdefault(get_document(item['_cls']), set()).add(item['_ref'].id)
|
||||
elif isinstance(item, (dict, list, tuple)) and depth - 1 <= self.max_depth:
|
||||
references = self._find_references(item, depth - 1)
|
||||
for key, refs in references.iteritems():
|
||||
reference_map.setdefault(key, []).extend(refs)
|
||||
reference_map.setdefault(key, set()).update(refs)
|
||||
|
||||
return reference_map
|
||||
|
||||
@@ -113,34 +125,38 @@ class DeReference(object):
|
||||
"""Fetch all references and convert to their document objects
|
||||
"""
|
||||
object_map = {}
|
||||
for col, dbrefs in self.reference_map.iteritems():
|
||||
keys = object_map.keys()
|
||||
refs = list(set([dbref for dbref in dbrefs if unicode(dbref).encode('utf-8') not in keys]))
|
||||
if hasattr(col, 'objects'): # We have a document class for the refs
|
||||
references = col.objects.in_bulk(refs)
|
||||
for collection, dbrefs in self.reference_map.iteritems():
|
||||
if hasattr(collection, 'objects'): # We have a document class for the refs
|
||||
col_name = collection._get_collection_name()
|
||||
refs = [dbref for dbref in dbrefs
|
||||
if (col_name, dbref) not in object_map]
|
||||
references = collection.objects.in_bulk(refs)
|
||||
for key, doc in references.iteritems():
|
||||
object_map[key] = doc
|
||||
object_map[(col_name, key)] = doc
|
||||
else: # Generic reference: use the refs data to convert to document
|
||||
if isinstance(doc_type, (ListField, DictField, MapField,)):
|
||||
continue
|
||||
|
||||
refs = [dbref for dbref in dbrefs
|
||||
if (collection, dbref) not in object_map]
|
||||
|
||||
if doc_type:
|
||||
references = doc_type._get_db()[col].find({'_id': {'$in': refs}})
|
||||
references = doc_type._get_db()[collection].find({'_id': {'$in': refs}})
|
||||
for ref in references:
|
||||
doc = doc_type._from_son(ref)
|
||||
object_map[doc.id] = doc
|
||||
object_map[(collection, doc.id)] = doc
|
||||
else:
|
||||
references = get_db()[col].find({'_id': {'$in': refs}})
|
||||
references = get_db()[collection].find({'_id': {'$in': refs}})
|
||||
for ref in references:
|
||||
if '_cls' in ref:
|
||||
doc = get_document(ref["_cls"])._from_son(ref)
|
||||
doc = get_document(ref['_cls'])._from_son(ref)
|
||||
elif doc_type is None:
|
||||
doc = get_document(
|
||||
''.join(x.capitalize()
|
||||
for x in col.split('_')))._from_son(ref)
|
||||
for x in collection.split('_')))._from_son(ref)
|
||||
else:
|
||||
doc = doc_type._from_son(ref)
|
||||
object_map[doc.id] = doc
|
||||
object_map[(collection, doc.id)] = doc
|
||||
return object_map
|
||||
|
||||
def _attach_objects(self, items, depth=0, instance=None, name=None):
|
||||
@@ -166,14 +182,22 @@ class DeReference(object):
|
||||
|
||||
if isinstance(items, (dict, SON)):
|
||||
if '_ref' in items:
|
||||
return self.object_map.get(items['_ref'].id, items)
|
||||
return self.object_map.get(
|
||||
(items['_ref'].collection, items['_ref'].id), items)
|
||||
elif '_cls' in items:
|
||||
doc = get_document(items['_cls'])._from_son(items)
|
||||
_cls = doc._data.pop('_cls', None)
|
||||
del items['_cls']
|
||||
doc._data = self._attach_objects(doc._data, depth, doc, None)
|
||||
if _cls is not None:
|
||||
doc._data['_cls'] = _cls
|
||||
return doc
|
||||
|
||||
if not hasattr(items, 'items'):
|
||||
is_list = True
|
||||
list_type = BaseList
|
||||
if isinstance(items, EmbeddedDocumentList):
|
||||
list_type = EmbeddedDocumentList
|
||||
as_tuple = isinstance(items, tuple)
|
||||
iterator = enumerate(items)
|
||||
data = []
|
||||
@@ -191,25 +215,27 @@ class DeReference(object):
|
||||
|
||||
if k in self.object_map and not is_list:
|
||||
data[k] = self.object_map[k]
|
||||
elif isinstance(v, Document):
|
||||
for field_name, field in v._fields.iteritems():
|
||||
elif isinstance(v, (Document, EmbeddedDocument)):
|
||||
for field_name in v._fields:
|
||||
v = data[k]._data.get(field_name, None)
|
||||
if isinstance(v, (DBRef)):
|
||||
data[k]._data[field_name] = self.object_map.get(v.id, v)
|
||||
if isinstance(v, DBRef):
|
||||
data[k]._data[field_name] = self.object_map.get(
|
||||
(v.collection, v.id), v)
|
||||
elif isinstance(v, (dict, SON)) and '_ref' in v:
|
||||
data[k]._data[field_name] = self.object_map.get(v['_ref'].id, v)
|
||||
elif isinstance(v, dict) and depth <= self.max_depth:
|
||||
data[k]._data[field_name] = self._attach_objects(v, depth, instance=instance, name=name)
|
||||
elif isinstance(v, (list, tuple)) and depth <= self.max_depth:
|
||||
data[k]._data[field_name] = self._attach_objects(v, depth, instance=instance, name=name)
|
||||
data[k]._data[field_name] = self.object_map.get(
|
||||
(v['_ref'].collection, v['_ref'].id), v)
|
||||
elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth:
|
||||
item_name = six.text_type('{0}.{1}.{2}').format(name, k, field_name)
|
||||
data[k]._data[field_name] = self._attach_objects(v, depth, instance=instance, name=item_name)
|
||||
elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth:
|
||||
data[k] = self._attach_objects(v, depth - 1, instance=instance, name=name)
|
||||
item_name = '%s.%s' % (name, k) if name else name
|
||||
data[k] = self._attach_objects(v, depth - 1, instance=instance, name=item_name)
|
||||
elif hasattr(v, 'id'):
|
||||
data[k] = self.object_map.get(v.id, v)
|
||||
data[k] = self.object_map.get((v.collection, v.id), v)
|
||||
|
||||
if instance and name:
|
||||
if is_list:
|
||||
return tuple(data) if as_tuple else BaseList(data, instance, name)
|
||||
return tuple(data) if as_tuple else list_type(data, instance, name)
|
||||
return BaseDict(data, instance, name)
|
||||
depth += 1
|
||||
return data
|
||||
|
||||
@@ -1,426 +0,0 @@
|
||||
from mongoengine import *
|
||||
|
||||
from django.utils.encoding import smart_str
|
||||
from django.contrib.auth.models import _user_get_all_permissions
|
||||
from django.contrib.auth.models import _user_has_perm
|
||||
from django.db import models
|
||||
from django.contrib.contenttypes.models import ContentTypeManager
|
||||
from django.contrib import auth
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
try:
|
||||
from django.contrib.auth.hashers import check_password, make_password
|
||||
except ImportError:
|
||||
"""Handle older versions of Django"""
|
||||
from django.utils.hashcompat import md5_constructor, sha_constructor
|
||||
|
||||
def get_hexdigest(algorithm, salt, raw_password):
|
||||
raw_password, salt = smart_str(raw_password), smart_str(salt)
|
||||
if algorithm == 'md5':
|
||||
return md5_constructor(salt + raw_password).hexdigest()
|
||||
elif algorithm == 'sha1':
|
||||
return sha_constructor(salt + raw_password).hexdigest()
|
||||
raise ValueError('Got unknown password algorithm type in password')
|
||||
|
||||
def check_password(raw_password, password):
|
||||
algo, salt, hash = password.split('$')
|
||||
return hash == get_hexdigest(algo, salt, raw_password)
|
||||
|
||||
def make_password(raw_password):
|
||||
from random import random
|
||||
algo = 'sha1'
|
||||
salt = get_hexdigest(algo, str(random()), str(random()))[:5]
|
||||
hash = get_hexdigest(algo, salt, raw_password)
|
||||
return '%s$%s$%s' % (algo, salt, hash)
|
||||
|
||||
from .utils import datetime_now
|
||||
|
||||
REDIRECT_FIELD_NAME = 'next'
|
||||
|
||||
class ContentType(Document):
|
||||
name = StringField(max_length=100)
|
||||
app_label = StringField(max_length=100)
|
||||
model = StringField(max_length=100, verbose_name=_('python model class name'),
|
||||
unique_with='app_label')
|
||||
objects = ContentTypeManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('content type')
|
||||
verbose_name_plural = _('content types')
|
||||
# db_table = 'django_content_type'
|
||||
# ordering = ('name',)
|
||||
# unique_together = (('app_label', 'model'),)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def model_class(self):
|
||||
"Returns the Python model class for this type of content."
|
||||
from django.db import models
|
||||
return models.get_model(self.app_label, self.model)
|
||||
|
||||
def get_object_for_this_type(self, **kwargs):
|
||||
"""
|
||||
Returns an object of this type for the keyword arguments given.
|
||||
Basically, this is a proxy around this object_type's get_object() model
|
||||
method. The ObjectNotExist exception, if thrown, will not be caught,
|
||||
so code that calls this method should catch it.
|
||||
"""
|
||||
return self.model_class()._default_manager.using(self._state.db).get(**kwargs)
|
||||
|
||||
def natural_key(self):
|
||||
return (self.app_label, self.model)
|
||||
|
||||
class SiteProfileNotAvailable(Exception):
|
||||
pass
|
||||
|
||||
class PermissionManager(models.Manager):
|
||||
def get_by_natural_key(self, codename, app_label, model):
|
||||
return self.get(
|
||||
codename=codename,
|
||||
content_type=ContentType.objects.get_by_natural_key(app_label, model)
|
||||
)
|
||||
|
||||
class Permission(Document):
|
||||
"""The permissions system provides a way to assign permissions to specific users and groups of users.
|
||||
|
||||
The permission system is used by the Django admin site, but may also be useful in your own code. The Django admin site uses permissions as follows:
|
||||
|
||||
- The "add" permission limits the user's ability to view the "add" form and add an object.
|
||||
- The "change" permission limits a user's ability to view the change list, view the "change" form and change an object.
|
||||
- The "delete" permission limits the ability to delete an object.
|
||||
|
||||
Permissions are set globally per type of object, not per specific object instance. It is possible to say "Mary may change news stories," but it's not currently possible to say "Mary may change news stories, but only the ones she created herself" or "Mary may only change news stories that have a certain status or publication date."
|
||||
|
||||
Three basic permissions -- add, change and delete -- are automatically created for each Django model.
|
||||
"""
|
||||
name = StringField(max_length=50, verbose_name=_('username'))
|
||||
content_type = ReferenceField(ContentType)
|
||||
codename = StringField(max_length=100, verbose_name=_('codename'))
|
||||
# FIXME: don't access field of the other class
|
||||
# unique_with=['content_type__app_label', 'content_type__model'])
|
||||
|
||||
objects = PermissionManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('permission')
|
||||
verbose_name_plural = _('permissions')
|
||||
# unique_together = (('content_type', 'codename'),)
|
||||
# ordering = ('content_type__app_label', 'content_type__model', 'codename')
|
||||
|
||||
def __unicode__(self):
|
||||
return u"%s | %s | %s" % (
|
||||
unicode(self.content_type.app_label),
|
||||
unicode(self.content_type),
|
||||
unicode(self.name))
|
||||
|
||||
def natural_key(self):
|
||||
return (self.codename,) + self.content_type.natural_key()
|
||||
natural_key.dependencies = ['contenttypes.contenttype']
|
||||
|
||||
class Group(Document):
|
||||
"""Groups are a generic way of categorizing users to apply permissions, or some other label, to those users. A user can belong to any number of groups.
|
||||
|
||||
A user in a group automatically has all the permissions granted to that group. For example, if the group Site editors has the permission can_edit_home_page, any user in that group will have that permission.
|
||||
|
||||
Beyond permissions, groups are a convenient way to categorize users to apply some label, or extended functionality, to them. For example, you could create a group 'Special users', and you could write code that would do special things to those users -- such as giving them access to a members-only portion of your site, or sending them members-only e-mail messages.
|
||||
"""
|
||||
name = StringField(max_length=80, unique=True, verbose_name=_('name'))
|
||||
# permissions = models.ManyToManyField(Permission, verbose_name=_('permissions'), blank=True)
|
||||
permissions = ListField(ReferenceField(Permission, verbose_name=_('permissions'), required=False))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('group')
|
||||
verbose_name_plural = _('groups')
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
class UserManager(models.Manager):
|
||||
def create_user(self, username, email, password=None):
|
||||
"""
|
||||
Creates and saves a User with the given username, e-mail and password.
|
||||
"""
|
||||
now = datetime_now()
|
||||
|
||||
# Normalize the address by lowercasing the domain part of the email
|
||||
# address.
|
||||
try:
|
||||
email_name, domain_part = email.strip().split('@', 1)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
email = '@'.join([email_name, domain_part.lower()])
|
||||
|
||||
user = self.model(username=username, email=email, is_staff=False,
|
||||
is_active=True, is_superuser=False, last_login=now,
|
||||
date_joined=now)
|
||||
|
||||
user.set_password(password)
|
||||
user.save(using=self._db)
|
||||
return user
|
||||
|
||||
def create_superuser(self, username, email, password):
|
||||
u = self.create_user(username, email, password)
|
||||
u.is_staff = True
|
||||
u.is_active = True
|
||||
u.is_superuser = True
|
||||
u.save(using=self._db)
|
||||
return u
|
||||
|
||||
def make_random_password(self, length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'):
|
||||
"Generates a random password with the given length and given allowed_chars"
|
||||
# Note that default value of allowed_chars does not have "I" or letters
|
||||
# that look like it -- just to avoid confusion.
|
||||
from random import choice
|
||||
return ''.join([choice(allowed_chars) for i in range(length)])
|
||||
|
||||
|
||||
|
||||
class User(Document):
|
||||
"""A User document that aims to mirror most of the API specified by Django
|
||||
at http://docs.djangoproject.com/en/dev/topics/auth/#users
|
||||
"""
|
||||
username = StringField(max_length=30, required=True,
|
||||
verbose_name=_('username'),
|
||||
help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
|
||||
|
||||
first_name = StringField(max_length=30,
|
||||
verbose_name=_('first name'))
|
||||
|
||||
last_name = StringField(max_length=30,
|
||||
verbose_name=_('last name'))
|
||||
email = EmailField(verbose_name=_('e-mail address'))
|
||||
password = StringField(max_length=128,
|
||||
verbose_name=_('password'),
|
||||
help_text=_("Use '[algo]$[iterations]$[salt]$[hexdigest]' or use the <a href=\"password/\">change password form</a>."))
|
||||
is_staff = BooleanField(default=False,
|
||||
verbose_name=_('staff status'),
|
||||
help_text=_("Designates whether the user can log into this admin site."))
|
||||
is_active = BooleanField(default=True,
|
||||
verbose_name=_('active'),
|
||||
help_text=_("Designates whether this user should be treated as active. Unselect this instead of deleting accounts."))
|
||||
is_superuser = BooleanField(default=False,
|
||||
verbose_name=_('superuser status'),
|
||||
help_text=_("Designates that this user has all permissions without explicitly assigning them."))
|
||||
last_login = DateTimeField(default=datetime_now,
|
||||
verbose_name=_('last login'))
|
||||
date_joined = DateTimeField(default=datetime_now,
|
||||
verbose_name=_('date joined'))
|
||||
|
||||
meta = {
|
||||
'allow_inheritance': True,
|
||||
'indexes': [
|
||||
{'fields': ['username'], 'unique': True, 'sparse': True}
|
||||
]
|
||||
}
|
||||
|
||||
def __unicode__(self):
|
||||
return self.username
|
||||
|
||||
def get_full_name(self):
|
||||
"""Returns the users first and last names, separated by a space.
|
||||
"""
|
||||
full_name = u'%s %s' % (self.first_name or '', self.last_name or '')
|
||||
return full_name.strip()
|
||||
|
||||
def is_anonymous(self):
|
||||
return False
|
||||
|
||||
def is_authenticated(self):
|
||||
return True
|
||||
|
||||
def set_password(self, raw_password):
|
||||
"""Sets the user's password - always use this rather than directly
|
||||
assigning to :attr:`~mongoengine.django.auth.User.password` as the
|
||||
password is hashed before storage.
|
||||
"""
|
||||
self.password = make_password(raw_password)
|
||||
self.save()
|
||||
return self
|
||||
|
||||
def check_password(self, raw_password):
|
||||
"""Checks the user's password against a provided password - always use
|
||||
this rather than directly comparing to
|
||||
:attr:`~mongoengine.django.auth.User.password` as the password is
|
||||
hashed before storage.
|
||||
"""
|
||||
return check_password(raw_password, self.password)
|
||||
|
||||
def get_all_permissions(self, obj=None):
|
||||
return _user_get_all_permissions(self, obj)
|
||||
|
||||
def has_perm(self, perm, obj=None):
|
||||
"""
|
||||
Returns True if the user has the specified permission. This method
|
||||
queries all available auth backends, but returns immediately if any
|
||||
backend returns True. Thus, a user who has permission from a single
|
||||
auth backend is assumed to have permission in general. If an object is
|
||||
provided, permissions for this specific object are checked.
|
||||
"""
|
||||
|
||||
# Active superusers have all permissions.
|
||||
if self.is_active and self.is_superuser:
|
||||
return True
|
||||
|
||||
# Otherwise we need to check the backends.
|
||||
return _user_has_perm(self, perm, obj)
|
||||
|
||||
@classmethod
|
||||
def create_user(cls, username, password, email=None):
|
||||
"""Create (and save) a new user with the given username, password and
|
||||
email address.
|
||||
"""
|
||||
now = datetime_now()
|
||||
|
||||
# Normalize the address by lowercasing the domain part of the email
|
||||
# address.
|
||||
if email is not None:
|
||||
try:
|
||||
email_name, domain_part = email.strip().split('@', 1)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
email = '@'.join([email_name, domain_part.lower()])
|
||||
|
||||
user = cls(username=username, email=email, date_joined=now)
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
return user
|
||||
|
||||
def get_all_permissions(self, obj=None):
|
||||
permissions = set()
|
||||
anon = self.is_anonymous()
|
||||
for backend in auth.get_backends():
|
||||
if not anon or backend.supports_anonymous_user:
|
||||
if hasattr(backend, "get_all_permissions"):
|
||||
if obj is not None:
|
||||
if backend.supports_object_permissions:
|
||||
permissions.update(
|
||||
backend.get_all_permissions(user, obj)
|
||||
)
|
||||
else:
|
||||
permissions.update(backend.get_all_permissions(self))
|
||||
return permissions
|
||||
|
||||
def get_and_delete_messages(self):
|
||||
return []
|
||||
|
||||
def has_perm(self, perm, obj=None):
|
||||
anon = self.is_anonymous()
|
||||
active = self.is_active
|
||||
for backend in auth.get_backends():
|
||||
if (not active and not anon and backend.supports_inactive_user) or \
|
||||
(not anon or backend.supports_anonymous_user):
|
||||
if hasattr(backend, "has_perm"):
|
||||
if obj is not None:
|
||||
if (backend.supports_object_permissions and
|
||||
backend.has_perm(self, perm, obj)):
|
||||
return True
|
||||
else:
|
||||
if backend.has_perm(self, perm):
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_perms(self, perm_list, obj=None):
|
||||
"""
|
||||
Returns True if the user has each of the specified permissions.
|
||||
If object is passed, it checks if the user has all required perms
|
||||
for this object.
|
||||
"""
|
||||
for perm in perm_list:
|
||||
if not self.has_perm(perm, obj):
|
||||
return False
|
||||
return True
|
||||
|
||||
def has_module_perms(self, app_label):
|
||||
anon = self.is_anonymous()
|
||||
active = self.is_active
|
||||
for backend in auth.get_backends():
|
||||
if (not active and not anon and backend.supports_inactive_user) or \
|
||||
(not anon or backend.supports_anonymous_user):
|
||||
if hasattr(backend, "has_module_perms"):
|
||||
if backend.has_module_perms(self, app_label):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_and_delete_messages(self):
|
||||
messages = []
|
||||
for m in self.message_set.all():
|
||||
messages.append(m.message)
|
||||
m.delete()
|
||||
return messages
|
||||
|
||||
def email_user(self, subject, message, from_email=None):
|
||||
"Sends an e-mail to this User."
|
||||
from django.core.mail import send_mail
|
||||
send_mail(subject, message, from_email, [self.email])
|
||||
|
||||
def get_profile(self):
|
||||
"""
|
||||
Returns site-specific profile for this user. Raises
|
||||
SiteProfileNotAvailable if this site does not allow profiles.
|
||||
"""
|
||||
if not hasattr(self, '_profile_cache'):
|
||||
from django.conf import settings
|
||||
if not getattr(settings, 'AUTH_PROFILE_MODULE', False):
|
||||
raise SiteProfileNotAvailable('You need to set AUTH_PROFILE_MO'
|
||||
'DULE in your project settings')
|
||||
try:
|
||||
app_label, model_name = settings.AUTH_PROFILE_MODULE.split('.')
|
||||
except ValueError:
|
||||
raise SiteProfileNotAvailable('app_label and model_name should'
|
||||
' be separated by a dot in the AUTH_PROFILE_MODULE set'
|
||||
'ting')
|
||||
|
||||
try:
|
||||
model = models.get_model(app_label, model_name)
|
||||
if model is None:
|
||||
raise SiteProfileNotAvailable('Unable to load the profile '
|
||||
'model, check AUTH_PROFILE_MODULE in your project sett'
|
||||
'ings')
|
||||
self._profile_cache = model._default_manager.using(self._state.db).get(user__id__exact=self.id)
|
||||
self._profile_cache.user = self
|
||||
except (ImportError, ImproperlyConfigured):
|
||||
raise SiteProfileNotAvailable
|
||||
return self._profile_cache
|
||||
|
||||
def _get_message_set(self):
|
||||
import warnings
|
||||
warnings.warn('The user messaging API is deprecated. Please update'
|
||||
' your code to use the new messages framework.',
|
||||
category=DeprecationWarning)
|
||||
return self._message_set
|
||||
message_set = property(_get_message_set)
|
||||
|
||||
|
||||
class MongoEngineBackend(object):
|
||||
"""Authenticate using MongoEngine and mongoengine.django.auth.User.
|
||||
"""
|
||||
|
||||
supports_object_permissions = False
|
||||
supports_anonymous_user = False
|
||||
supports_inactive_user = False
|
||||
|
||||
def authenticate(self, username=None, password=None):
|
||||
user = User.objects(username=username).first()
|
||||
if user:
|
||||
if password and user.check_password(password):
|
||||
backend = auth.get_backends()[0]
|
||||
user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__)
|
||||
return user
|
||||
return None
|
||||
|
||||
def get_user(self, user_id):
|
||||
return User.objects.with_id(user_id)
|
||||
|
||||
|
||||
def get_user(userid):
|
||||
"""Returns a User object from an id (User.id). Django's equivalent takes
|
||||
request, but taking an id instead leaves it up to the developer to store
|
||||
the id in any way they want (session, signed cookie, etc.)
|
||||
"""
|
||||
if not userid:
|
||||
return AnonymousUser()
|
||||
return MongoEngineBackend().get_user(userid) or AnonymousUser()
|
||||
@@ -1,102 +0,0 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.sessions.backends.base import SessionBase, CreateError
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.utils.encoding import force_unicode
|
||||
|
||||
from mongoengine.document import Document
|
||||
from mongoengine import fields
|
||||
from mongoengine.queryset import OperationError
|
||||
from mongoengine.connection import DEFAULT_CONNECTION_NAME
|
||||
|
||||
from .utils import datetime_now
|
||||
|
||||
|
||||
MONGOENGINE_SESSION_DB_ALIAS = getattr(
|
||||
settings, 'MONGOENGINE_SESSION_DB_ALIAS',
|
||||
DEFAULT_CONNECTION_NAME)
|
||||
|
||||
# a setting for the name of the collection used to store sessions
|
||||
MONGOENGINE_SESSION_COLLECTION = getattr(
|
||||
settings, 'MONGOENGINE_SESSION_COLLECTION',
|
||||
'django_session')
|
||||
|
||||
# a setting for whether session data is stored encoded or not
|
||||
MONGOENGINE_SESSION_DATA_ENCODE = getattr(
|
||||
settings, 'MONGOENGINE_SESSION_DATA_ENCODE',
|
||||
True)
|
||||
|
||||
|
||||
class MongoSession(Document):
|
||||
session_key = fields.StringField(primary_key=True, max_length=40)
|
||||
session_data = fields.StringField() if MONGOENGINE_SESSION_DATA_ENCODE \
|
||||
else fields.DictField()
|
||||
expire_date = fields.DateTimeField()
|
||||
|
||||
meta = {
|
||||
'collection': MONGOENGINE_SESSION_COLLECTION,
|
||||
'db_alias': MONGOENGINE_SESSION_DB_ALIAS,
|
||||
'allow_inheritance': False,
|
||||
'indexes': [
|
||||
{
|
||||
'fields': ['expire_date'],
|
||||
'expireAfterSeconds': settings.SESSION_COOKIE_AGE
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def get_decoded(self):
|
||||
return SessionStore().decode(self.session_data)
|
||||
|
||||
|
||||
class SessionStore(SessionBase):
|
||||
"""A MongoEngine-based session store for Django.
|
||||
"""
|
||||
|
||||
def load(self):
|
||||
try:
|
||||
s = MongoSession.objects(session_key=self.session_key,
|
||||
expire_date__gt=datetime_now)[0]
|
||||
if MONGOENGINE_SESSION_DATA_ENCODE:
|
||||
return self.decode(force_unicode(s.session_data))
|
||||
else:
|
||||
return s.session_data
|
||||
except (IndexError, SuspiciousOperation):
|
||||
self.create()
|
||||
return {}
|
||||
|
||||
def exists(self, session_key):
|
||||
return bool(MongoSession.objects(session_key=session_key).first())
|
||||
|
||||
def create(self):
|
||||
while True:
|
||||
self._session_key = self._get_new_session_key()
|
||||
try:
|
||||
self.save(must_create=True)
|
||||
except CreateError:
|
||||
continue
|
||||
self.modified = True
|
||||
self._session_cache = {}
|
||||
return
|
||||
|
||||
def save(self, must_create=False):
|
||||
if self.session_key is None:
|
||||
self._session_key = self._get_new_session_key()
|
||||
s = MongoSession(session_key=self.session_key)
|
||||
if MONGOENGINE_SESSION_DATA_ENCODE:
|
||||
s.session_data = self.encode(self._get_session(no_load=must_create))
|
||||
else:
|
||||
s.session_data = self._get_session(no_load=must_create)
|
||||
s.expire_date = self.get_expiry_date()
|
||||
try:
|
||||
s.save(force_insert=must_create, safe=True)
|
||||
except OperationError:
|
||||
if must_create:
|
||||
raise CreateError
|
||||
raise
|
||||
|
||||
def delete(self, session_key=None):
|
||||
if session_key is None:
|
||||
if self.session_key is None:
|
||||
return
|
||||
session_key = self.session_key
|
||||
MongoSession.objects(session_key=session_key).delete()
|
||||
@@ -1,47 +0,0 @@
|
||||
from mongoengine.queryset import QuerySet
|
||||
from mongoengine.base import BaseDocument
|
||||
from mongoengine.errors import ValidationError
|
||||
|
||||
def _get_queryset(cls):
|
||||
"""Inspired by django.shortcuts.*"""
|
||||
if isinstance(cls, QuerySet):
|
||||
return cls
|
||||
else:
|
||||
return cls.objects
|
||||
|
||||
def get_document_or_404(cls, *args, **kwargs):
|
||||
"""
|
||||
Uses get() to return an document, or raises a Http404 exception if the document
|
||||
does not exist.
|
||||
|
||||
cls may be a Document or QuerySet object. All other passed
|
||||
arguments and keyword arguments are used in the get() query.
|
||||
|
||||
Note: Like with get(), an MultipleObjectsReturned will be raised if more than one
|
||||
object is found.
|
||||
|
||||
Inspired by django.shortcuts.*
|
||||
"""
|
||||
queryset = _get_queryset(cls)
|
||||
try:
|
||||
return queryset.get(*args, **kwargs)
|
||||
except (queryset._document.DoesNotExist, ValidationError):
|
||||
from django.http import Http404
|
||||
raise Http404('No %s matches the given query.' % queryset._document._class_name)
|
||||
|
||||
def get_list_or_404(cls, *args, **kwargs):
|
||||
"""
|
||||
Uses filter() to return a list of documents, or raise a Http404 exception if
|
||||
the list is empty.
|
||||
|
||||
cls may be a Document or QuerySet object. All other passed
|
||||
arguments and keyword arguments are used in the filter() query.
|
||||
|
||||
Inspired by django.shortcuts.*
|
||||
"""
|
||||
queryset = _get_queryset(cls)
|
||||
obj_list = list(queryset.filter(*args, **kwargs))
|
||||
if not obj_list:
|
||||
from django.http import Http404
|
||||
raise Http404('No %s matches the given query.' % queryset._document._class_name)
|
||||
return obj_list
|
||||
@@ -1,112 +0,0 @@
|
||||
import os
|
||||
import itertools
|
||||
import urlparse
|
||||
|
||||
from mongoengine import *
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import Storage
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
|
||||
class FileDocument(Document):
|
||||
"""A document used to store a single file in GridFS.
|
||||
"""
|
||||
file = FileField()
|
||||
|
||||
|
||||
class GridFSStorage(Storage):
|
||||
"""A custom storage backend to store files in GridFS
|
||||
"""
|
||||
|
||||
def __init__(self, base_url=None):
|
||||
|
||||
if base_url is None:
|
||||
base_url = settings.MEDIA_URL
|
||||
self.base_url = base_url
|
||||
self.document = FileDocument
|
||||
self.field = 'file'
|
||||
|
||||
def delete(self, name):
|
||||
"""Deletes the specified file from the storage system.
|
||||
"""
|
||||
if self.exists(name):
|
||||
doc = self.document.objects.first()
|
||||
field = getattr(doc, self.field)
|
||||
self._get_doc_with_name(name).delete() # Delete the FileField
|
||||
field.delete() # Delete the FileDocument
|
||||
|
||||
def exists(self, name):
|
||||
"""Returns True if a file referened by the given name already exists in the
|
||||
storage system, or False if the name is available for a new file.
|
||||
"""
|
||||
doc = self._get_doc_with_name(name)
|
||||
if doc:
|
||||
field = getattr(doc, self.field)
|
||||
return bool(field.name)
|
||||
else:
|
||||
return False
|
||||
|
||||
def listdir(self, path=None):
|
||||
"""Lists the contents of the specified path, returning a 2-tuple of lists;
|
||||
the first item being directories, the second item being files.
|
||||
"""
|
||||
def name(doc):
|
||||
return getattr(doc, self.field).name
|
||||
docs = self.document.objects
|
||||
return [], [name(d) for d in docs if name(d)]
|
||||
|
||||
def size(self, name):
|
||||
"""Returns the total size, in bytes, of the file specified by name.
|
||||
"""
|
||||
doc = self._get_doc_with_name(name)
|
||||
if doc:
|
||||
return getattr(doc, self.field).length
|
||||
else:
|
||||
raise ValueError("No such file or directory: '%s'" % name)
|
||||
|
||||
def url(self, name):
|
||||
"""Returns an absolute URL where the file's contents can be accessed
|
||||
directly by a web browser.
|
||||
"""
|
||||
if self.base_url is None:
|
||||
raise ValueError("This file is not accessible via a URL.")
|
||||
return urlparse.urljoin(self.base_url, name).replace('\\', '/')
|
||||
|
||||
def _get_doc_with_name(self, name):
|
||||
"""Find the documents in the store with the given name
|
||||
"""
|
||||
docs = self.document.objects
|
||||
doc = [d for d in docs if getattr(d, self.field).name == name]
|
||||
if doc:
|
||||
return doc[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def _open(self, name, mode='rb'):
|
||||
doc = self._get_doc_with_name(name)
|
||||
if doc:
|
||||
return getattr(doc, self.field)
|
||||
else:
|
||||
raise ValueError("No file found with the name '%s'." % name)
|
||||
|
||||
def get_available_name(self, name):
|
||||
"""Returns a filename that's free on the target storage system, and
|
||||
available for new content to be written to.
|
||||
"""
|
||||
file_root, file_ext = os.path.splitext(name)
|
||||
# If the filename already exists, add an underscore and a number (before
|
||||
# the file extension, if one exists) to the filename until the generated
|
||||
# filename doesn't exist.
|
||||
count = itertools.count(1)
|
||||
while self.exists(name):
|
||||
# file_ext includes the dot.
|
||||
name = os.path.join("%s_%s%s" % (file_root, count.next(), file_ext))
|
||||
|
||||
return name
|
||||
|
||||
def _save(self, name, content):
|
||||
doc = self.document()
|
||||
getattr(doc, self.field).put(content, filename=name)
|
||||
doc.save()
|
||||
|
||||
return name
|
||||
@@ -1,39 +0,0 @@
|
||||
#coding: utf-8
|
||||
from nose.plugins.skip import SkipTest
|
||||
|
||||
from mongoengine.python_support import PY3
|
||||
from mongoengine import connect
|
||||
|
||||
try:
|
||||
from django.test import TestCase
|
||||
from django.conf import settings
|
||||
except Exception as err:
|
||||
if PY3:
|
||||
from unittest import TestCase
|
||||
# Dummy value so no error
|
||||
class settings:
|
||||
MONGO_DATABASE_NAME = 'dummy'
|
||||
else:
|
||||
raise err
|
||||
|
||||
|
||||
class MongoTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
if PY3:
|
||||
raise SkipTest('django does not have Python 3 support')
|
||||
|
||||
"""
|
||||
TestCase class that clear the collection between the tests
|
||||
"""
|
||||
db_name = 'test_%s' % settings.MONGO_DATABASE_NAME
|
||||
def __init__(self, methodName='runtest'):
|
||||
self.db = connect(self.db_name).get_db()
|
||||
super(MongoTestCase, self).__init__(methodName)
|
||||
|
||||
def _post_teardown(self):
|
||||
super(MongoTestCase, self)._post_teardown()
|
||||
for collection in self.db.collection_names():
|
||||
if collection == 'system.indexes':
|
||||
continue
|
||||
self.db.drop_collection(collection)
|
||||
@@ -1,6 +0,0 @@
|
||||
try:
|
||||
# django >= 1.4
|
||||
from django.utils.timezone import now as datetime_now
|
||||
except ImportError:
|
||||
from datetime import datetime
|
||||
datetime_now = datetime.now
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,11 @@
|
||||
from collections import defaultdict
|
||||
|
||||
from mongoengine.python_support import txt_type
|
||||
import six
|
||||
|
||||
|
||||
__all__ = ('NotRegistered', 'InvalidDocumentError', 'ValidationError')
|
||||
__all__ = ('NotRegistered', 'InvalidDocumentError', 'LookUpError',
|
||||
'DoesNotExist', 'MultipleObjectsReturned', 'InvalidQueryError',
|
||||
'OperationError', 'NotUniqueError', 'FieldDoesNotExist',
|
||||
'ValidationError', 'SaveConditionError')
|
||||
|
||||
|
||||
class NotRegistered(Exception):
|
||||
@@ -38,6 +40,21 @@ class NotUniqueError(OperationError):
|
||||
pass
|
||||
|
||||
|
||||
class SaveConditionError(OperationError):
|
||||
pass
|
||||
|
||||
|
||||
class FieldDoesNotExist(Exception):
|
||||
"""Raised when trying to set a field
|
||||
not declared in a :class:`~mongoengine.Document`
|
||||
or an :class:`~mongoengine.EmbeddedDocument`.
|
||||
|
||||
To avoid this behavior on data loading,
|
||||
you should set the :attr:`strict` to ``False``
|
||||
in the :attr:`meta` dictionary.
|
||||
"""
|
||||
|
||||
|
||||
class ValidationError(AssertionError):
|
||||
"""Validation exception.
|
||||
|
||||
@@ -53,13 +70,13 @@ class ValidationError(AssertionError):
|
||||
field_name = None
|
||||
_message = None
|
||||
|
||||
def __init__(self, message="", **kwargs):
|
||||
def __init__(self, message='', **kwargs):
|
||||
self.errors = kwargs.get('errors', {})
|
||||
self.field_name = kwargs.get('field_name')
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return txt_type(self.message)
|
||||
return six.text_type(self.message)
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%s,)' % (self.__class__.__name__, self.message)
|
||||
@@ -93,16 +110,20 @@ class ValidationError(AssertionError):
|
||||
errors_dict = {}
|
||||
if not source:
|
||||
return errors_dict
|
||||
|
||||
if isinstance(source, dict):
|
||||
for field_name, error in source.iteritems():
|
||||
errors_dict[field_name] = build_dict(error)
|
||||
elif isinstance(source, ValidationError) and source.errors:
|
||||
return build_dict(source.errors)
|
||||
else:
|
||||
return unicode(source)
|
||||
return six.text_type(source)
|
||||
|
||||
return errors_dict
|
||||
|
||||
if not self.errors:
|
||||
return {}
|
||||
|
||||
return build_dict(self.errors)
|
||||
|
||||
def _format_errors(self):
|
||||
@@ -111,14 +132,14 @@ class ValidationError(AssertionError):
|
||||
def generate_key(value, prefix=''):
|
||||
if isinstance(value, list):
|
||||
value = ' '.join([generate_key(k) for k in value])
|
||||
if isinstance(value, dict):
|
||||
elif isinstance(value, dict):
|
||||
value = ' '.join(
|
||||
[generate_key(v, k) for k, v in value.iteritems()])
|
||||
[generate_key(v, k) for k, v in value.iteritems()])
|
||||
|
||||
results = "%s.%s" % (prefix, value) if prefix else value
|
||||
results = '%s.%s' % (prefix, value) if prefix else value
|
||||
return results
|
||||
|
||||
error_dict = defaultdict(list)
|
||||
for k, v in self.to_dict().iteritems():
|
||||
error_dict[generate_key(v)].append(k)
|
||||
return ' '.join(["%s: %s" % (k, v) for k, v in error_dict.iteritems()])
|
||||
return ' '.join(['%s: %s' % (k, v) for k, v in error_dict.iteritems()])
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,61 +1,25 @@
|
||||
"""Helper functions and types to aid with Python 2.5 - 3 support."""
|
||||
"""
|
||||
Helper functions, constants, and types to aid with Python v2.7 - v3.x and
|
||||
PyMongo v2.7 - v3.x support.
|
||||
"""
|
||||
import pymongo
|
||||
import six
|
||||
|
||||
import sys
|
||||
|
||||
PY3 = sys.version_info[0] == 3
|
||||
PY25 = sys.version_info[:2] == (2, 5)
|
||||
UNICODE_KWARGS = int(''.join([str(x) for x in sys.version_info[:3]])) > 264
|
||||
|
||||
if PY3:
|
||||
import codecs
|
||||
from io import BytesIO as StringIO
|
||||
# return s converted to binary. b('test') should be equivalent to b'test'
|
||||
def b(s):
|
||||
return codecs.latin_1_encode(s)[0]
|
||||
|
||||
bin_type = bytes
|
||||
txt_type = str
|
||||
if pymongo.version_tuple[0] < 3:
|
||||
IS_PYMONGO_3 = False
|
||||
else:
|
||||
IS_PYMONGO_3 = True
|
||||
|
||||
|
||||
# six.BytesIO resolves to StringIO.StringIO in Py2 and io.BytesIO in Py3.
|
||||
StringIO = six.BytesIO
|
||||
|
||||
# Additionally for Py2, try to use the faster cStringIO, if available
|
||||
if not six.PY3:
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
import cStringIO
|
||||
except ImportError:
|
||||
from StringIO import StringIO
|
||||
|
||||
# Conversion to binary only necessary in Python 3
|
||||
def b(s):
|
||||
return s
|
||||
|
||||
bin_type = str
|
||||
txt_type = unicode
|
||||
|
||||
str_types = (bin_type, txt_type)
|
||||
|
||||
if PY25:
|
||||
def product(*args, **kwds):
|
||||
pools = map(tuple, args) * kwds.get('repeat', 1)
|
||||
result = [[]]
|
||||
for pool in pools:
|
||||
result = [x + [y] for x in result for y in pool]
|
||||
for prod in result:
|
||||
yield tuple(prod)
|
||||
reduce = reduce
|
||||
else:
|
||||
from itertools import product
|
||||
from functools import reduce
|
||||
|
||||
|
||||
# For use with Python 2.5
|
||||
# converts all keys from unicode to str for d and all nested dictionaries
|
||||
def to_str_keys_recursive(d):
|
||||
if isinstance(d, list):
|
||||
for val in d:
|
||||
if isinstance(val, (dict, list)):
|
||||
to_str_keys_recursive(val)
|
||||
elif isinstance(d, dict):
|
||||
for key, val in d.items():
|
||||
if isinstance(val, (dict, list)):
|
||||
to_str_keys_recursive(val)
|
||||
if isinstance(key, unicode):
|
||||
d[str(key)] = d.pop(key)
|
||||
pass
|
||||
else:
|
||||
raise ValueError("non list/dict parameter not allowed")
|
||||
StringIO = cStringIO.StringIO
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
from mongoengine.errors import (DoesNotExist, MultipleObjectsReturned,
|
||||
InvalidQueryError, OperationError,
|
||||
NotUniqueError)
|
||||
from mongoengine.errors import *
|
||||
from mongoengine.queryset.field_list import *
|
||||
from mongoengine.queryset.manager import *
|
||||
from mongoengine.queryset.queryset import *
|
||||
from mongoengine.queryset.transform import *
|
||||
from mongoengine.queryset.visitor import *
|
||||
|
||||
__all__ = (field_list.__all__ + manager.__all__ + queryset.__all__ +
|
||||
transform.__all__ + visitor.__all__)
|
||||
# Expose just the public subset of all imported objects and constants.
|
||||
__all__ = (
|
||||
'QuerySet', 'QuerySetNoCache', 'Q', 'queryset_manager', 'QuerySetManager',
|
||||
'QueryFieldList', 'DO_NOTHING', 'NULLIFY', 'CASCADE', 'DENY', 'PULL',
|
||||
|
||||
# Errors that might be related to a queryset, mostly here for backward
|
||||
# compatibility
|
||||
'DoesNotExist', 'InvalidQueryError', 'MultipleObjectsReturned',
|
||||
'NotUniqueError', 'OperationError',
|
||||
)
|
||||
|
||||
1889
mongoengine/queryset/base.py
Normal file
1889
mongoengine/queryset/base.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
|
||||
__all__ = ('QueryFieldList',)
|
||||
|
||||
|
||||
@@ -7,11 +6,20 @@ class QueryFieldList(object):
|
||||
ONLY = 1
|
||||
EXCLUDE = 0
|
||||
|
||||
def __init__(self, fields=[], value=ONLY, always_include=[]):
|
||||
def __init__(self, fields=None, value=ONLY, always_include=None, _only_called=False):
|
||||
"""The QueryFieldList builder
|
||||
|
||||
:param fields: A list of fields used in `.only()` or `.exclude()`
|
||||
:param value: How to handle the fields; either `ONLY` or `EXCLUDE`
|
||||
:param always_include: Any fields to always_include eg `_cls`
|
||||
:param _only_called: Has `.only()` been called? If so its a set of fields
|
||||
otherwise it performs a union.
|
||||
"""
|
||||
self.value = value
|
||||
self.fields = set(fields)
|
||||
self.always_include = set(always_include)
|
||||
self.fields = set(fields or [])
|
||||
self.always_include = set(always_include or [])
|
||||
self._id = None
|
||||
self._only_called = _only_called
|
||||
self.slice = {}
|
||||
|
||||
def __add__(self, f):
|
||||
@@ -26,7 +34,10 @@ class QueryFieldList(object):
|
||||
self.slice = {}
|
||||
elif self.value is self.ONLY and f.value is self.ONLY:
|
||||
self._clean_slice()
|
||||
self.fields = self.fields.intersection(f.fields)
|
||||
if self._only_called:
|
||||
self.fields = self.fields.union(f.fields)
|
||||
else:
|
||||
self.fields = f.fields
|
||||
elif self.value is self.EXCLUDE and f.value is self.EXCLUDE:
|
||||
self.fields = self.fields.union(f.fields)
|
||||
self._clean_slice()
|
||||
@@ -43,16 +54,20 @@ class QueryFieldList(object):
|
||||
|
||||
if self.always_include:
|
||||
if self.value is self.ONLY and self.fields:
|
||||
self.fields = self.fields.union(self.always_include)
|
||||
if sorted(self.slice.keys()) != sorted(self.fields):
|
||||
self.fields = self.fields.union(self.always_include)
|
||||
else:
|
||||
self.fields -= self.always_include
|
||||
|
||||
if getattr(f, '_only_called', False):
|
||||
self._only_called = True
|
||||
return self
|
||||
|
||||
def __nonzero__(self):
|
||||
return bool(self.fields)
|
||||
|
||||
def as_dict(self):
|
||||
field_list = dict((field, self.value) for field in self.fields)
|
||||
field_list = {field: self.value for field in self.fields}
|
||||
if self.slice:
|
||||
field_list.update(self.slice)
|
||||
if self._id is not None:
|
||||
|
||||
@@ -29,7 +29,7 @@ class QuerySetManager(object):
|
||||
Document.objects is accessed.
|
||||
"""
|
||||
if instance is not None:
|
||||
# Document class being used rather than a document object
|
||||
# Document object being used rather than a document class
|
||||
return self
|
||||
|
||||
# owner is the document that contains the QuerySetManager
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,49 +1,57 @@
|
||||
from collections import defaultdict
|
||||
|
||||
from bson import SON
|
||||
from bson import ObjectId, SON
|
||||
from bson.dbref import DBRef
|
||||
import pymongo
|
||||
import six
|
||||
|
||||
from mongoengine.base import UPDATE_OPERATORS
|
||||
from mongoengine.common import _import_class
|
||||
from mongoengine.errors import InvalidQueryError, LookUpError
|
||||
from mongoengine.connection import get_connection
|
||||
from mongoengine.errors import InvalidQueryError
|
||||
from mongoengine.python_support import IS_PYMONGO_3
|
||||
|
||||
__all__ = ('query', 'update')
|
||||
|
||||
|
||||
COMPARISON_OPERATORS = ('ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'mod',
|
||||
'all', 'size', 'exists', 'not')
|
||||
GEO_OPERATORS = ('within_distance', 'within_spherical_distance',
|
||||
'within_box', 'within_polygon', 'near', 'near_sphere',
|
||||
'max_distance')
|
||||
STRING_OPERATORS = ('contains', 'icontains', 'startswith',
|
||||
'istartswith', 'endswith', 'iendswith',
|
||||
'exact', 'iexact')
|
||||
CUSTOM_OPERATORS = ('match',)
|
||||
MATCH_OPERATORS = (COMPARISON_OPERATORS + GEO_OPERATORS +
|
||||
STRING_OPERATORS + CUSTOM_OPERATORS)
|
||||
|
||||
UPDATE_OPERATORS = ('set', 'unset', 'inc', 'dec', 'pop', 'push',
|
||||
'push_all', 'pull', 'pull_all', 'add_to_set')
|
||||
'all', 'size', 'exists', 'not', 'elemMatch', 'type')
|
||||
GEO_OPERATORS = ('within_distance', 'within_spherical_distance',
|
||||
'within_box', 'within_polygon', 'near', 'near_sphere',
|
||||
'max_distance', 'min_distance', 'geo_within', 'geo_within_box',
|
||||
'geo_within_polygon', 'geo_within_center',
|
||||
'geo_within_sphere', 'geo_intersects')
|
||||
STRING_OPERATORS = ('contains', 'icontains', 'startswith',
|
||||
'istartswith', 'endswith', 'iendswith',
|
||||
'exact', 'iexact')
|
||||
CUSTOM_OPERATORS = ('match',)
|
||||
MATCH_OPERATORS = (COMPARISON_OPERATORS + GEO_OPERATORS +
|
||||
STRING_OPERATORS + CUSTOM_OPERATORS)
|
||||
|
||||
|
||||
def query(_doc_cls=None, _field_operation=False, **query):
|
||||
"""Transform a query from Django-style format to Mongo format.
|
||||
"""
|
||||
# TODO make this less complex
|
||||
def query(_doc_cls=None, **kwargs):
|
||||
"""Transform a query from Django-style format to Mongo format."""
|
||||
mongo_query = {}
|
||||
merge_query = defaultdict(list)
|
||||
for key, value in sorted(query.items()):
|
||||
if key == "__raw__":
|
||||
for key, value in sorted(kwargs.items()):
|
||||
if key == '__raw__':
|
||||
mongo_query.update(value)
|
||||
continue
|
||||
|
||||
parts = key.split('__')
|
||||
parts = key.rsplit('__')
|
||||
indices = [(i, p) for i, p in enumerate(parts) if p.isdigit()]
|
||||
parts = [part for part in parts if not part.isdigit()]
|
||||
# Check for an operator and transform to mongo-style if there is
|
||||
op = None
|
||||
if parts[-1] in MATCH_OPERATORS:
|
||||
if len(parts) > 1 and parts[-1] in MATCH_OPERATORS:
|
||||
op = parts.pop()
|
||||
|
||||
# Allow to escape operator-like field name by __
|
||||
if len(parts) > 1 and parts[-1] == '':
|
||||
parts.pop()
|
||||
|
||||
negate = False
|
||||
if parts[-1] == 'not':
|
||||
if len(parts) > 1 and parts[-1] == 'not':
|
||||
parts.pop()
|
||||
negate = True
|
||||
|
||||
@@ -51,18 +59,25 @@ def query(_doc_cls=None, _field_operation=False, **query):
|
||||
# Switch field names to proper names [set in Field(name='foo')]
|
||||
try:
|
||||
fields = _doc_cls._lookup_field(parts)
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
raise InvalidQueryError(e)
|
||||
parts = []
|
||||
|
||||
CachedReferenceField = _import_class('CachedReferenceField')
|
||||
GenericReferenceField = _import_class('GenericReferenceField')
|
||||
|
||||
cleaned_fields = []
|
||||
for field in fields:
|
||||
append_field = True
|
||||
if isinstance(field, basestring):
|
||||
if isinstance(field, six.string_types):
|
||||
parts.append(field)
|
||||
append_field = False
|
||||
# is last and CachedReferenceField
|
||||
elif isinstance(field, CachedReferenceField) and fields[-1] == field:
|
||||
parts.append('%s._id' % field.db_field)
|
||||
else:
|
||||
parts.append(field.db_field)
|
||||
|
||||
if append_field:
|
||||
cleaned_fields.append(field)
|
||||
|
||||
@@ -72,45 +87,65 @@ def query(_doc_cls=None, _field_operation=False, **query):
|
||||
singular_ops = [None, 'ne', 'gt', 'gte', 'lt', 'lte', 'not']
|
||||
singular_ops += STRING_OPERATORS
|
||||
if op in singular_ops:
|
||||
if isinstance(field, basestring):
|
||||
if isinstance(field, six.string_types):
|
||||
if (op in STRING_OPERATORS and
|
||||
isinstance(value, basestring)):
|
||||
isinstance(value, six.string_types)):
|
||||
StringField = _import_class('StringField')
|
||||
value = StringField.prepare_query_value(op, value)
|
||||
else:
|
||||
value = field
|
||||
else:
|
||||
value = field.prepare_query_value(op, value)
|
||||
elif op in ('in', 'nin', 'all', 'near'):
|
||||
# 'in', 'nin' and 'all' require a list of values
|
||||
value = [field.prepare_query_value(op, v) for v in value]
|
||||
|
||||
if isinstance(field, CachedReferenceField) and value:
|
||||
value = value['_id']
|
||||
|
||||
elif op in ('in', 'nin', 'all', 'near') and not isinstance(value, dict):
|
||||
# Raise an error if the in/nin/all/near param is not iterable. We need a
|
||||
# special check for BaseDocument, because - although it's iterable - using
|
||||
# it as such in the context of this method is most definitely a mistake.
|
||||
BaseDocument = _import_class('BaseDocument')
|
||||
if isinstance(value, BaseDocument):
|
||||
raise TypeError("When using the `in`, `nin`, `all`, or "
|
||||
"`near`-operators you can\'t use a "
|
||||
"`Document`, you must wrap your object "
|
||||
"in a list (object -> [object]).")
|
||||
elif not hasattr(value, '__iter__'):
|
||||
raise TypeError("The `in`, `nin`, `all`, or "
|
||||
"`near`-operators must be applied to an "
|
||||
"iterable (e.g. a list).")
|
||||
else:
|
||||
value = [field.prepare_query_value(op, v) for v in value]
|
||||
|
||||
# If we're querying a GenericReferenceField, we need to alter the
|
||||
# key depending on the value:
|
||||
# * If the value is a DBRef, the key should be "field_name._ref".
|
||||
# * If the value is an ObjectId, the key should be "field_name._ref.$id".
|
||||
if isinstance(field, GenericReferenceField):
|
||||
if isinstance(value, DBRef):
|
||||
parts[-1] += '._ref'
|
||||
elif isinstance(value, ObjectId):
|
||||
parts[-1] += '._ref.$id'
|
||||
|
||||
# if op and op not in COMPARISON_OPERATORS:
|
||||
if op:
|
||||
if op in GEO_OPERATORS:
|
||||
if op == "within_distance":
|
||||
value = {'$within': {'$center': value}}
|
||||
elif op == "within_spherical_distance":
|
||||
value = {'$within': {'$centerSphere': value}}
|
||||
elif op == "within_polygon":
|
||||
value = {'$within': {'$polygon': value}}
|
||||
elif op == "near":
|
||||
value = {'$near': value}
|
||||
elif op == "near_sphere":
|
||||
value = {'$nearSphere': value}
|
||||
elif op == 'within_box':
|
||||
value = {'$within': {'$box': value}}
|
||||
elif op == "max_distance":
|
||||
value = {'$maxDistance': value}
|
||||
value = _geo_operator(field, op, value)
|
||||
elif op in ('match', 'elemMatch'):
|
||||
ListField = _import_class('ListField')
|
||||
EmbeddedDocumentField = _import_class('EmbeddedDocumentField')
|
||||
if (
|
||||
isinstance(value, dict) and
|
||||
isinstance(field, ListField) and
|
||||
isinstance(field.field, EmbeddedDocumentField)
|
||||
):
|
||||
value = query(field.field.document_type, **value)
|
||||
else:
|
||||
raise NotImplementedError("Geo method '%s' has not "
|
||||
"been implemented" % op)
|
||||
value = field.prepare_query_value(op, value)
|
||||
value = {'$elemMatch': value}
|
||||
elif op in CUSTOM_OPERATORS:
|
||||
if op == 'match':
|
||||
value = {"$elemMatch": value}
|
||||
else:
|
||||
NotImplementedError("Custom method '%s' has not "
|
||||
"been implemented" % op)
|
||||
NotImplementedError('Custom method "%s" has not '
|
||||
'been implemented' % op)
|
||||
elif op not in STRING_OPERATORS:
|
||||
value = {'$' + op: value}
|
||||
|
||||
@@ -119,21 +154,42 @@ def query(_doc_cls=None, _field_operation=False, **query):
|
||||
|
||||
for i, part in indices:
|
||||
parts.insert(i, part)
|
||||
|
||||
key = '.'.join(parts)
|
||||
|
||||
if op is None or key not in mongo_query:
|
||||
mongo_query[key] = value
|
||||
elif key in mongo_query:
|
||||
if key in mongo_query and isinstance(mongo_query[key], dict):
|
||||
if isinstance(mongo_query[key], dict):
|
||||
mongo_query[key].update(value)
|
||||
# $maxDistance needs to come last - convert to SON
|
||||
if '$maxDistance' in mongo_query[key]:
|
||||
value_dict = mongo_query[key]
|
||||
# $max/minDistance needs to come last - convert to SON
|
||||
value_dict = mongo_query[key]
|
||||
if ('$maxDistance' in value_dict or '$minDistance' in value_dict) and \
|
||||
('$near' in value_dict or '$nearSphere' in value_dict):
|
||||
value_son = SON()
|
||||
for k, v in value_dict.iteritems():
|
||||
if k == '$maxDistance':
|
||||
if k == '$maxDistance' or k == '$minDistance':
|
||||
continue
|
||||
value_son[k] = v
|
||||
value_son['$maxDistance'] = value_dict['$maxDistance']
|
||||
# Required for MongoDB >= 2.6, may fail when combining
|
||||
# PyMongo 3+ and MongoDB < 2.6
|
||||
near_embedded = False
|
||||
for near_op in ('$near', '$nearSphere'):
|
||||
if isinstance(value_dict.get(near_op), dict) and (
|
||||
IS_PYMONGO_3 or get_connection().max_wire_version > 1):
|
||||
value_son[near_op] = SON(value_son[near_op])
|
||||
if '$maxDistance' in value_dict:
|
||||
value_son[near_op][
|
||||
'$maxDistance'] = value_dict['$maxDistance']
|
||||
if '$minDistance' in value_dict:
|
||||
value_son[near_op][
|
||||
'$minDistance'] = value_dict['$minDistance']
|
||||
near_embedded = True
|
||||
if not near_embedded:
|
||||
if '$maxDistance' in value_dict:
|
||||
value_son['$maxDistance'] = value_dict['$maxDistance']
|
||||
if '$minDistance' in value_dict:
|
||||
value_son['$minDistance'] = value_dict['$minDistance']
|
||||
mongo_query[key] = value_son
|
||||
else:
|
||||
# Store for manually merging later
|
||||
@@ -144,9 +200,9 @@ def query(_doc_cls=None, _field_operation=False, **query):
|
||||
merge_query[k].append(mongo_query[k])
|
||||
del mongo_query[k]
|
||||
if isinstance(v, list):
|
||||
value = [{k:val} for val in v]
|
||||
value = [{k: val} for val in v]
|
||||
if '$and' in mongo_query.keys():
|
||||
mongo_query['$and'].append(value)
|
||||
mongo_query['$and'].extend(value)
|
||||
else:
|
||||
mongo_query['$and'] = value
|
||||
|
||||
@@ -154,14 +210,18 @@ def query(_doc_cls=None, _field_operation=False, **query):
|
||||
|
||||
|
||||
def update(_doc_cls=None, **update):
|
||||
"""Transform an update spec from Django-style format to Mongo format.
|
||||
"""Transform an update spec from Django-style format to Mongo
|
||||
format.
|
||||
"""
|
||||
mongo_update = {}
|
||||
for key, value in update.items():
|
||||
if key == "__raw__":
|
||||
if key == '__raw__':
|
||||
mongo_update.update(value)
|
||||
continue
|
||||
parts = key.split('__')
|
||||
# if there is no operator, default to 'set'
|
||||
if len(parts) < 3 and parts[0] not in UPDATE_OPERATORS:
|
||||
parts.insert(0, 'set')
|
||||
# Check for an operator and transform to mongo-style if there is
|
||||
op = None
|
||||
if parts[0] in UPDATE_OPERATORS:
|
||||
@@ -173,27 +233,33 @@ def update(_doc_cls=None, **update):
|
||||
# Support decrement by flipping a positive value's sign
|
||||
# and using 'inc'
|
||||
op = 'inc'
|
||||
if value > 0:
|
||||
value = -value
|
||||
value = -value
|
||||
elif op == 'add_to_set':
|
||||
op = op.replace('_to_set', 'ToSet')
|
||||
op = 'addToSet'
|
||||
elif op == 'set_on_insert':
|
||||
op = 'setOnInsert'
|
||||
|
||||
match = None
|
||||
if parts[-1] in COMPARISON_OPERATORS:
|
||||
match = parts.pop()
|
||||
|
||||
# Allow to escape operator-like field name by __
|
||||
if len(parts) > 1 and parts[-1] == '':
|
||||
parts.pop()
|
||||
|
||||
if _doc_cls:
|
||||
# Switch field names to proper names [set in Field(name='foo')]
|
||||
try:
|
||||
fields = _doc_cls._lookup_field(parts)
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
raise InvalidQueryError(e)
|
||||
parts = []
|
||||
|
||||
cleaned_fields = []
|
||||
appended_sub_field = False
|
||||
for field in fields:
|
||||
append_field = True
|
||||
if isinstance(field, basestring):
|
||||
if isinstance(field, six.string_types):
|
||||
# Convert the S operator to $
|
||||
if field == 'S':
|
||||
field = '$'
|
||||
@@ -202,21 +268,36 @@ def update(_doc_cls=None, **update):
|
||||
else:
|
||||
parts.append(field.db_field)
|
||||
if append_field:
|
||||
appended_sub_field = False
|
||||
cleaned_fields.append(field)
|
||||
if hasattr(field, 'field'):
|
||||
cleaned_fields.append(field.field)
|
||||
appended_sub_field = True
|
||||
|
||||
# Convert value to proper value
|
||||
field = cleaned_fields[-1]
|
||||
if appended_sub_field:
|
||||
field = cleaned_fields[-2]
|
||||
else:
|
||||
field = cleaned_fields[-1]
|
||||
|
||||
if op in (None, 'set', 'push', 'pull'):
|
||||
GeoJsonBaseField = _import_class('GeoJsonBaseField')
|
||||
if isinstance(field, GeoJsonBaseField):
|
||||
value = field.to_mongo(value)
|
||||
|
||||
if op == 'push' and isinstance(value, (list, tuple, set)):
|
||||
value = [field.prepare_query_value(op, v) for v in value]
|
||||
elif op in (None, 'set', 'push', 'pull'):
|
||||
if field.required or value is not None:
|
||||
value = field.prepare_query_value(op, value)
|
||||
elif op in ('pushAll', 'pullAll'):
|
||||
value = [field.prepare_query_value(op, v) for v in value]
|
||||
elif op == 'addToSet':
|
||||
elif op in ('addToSet', 'setOnInsert'):
|
||||
if isinstance(value, (list, tuple, set)):
|
||||
value = [field.prepare_query_value(op, v) for v in value]
|
||||
elif field.required or value is not None:
|
||||
value = field.prepare_query_value(op, value)
|
||||
elif op == 'unset':
|
||||
value = 1
|
||||
|
||||
if match:
|
||||
match = '$' + match
|
||||
@@ -225,28 +306,136 @@ def update(_doc_cls=None, **update):
|
||||
key = '.'.join(parts)
|
||||
|
||||
if not op:
|
||||
raise InvalidQueryError("Updates must supply an operation "
|
||||
"eg: set__FIELD=value")
|
||||
raise InvalidQueryError('Updates must supply an operation '
|
||||
'eg: set__FIELD=value')
|
||||
|
||||
if 'pull' in op and '.' in key:
|
||||
# Dot operators don't work on pull operations
|
||||
# it uses nested dict syntax
|
||||
# unless they point to a list field
|
||||
# Otherwise it uses nested dict syntax
|
||||
if op == 'pullAll':
|
||||
raise InvalidQueryError("pullAll operations only support "
|
||||
"a single field depth")
|
||||
raise InvalidQueryError('pullAll operations only support '
|
||||
'a single field depth')
|
||||
|
||||
# Look for the last list field and use dot notation until there
|
||||
field_classes = [c.__class__ for c in cleaned_fields]
|
||||
field_classes.reverse()
|
||||
ListField = _import_class('ListField')
|
||||
if ListField in field_classes:
|
||||
# Join all fields via dot notation to the last ListField
|
||||
# Then process as normal
|
||||
last_listField = len(
|
||||
cleaned_fields) - field_classes.index(ListField)
|
||||
key = '.'.join(parts[:last_listField])
|
||||
parts = parts[last_listField:]
|
||||
parts.insert(0, key)
|
||||
|
||||
parts.reverse()
|
||||
for key in parts:
|
||||
value = {key: value}
|
||||
elif op == 'addToSet' and isinstance(value, list):
|
||||
value = {key: {"$each": value}}
|
||||
value = {key: {'$each': value}}
|
||||
elif op == 'push':
|
||||
if parts[-1].isdigit():
|
||||
key = parts[0]
|
||||
position = int(parts[-1])
|
||||
# $position expects an iterable. If pushing a single value,
|
||||
# wrap it in a list.
|
||||
if not isinstance(value, (set, tuple, list)):
|
||||
value = [value]
|
||||
value = {key: {'$each': value, '$position': position}}
|
||||
elif isinstance(value, list):
|
||||
value = {key: {'$each': value}}
|
||||
else:
|
||||
value = {key: value}
|
||||
else:
|
||||
value = {key: value}
|
||||
key = '$' + op
|
||||
|
||||
if key not in mongo_update:
|
||||
mongo_update[key] = value
|
||||
elif key in mongo_update and isinstance(mongo_update[key], dict):
|
||||
mongo_update[key].update(value)
|
||||
|
||||
return mongo_update
|
||||
|
||||
|
||||
def _geo_operator(field, op, value):
|
||||
"""Helper to return the query for a given geo query."""
|
||||
if op == 'max_distance':
|
||||
value = {'$maxDistance': value}
|
||||
elif op == 'min_distance':
|
||||
value = {'$minDistance': value}
|
||||
elif field._geo_index == pymongo.GEO2D:
|
||||
if op == 'within_distance':
|
||||
value = {'$within': {'$center': value}}
|
||||
elif op == 'within_spherical_distance':
|
||||
value = {'$within': {'$centerSphere': value}}
|
||||
elif op == 'within_polygon':
|
||||
value = {'$within': {'$polygon': value}}
|
||||
elif op == 'near':
|
||||
value = {'$near': value}
|
||||
elif op == 'near_sphere':
|
||||
value = {'$nearSphere': value}
|
||||
elif op == 'within_box':
|
||||
value = {'$within': {'$box': value}}
|
||||
else:
|
||||
raise NotImplementedError('Geo method "%s" has not been '
|
||||
'implemented for a GeoPointField' % op)
|
||||
else:
|
||||
if op == 'geo_within':
|
||||
value = {'$geoWithin': _infer_geometry(value)}
|
||||
elif op == 'geo_within_box':
|
||||
value = {'$geoWithin': {'$box': value}}
|
||||
elif op == 'geo_within_polygon':
|
||||
value = {'$geoWithin': {'$polygon': value}}
|
||||
elif op == 'geo_within_center':
|
||||
value = {'$geoWithin': {'$center': value}}
|
||||
elif op == 'geo_within_sphere':
|
||||
value = {'$geoWithin': {'$centerSphere': value}}
|
||||
elif op == 'geo_intersects':
|
||||
value = {'$geoIntersects': _infer_geometry(value)}
|
||||
elif op == 'near':
|
||||
value = {'$near': _infer_geometry(value)}
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
'Geo method "%s" has not been implemented for a %s '
|
||||
% (op, field._name)
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def _infer_geometry(value):
|
||||
"""Helper method that tries to infer the $geometry shape for a
|
||||
given value.
|
||||
"""
|
||||
if isinstance(value, dict):
|
||||
if '$geometry' in value:
|
||||
return value
|
||||
elif 'coordinates' in value and 'type' in value:
|
||||
return {'$geometry': value}
|
||||
raise InvalidQueryError('Invalid $geometry dictionary should have '
|
||||
'type and coordinates keys')
|
||||
elif isinstance(value, (list, set)):
|
||||
# TODO: shouldn't we test value[0][0][0][0] to see if it is MultiPolygon?
|
||||
# TODO: should both TypeError and IndexError be alike interpreted?
|
||||
|
||||
try:
|
||||
value[0][0][0]
|
||||
return {'$geometry': {'type': 'Polygon', 'coordinates': value}}
|
||||
except (TypeError, IndexError):
|
||||
pass
|
||||
|
||||
try:
|
||||
value[0][0]
|
||||
return {'$geometry': {'type': 'LineString', 'coordinates': value}}
|
||||
except (TypeError, IndexError):
|
||||
pass
|
||||
|
||||
try:
|
||||
value[0]
|
||||
return {'$geometry': {'type': 'Point', 'coordinates': value}}
|
||||
except (TypeError, IndexError):
|
||||
pass
|
||||
|
||||
raise InvalidQueryError('Invalid $geometry data. Can be either a '
|
||||
'dictionary or (nested) lists of coordinate(s)')
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import copy
|
||||
|
||||
from mongoengine.errors import InvalidQueryError
|
||||
from mongoengine.python_support import product, reduce
|
||||
|
||||
from mongoengine.queryset import transform
|
||||
|
||||
__all__ = ('Q',)
|
||||
@@ -23,8 +21,12 @@ class QNodeVisitor(object):
|
||||
return query
|
||||
|
||||
|
||||
class DuplicateQueryConditionsError(InvalidQueryError):
|
||||
pass
|
||||
|
||||
|
||||
class SimplificationVisitor(QNodeVisitor):
|
||||
"""Simplifies query trees by combinging unnecessary 'and' connection nodes
|
||||
"""Simplifies query trees by combining unnecessary 'and' connection nodes
|
||||
into a single Q-object.
|
||||
"""
|
||||
|
||||
@@ -33,7 +35,11 @@ class SimplificationVisitor(QNodeVisitor):
|
||||
# The simplification only applies to 'simple' queries
|
||||
if all(isinstance(node, Q) for node in combination.children):
|
||||
queries = [n.query for n in combination.children]
|
||||
return Q(**self._query_conjunction(queries))
|
||||
try:
|
||||
return Q(**self._query_conjunction(queries))
|
||||
except DuplicateQueryConditionsError:
|
||||
# Cannot be simplified
|
||||
pass
|
||||
return combination
|
||||
|
||||
def _query_conjunction(self, queries):
|
||||
@@ -47,8 +53,7 @@ class SimplificationVisitor(QNodeVisitor):
|
||||
# to a single field
|
||||
intersection = ops.intersection(query_ops)
|
||||
if intersection:
|
||||
msg = 'Duplicate query conditions: '
|
||||
raise InvalidQueryError(msg + ', '.join(intersection))
|
||||
raise DuplicateQueryConditionsError()
|
||||
|
||||
query_ops.update(ops)
|
||||
combined_query.update(copy.deepcopy(query))
|
||||
@@ -64,9 +69,9 @@ class QueryCompilerVisitor(QNodeVisitor):
|
||||
self.document = document
|
||||
|
||||
def visit_combination(self, combination):
|
||||
operator = "$and"
|
||||
operator = '$and'
|
||||
if combination.operation == combination.OR:
|
||||
operator = "$or"
|
||||
operator = '$or'
|
||||
return {operator: combination.children}
|
||||
|
||||
def visit_query(self, query):
|
||||
@@ -74,8 +79,7 @@ class QueryCompilerVisitor(QNodeVisitor):
|
||||
|
||||
|
||||
class QNode(object):
|
||||
"""Base class for nodes in query trees.
|
||||
"""
|
||||
"""Base class for nodes in query trees."""
|
||||
|
||||
AND = 0
|
||||
OR = 1
|
||||
@@ -89,7 +93,8 @@ class QNode(object):
|
||||
raise NotImplementedError
|
||||
|
||||
def _combine(self, other, operation):
|
||||
"""Combine this node with another node into a QCombination object.
|
||||
"""Combine this node with another node into a QCombination
|
||||
object.
|
||||
"""
|
||||
if getattr(other, 'empty', True):
|
||||
return self
|
||||
@@ -111,8 +116,8 @@ class QNode(object):
|
||||
|
||||
|
||||
class QCombination(QNode):
|
||||
"""Represents the combination of several conditions by a given logical
|
||||
operator.
|
||||
"""Represents the combination of several conditions by a given
|
||||
logical operator.
|
||||
"""
|
||||
|
||||
def __init__(self, operation, children):
|
||||
@@ -122,8 +127,7 @@ class QCombination(QNode):
|
||||
# If the child is a combination of the same type, we can merge its
|
||||
# children directly into this combinations children
|
||||
if isinstance(node, QCombination) and node.operation == operation:
|
||||
# self.children += node.children
|
||||
self.children.append(node)
|
||||
self.children += node.children
|
||||
else:
|
||||
self.children.append(node)
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__all__ = ['pre_init', 'post_init', 'pre_save', 'post_save',
|
||||
'pre_delete', 'post_delete']
|
||||
__all__ = ('pre_init', 'post_init', 'pre_save', 'pre_save_post_validation',
|
||||
'post_save', 'pre_delete', 'post_delete')
|
||||
|
||||
signals_available = False
|
||||
try:
|
||||
from blinker import Namespace
|
||||
|
||||
signals_available = True
|
||||
except ImportError:
|
||||
class Namespace(object):
|
||||
@@ -27,11 +26,13 @@ except ImportError:
|
||||
raise RuntimeError('signalling support is unavailable '
|
||||
'because the blinker library is '
|
||||
'not installed.')
|
||||
send = lambda *a, **kw: None
|
||||
|
||||
send = lambda *a, **kw: None # noqa
|
||||
connect = disconnect = has_receivers_for = receivers_for = \
|
||||
temporarily_connected_to = _fail
|
||||
del _fail
|
||||
|
||||
|
||||
# the namespace for code signals. If you are not mongoengine code, do
|
||||
# not put signals in here. Create your own namespace instead.
|
||||
_signals = Namespace()
|
||||
@@ -39,6 +40,7 @@ _signals = Namespace()
|
||||
pre_init = _signals.signal('pre_init')
|
||||
post_init = _signals.signal('post_init')
|
||||
pre_save = _signals.signal('pre_save')
|
||||
pre_save_post_validation = _signals.signal('pre_save_post_validation')
|
||||
post_save = _signals.signal('post_save')
|
||||
pre_delete = _signals.signal('pre_delete')
|
||||
post_delete = _signals.signal('post_delete')
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
%define srcname mongoengine
|
||||
|
||||
Name: python-%{srcname}
|
||||
Version: 0.7.9
|
||||
Version: 0.8.7
|
||||
Release: 1%{?dist}
|
||||
Summary: A Python Document-Object Mapper for working with MongoDB
|
||||
|
||||
@@ -51,4 +51,4 @@ rm -rf $RPM_BUILD_ROOT
|
||||
# %{python_sitearch}/*
|
||||
|
||||
%changelog
|
||||
* See: http://readthedocs.org/docs/mongoengine-odm/en/latest/changelog.html
|
||||
* See: http://docs.mongoengine.org/en/latest/changelog.html
|
||||
@@ -1 +1,7 @@
|
||||
pymongo
|
||||
nose
|
||||
pymongo>=2.7.1
|
||||
six==1.10.0
|
||||
flake8
|
||||
flake8-import-order
|
||||
Sphinx==1.5.5
|
||||
sphinx-rtd-theme==0.2.4
|
||||
|
||||
20
setup.cfg
20
setup.cfg
@@ -1,11 +1,11 @@
|
||||
[nosetests]
|
||||
verbosity = 3
|
||||
detailed-errors = 1
|
||||
#with-coverage = 1
|
||||
#cover-erase = 1
|
||||
#cover-html = 1
|
||||
#cover-html-dir = ../htmlcov
|
||||
#cover-package = mongoengine
|
||||
py3where = build
|
||||
where = tests
|
||||
#tests = document/__init__.py
|
||||
verbosity=2
|
||||
detailed-errors=1
|
||||
tests=tests
|
||||
cover-package=mongoengine
|
||||
|
||||
[flake8]
|
||||
ignore=E501,F401,F403,F405,I201
|
||||
exclude=build,dist,docs,venv,venv3,.tox,.eggs,tests
|
||||
max-complexity=47
|
||||
application-import-names=mongoengine,tests
|
||||
|
||||
84
setup.py
84
setup.py
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
import sys
|
||||
from setuptools import setup, find_packages
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
# Hack to silence atexit traceback in newer python versions
|
||||
try:
|
||||
@@ -8,20 +8,25 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
DESCRIPTION = 'MongoEngine is a Python Object-Document ' + \
|
||||
'Mapper for working with MongoDB.'
|
||||
LONG_DESCRIPTION = None
|
||||
DESCRIPTION = (
|
||||
'MongoEngine is a Python Object-Document '
|
||||
'Mapper for working with MongoDB.'
|
||||
)
|
||||
|
||||
try:
|
||||
LONG_DESCRIPTION = open('README.rst').read()
|
||||
except:
|
||||
pass
|
||||
with open('README.rst') as fin:
|
||||
LONG_DESCRIPTION = fin.read()
|
||||
except Exception:
|
||||
LONG_DESCRIPTION = None
|
||||
|
||||
|
||||
def get_version(version_tuple):
|
||||
if not isinstance(version_tuple[-1], int):
|
||||
return '.'.join(map(str, version_tuple[:-1])) + version_tuple[-1]
|
||||
"""Return the version tuple as a string, e.g. for (0, 10, 7),
|
||||
return '0.10.7'.
|
||||
"""
|
||||
return '.'.join(map(str, version_tuple))
|
||||
|
||||
|
||||
# Dirty hack to get version number from monogengine/__init__.py - we can't
|
||||
# import it as it depends on PyMongo and PyMongo isn't installed until this
|
||||
# file is read
|
||||
@@ -29,7 +34,6 @@ init = os.path.join(os.path.dirname(__file__), 'mongoengine', '__init__.py')
|
||||
version_line = list(filter(lambda l: l.startswith('VERSION'), open(init)))[0]
|
||||
|
||||
VERSION = get_version(eval(version_line.split('=')[-1]))
|
||||
print(VERSION)
|
||||
|
||||
CLASSIFIERS = [
|
||||
'Development Status :: 4 - Beta',
|
||||
@@ -38,44 +42,46 @@ CLASSIFIERS = [
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
"Programming Language :: Python :: 2",
|
||||
"Programming Language :: Python :: 2.5",
|
||||
"Programming Language :: Python :: 2.6",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.1",
|
||||
"Programming Language :: Python :: 3.2",
|
||||
"Programming Language :: Python :: 3.3",
|
||||
"Programming Language :: Python :: 3.4",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
'Topic :: Database',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
]
|
||||
|
||||
extra_opts = {}
|
||||
extra_opts = {
|
||||
'packages': find_packages(exclude=['tests', 'tests.*']),
|
||||
'tests_require': ['nose', 'coverage==4.2', 'blinker', 'Pillow>=2.0.0']
|
||||
}
|
||||
if sys.version_info[0] == 3:
|
||||
extra_opts['use_2to3'] = True
|
||||
extra_opts['tests_require'] = ['nose', 'coverage', 'blinker']
|
||||
extra_opts['packages'] = find_packages(exclude=('tests',))
|
||||
if "test" in sys.argv or "nosetests" in sys.argv:
|
||||
extra_opts['packages'].append("tests")
|
||||
extra_opts['package_data'] = {"tests": ["fields/mongoengine.png"]}
|
||||
if 'test' in sys.argv or 'nosetests' in sys.argv:
|
||||
extra_opts['packages'] = find_packages()
|
||||
extra_opts['package_data'] = {
|
||||
'tests': ['fields/mongoengine.png', 'fields/mongodb_leaf.png']}
|
||||
else:
|
||||
extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'django==1.4.2', 'PIL']
|
||||
extra_opts['packages'] = find_packages(exclude=('tests',))
|
||||
extra_opts['tests_require'] += ['python-dateutil']
|
||||
|
||||
setup(name='mongoengine',
|
||||
version=VERSION,
|
||||
author='Harry Marr',
|
||||
author_email='harry.marr@{nospam}gmail.com',
|
||||
maintainer="Ross Lawley",
|
||||
maintainer_email="ross.lawley@{nospam}gmail.com",
|
||||
url='http://mongoengine.org/',
|
||||
download_url='https://github.com/MongoEngine/mongoengine/tarball/master',
|
||||
license='MIT',
|
||||
include_package_data=True,
|
||||
description=DESCRIPTION,
|
||||
long_description=LONG_DESCRIPTION,
|
||||
platforms=['any'],
|
||||
classifiers=CLASSIFIERS,
|
||||
install_requires=['pymongo'],
|
||||
test_suite='nose.collector',
|
||||
**extra_opts
|
||||
setup(
|
||||
name='mongoengine',
|
||||
version=VERSION,
|
||||
author='Harry Marr',
|
||||
author_email='harry.marr@gmail.com',
|
||||
maintainer="Stefan Wojcik",
|
||||
maintainer_email="wojcikstefan@gmail.com",
|
||||
url='http://mongoengine.org/',
|
||||
download_url='https://github.com/MongoEngine/mongoengine/tarball/master',
|
||||
license='MIT',
|
||||
include_package_data=True,
|
||||
description=DESCRIPTION,
|
||||
long_description=LONG_DESCRIPTION,
|
||||
platforms=['any'],
|
||||
classifiers=CLASSIFIERS,
|
||||
install_requires=['pymongo>=2.7.1', 'six'],
|
||||
test_suite='nose.collector',
|
||||
**extra_opts
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from all_warnings import AllWarnings
|
||||
from document import *
|
||||
from queryset import *
|
||||
from queryset import *
|
||||
from fields import *
|
||||
|
||||
@@ -3,8 +3,6 @@ This test has been put into a module. This is because it tests warnings that
|
||||
only get triggered on first hit. This way we can ensure its imported into the
|
||||
top level and called first by the test suite.
|
||||
"""
|
||||
import sys
|
||||
sys.path[0:0] = [""]
|
||||
import unittest
|
||||
import warnings
|
||||
|
||||
@@ -17,7 +15,7 @@ __all__ = ('AllWarnings', )
|
||||
class AllWarnings(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
conn = connect(db='mongoenginetest')
|
||||
connect(db='mongoenginetest')
|
||||
self.warning_list = []
|
||||
self.showwarning_default = warnings.showwarning
|
||||
warnings.showwarning = self.append_to_warning_list
|
||||
@@ -30,53 +28,6 @@ class AllWarnings(unittest.TestCase):
|
||||
# restore default handling of warnings
|
||||
warnings.showwarning = self.showwarning_default
|
||||
|
||||
def test_dbref_reference_field_future_warning(self):
|
||||
|
||||
class Person(Document):
|
||||
name = StringField()
|
||||
parent = ReferenceField('self')
|
||||
|
||||
Person.drop_collection()
|
||||
|
||||
p1 = Person()
|
||||
p1.parent = None
|
||||
p1.save()
|
||||
|
||||
p2 = Person(name="Wilson Jr")
|
||||
p2.parent = p1
|
||||
p2.save(cascade=False)
|
||||
|
||||
self.assertTrue(len(self.warning_list) > 0)
|
||||
warning = self.warning_list[0]
|
||||
self.assertEqual(FutureWarning, warning["category"])
|
||||
self.assertTrue("ReferenceFields will default to using ObjectId"
|
||||
in str(warning["message"]))
|
||||
|
||||
def test_document_save_cascade_future_warning(self):
|
||||
|
||||
class Person(Document):
|
||||
name = StringField()
|
||||
parent = ReferenceField('self')
|
||||
|
||||
Person.drop_collection()
|
||||
|
||||
p1 = Person(name="Wilson Snr")
|
||||
p1.parent = None
|
||||
p1.save()
|
||||
|
||||
p2 = Person(name="Wilson Jr")
|
||||
p2.parent = p1
|
||||
p2.parent.name = "Poppa Wilson"
|
||||
p2.save()
|
||||
|
||||
self.assertTrue(len(self.warning_list) > 0)
|
||||
if len(self.warning_list) > 1:
|
||||
print self.warning_list
|
||||
warning = self.warning_list[0]
|
||||
self.assertEqual(FutureWarning, warning["category"])
|
||||
self.assertTrue("Cascading saves will default to off in 0.8"
|
||||
in str(warning["message"]))
|
||||
|
||||
def test_document_collection_syntax_warning(self):
|
||||
|
||||
class NonAbstractBase(Document):
|
||||
@@ -89,6 +40,3 @@ class AllWarnings(unittest.TestCase):
|
||||
self.assertEqual(SyntaxWarning, warning["category"])
|
||||
self.assertEqual('non_abstract_base',
|
||||
InheritedDocumentFailTest._get_collection_name())
|
||||
|
||||
import sys
|
||||
sys.path[0:0] = [""]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import sys
|
||||
sys.path[0:0] = [""]
|
||||
import unittest
|
||||
|
||||
from class_methods import *
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import with_statement
|
||||
import sys
|
||||
sys.path[0:0] = [""]
|
||||
import unittest
|
||||
|
||||
from mongoengine import *
|
||||
|
||||
from mongoengine.queryset import NULLIFY
|
||||
from mongoengine.queryset import NULLIFY, PULL
|
||||
from mongoengine.connection import get_db
|
||||
|
||||
__all__ = ("ClassMethodsTest", )
|
||||
@@ -37,9 +34,9 @@ class ClassMethodsTest(unittest.TestCase):
|
||||
def test_definition(self):
|
||||
"""Ensure that document may be defined using fields.
|
||||
"""
|
||||
self.assertEqual(['age', 'id', 'name'],
|
||||
self.assertEqual(['_cls', 'age', 'id', 'name'],
|
||||
sorted(self.Person._fields.keys()))
|
||||
self.assertEqual(["IntField", "ObjectIdField", "StringField"],
|
||||
self.assertEqual(["IntField", "ObjectIdField", "StringField", "StringField"],
|
||||
sorted([x.__class__.__name__ for x in
|
||||
self.Person._fields.values()]))
|
||||
|
||||
@@ -86,6 +83,172 @@ class ClassMethodsTest(unittest.TestCase):
|
||||
self.assertEqual(self.Person._meta['delete_rules'],
|
||||
{(Job, 'employee'): NULLIFY})
|
||||
|
||||
def test_compare_indexes(self):
|
||||
""" Ensure that the indexes are properly created and that
|
||||
compare_indexes identifies the missing/extra indexes
|
||||
"""
|
||||
|
||||
class BlogPost(Document):
|
||||
author = StringField()
|
||||
title = StringField()
|
||||
description = StringField()
|
||||
tags = StringField()
|
||||
|
||||
meta = {
|
||||
'indexes': [('author', 'title')]
|
||||
}
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
BlogPost.ensure_indexes()
|
||||
self.assertEqual(BlogPost.compare_indexes(), { 'missing': [], 'extra': [] })
|
||||
|
||||
BlogPost.ensure_index(['author', 'description'])
|
||||
self.assertEqual(BlogPost.compare_indexes(), { 'missing': [], 'extra': [[('author', 1), ('description', 1)]] })
|
||||
|
||||
BlogPost._get_collection().drop_index('author_1_description_1')
|
||||
self.assertEqual(BlogPost.compare_indexes(), { 'missing': [], 'extra': [] })
|
||||
|
||||
BlogPost._get_collection().drop_index('author_1_title_1')
|
||||
self.assertEqual(BlogPost.compare_indexes(), { 'missing': [[('author', 1), ('title', 1)]], 'extra': [] })
|
||||
|
||||
def test_compare_indexes_inheritance(self):
|
||||
""" Ensure that the indexes are properly created and that
|
||||
compare_indexes identifies the missing/extra indexes for subclassed
|
||||
documents (_cls included)
|
||||
"""
|
||||
|
||||
class BlogPost(Document):
|
||||
author = StringField()
|
||||
title = StringField()
|
||||
description = StringField()
|
||||
|
||||
meta = {
|
||||
'allow_inheritance': True
|
||||
}
|
||||
|
||||
class BlogPostWithTags(BlogPost):
|
||||
tags = StringField()
|
||||
tag_list = ListField(StringField())
|
||||
|
||||
meta = {
|
||||
'indexes': [('author', 'tags')]
|
||||
}
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
BlogPost.ensure_indexes()
|
||||
BlogPostWithTags.ensure_indexes()
|
||||
self.assertEqual(BlogPost.compare_indexes(), { 'missing': [], 'extra': [] })
|
||||
|
||||
BlogPostWithTags.ensure_index(['author', 'tag_list'])
|
||||
self.assertEqual(BlogPost.compare_indexes(), { 'missing': [], 'extra': [[('_cls', 1), ('author', 1), ('tag_list', 1)]] })
|
||||
|
||||
BlogPostWithTags._get_collection().drop_index('_cls_1_author_1_tag_list_1')
|
||||
self.assertEqual(BlogPost.compare_indexes(), { 'missing': [], 'extra': [] })
|
||||
|
||||
BlogPostWithTags._get_collection().drop_index('_cls_1_author_1_tags_1')
|
||||
self.assertEqual(BlogPost.compare_indexes(), { 'missing': [[('_cls', 1), ('author', 1), ('tags', 1)]], 'extra': [] })
|
||||
|
||||
def test_compare_indexes_multiple_subclasses(self):
|
||||
""" Ensure that compare_indexes behaves correctly if called from a
|
||||
class, which base class has multiple subclasses
|
||||
"""
|
||||
|
||||
class BlogPost(Document):
|
||||
author = StringField()
|
||||
title = StringField()
|
||||
description = StringField()
|
||||
|
||||
meta = {
|
||||
'allow_inheritance': True
|
||||
}
|
||||
|
||||
class BlogPostWithTags(BlogPost):
|
||||
tags = StringField()
|
||||
tag_list = ListField(StringField())
|
||||
|
||||
meta = {
|
||||
'indexes': [('author', 'tags')]
|
||||
}
|
||||
|
||||
class BlogPostWithCustomField(BlogPost):
|
||||
custom = DictField()
|
||||
|
||||
meta = {
|
||||
'indexes': [('author', 'custom')]
|
||||
}
|
||||
|
||||
BlogPost.ensure_indexes()
|
||||
BlogPostWithTags.ensure_indexes()
|
||||
BlogPostWithCustomField.ensure_indexes()
|
||||
|
||||
self.assertEqual(BlogPost.compare_indexes(), { 'missing': [], 'extra': [] })
|
||||
self.assertEqual(BlogPostWithTags.compare_indexes(), { 'missing': [], 'extra': [] })
|
||||
self.assertEqual(BlogPostWithCustomField.compare_indexes(), { 'missing': [], 'extra': [] })
|
||||
|
||||
def test_list_indexes_inheritance(self):
|
||||
""" ensure that all of the indexes are listed regardless of the super-
|
||||
or sub-class that we call it from
|
||||
"""
|
||||
|
||||
class BlogPost(Document):
|
||||
author = StringField()
|
||||
title = StringField()
|
||||
description = StringField()
|
||||
|
||||
meta = {
|
||||
'allow_inheritance': True
|
||||
}
|
||||
|
||||
class BlogPostWithTags(BlogPost):
|
||||
tags = StringField()
|
||||
|
||||
meta = {
|
||||
'indexes': [('author', 'tags')]
|
||||
}
|
||||
|
||||
class BlogPostWithTagsAndExtraText(BlogPostWithTags):
|
||||
extra_text = StringField()
|
||||
|
||||
meta = {
|
||||
'indexes': [('author', 'tags', 'extra_text')]
|
||||
}
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
BlogPost.ensure_indexes()
|
||||
BlogPostWithTags.ensure_indexes()
|
||||
BlogPostWithTagsAndExtraText.ensure_indexes()
|
||||
|
||||
self.assertEqual(BlogPost.list_indexes(),
|
||||
BlogPostWithTags.list_indexes())
|
||||
self.assertEqual(BlogPost.list_indexes(),
|
||||
BlogPostWithTagsAndExtraText.list_indexes())
|
||||
self.assertEqual(BlogPost.list_indexes(),
|
||||
[[('_cls', 1), ('author', 1), ('tags', 1)],
|
||||
[('_cls', 1), ('author', 1), ('tags', 1), ('extra_text', 1)],
|
||||
[(u'_id', 1)], [('_cls', 1)]])
|
||||
|
||||
def test_register_delete_rule_inherited(self):
|
||||
|
||||
class Vaccine(Document):
|
||||
name = StringField(required=True)
|
||||
|
||||
meta = {"indexes": ["name"]}
|
||||
|
||||
class Animal(Document):
|
||||
family = StringField(required=True)
|
||||
vaccine_made = ListField(ReferenceField("Vaccine", reverse_delete_rule=PULL))
|
||||
|
||||
meta = {"allow_inheritance": True, "indexes": ["family"]}
|
||||
|
||||
class Cat(Animal):
|
||||
name = StringField(required=True)
|
||||
|
||||
self.assertEqual(Vaccine._meta['delete_rules'][(Animal, 'vaccine_made')], PULL)
|
||||
self.assertEqual(Vaccine._meta['delete_rules'][(Cat, 'vaccine_made')], PULL)
|
||||
|
||||
def test_collection_naming(self):
|
||||
"""Ensure that a collection with a specified name may be used.
|
||||
"""
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
sys.path[0:0] = [""]
|
||||
import unittest
|
||||
|
||||
from bson import SON
|
||||
from mongoengine import *
|
||||
from mongoengine.connection import get_db
|
||||
|
||||
@@ -92,6 +91,7 @@ class DeltaTest(unittest.TestCase):
|
||||
def delta_recursive(self, DocClass, EmbeddedClass):
|
||||
|
||||
class Embedded(EmbeddedClass):
|
||||
id = StringField()
|
||||
string_field = StringField()
|
||||
int_field = IntField()
|
||||
dict_field = DictField()
|
||||
@@ -113,6 +113,7 @@ class DeltaTest(unittest.TestCase):
|
||||
self.assertEqual(doc._delta(), ({}, {}))
|
||||
|
||||
embedded_1 = Embedded()
|
||||
embedded_1.id = "010101"
|
||||
embedded_1.string_field = 'hello'
|
||||
embedded_1.int_field = 1
|
||||
embedded_1.dict_field = {'hello': 'world'}
|
||||
@@ -122,6 +123,7 @@ class DeltaTest(unittest.TestCase):
|
||||
self.assertEqual(doc._get_changed_fields(), ['embedded_field'])
|
||||
|
||||
embedded_delta = {
|
||||
'id': "010101",
|
||||
'string_field': 'hello',
|
||||
'int_field': 1,
|
||||
'dict_field': {'hello': 'world'},
|
||||
@@ -129,14 +131,14 @@ class DeltaTest(unittest.TestCase):
|
||||
}
|
||||
self.assertEqual(doc.embedded_field._delta(), (embedded_delta, {}))
|
||||
self.assertEqual(doc._delta(),
|
||||
({'embedded_field': embedded_delta}, {}))
|
||||
({'embedded_field': embedded_delta}, {}))
|
||||
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
|
||||
doc.embedded_field.dict_field = {}
|
||||
self.assertEqual(doc._get_changed_fields(),
|
||||
['embedded_field.dict_field'])
|
||||
['embedded_field.dict_field'])
|
||||
self.assertEqual(doc.embedded_field._delta(), ({}, {'dict_field': 1}))
|
||||
self.assertEqual(doc._delta(), ({}, {'embedded_field.dict_field': 1}))
|
||||
doc.save()
|
||||
@@ -145,7 +147,7 @@ class DeltaTest(unittest.TestCase):
|
||||
|
||||
doc.embedded_field.list_field = []
|
||||
self.assertEqual(doc._get_changed_fields(),
|
||||
['embedded_field.list_field'])
|
||||
['embedded_field.list_field'])
|
||||
self.assertEqual(doc.embedded_field._delta(), ({}, {'list_field': 1}))
|
||||
self.assertEqual(doc._delta(), ({}, {'embedded_field.list_field': 1}))
|
||||
doc.save()
|
||||
@@ -160,7 +162,7 @@ class DeltaTest(unittest.TestCase):
|
||||
|
||||
doc.embedded_field.list_field = ['1', 2, embedded_2]
|
||||
self.assertEqual(doc._get_changed_fields(),
|
||||
['embedded_field.list_field'])
|
||||
['embedded_field.list_field'])
|
||||
|
||||
self.assertEqual(doc.embedded_field._delta(), ({
|
||||
'list_field': ['1', 2, {
|
||||
@@ -192,11 +194,11 @@ class DeltaTest(unittest.TestCase):
|
||||
|
||||
doc.embedded_field.list_field[2].string_field = 'world'
|
||||
self.assertEqual(doc._get_changed_fields(),
|
||||
['embedded_field.list_field.2.string_field'])
|
||||
['embedded_field.list_field.2.string_field'])
|
||||
self.assertEqual(doc.embedded_field._delta(),
|
||||
({'list_field.2.string_field': 'world'}, {}))
|
||||
({'list_field.2.string_field': 'world'}, {}))
|
||||
self.assertEqual(doc._delta(),
|
||||
({'embedded_field.list_field.2.string_field': 'world'}, {}))
|
||||
({'embedded_field.list_field.2.string_field': 'world'}, {}))
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
self.assertEqual(doc.embedded_field.list_field[2].string_field,
|
||||
@@ -206,59 +208,58 @@ class DeltaTest(unittest.TestCase):
|
||||
doc.embedded_field.list_field[2].string_field = 'hello world'
|
||||
doc.embedded_field.list_field[2] = doc.embedded_field.list_field[2]
|
||||
self.assertEqual(doc._get_changed_fields(),
|
||||
['embedded_field.list_field'])
|
||||
self.assertEqual(doc.embedded_field._delta(), ({
|
||||
'list_field': ['1', 2, {
|
||||
'_cls': 'Embedded',
|
||||
'string_field': 'hello world',
|
||||
'int_field': 1,
|
||||
'list_field': ['1', 2, {'hello': 'world'}],
|
||||
'dict_field': {'hello': 'world'}}]}, {}))
|
||||
self.assertEqual(doc._delta(), ({
|
||||
'embedded_field.list_field': ['1', 2, {
|
||||
['embedded_field.list_field.2'])
|
||||
self.assertEqual(doc.embedded_field._delta(), ({'list_field.2': {
|
||||
'_cls': 'Embedded',
|
||||
'string_field': 'hello world',
|
||||
'int_field': 1,
|
||||
'list_field': ['1', 2, {'hello': 'world'}],
|
||||
'dict_field': {'hello': 'world'}}
|
||||
]}, {}))
|
||||
}, {}))
|
||||
self.assertEqual(doc._delta(), ({'embedded_field.list_field.2': {
|
||||
'_cls': 'Embedded',
|
||||
'string_field': 'hello world',
|
||||
'int_field': 1,
|
||||
'list_field': ['1', 2, {'hello': 'world'}],
|
||||
'dict_field': {'hello': 'world'}}
|
||||
}, {}))
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
self.assertEqual(doc.embedded_field.list_field[2].string_field,
|
||||
'hello world')
|
||||
'hello world')
|
||||
|
||||
# Test list native methods
|
||||
doc.embedded_field.list_field[2].list_field.pop(0)
|
||||
self.assertEqual(doc._delta(),
|
||||
({'embedded_field.list_field.2.list_field':
|
||||
[2, {'hello': 'world'}]}, {}))
|
||||
({'embedded_field.list_field.2.list_field':
|
||||
[2, {'hello': 'world'}]}, {}))
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
|
||||
doc.embedded_field.list_field[2].list_field.append(1)
|
||||
self.assertEqual(doc._delta(),
|
||||
({'embedded_field.list_field.2.list_field':
|
||||
[2, {'hello': 'world'}, 1]}, {}))
|
||||
({'embedded_field.list_field.2.list_field':
|
||||
[2, {'hello': 'world'}, 1]}, {}))
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
self.assertEqual(doc.embedded_field.list_field[2].list_field,
|
||||
[2, {'hello': 'world'}, 1])
|
||||
[2, {'hello': 'world'}, 1])
|
||||
|
||||
doc.embedded_field.list_field[2].list_field.sort(key=str)
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
self.assertEqual(doc.embedded_field.list_field[2].list_field,
|
||||
[1, 2, {'hello': 'world'}])
|
||||
[1, 2, {'hello': 'world'}])
|
||||
|
||||
del(doc.embedded_field.list_field[2].list_field[2]['hello'])
|
||||
del doc.embedded_field.list_field[2].list_field[2]['hello']
|
||||
self.assertEqual(doc._delta(),
|
||||
({'embedded_field.list_field.2.list_field': [1, 2, {}]}, {}))
|
||||
({}, {'embedded_field.list_field.2.list_field.2.hello': 1}))
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
|
||||
del(doc.embedded_field.list_field[2].list_field)
|
||||
del doc.embedded_field.list_field[2].list_field
|
||||
self.assertEqual(doc._delta(),
|
||||
({}, {'embedded_field.list_field.2.list_field': 1}))
|
||||
({}, {'embedded_field.list_field.2.list_field': 1}))
|
||||
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
@@ -269,9 +270,9 @@ class DeltaTest(unittest.TestCase):
|
||||
|
||||
doc.dict_field['Embedded'].string_field = 'Hello World'
|
||||
self.assertEqual(doc._get_changed_fields(),
|
||||
['dict_field.Embedded.string_field'])
|
||||
['dict_field.Embedded.string_field'])
|
||||
self.assertEqual(doc._delta(),
|
||||
({'dict_field.Embedded.string_field': 'Hello World'}, {}))
|
||||
({'dict_field.Embedded.string_field': 'Hello World'}, {}))
|
||||
|
||||
def test_circular_reference_deltas(self):
|
||||
self.circular_reference_deltas(Document, Document)
|
||||
@@ -289,10 +290,11 @@ class DeltaTest(unittest.TestCase):
|
||||
name = StringField()
|
||||
owner = ReferenceField('Person')
|
||||
|
||||
person = Person(name="owner")
|
||||
person.save()
|
||||
organization = Organization(name="company")
|
||||
organization.save()
|
||||
Person.drop_collection()
|
||||
Organization.drop_collection()
|
||||
|
||||
person = Person(name="owner").save()
|
||||
organization = Organization(name="company").save()
|
||||
|
||||
person.owns.append(organization)
|
||||
organization.owner = person
|
||||
@@ -311,29 +313,24 @@ class DeltaTest(unittest.TestCase):
|
||||
self.circular_reference_deltas_2(DynamicDocument, Document)
|
||||
self.circular_reference_deltas_2(DynamicDocument, DynamicDocument)
|
||||
|
||||
def circular_reference_deltas_2(self, DocClass1, DocClass2):
|
||||
def circular_reference_deltas_2(self, DocClass1, DocClass2, dbref=True):
|
||||
|
||||
class Person(DocClass1):
|
||||
name = StringField()
|
||||
owns = ListField(ReferenceField('Organization'))
|
||||
employer = ReferenceField('Organization')
|
||||
owns = ListField(ReferenceField('Organization', dbref=dbref))
|
||||
employer = ReferenceField('Organization', dbref=dbref)
|
||||
|
||||
class Organization(DocClass2):
|
||||
name = StringField()
|
||||
owner = ReferenceField('Person')
|
||||
employees = ListField(ReferenceField('Person'))
|
||||
owner = ReferenceField('Person', dbref=dbref)
|
||||
employees = ListField(ReferenceField('Person', dbref=dbref))
|
||||
|
||||
Person.drop_collection()
|
||||
Organization.drop_collection()
|
||||
|
||||
person = Person(name="owner")
|
||||
person.save()
|
||||
|
||||
employee = Person(name="employee")
|
||||
employee.save()
|
||||
|
||||
organization = Organization(name="company")
|
||||
organization.save()
|
||||
person = Person(name="owner").save()
|
||||
employee = Person(name="employee").save()
|
||||
organization = Organization(name="company").save()
|
||||
|
||||
person.owns.append(organization)
|
||||
organization.owner = person
|
||||
@@ -353,6 +350,8 @@ class DeltaTest(unittest.TestCase):
|
||||
self.assertEqual(o.owner, p)
|
||||
self.assertEqual(e.employer, o)
|
||||
|
||||
return person, organization, employee
|
||||
|
||||
def test_delta_db_field(self):
|
||||
self.delta_db_field(Document)
|
||||
self.delta_db_field(DynamicDocument)
|
||||
@@ -549,22 +548,21 @@ class DeltaTest(unittest.TestCase):
|
||||
doc.embedded_field.list_field[2].string_field = 'hello world'
|
||||
doc.embedded_field.list_field[2] = doc.embedded_field.list_field[2]
|
||||
self.assertEqual(doc._get_changed_fields(),
|
||||
['db_embedded_field.db_list_field'])
|
||||
self.assertEqual(doc.embedded_field._delta(), ({
|
||||
'db_list_field': ['1', 2, {
|
||||
['db_embedded_field.db_list_field.2'])
|
||||
self.assertEqual(doc.embedded_field._delta(), ({'db_list_field.2': {
|
||||
'_cls': 'Embedded',
|
||||
'db_string_field': 'hello world',
|
||||
'db_int_field': 1,
|
||||
'db_list_field': ['1', 2, {'hello': 'world'}],
|
||||
'db_dict_field': {'hello': 'world'}}]}, {}))
|
||||
'db_dict_field': {'hello': 'world'}}}, {}))
|
||||
self.assertEqual(doc._delta(), ({
|
||||
'db_embedded_field.db_list_field': ['1', 2, {
|
||||
'db_embedded_field.db_list_field.2': {
|
||||
'_cls': 'Embedded',
|
||||
'db_string_field': 'hello world',
|
||||
'db_int_field': 1,
|
||||
'db_list_field': ['1', 2, {'hello': 'world'}],
|
||||
'db_dict_field': {'hello': 'world'}}
|
||||
]}, {}))
|
||||
}, {}))
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
self.assertEqual(doc.embedded_field.list_field[2].string_field,
|
||||
@@ -593,14 +591,13 @@ class DeltaTest(unittest.TestCase):
|
||||
self.assertEqual(doc.embedded_field.list_field[2].list_field,
|
||||
[1, 2, {'hello': 'world'}])
|
||||
|
||||
del(doc.embedded_field.list_field[2].list_field[2]['hello'])
|
||||
del doc.embedded_field.list_field[2].list_field[2]['hello']
|
||||
self.assertEqual(doc._delta(),
|
||||
({'db_embedded_field.db_list_field.2.db_list_field':
|
||||
[1, 2, {}]}, {}))
|
||||
({}, {'db_embedded_field.db_list_field.2.db_list_field.2.hello': 1}))
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
|
||||
del(doc.embedded_field.list_field[2].list_field)
|
||||
del doc.embedded_field.list_field[2].list_field
|
||||
self.assertEqual(doc._delta(), ({},
|
||||
{'db_embedded_field.db_list_field.2.db_list_field': 1}))
|
||||
|
||||
@@ -612,13 +609,13 @@ class DeltaTest(unittest.TestCase):
|
||||
Person.drop_collection()
|
||||
|
||||
p = Person(name="James", age=34)
|
||||
self.assertEqual(p._delta(), ({'age': 34, 'name': 'James',
|
||||
'_cls': 'Person'}, {}))
|
||||
self.assertEqual(p._delta(), (
|
||||
SON([('_cls', 'Person'), ('name', 'James'), ('age', 34)]), {}))
|
||||
|
||||
p.doc = 123
|
||||
del(p.doc)
|
||||
self.assertEqual(p._delta(), ({'age': 34, 'name': 'James',
|
||||
'_cls': 'Person'}, {'doc': 1}))
|
||||
del p.doc
|
||||
self.assertEqual(p._delta(), (
|
||||
SON([('_cls', 'Person'), ('name', 'James'), ('age', 34)]), {}))
|
||||
|
||||
p = Person()
|
||||
p.name = "Dean"
|
||||
@@ -630,14 +627,14 @@ class DeltaTest(unittest.TestCase):
|
||||
self.assertEqual(p._get_changed_fields(), ['age'])
|
||||
self.assertEqual(p._delta(), ({'age': 24}, {}))
|
||||
|
||||
p = self.Person.objects(age=22).get()
|
||||
p = Person.objects(age=22).get()
|
||||
p.age = 24
|
||||
self.assertEqual(p.age, 24)
|
||||
self.assertEqual(p._get_changed_fields(), ['age'])
|
||||
self.assertEqual(p._delta(), ({'age': 24}, {}))
|
||||
|
||||
p.save()
|
||||
self.assertEqual(1, self.Person.objects(age=24).count())
|
||||
self.assertEqual(1, Person.objects(age=24).count())
|
||||
|
||||
def test_dynamic_delta(self):
|
||||
|
||||
@@ -684,6 +681,187 @@ class DeltaTest(unittest.TestCase):
|
||||
self.assertEqual(doc._get_changed_fields(), ['list_field'])
|
||||
self.assertEqual(doc._delta(), ({}, {'list_field': 1}))
|
||||
|
||||
def test_delta_with_dbref_true(self):
|
||||
person, organization, employee = self.circular_reference_deltas_2(Document, Document, True)
|
||||
employee.name = 'test'
|
||||
|
||||
self.assertEqual(organization._get_changed_fields(), [])
|
||||
|
||||
updates, removals = organization._delta()
|
||||
self.assertEqual({}, removals)
|
||||
self.assertEqual({}, updates)
|
||||
|
||||
organization.employees.append(person)
|
||||
updates, removals = organization._delta()
|
||||
self.assertEqual({}, removals)
|
||||
self.assertTrue('employees' in updates)
|
||||
|
||||
def test_delta_with_dbref_false(self):
|
||||
person, organization, employee = self.circular_reference_deltas_2(Document, Document, False)
|
||||
employee.name = 'test'
|
||||
|
||||
self.assertEqual(organization._get_changed_fields(), [])
|
||||
|
||||
updates, removals = organization._delta()
|
||||
self.assertEqual({}, removals)
|
||||
self.assertEqual({}, updates)
|
||||
|
||||
organization.employees.append(person)
|
||||
updates, removals = organization._delta()
|
||||
self.assertEqual({}, removals)
|
||||
self.assertTrue('employees' in updates)
|
||||
|
||||
def test_nested_nested_fields_mark_as_changed(self):
|
||||
class EmbeddedDoc(EmbeddedDocument):
|
||||
name = StringField()
|
||||
|
||||
class MyDoc(Document):
|
||||
subs = MapField(MapField(EmbeddedDocumentField(EmbeddedDoc)))
|
||||
name = StringField()
|
||||
|
||||
MyDoc.drop_collection()
|
||||
|
||||
mydoc = MyDoc(name='testcase1', subs={'a': {'b': EmbeddedDoc(name='foo')}}).save()
|
||||
|
||||
mydoc = MyDoc.objects.first()
|
||||
subdoc = mydoc.subs['a']['b']
|
||||
subdoc.name = 'bar'
|
||||
|
||||
self.assertEqual(["name"], subdoc._get_changed_fields())
|
||||
self.assertEqual(["subs.a.b.name"], mydoc._get_changed_fields())
|
||||
|
||||
mydoc._clear_changed_fields()
|
||||
self.assertEqual([], mydoc._get_changed_fields())
|
||||
|
||||
def test_lower_level_mark_as_changed(self):
|
||||
class EmbeddedDoc(EmbeddedDocument):
|
||||
name = StringField()
|
||||
|
||||
class MyDoc(Document):
|
||||
subs = MapField(EmbeddedDocumentField(EmbeddedDoc))
|
||||
|
||||
MyDoc.drop_collection()
|
||||
|
||||
MyDoc().save()
|
||||
|
||||
mydoc = MyDoc.objects.first()
|
||||
mydoc.subs['a'] = EmbeddedDoc()
|
||||
self.assertEqual(["subs.a"], mydoc._get_changed_fields())
|
||||
|
||||
subdoc = mydoc.subs['a']
|
||||
subdoc.name = 'bar'
|
||||
|
||||
self.assertEqual(["name"], subdoc._get_changed_fields())
|
||||
self.assertEqual(["subs.a"], mydoc._get_changed_fields())
|
||||
mydoc.save()
|
||||
|
||||
mydoc._clear_changed_fields()
|
||||
self.assertEqual([], mydoc._get_changed_fields())
|
||||
|
||||
def test_upper_level_mark_as_changed(self):
|
||||
class EmbeddedDoc(EmbeddedDocument):
|
||||
name = StringField()
|
||||
|
||||
class MyDoc(Document):
|
||||
subs = MapField(EmbeddedDocumentField(EmbeddedDoc))
|
||||
|
||||
MyDoc.drop_collection()
|
||||
|
||||
MyDoc(subs={'a': EmbeddedDoc(name='foo')}).save()
|
||||
|
||||
mydoc = MyDoc.objects.first()
|
||||
subdoc = mydoc.subs['a']
|
||||
subdoc.name = 'bar'
|
||||
|
||||
self.assertEqual(["name"], subdoc._get_changed_fields())
|
||||
self.assertEqual(["subs.a.name"], mydoc._get_changed_fields())
|
||||
|
||||
mydoc.subs['a'] = EmbeddedDoc()
|
||||
self.assertEqual(["subs.a"], mydoc._get_changed_fields())
|
||||
mydoc.save()
|
||||
|
||||
mydoc._clear_changed_fields()
|
||||
self.assertEqual([], mydoc._get_changed_fields())
|
||||
|
||||
def test_referenced_object_changed_attributes(self):
|
||||
"""Ensures that when you save a new reference to a field, the referenced object isn't altered"""
|
||||
|
||||
class Organization(Document):
|
||||
name = StringField()
|
||||
|
||||
class User(Document):
|
||||
name = StringField()
|
||||
org = ReferenceField('Organization', required=True)
|
||||
|
||||
Organization.drop_collection()
|
||||
User.drop_collection()
|
||||
|
||||
org1 = Organization(name='Org 1')
|
||||
org1.save()
|
||||
|
||||
org2 = Organization(name='Org 2')
|
||||
org2.save()
|
||||
|
||||
user = User(name='Fred', org=org1)
|
||||
user.save()
|
||||
|
||||
org1.reload()
|
||||
org2.reload()
|
||||
user.reload()
|
||||
self.assertEqual(org1.name, 'Org 1')
|
||||
self.assertEqual(org2.name, 'Org 2')
|
||||
self.assertEqual(user.name, 'Fred')
|
||||
|
||||
user.name = 'Harold'
|
||||
user.org = org2
|
||||
|
||||
org2.name = 'New Org 2'
|
||||
self.assertEqual(org2.name, 'New Org 2')
|
||||
|
||||
user.save()
|
||||
org2.save()
|
||||
|
||||
self.assertEqual(org2.name, 'New Org 2')
|
||||
org2.reload()
|
||||
self.assertEqual(org2.name, 'New Org 2')
|
||||
|
||||
def test_delta_for_nested_map_fields(self):
|
||||
class UInfoDocument(Document):
|
||||
phone = StringField()
|
||||
|
||||
class EmbeddedRole(EmbeddedDocument):
|
||||
type = StringField()
|
||||
|
||||
class EmbeddedUser(EmbeddedDocument):
|
||||
name = StringField()
|
||||
roles = MapField(field=EmbeddedDocumentField(EmbeddedRole))
|
||||
rolist = ListField(field=EmbeddedDocumentField(EmbeddedRole))
|
||||
info = ReferenceField(UInfoDocument)
|
||||
|
||||
class Doc(Document):
|
||||
users = MapField(field=EmbeddedDocumentField(EmbeddedUser))
|
||||
num = IntField(default=-1)
|
||||
|
||||
Doc.drop_collection()
|
||||
|
||||
doc = Doc(num=1)
|
||||
doc.users["007"] = EmbeddedUser(name="Agent007")
|
||||
doc.save()
|
||||
|
||||
uinfo = UInfoDocument(phone="79089269066")
|
||||
uinfo.save()
|
||||
|
||||
d = Doc.objects(num=1).first()
|
||||
d.users["007"]["roles"]["666"] = EmbeddedRole(type="superadmin")
|
||||
d.users["007"]["rolist"].append(EmbeddedRole(type="oops"))
|
||||
d.users["007"]["info"] = uinfo
|
||||
delta = d._delta()
|
||||
self.assertEqual(True, "users.007.roles.666" in delta[0])
|
||||
self.assertEqual(True, "users.007.rolist" in delta[0])
|
||||
self.assertEqual(True, "users.007.info" in delta[0])
|
||||
self.assertEqual('superadmin', delta[0]["users.007.roles.666"]["type"])
|
||||
self.assertEqual('oops', delta[0]["users.007.rolist"][0]["type"])
|
||||
self.assertEqual(uinfo.id, delta[0]["users.007.info"])
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import unittest
|
||||
import sys
|
||||
sys.path[0:0] = [""]
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine.connection import get_db
|
||||
@@ -31,8 +29,9 @@ class DynamicTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(p.to_mongo(), {"_cls": "Person", "name": "James",
|
||||
"age": 34})
|
||||
|
||||
self.assertEqual(p.to_mongo().keys(), ["_cls", "name", "age"])
|
||||
p.save()
|
||||
self.assertEqual(p.to_mongo().keys(), ["_id", "_cls", "name", "age"])
|
||||
|
||||
self.assertEqual(self.Person.objects.first().age, 34)
|
||||
|
||||
@@ -71,7 +70,7 @@ class DynamicTest(unittest.TestCase):
|
||||
obj = collection.find_one()
|
||||
self.assertEqual(sorted(obj.keys()), ['_cls', '_id', 'misc', 'name'])
|
||||
|
||||
del(p.misc)
|
||||
del p.misc
|
||||
p.save()
|
||||
|
||||
p = self.Person.objects.get()
|
||||
@@ -80,6 +79,25 @@ class DynamicTest(unittest.TestCase):
|
||||
obj = collection.find_one()
|
||||
self.assertEqual(sorted(obj.keys()), ['_cls', '_id', 'name'])
|
||||
|
||||
def test_reload_after_unsetting(self):
|
||||
p = self.Person()
|
||||
p.misc = 22
|
||||
p.save()
|
||||
p.update(unset__misc=1)
|
||||
p.reload()
|
||||
|
||||
def test_reload_dynamic_field(self):
|
||||
self.Person.objects.delete()
|
||||
p = self.Person.objects.create()
|
||||
p.update(age=1)
|
||||
|
||||
self.assertEqual(len(p._data), 3)
|
||||
self.assertEqual(sorted(p._data.keys()), ['_cls', 'id', 'name'])
|
||||
|
||||
p.reload()
|
||||
self.assertEqual(len(p._data), 4)
|
||||
self.assertEqual(sorted(p._data.keys()), ['_cls', 'age', 'id', 'name'])
|
||||
|
||||
def test_dynamic_document_queries(self):
|
||||
"""Ensure we can query dynamic fields"""
|
||||
p = self.Person()
|
||||
@@ -121,6 +139,13 @@ class DynamicTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(1, self.Person.objects(misc__hello='world').count())
|
||||
|
||||
def test_three_level_complex_data_lookups(self):
|
||||
"""Ensure you can query three level document dynamic fields"""
|
||||
p = self.Person.objects.create(
|
||||
misc={'hello': {'hello2': 'world'}}
|
||||
)
|
||||
self.assertEqual(1, self.Person.objects(misc__hello__hello2='world').count())
|
||||
|
||||
def test_complex_embedded_document_validation(self):
|
||||
"""Ensure embedded dynamic documents may be validated"""
|
||||
class Embedded(DynamicEmbeddedDocument):
|
||||
@@ -291,6 +316,58 @@ class DynamicTest(unittest.TestCase):
|
||||
person.save()
|
||||
self.assertEqual(Person.objects.first().age, 35)
|
||||
|
||||
def test_dynamic_embedded_works_with_only(self):
|
||||
"""Ensure custom fieldnames on a dynamic embedded document are found by qs.only()"""
|
||||
|
||||
class Address(DynamicEmbeddedDocument):
|
||||
city = StringField()
|
||||
|
||||
class Person(DynamicDocument):
|
||||
address = EmbeddedDocumentField(Address)
|
||||
|
||||
Person.drop_collection()
|
||||
|
||||
Person(name="Eric", address=Address(city="San Francisco", street_number="1337")).save()
|
||||
|
||||
self.assertEqual(Person.objects.first().address.street_number, '1337')
|
||||
self.assertEqual(Person.objects.only('address__street_number').first().address.street_number, '1337')
|
||||
|
||||
def test_dynamic_and_embedded_dict_access(self):
|
||||
"""Ensure embedded dynamic documents work with dict[] style access"""
|
||||
|
||||
class Address(EmbeddedDocument):
|
||||
city = StringField()
|
||||
|
||||
class Person(DynamicDocument):
|
||||
name = StringField()
|
||||
|
||||
Person.drop_collection()
|
||||
|
||||
Person(name="Ross", address=Address(city="London")).save()
|
||||
|
||||
person = Person.objects.first()
|
||||
person.attrval = "This works"
|
||||
|
||||
person["phone"] = "555-1212" # but this should too
|
||||
|
||||
# Same thing two levels deep
|
||||
person["address"]["city"] = "Lundenne"
|
||||
person.save()
|
||||
|
||||
self.assertEqual(Person.objects.first().address.city, "Lundenne")
|
||||
|
||||
self.assertEqual(Person.objects.first().phone, "555-1212")
|
||||
|
||||
person = Person.objects.first()
|
||||
person.address = Address(city="Londinium")
|
||||
person.save()
|
||||
|
||||
self.assertEqual(Person.objects.first().address.city, "Londinium")
|
||||
|
||||
person = Person.objects.first()
|
||||
person["age"] = 35
|
||||
person.save()
|
||||
self.assertEqual(Person.objects.first().age, 35)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import with_statement
|
||||
import unittest
|
||||
import sys
|
||||
sys.path[0:0] = [""]
|
||||
|
||||
import os
|
||||
import pymongo
|
||||
|
||||
from nose.plugins.skip import SkipTest
|
||||
from datetime import datetime
|
||||
import pymongo
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine.connection import get_db, get_connection
|
||||
from mongoengine.connection import get_db
|
||||
|
||||
from tests.utils import get_mongodb_version, needs_mongodb_v26
|
||||
|
||||
__all__ = ("IndexesTest", )
|
||||
|
||||
@@ -19,7 +17,7 @@ __all__ = ("IndexesTest", )
|
||||
class IndexesTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
connect(db='mongoenginetest')
|
||||
self.connection = connect(db='mongoenginetest')
|
||||
self.db = get_db()
|
||||
|
||||
class Person(Document):
|
||||
@@ -33,10 +31,7 @@ class IndexesTest(unittest.TestCase):
|
||||
self.Person = Person
|
||||
|
||||
def tearDown(self):
|
||||
for collection in self.db.collection_names():
|
||||
if 'system.' in collection:
|
||||
continue
|
||||
self.db.drop_collection(collection)
|
||||
self.connection.drop_database(self.db)
|
||||
|
||||
def test_indexes_document(self):
|
||||
"""Ensure that indexes are used when meta[indexes] is specified for
|
||||
@@ -144,7 +139,7 @@ class IndexesTest(unittest.TestCase):
|
||||
meta = {
|
||||
'indexes': [
|
||||
{
|
||||
'fields': ('title',),
|
||||
'fields': ('title',),
|
||||
},
|
||||
],
|
||||
'allow_inheritance': True,
|
||||
@@ -157,6 +152,35 @@ class IndexesTest(unittest.TestCase):
|
||||
self.assertEqual([{'fields': [('_cls', 1), ('title', 1)]}],
|
||||
A._meta['index_specs'])
|
||||
|
||||
def test_index_no_cls(self):
|
||||
"""Ensure index specs are inhertited correctly"""
|
||||
|
||||
class A(Document):
|
||||
title = StringField()
|
||||
meta = {
|
||||
'indexes': [
|
||||
{'fields': ('title',), 'cls': False},
|
||||
],
|
||||
'allow_inheritance': True,
|
||||
'index_cls': False
|
||||
}
|
||||
|
||||
self.assertEqual([('title', 1)], A._meta['index_specs'][0]['fields'])
|
||||
A._get_collection().drop_indexes()
|
||||
A.ensure_indexes()
|
||||
info = A._get_collection().index_information()
|
||||
self.assertEqual(len(info.keys()), 2)
|
||||
|
||||
class B(A):
|
||||
c = StringField()
|
||||
d = StringField()
|
||||
meta = {
|
||||
'indexes': [{'fields': ['c']}, {'fields': ['d'], 'cls': True}],
|
||||
'allow_inheritance': True
|
||||
}
|
||||
self.assertEqual([('c', 1)], B._meta['index_specs'][1]['fields'])
|
||||
self.assertEqual([('_cls', 1), ('d', 1)], B._meta['index_specs'][2]['fields'])
|
||||
|
||||
def test_build_index_spec_is_not_destructive(self):
|
||||
|
||||
class MyDoc(Document):
|
||||
@@ -217,7 +241,7 @@ class IndexesTest(unittest.TestCase):
|
||||
}
|
||||
|
||||
self.assertEqual([{'fields': [('location.point', '2d')]}],
|
||||
Place._meta['index_specs'])
|
||||
Place._meta['index_specs'])
|
||||
|
||||
Place.ensure_indexes()
|
||||
info = Place._get_collection().index_information()
|
||||
@@ -231,8 +255,7 @@ class IndexesTest(unittest.TestCase):
|
||||
location = DictField()
|
||||
|
||||
class Place(Document):
|
||||
current = DictField(
|
||||
field=EmbeddedDocumentField('EmbeddedLocation'))
|
||||
current = DictField(field=EmbeddedDocumentField('EmbeddedLocation'))
|
||||
meta = {
|
||||
'allow_inheritance': True,
|
||||
'indexes': [
|
||||
@@ -241,13 +264,67 @@ class IndexesTest(unittest.TestCase):
|
||||
}
|
||||
|
||||
self.assertEqual([{'fields': [('current.location.point', '2d')]}],
|
||||
Place._meta['index_specs'])
|
||||
Place._meta['index_specs'])
|
||||
|
||||
Place.ensure_indexes()
|
||||
info = Place._get_collection().index_information()
|
||||
info = [value['key'] for key, value in info.iteritems()]
|
||||
self.assertTrue([('current.location.point', '2d')] in info)
|
||||
|
||||
def test_explicit_geosphere_index(self):
|
||||
"""Ensure that geosphere indexes work when created via meta[indexes]
|
||||
"""
|
||||
class Place(Document):
|
||||
location = DictField()
|
||||
meta = {
|
||||
'allow_inheritance': True,
|
||||
'indexes': [
|
||||
'(location.point',
|
||||
]
|
||||
}
|
||||
|
||||
self.assertEqual([{'fields': [('location.point', '2dsphere')]}],
|
||||
Place._meta['index_specs'])
|
||||
|
||||
Place.ensure_indexes()
|
||||
info = Place._get_collection().index_information()
|
||||
info = [value['key'] for key, value in info.iteritems()]
|
||||
self.assertTrue([('location.point', '2dsphere')] in info)
|
||||
|
||||
def test_explicit_geohaystack_index(self):
|
||||
"""Ensure that geohaystack indexes work when created via meta[indexes]
|
||||
"""
|
||||
raise SkipTest('GeoHaystack index creation is not supported for now'
|
||||
'from meta, as it requires a bucketSize parameter.')
|
||||
|
||||
class Place(Document):
|
||||
location = DictField()
|
||||
name = StringField()
|
||||
meta = {
|
||||
'indexes': [
|
||||
(')location.point', 'name')
|
||||
]
|
||||
}
|
||||
self.assertEqual([{'fields': [('location.point', 'geoHaystack'), ('name', 1)]}],
|
||||
Place._meta['index_specs'])
|
||||
|
||||
Place.ensure_indexes()
|
||||
info = Place._get_collection().index_information()
|
||||
info = [value['key'] for key, value in info.iteritems()]
|
||||
self.assertTrue([('location.point', 'geoHaystack')] in info)
|
||||
|
||||
def test_create_geohaystack_index(self):
|
||||
"""Ensure that geohaystack indexes can be created
|
||||
"""
|
||||
class Place(Document):
|
||||
location = DictField()
|
||||
name = StringField()
|
||||
|
||||
Place.create_index({'fields': (')location.point', 'name')}, bucketSize=10)
|
||||
info = Place._get_collection().index_information()
|
||||
info = [value['key'] for key, value in info.iteritems()]
|
||||
self.assertTrue([('location.point', 'geoHaystack'), ('name', 1)] in info)
|
||||
|
||||
def test_dictionary_indexes(self):
|
||||
"""Ensure that indexes are used when meta[indexes] contains
|
||||
dictionaries instead of lists.
|
||||
@@ -264,7 +341,7 @@ class IndexesTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual([{'fields': [('addDate', -1)], 'unique': True,
|
||||
'sparse': True}],
|
||||
BlogPost._meta['index_specs'])
|
||||
BlogPost._meta['index_specs'])
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
@@ -314,20 +391,27 @@ class IndexesTest(unittest.TestCase):
|
||||
"""
|
||||
class User(Document):
|
||||
meta = {
|
||||
'allow_inheritance': True,
|
||||
'indexes': ['user_guid'],
|
||||
'auto_create_index': False
|
||||
}
|
||||
user_guid = StringField(required=True)
|
||||
|
||||
class MongoUser(User):
|
||||
pass
|
||||
|
||||
User.drop_collection()
|
||||
|
||||
u = User(user_guid='123')
|
||||
u.save()
|
||||
User(user_guid='123').save()
|
||||
MongoUser(user_guid='123').save()
|
||||
|
||||
self.assertEqual(1, User.objects.count())
|
||||
self.assertEqual(2, User.objects.count())
|
||||
info = User.objects._collection.index_information()
|
||||
self.assertEqual(info.keys(), ['_id_'])
|
||||
User.drop_collection()
|
||||
|
||||
User.ensure_indexes()
|
||||
info = User.objects._collection.index_information()
|
||||
self.assertEqual(sorted(info.keys()), ['_cls_1_user_guid_1', '_id_'])
|
||||
|
||||
def test_embedded_document_index(self):
|
||||
"""Tests settings an index on an embedded document
|
||||
@@ -349,7 +433,6 @@ class IndexesTest(unittest.TestCase):
|
||||
|
||||
info = BlogPost.objects._collection.index_information()
|
||||
self.assertEqual(sorted(info.keys()), ['_id_', 'date.yr_-1'])
|
||||
BlogPost.drop_collection()
|
||||
|
||||
def test_list_embedded_document_index(self):
|
||||
"""Ensure list embedded documents can be indexed
|
||||
@@ -374,10 +457,8 @@ class IndexesTest(unittest.TestCase):
|
||||
self.assertEqual(sorted(info.keys()), ['_id_', 'tags.tag_1'])
|
||||
|
||||
post1 = BlogPost(title="Embedded Indexes tests in place",
|
||||
tags=[Tag(name="about"), Tag(name="time")]
|
||||
)
|
||||
tags=[Tag(name="about"), Tag(name="time")])
|
||||
post1.save()
|
||||
BlogPost.drop_collection()
|
||||
|
||||
def test_recursive_embedded_objects_dont_break_indexes(self):
|
||||
|
||||
@@ -392,56 +473,53 @@ class IndexesTest(unittest.TestCase):
|
||||
info = RecursiveDocument._get_collection().index_information()
|
||||
self.assertEqual(sorted(info.keys()), ['_cls_1', '_id_'])
|
||||
|
||||
def test_geo_indexes_recursion(self):
|
||||
|
||||
class Location(Document):
|
||||
name = StringField()
|
||||
location = GeoPointField()
|
||||
|
||||
class Parent(Document):
|
||||
name = StringField()
|
||||
location = ReferenceField(Location, dbref=False)
|
||||
|
||||
Location.drop_collection()
|
||||
Parent.drop_collection()
|
||||
|
||||
list(Parent.objects)
|
||||
|
||||
collection = Parent._get_collection()
|
||||
info = collection.index_information()
|
||||
|
||||
self.assertFalse('location_2d' in info)
|
||||
|
||||
self.assertEqual(len(Parent._geo_indices()), 0)
|
||||
self.assertEqual(len(Location._geo_indices()), 1)
|
||||
|
||||
def test_covered_index(self):
|
||||
"""Ensure that covered indexes can be used
|
||||
"""
|
||||
|
||||
class Test(Document):
|
||||
a = IntField()
|
||||
b = IntField()
|
||||
|
||||
meta = {
|
||||
'indexes': ['a'],
|
||||
'allow_inheritance': False
|
||||
}
|
||||
}
|
||||
|
||||
Test.drop_collection()
|
||||
|
||||
obj = Test(a=1)
|
||||
obj.save()
|
||||
|
||||
IS_MONGODB_3 = get_mongodb_version()[0] >= 3
|
||||
|
||||
# Need to be explicit about covered indexes as mongoDB doesn't know if
|
||||
# the documents returned might have more keys in that here.
|
||||
query_plan = Test.objects(id=obj.id).exclude('a').explain()
|
||||
self.assertFalse(query_plan['indexOnly'])
|
||||
if not IS_MONGODB_3:
|
||||
self.assertFalse(query_plan['indexOnly'])
|
||||
else:
|
||||
self.assertEqual(query_plan.get('queryPlanner').get('winningPlan').get('inputStage').get('stage'), 'IDHACK')
|
||||
|
||||
query_plan = Test.objects(id=obj.id).only('id').explain()
|
||||
self.assertTrue(query_plan['indexOnly'])
|
||||
if not IS_MONGODB_3:
|
||||
self.assertTrue(query_plan['indexOnly'])
|
||||
else:
|
||||
self.assertEqual(query_plan.get('queryPlanner').get('winningPlan').get('inputStage').get('stage'), 'IDHACK')
|
||||
|
||||
query_plan = Test.objects(a=1).only('a').exclude('id').explain()
|
||||
self.assertTrue(query_plan['indexOnly'])
|
||||
if not IS_MONGODB_3:
|
||||
self.assertTrue(query_plan['indexOnly'])
|
||||
else:
|
||||
self.assertEqual(query_plan.get('queryPlanner').get('winningPlan').get('inputStage').get('stage'), 'IXSCAN')
|
||||
self.assertEqual(query_plan.get('queryPlanner').get('winningPlan').get('stage'), 'PROJECTION')
|
||||
|
||||
query_plan = Test.objects(a=1).explain()
|
||||
if not IS_MONGODB_3:
|
||||
self.assertFalse(query_plan['indexOnly'])
|
||||
else:
|
||||
self.assertEqual(query_plan.get('queryPlanner').get('winningPlan').get('inputStage').get('stage'), 'IXSCAN')
|
||||
self.assertEqual(query_plan.get('queryPlanner').get('winningPlan').get('stage'), 'FETCH')
|
||||
|
||||
def test_index_on_id(self):
|
||||
|
||||
@@ -474,23 +552,29 @@ class IndexesTest(unittest.TestCase):
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
for i in xrange(0, 10):
|
||||
tags = [("tag %i" % n) for n in xrange(0, i % 2)]
|
||||
for i in range(0, 10):
|
||||
tags = [("tag %i" % n) for n in range(0, i % 2)]
|
||||
BlogPost(tags=tags).save()
|
||||
|
||||
self.assertEqual(BlogPost.objects.count(), 10)
|
||||
self.assertEqual(BlogPost.objects.hint().count(), 10)
|
||||
self.assertEqual(BlogPost.objects.hint([('tags', 1)]).count(), 10)
|
||||
|
||||
self.assertEqual(BlogPost.objects.hint([('ZZ', 1)]).count(), 10)
|
||||
# PyMongo 3.0 bug only, works correctly with 2.X and 3.0.1+ versions
|
||||
if pymongo.version != '3.0':
|
||||
self.assertEqual(BlogPost.objects.hint([('tags', 1)]).count(), 10)
|
||||
|
||||
def invalid_index():
|
||||
BlogPost.objects.hint('tags')
|
||||
self.assertRaises(TypeError, invalid_index)
|
||||
self.assertEqual(BlogPost.objects.hint([('ZZ', 1)]).count(), 10)
|
||||
|
||||
if pymongo.version >= '2.8':
|
||||
self.assertEqual(BlogPost.objects.hint('tags').count(), 10)
|
||||
else:
|
||||
def invalid_index():
|
||||
BlogPost.objects.hint('tags').next()
|
||||
self.assertRaises(TypeError, invalid_index)
|
||||
|
||||
def invalid_index_2():
|
||||
return BlogPost.objects.hint(('tags', 1))
|
||||
self.assertRaises(TypeError, invalid_index_2)
|
||||
return BlogPost.objects.hint(('tags', 1)).next()
|
||||
self.assertRaises(Exception, invalid_index_2)
|
||||
|
||||
def test_unique(self):
|
||||
"""Ensure that uniqueness constraints are applied to fields.
|
||||
@@ -535,8 +619,6 @@ class IndexesTest(unittest.TestCase):
|
||||
post3 = BlogPost(title='test3', date=Date(year=2010), slug='test')
|
||||
self.assertRaises(OperationError, post3.save)
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
def test_unique_embedded_document(self):
|
||||
"""Ensure that uniqueness constraints are applied to fields on embedded documents.
|
||||
"""
|
||||
@@ -564,8 +646,36 @@ class IndexesTest(unittest.TestCase):
|
||||
sub=SubDocument(year=2010, slug='test'))
|
||||
self.assertRaises(NotUniqueError, post3.save)
|
||||
|
||||
def test_unique_embedded_document_in_list(self):
|
||||
"""
|
||||
Ensure that the uniqueness constraints are applied to fields in
|
||||
embedded documents, even when the embedded documents in in a
|
||||
list field.
|
||||
"""
|
||||
class SubDocument(EmbeddedDocument):
|
||||
year = IntField(db_field='yr')
|
||||
slug = StringField(unique=True)
|
||||
|
||||
class BlogPost(Document):
|
||||
title = StringField()
|
||||
subs = ListField(EmbeddedDocumentField(SubDocument))
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
post1 = BlogPost(
|
||||
title='test1', subs=[
|
||||
SubDocument(year=2009, slug='conflict'),
|
||||
SubDocument(year=2009, slug='conflict')
|
||||
]
|
||||
)
|
||||
post1.save()
|
||||
|
||||
post2 = BlogPost(
|
||||
title='test2', subs=[SubDocument(year=2014, slug='conflict')]
|
||||
)
|
||||
|
||||
self.assertRaises(NotUniqueError, post2.save)
|
||||
|
||||
def test_unique_with_embedded_document_and_embedded_unique(self):
|
||||
"""Ensure that uniqueness constraints are applied to fields on
|
||||
embedded documents. And work with unique_with as well.
|
||||
@@ -599,8 +709,6 @@ class IndexesTest(unittest.TestCase):
|
||||
sub=SubDocument(year=2009, slug='test-1'))
|
||||
self.assertRaises(NotUniqueError, post3.save)
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
def test_ttl_indexes(self):
|
||||
|
||||
class Log(Document):
|
||||
@@ -613,19 +721,11 @@ class IndexesTest(unittest.TestCase):
|
||||
|
||||
Log.drop_collection()
|
||||
|
||||
if pymongo.version_tuple[0] < 2 and pymongo.version_tuple[1] < 3:
|
||||
raise SkipTest('pymongo needs to be 2.3 or higher for this test')
|
||||
|
||||
connection = get_connection()
|
||||
version_array = connection.server_info()['versionArray']
|
||||
if version_array[0] < 2 and version_array[1] < 2:
|
||||
raise SkipTest('MongoDB needs to be 2.2 or higher for this test')
|
||||
|
||||
# Indexes are lazy so use list() to perform query
|
||||
list(Log.objects)
|
||||
info = Log.objects._collection.index_information()
|
||||
self.assertEqual(3600,
|
||||
info['created_1']['expireAfterSeconds'])
|
||||
info['created_1']['expireAfterSeconds'])
|
||||
|
||||
def test_unique_and_indexes(self):
|
||||
"""Ensure that 'unique' constraints aren't overridden by
|
||||
@@ -648,13 +748,11 @@ class IndexesTest(unittest.TestCase):
|
||||
raise AssertionError("We saved a dupe!")
|
||||
except NotUniqueError:
|
||||
pass
|
||||
Customer.drop_collection()
|
||||
|
||||
def test_unique_and_primary(self):
|
||||
"""If you set a field as primary, then unexpected behaviour can occur.
|
||||
You won't create a duplicate but you will update an existing document.
|
||||
"""
|
||||
|
||||
class User(Document):
|
||||
name = StringField(primary_key=True, unique=True)
|
||||
password = StringField()
|
||||
@@ -670,8 +768,23 @@ class IndexesTest(unittest.TestCase):
|
||||
self.assertEqual(User.objects.count(), 1)
|
||||
self.assertEqual(User.objects.get().password, 'secret2')
|
||||
|
||||
def test_unique_and_primary_create(self):
|
||||
"""Create a new record with a duplicate primary key
|
||||
throws an exception
|
||||
"""
|
||||
class User(Document):
|
||||
name = StringField(primary_key=True)
|
||||
password = StringField()
|
||||
|
||||
User.drop_collection()
|
||||
|
||||
User.objects.create(name='huangz', password='secret')
|
||||
with self.assertRaises(NotUniqueError):
|
||||
User.objects.create(name='huangz', password='secret2')
|
||||
|
||||
self.assertEqual(User.objects.count(), 1)
|
||||
self.assertEqual(User.objects.get().password, 'secret')
|
||||
|
||||
def test_index_with_pk(self):
|
||||
"""Ensure you can use `pk` as part of a query"""
|
||||
|
||||
@@ -698,33 +811,210 @@ class IndexesTest(unittest.TestCase):
|
||||
name = StringField(required=True)
|
||||
term = StringField(required=True)
|
||||
|
||||
class Report(Document):
|
||||
class ReportEmbedded(Document):
|
||||
key = EmbeddedDocumentField(CompoundKey, primary_key=True)
|
||||
text = StringField()
|
||||
|
||||
Report.drop_collection()
|
||||
|
||||
my_key = CompoundKey(name="n", term="ok")
|
||||
report = Report(text="OK", key=my_key).save()
|
||||
report = ReportEmbedded(text="OK", key=my_key).save()
|
||||
|
||||
self.assertEqual({'text': 'OK', '_id': {'term': 'ok', 'name': 'n'}},
|
||||
report.to_mongo())
|
||||
self.assertEqual(report, Report.objects.get(pk=my_key))
|
||||
self.assertEqual(report, ReportEmbedded.objects.get(pk=my_key))
|
||||
|
||||
def test_compound_key_dictfield(self):
|
||||
|
||||
class Report(Document):
|
||||
class ReportDictField(Document):
|
||||
key = DictField(primary_key=True)
|
||||
text = StringField()
|
||||
|
||||
Report.drop_collection()
|
||||
|
||||
my_key = {"name": "n", "term": "ok"}
|
||||
report = Report(text="OK", key=my_key).save()
|
||||
report = ReportDictField(text="OK", key=my_key).save()
|
||||
|
||||
self.assertEqual({'text': 'OK', '_id': {'term': 'ok', 'name': 'n'}},
|
||||
report.to_mongo())
|
||||
self.assertEqual(report, Report.objects.get(pk=my_key))
|
||||
|
||||
# We can't directly call ReportDictField.objects.get(pk=my_key),
|
||||
# because dicts are unordered, and if the order in MongoDB is
|
||||
# different than the one in `my_key`, this test will fail.
|
||||
self.assertEqual(report, ReportDictField.objects.get(pk__name=my_key['name']))
|
||||
self.assertEqual(report, ReportDictField.objects.get(pk__term=my_key['term']))
|
||||
|
||||
def test_string_indexes(self):
|
||||
|
||||
class MyDoc(Document):
|
||||
provider_ids = DictField()
|
||||
meta = {
|
||||
"indexes": ["provider_ids.foo", "provider_ids.bar"],
|
||||
}
|
||||
|
||||
info = MyDoc.objects._collection.index_information()
|
||||
info = [value['key'] for key, value in info.iteritems()]
|
||||
self.assertTrue([('provider_ids.foo', 1)] in info)
|
||||
self.assertTrue([('provider_ids.bar', 1)] in info)
|
||||
|
||||
def test_sparse_compound_indexes(self):
|
||||
|
||||
class MyDoc(Document):
|
||||
provider_ids = DictField()
|
||||
meta = {
|
||||
"indexes": [{'fields': ("provider_ids.foo", "provider_ids.bar"),
|
||||
'sparse': True}],
|
||||
}
|
||||
|
||||
info = MyDoc.objects._collection.index_information()
|
||||
self.assertEqual([('provider_ids.foo', 1), ('provider_ids.bar', 1)],
|
||||
info['provider_ids.foo_1_provider_ids.bar_1']['key'])
|
||||
self.assertTrue(info['provider_ids.foo_1_provider_ids.bar_1']['sparse'])
|
||||
|
||||
@needs_mongodb_v26
|
||||
def test_text_indexes(self):
|
||||
class Book(Document):
|
||||
title = DictField()
|
||||
meta = {
|
||||
"indexes": ["$title"],
|
||||
}
|
||||
|
||||
indexes = Book.objects._collection.index_information()
|
||||
self.assertTrue("title_text" in indexes)
|
||||
key = indexes["title_text"]["key"]
|
||||
self.assertTrue(('_fts', 'text') in key)
|
||||
|
||||
def test_hashed_indexes(self):
|
||||
|
||||
class Book(Document):
|
||||
ref_id = StringField()
|
||||
meta = {
|
||||
"indexes": ["#ref_id"],
|
||||
}
|
||||
|
||||
indexes = Book.objects._collection.index_information()
|
||||
self.assertTrue("ref_id_hashed" in indexes)
|
||||
self.assertTrue(('ref_id', 'hashed') in indexes["ref_id_hashed"]["key"])
|
||||
|
||||
def test_indexes_after_database_drop(self):
|
||||
"""
|
||||
Test to ensure that indexes are re-created on a collection even
|
||||
after the database has been dropped.
|
||||
|
||||
Issue #812
|
||||
"""
|
||||
# Use a new connection and database since dropping the database could
|
||||
# cause concurrent tests to fail.
|
||||
connection = connect(db='tempdatabase',
|
||||
alias='test_indexes_after_database_drop')
|
||||
|
||||
class BlogPost(Document):
|
||||
title = StringField()
|
||||
slug = StringField(unique=True)
|
||||
|
||||
meta = {'db_alias': 'test_indexes_after_database_drop'}
|
||||
|
||||
try:
|
||||
BlogPost.drop_collection()
|
||||
|
||||
# Create Post #1
|
||||
post1 = BlogPost(title='test1', slug='test')
|
||||
post1.save()
|
||||
|
||||
# Drop the Database
|
||||
connection.drop_database('tempdatabase')
|
||||
|
||||
# Re-create Post #1
|
||||
post1 = BlogPost(title='test1', slug='test')
|
||||
post1.save()
|
||||
|
||||
# Create Post #2
|
||||
post2 = BlogPost(title='test2', slug='test')
|
||||
self.assertRaises(NotUniqueError, post2.save)
|
||||
finally:
|
||||
# Drop the temporary database at the end
|
||||
connection.drop_database('tempdatabase')
|
||||
|
||||
|
||||
def test_index_dont_send_cls_option(self):
|
||||
"""
|
||||
Ensure that 'cls' option is not sent through ensureIndex. We shouldn't
|
||||
send internal MongoEngine arguments that are not a part of the index
|
||||
spec.
|
||||
|
||||
This is directly related to the fact that MongoDB doesn't validate the
|
||||
options that are passed to ensureIndex. For more details, see:
|
||||
https://jira.mongodb.org/browse/SERVER-769
|
||||
"""
|
||||
class TestDoc(Document):
|
||||
txt = StringField()
|
||||
|
||||
meta = {
|
||||
'allow_inheritance': True,
|
||||
'indexes': [
|
||||
{'fields': ('txt',), 'cls': False}
|
||||
]
|
||||
}
|
||||
|
||||
class TestChildDoc(TestDoc):
|
||||
txt2 = StringField()
|
||||
|
||||
meta = {
|
||||
'indexes': [
|
||||
{'fields': ('txt2',), 'cls': False}
|
||||
]
|
||||
}
|
||||
|
||||
TestDoc.drop_collection()
|
||||
TestDoc.ensure_indexes()
|
||||
TestChildDoc.ensure_indexes()
|
||||
|
||||
index_info = TestDoc._get_collection().index_information()
|
||||
for key in index_info:
|
||||
del index_info[key]['v'] # drop the index version - we don't care about that here
|
||||
if 'ns' in index_info[key]:
|
||||
del index_info[key]['ns'] # drop the index namespace - we don't care about that here, MongoDB 3+
|
||||
if 'dropDups' in index_info[key]:
|
||||
del index_info[key]['dropDups'] # drop the index dropDups - it is deprecated in MongoDB 3+
|
||||
|
||||
self.assertEqual(index_info, {
|
||||
'txt_1': {
|
||||
'key': [('txt', 1)],
|
||||
'background': False
|
||||
},
|
||||
'_id_': {
|
||||
'key': [('_id', 1)],
|
||||
},
|
||||
'txt2_1': {
|
||||
'key': [('txt2', 1)],
|
||||
'background': False
|
||||
},
|
||||
'_cls_1': {
|
||||
'key': [('_cls', 1)],
|
||||
'background': False,
|
||||
}
|
||||
})
|
||||
|
||||
def test_compound_index_underscore_cls_not_overwritten(self):
|
||||
"""
|
||||
Test that the compound index doesn't get another _cls when it is specified
|
||||
"""
|
||||
class TestDoc(Document):
|
||||
shard_1 = StringField()
|
||||
txt_1 = StringField()
|
||||
|
||||
meta = {
|
||||
'collection': 'test',
|
||||
'allow_inheritance': True,
|
||||
'sparse': True,
|
||||
'shard_key': 'shard_1',
|
||||
'indexes': [
|
||||
('shard_1', '_cls', 'txt_1'),
|
||||
]
|
||||
}
|
||||
|
||||
TestDoc.drop_collection()
|
||||
TestDoc.ensure_indexes()
|
||||
|
||||
index_info = TestDoc._get_collection().index_information()
|
||||
self.assertTrue('shard_1_1__cls_1_txt_1_1' in index_info)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
sys.path[0:0] = [""]
|
||||
import unittest
|
||||
import warnings
|
||||
|
||||
@@ -143,7 +141,7 @@ class InheritanceTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(Animal._superclasses, ())
|
||||
self.assertEqual(Animal._subclasses, ('Animal', 'Animal.Fish',
|
||||
'Animal.Fish.Pike'))
|
||||
'Animal.Fish.Pike'))
|
||||
|
||||
self.assertEqual(Fish._superclasses, ('Animal', ))
|
||||
self.assertEqual(Fish._subclasses, ('Animal.Fish', 'Animal.Fish.Pike'))
|
||||
@@ -163,11 +161,66 @@ class InheritanceTest(unittest.TestCase):
|
||||
class Employee(Person):
|
||||
salary = IntField()
|
||||
|
||||
self.assertEqual(['age', 'id', 'name', 'salary'],
|
||||
self.assertEqual(['_cls', 'age', 'id', 'name', 'salary'],
|
||||
sorted(Employee._fields.keys()))
|
||||
self.assertEqual(Employee._get_collection_name(),
|
||||
Person._get_collection_name())
|
||||
|
||||
def test_inheritance_to_mongo_keys(self):
|
||||
"""Ensure that document may inherit fields from a superclass document.
|
||||
"""
|
||||
class Person(Document):
|
||||
name = StringField()
|
||||
age = IntField()
|
||||
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
class Employee(Person):
|
||||
salary = IntField()
|
||||
|
||||
self.assertEqual(['_cls', 'age', 'id', 'name', 'salary'],
|
||||
sorted(Employee._fields.keys()))
|
||||
self.assertEqual(Person(name="Bob", age=35).to_mongo().keys(),
|
||||
['_cls', 'name', 'age'])
|
||||
self.assertEqual(Employee(name="Bob", age=35, salary=0).to_mongo().keys(),
|
||||
['_cls', 'name', 'age', 'salary'])
|
||||
self.assertEqual(Employee._get_collection_name(),
|
||||
Person._get_collection_name())
|
||||
|
||||
def test_indexes_and_multiple_inheritance(self):
|
||||
""" Ensure that all of the indexes are created for a document with
|
||||
multiple inheritance.
|
||||
"""
|
||||
|
||||
class A(Document):
|
||||
a = StringField()
|
||||
|
||||
meta = {
|
||||
'allow_inheritance': True,
|
||||
'indexes': ['a']
|
||||
}
|
||||
|
||||
class B(Document):
|
||||
b = StringField()
|
||||
|
||||
meta = {
|
||||
'allow_inheritance': True,
|
||||
'indexes': ['b']
|
||||
}
|
||||
|
||||
class C(A, B):
|
||||
pass
|
||||
|
||||
A.drop_collection()
|
||||
B.drop_collection()
|
||||
C.drop_collection()
|
||||
|
||||
C.ensure_indexes()
|
||||
|
||||
self.assertEqual(
|
||||
sorted([idx['key'] for idx in C._get_collection().index_information().values()]),
|
||||
sorted([[(u'_cls', 1), (u'b', 1)], [(u'_id', 1)], [(u'_cls', 1), (u'a', 1)]])
|
||||
)
|
||||
|
||||
def test_polymorphic_queries(self):
|
||||
"""Ensure that the correct subclasses are returned from a query
|
||||
@@ -197,41 +250,36 @@ class InheritanceTest(unittest.TestCase):
|
||||
classes = [obj.__class__ for obj in Human.objects]
|
||||
self.assertEqual(classes, [Human])
|
||||
|
||||
|
||||
def test_allow_inheritance(self):
|
||||
"""Ensure that inheritance may be disabled on simple classes and that
|
||||
_cls and _subclasses will not be used.
|
||||
"""Ensure that inheritance is disabled by default on simple
|
||||
classes and that _cls will not be used.
|
||||
"""
|
||||
|
||||
class Animal(Document):
|
||||
name = StringField()
|
||||
|
||||
def create_dog_class():
|
||||
# can't inherit because Animal didn't explicitly allow inheritance
|
||||
with self.assertRaises(ValueError):
|
||||
class Dog(Animal):
|
||||
pass
|
||||
|
||||
self.assertRaises(ValueError, create_dog_class)
|
||||
|
||||
# Check that _cls etc aren't present on simple documents
|
||||
dog = Animal(name='dog')
|
||||
dog.save()
|
||||
dog = Animal(name='dog').save()
|
||||
self.assertEqual(dog.to_mongo().keys(), ['_id', 'name'])
|
||||
|
||||
collection = self.db[Animal._get_collection_name()]
|
||||
obj = collection.find_one()
|
||||
self.assertFalse('_cls' in obj)
|
||||
|
||||
def test_cant_turn_off_inheritance_on_subclass(self):
|
||||
"""Ensure if inheritance is on in a subclass you cant turn it off
|
||||
"""Ensure if inheritance is on in a subclass you cant turn it off.
|
||||
"""
|
||||
|
||||
class Animal(Document):
|
||||
name = StringField()
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
def create_mammal_class():
|
||||
with self.assertRaises(ValueError):
|
||||
class Mammal(Animal):
|
||||
meta = {'allow_inheritance': False}
|
||||
self.assertRaises(ValueError, create_mammal_class)
|
||||
|
||||
def test_allow_inheritance_abstract_document(self):
|
||||
"""Ensure that abstract documents can set inheritance rules and that
|
||||
@@ -244,28 +292,87 @@ class InheritanceTest(unittest.TestCase):
|
||||
class Animal(FinalDocument):
|
||||
name = StringField()
|
||||
|
||||
def create_mammal_class():
|
||||
with self.assertRaises(ValueError):
|
||||
class Mammal(Animal):
|
||||
pass
|
||||
self.assertRaises(ValueError, create_mammal_class)
|
||||
|
||||
# Check that _cls isn't present in simple documents
|
||||
doc = Animal(name='dog')
|
||||
self.assertFalse('_cls' in doc.to_mongo())
|
||||
|
||||
def test_allow_inheritance_embedded_document(self):
|
||||
"""Ensure embedded documents respect inheritance
|
||||
"""
|
||||
def test_abstract_handle_ids_in_metaclass_properly(self):
|
||||
|
||||
class City(Document):
|
||||
continent = StringField()
|
||||
meta = {'abstract': True,
|
||||
'allow_inheritance': False}
|
||||
|
||||
class EuropeanCity(City):
|
||||
name = StringField()
|
||||
|
||||
berlin = EuropeanCity(name='Berlin', continent='Europe')
|
||||
self.assertEqual(len(berlin._db_field_map), len(berlin._fields_ordered))
|
||||
self.assertEqual(len(berlin._reverse_db_field_map), len(berlin._fields_ordered))
|
||||
self.assertEqual(len(berlin._fields_ordered), 3)
|
||||
self.assertEqual(berlin._fields_ordered[0], 'id')
|
||||
|
||||
def test_auto_id_not_set_if_specific_in_parent_class(self):
|
||||
|
||||
class City(Document):
|
||||
continent = StringField()
|
||||
city_id = IntField(primary_key=True)
|
||||
meta = {'abstract': True,
|
||||
'allow_inheritance': False}
|
||||
|
||||
class EuropeanCity(City):
|
||||
name = StringField()
|
||||
|
||||
berlin = EuropeanCity(name='Berlin', continent='Europe')
|
||||
self.assertEqual(len(berlin._db_field_map), len(berlin._fields_ordered))
|
||||
self.assertEqual(len(berlin._reverse_db_field_map), len(berlin._fields_ordered))
|
||||
self.assertEqual(len(berlin._fields_ordered), 3)
|
||||
self.assertEqual(berlin._fields_ordered[0], 'city_id')
|
||||
|
||||
def test_auto_id_vs_non_pk_id_field(self):
|
||||
|
||||
class City(Document):
|
||||
continent = StringField()
|
||||
id = IntField()
|
||||
meta = {'abstract': True,
|
||||
'allow_inheritance': False}
|
||||
|
||||
class EuropeanCity(City):
|
||||
name = StringField()
|
||||
|
||||
berlin = EuropeanCity(name='Berlin', continent='Europe')
|
||||
self.assertEqual(len(berlin._db_field_map), len(berlin._fields_ordered))
|
||||
self.assertEqual(len(berlin._reverse_db_field_map), len(berlin._fields_ordered))
|
||||
self.assertEqual(len(berlin._fields_ordered), 4)
|
||||
self.assertEqual(berlin._fields_ordered[0], 'auto_id_0')
|
||||
berlin.save()
|
||||
self.assertEqual(berlin.pk, berlin.auto_id_0)
|
||||
|
||||
def test_abstract_document_creation_does_not_fail(self):
|
||||
class City(Document):
|
||||
continent = StringField()
|
||||
meta = {'abstract': True,
|
||||
'allow_inheritance': False}
|
||||
|
||||
bkk = City(continent='asia')
|
||||
self.assertEqual(None, bkk.pk)
|
||||
# TODO: expected error? Shouldn't we create a new error type?
|
||||
with self.assertRaises(KeyError):
|
||||
setattr(bkk, 'pk', 1)
|
||||
|
||||
def test_allow_inheritance_embedded_document(self):
|
||||
"""Ensure embedded documents respect inheritance."""
|
||||
class Comment(EmbeddedDocument):
|
||||
content = StringField()
|
||||
|
||||
def create_special_comment():
|
||||
with self.assertRaises(ValueError):
|
||||
class SpecialComment(Comment):
|
||||
pass
|
||||
|
||||
self.assertRaises(ValueError, create_special_comment)
|
||||
|
||||
doc = Comment(content='test')
|
||||
self.assertFalse('_cls' in doc.to_mongo())
|
||||
|
||||
@@ -294,7 +401,7 @@ class InheritanceTest(unittest.TestCase):
|
||||
try:
|
||||
class MyDocument(DateCreatedDocument, DateUpdatedDocument):
|
||||
pass
|
||||
except:
|
||||
except Exception:
|
||||
self.assertTrue(False, "Couldn't create MyDocument class")
|
||||
|
||||
def test_abstract_documents(self):
|
||||
@@ -337,11 +444,21 @@ class InheritanceTest(unittest.TestCase):
|
||||
self.assertEqual(Guppy._get_collection_name(), 'fish')
|
||||
self.assertEqual(Human._get_collection_name(), 'human')
|
||||
|
||||
def create_bad_abstract():
|
||||
# ensure that a subclass of a non-abstract class can't be abstract
|
||||
with self.assertRaises(ValueError):
|
||||
class EvilHuman(Human):
|
||||
evil = BooleanField(default=True)
|
||||
meta = {'abstract': True}
|
||||
self.assertRaises(ValueError, create_bad_abstract)
|
||||
|
||||
def test_abstract_embedded_documents(self):
|
||||
# 789: EmbeddedDocument shouldn't inherit abstract
|
||||
class A(EmbeddedDocument):
|
||||
meta = {"abstract": True}
|
||||
|
||||
class B(A):
|
||||
pass
|
||||
|
||||
self.assertFalse(B._meta["abstract"])
|
||||
|
||||
def test_inherited_collections(self):
|
||||
"""Ensure that subclassed documents don't override parents'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,3 @@
|
||||
import sys
|
||||
sys.path[0:0] = [""]
|
||||
|
||||
import unittest
|
||||
import uuid
|
||||
|
||||
@@ -20,6 +17,28 @@ class TestJson(unittest.TestCase):
|
||||
def setUp(self):
|
||||
connect(db='mongoenginetest')
|
||||
|
||||
def test_json_names(self):
|
||||
"""
|
||||
Going to test reported issue:
|
||||
https://github.com/MongoEngine/mongoengine/issues/654
|
||||
where the reporter asks for the availability to perform
|
||||
a to_json with the original class names and not the abreviated
|
||||
mongodb document keys
|
||||
"""
|
||||
class Embedded(EmbeddedDocument):
|
||||
string = StringField(db_field='s')
|
||||
|
||||
class Doc(Document):
|
||||
string = StringField(db_field='s')
|
||||
embedded = EmbeddedDocumentField(Embedded, db_field='e')
|
||||
|
||||
doc = Doc( string="Hello", embedded=Embedded(string="Inner Hello"))
|
||||
doc_json = doc.to_json(sort_keys=True, use_db_field=False,separators=(',', ':'))
|
||||
|
||||
expected_json = """{"embedded":{"string":"Inner Hello"},"string":"Hello"}"""
|
||||
|
||||
self.assertEqual( doc_json, expected_json)
|
||||
|
||||
def test_json_simple(self):
|
||||
|
||||
class Embedded(EmbeddedDocument):
|
||||
@@ -29,8 +48,16 @@ class TestJson(unittest.TestCase):
|
||||
string = StringField()
|
||||
embedded_field = EmbeddedDocumentField(Embedded)
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.string == other.string and
|
||||
self.embedded_field == other.embedded_field)
|
||||
|
||||
doc = Doc(string="Hi", embedded_field=Embedded(string="Hi"))
|
||||
|
||||
doc_json = doc.to_json(sort_keys=True, separators=(',', ':'))
|
||||
expected_json = """{"embedded_field":{"string":"Hi"},"string":"Hi"}"""
|
||||
self.assertEqual(doc_json, expected_json)
|
||||
|
||||
self.assertEqual(doc, Doc.from_json(doc.to_json()))
|
||||
|
||||
def test_json_complex(self):
|
||||
@@ -73,6 +100,10 @@ class TestJson(unittest.TestCase):
|
||||
generic_embedded_document_field = GenericEmbeddedDocumentField(
|
||||
default=lambda: EmbeddedDoc())
|
||||
|
||||
def __eq__(self, other):
|
||||
import json
|
||||
return json.loads(self.to_json()) == json.loads(other.to_json())
|
||||
|
||||
doc = Doc()
|
||||
self.assertEqual(doc, Doc.from_json(doc.to_json()))
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
sys.path[0:0] = [""]
|
||||
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
|
||||
@@ -60,7 +57,7 @@ class ValidatorErrorTest(unittest.TestCase):
|
||||
|
||||
try:
|
||||
User().validate()
|
||||
except ValidationError, e:
|
||||
except ValidationError as e:
|
||||
self.assertTrue("User:None" in e.message)
|
||||
self.assertEqual(e.to_dict(), {
|
||||
'username': 'Field is required',
|
||||
@@ -70,7 +67,7 @@ class ValidatorErrorTest(unittest.TestCase):
|
||||
user.name = None
|
||||
try:
|
||||
user.save()
|
||||
except ValidationError, e:
|
||||
except ValidationError as e:
|
||||
self.assertTrue("User:RossC0" in e.message)
|
||||
self.assertEqual(e.to_dict(), {
|
||||
'name': 'Field is required'})
|
||||
@@ -118,7 +115,7 @@ class ValidatorErrorTest(unittest.TestCase):
|
||||
|
||||
try:
|
||||
Doc(id="bad").validate()
|
||||
except ValidationError, e:
|
||||
except ValidationError as e:
|
||||
self.assertTrue("SubDoc:None" in e.message)
|
||||
self.assertEqual(e.to_dict(), {
|
||||
"e": {'val': 'OK could not be converted to int'}})
|
||||
@@ -136,11 +133,82 @@ class ValidatorErrorTest(unittest.TestCase):
|
||||
doc.e.val = "OK"
|
||||
try:
|
||||
doc.save()
|
||||
except ValidationError, e:
|
||||
except ValidationError as e:
|
||||
self.assertTrue("Doc:test" in e.message)
|
||||
self.assertEqual(e.to_dict(), {
|
||||
"e": {'val': 'OK could not be converted to int'}})
|
||||
|
||||
def test_embedded_weakref(self):
|
||||
|
||||
class SubDoc(EmbeddedDocument):
|
||||
val = IntField(required=True)
|
||||
|
||||
class Doc(Document):
|
||||
e = EmbeddedDocumentField(SubDoc, db_field='eb')
|
||||
|
||||
Doc.drop_collection()
|
||||
|
||||
d1 = Doc()
|
||||
d2 = Doc()
|
||||
|
||||
s = SubDoc()
|
||||
|
||||
self.assertRaises(ValidationError, s.validate)
|
||||
|
||||
d1.e = s
|
||||
d2.e = s
|
||||
|
||||
del d1
|
||||
|
||||
self.assertRaises(ValidationError, d2.validate)
|
||||
|
||||
def test_parent_reference_in_child_document(self):
|
||||
"""
|
||||
Test to ensure a ReferenceField can store a reference to a parent
|
||||
class when inherited. Issue #954.
|
||||
"""
|
||||
class Parent(Document):
|
||||
meta = {'allow_inheritance': True}
|
||||
reference = ReferenceField('self')
|
||||
|
||||
class Child(Parent):
|
||||
pass
|
||||
|
||||
parent = Parent()
|
||||
parent.save()
|
||||
|
||||
child = Child(reference=parent)
|
||||
|
||||
# Saving child should not raise a ValidationError
|
||||
try:
|
||||
child.save()
|
||||
except ValidationError as e:
|
||||
self.fail("ValidationError raised: %s" % e.message)
|
||||
|
||||
def test_parent_reference_set_as_attribute_in_child_document(self):
|
||||
"""
|
||||
Test to ensure a ReferenceField can store a reference to a parent
|
||||
class when inherited and when set via attribute. Issue #954.
|
||||
"""
|
||||
class Parent(Document):
|
||||
meta = {'allow_inheritance': True}
|
||||
reference = ReferenceField('self')
|
||||
|
||||
class Child(Parent):
|
||||
pass
|
||||
|
||||
parent = Parent()
|
||||
parent.save()
|
||||
|
||||
child = Child()
|
||||
child.reference = parent
|
||||
|
||||
# Saving the child should not raise a ValidationError
|
||||
try:
|
||||
child.save()
|
||||
except ValidationError as e:
|
||||
self.fail("ValidationError raised: %s" % e.message)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
from fields import *
|
||||
from file_tests import *
|
||||
from file_tests import *
|
||||
from geo import *
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,28 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import with_statement
|
||||
import sys
|
||||
sys.path[0:0] = [""]
|
||||
|
||||
import copy
|
||||
import os
|
||||
import unittest
|
||||
import tempfile
|
||||
|
||||
import gridfs
|
||||
import six
|
||||
|
||||
from nose.plugins.skip import SkipTest
|
||||
from mongoengine import *
|
||||
from mongoengine.connection import get_db
|
||||
from mongoengine.python_support import PY3, b, StringIO
|
||||
from mongoengine.python_support import StringIO
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
HAS_PIL = True
|
||||
except ImportError:
|
||||
HAS_PIL = False
|
||||
|
||||
from tests.utils import MongoDBTestCase
|
||||
|
||||
TEST_IMAGE_PATH = os.path.join(os.path.dirname(__file__), 'mongoengine.png')
|
||||
TEST_IMAGE2_PATH = os.path.join(os.path.dirname(__file__), 'mongodb_leaf.png')
|
||||
|
||||
|
||||
class FileTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
connect(db='mongoenginetest')
|
||||
self.db = get_db()
|
||||
class FileTest(MongoDBTestCase):
|
||||
|
||||
def tearDown(self):
|
||||
self.db.drop_collection('fs.files')
|
||||
@@ -43,15 +45,16 @@ class FileTest(unittest.TestCase):
|
||||
|
||||
PutFile.drop_collection()
|
||||
|
||||
text = b('Hello, World!')
|
||||
text = six.b('Hello, World!')
|
||||
content_type = 'text/plain'
|
||||
|
||||
putfile = PutFile()
|
||||
putfile.the_file.put(text, content_type=content_type)
|
||||
putfile.the_file.put(text, content_type=content_type, filename="hello")
|
||||
putfile.save()
|
||||
|
||||
result = PutFile.objects.first()
|
||||
self.assertTrue(putfile == result)
|
||||
self.assertEqual("%s" % result.the_file, "<GridFSProxy: hello>")
|
||||
self.assertEqual(result.the_file.read(), text)
|
||||
self.assertEqual(result.the_file.content_type, content_type)
|
||||
result.the_file.delete() # Remove file from GridFS
|
||||
@@ -81,8 +84,8 @@ class FileTest(unittest.TestCase):
|
||||
|
||||
StreamFile.drop_collection()
|
||||
|
||||
text = b('Hello, World!')
|
||||
more_text = b('Foo Bar')
|
||||
text = six.b('Hello, World!')
|
||||
more_text = six.b('Foo Bar')
|
||||
content_type = 'text/plain'
|
||||
|
||||
streamfile = StreamFile()
|
||||
@@ -105,15 +108,51 @@ class FileTest(unittest.TestCase):
|
||||
result.the_file.delete()
|
||||
|
||||
# Ensure deleted file returns None
|
||||
self.assertTrue(result.the_file.read() == None)
|
||||
self.assertTrue(result.the_file.read() is None)
|
||||
|
||||
def test_file_fields_stream_after_none(self):
|
||||
"""Ensure that a file field can be written to after it has been saved as
|
||||
None
|
||||
"""
|
||||
class StreamFile(Document):
|
||||
the_file = FileField()
|
||||
|
||||
StreamFile.drop_collection()
|
||||
|
||||
text = six.b('Hello, World!')
|
||||
more_text = six.b('Foo Bar')
|
||||
content_type = 'text/plain'
|
||||
|
||||
streamfile = StreamFile()
|
||||
streamfile.save()
|
||||
streamfile.the_file.new_file()
|
||||
streamfile.the_file.write(text)
|
||||
streamfile.the_file.write(more_text)
|
||||
streamfile.the_file.close()
|
||||
streamfile.save()
|
||||
|
||||
result = StreamFile.objects.first()
|
||||
self.assertTrue(streamfile == result)
|
||||
self.assertEqual(result.the_file.read(), text + more_text)
|
||||
# self.assertEqual(result.the_file.content_type, content_type)
|
||||
result.the_file.seek(0)
|
||||
self.assertEqual(result.the_file.tell(), 0)
|
||||
self.assertEqual(result.the_file.read(len(text)), text)
|
||||
self.assertEqual(result.the_file.tell(), len(text))
|
||||
self.assertEqual(result.the_file.read(len(more_text)), more_text)
|
||||
self.assertEqual(result.the_file.tell(), len(text + more_text))
|
||||
result.the_file.delete()
|
||||
|
||||
# Ensure deleted file returns None
|
||||
self.assertTrue(result.the_file.read() is None)
|
||||
|
||||
def test_file_fields_set(self):
|
||||
|
||||
class SetFile(Document):
|
||||
the_file = FileField()
|
||||
|
||||
text = b('Hello, World!')
|
||||
more_text = b('Foo Bar')
|
||||
text = six.b('Hello, World!')
|
||||
more_text = six.b('Foo Bar')
|
||||
|
||||
SetFile.drop_collection()
|
||||
|
||||
@@ -142,7 +181,7 @@ class FileTest(unittest.TestCase):
|
||||
GridDocument.drop_collection()
|
||||
|
||||
with tempfile.TemporaryFile() as f:
|
||||
f.write(b("Hello World!"))
|
||||
f.write(six.b("Hello World!"))
|
||||
f.flush()
|
||||
|
||||
# Test without default
|
||||
@@ -159,7 +198,7 @@ class FileTest(unittest.TestCase):
|
||||
self.assertEqual(doc_b.the_file.grid_id, doc_c.the_file.grid_id)
|
||||
|
||||
# Test with default
|
||||
doc_d = GridDocument(the_file=b(''))
|
||||
doc_d = GridDocument(the_file=six.b(''))
|
||||
doc_d.save()
|
||||
|
||||
doc_e = GridDocument.objects.with_id(doc_d.id)
|
||||
@@ -185,7 +224,7 @@ class FileTest(unittest.TestCase):
|
||||
# First instance
|
||||
test_file = TestFile()
|
||||
test_file.name = "Hello, World!"
|
||||
test_file.the_file.put(b('Hello, World!'))
|
||||
test_file.the_file.put(six.b('Hello, World!'))
|
||||
test_file.save()
|
||||
|
||||
# Second instance
|
||||
@@ -217,6 +256,19 @@ class FileTest(unittest.TestCase):
|
||||
self.assertEqual(marmot.photo.content_type, 'image/jpeg')
|
||||
self.assertEqual(marmot.photo.foo, 'bar')
|
||||
|
||||
def test_file_reassigning(self):
|
||||
class TestFile(Document):
|
||||
the_file = FileField()
|
||||
TestFile.drop_collection()
|
||||
|
||||
test_file = TestFile(the_file=open(TEST_IMAGE_PATH, 'rb')).save()
|
||||
self.assertEqual(test_file.the_file.get().length, 8313)
|
||||
|
||||
test_file = TestFile.objects.first()
|
||||
test_file.the_file = open(TEST_IMAGE2_PATH, 'rb')
|
||||
test_file.save()
|
||||
self.assertEqual(test_file.the_file.get().length, 4971)
|
||||
|
||||
def test_file_boolean(self):
|
||||
"""Ensure that a boolean test of a FileField indicates its presence
|
||||
"""
|
||||
@@ -226,7 +278,7 @@ class FileTest(unittest.TestCase):
|
||||
|
||||
test_file = TestFile()
|
||||
self.assertFalse(bool(test_file.the_file))
|
||||
test_file.the_file.put(b('Hello, World!'), content_type='text/plain')
|
||||
test_file.the_file.put(six.b('Hello, World!'), content_type='text/plain')
|
||||
test_file.save()
|
||||
self.assertTrue(bool(test_file.the_file))
|
||||
|
||||
@@ -241,15 +293,91 @@ class FileTest(unittest.TestCase):
|
||||
test_file = TestFile()
|
||||
self.assertFalse(test_file.the_file in [{"test": 1}])
|
||||
|
||||
def test_file_disk_space(self):
|
||||
""" Test disk space usage when we delete/replace a file """
|
||||
class TestFile(Document):
|
||||
the_file = FileField()
|
||||
|
||||
text = six.b('Hello, World!')
|
||||
content_type = 'text/plain'
|
||||
|
||||
testfile = TestFile()
|
||||
testfile.the_file.put(text, content_type=content_type, filename="hello")
|
||||
testfile.save()
|
||||
|
||||
# Now check fs.files and fs.chunks
|
||||
db = TestFile._get_db()
|
||||
|
||||
files = db.fs.files.find()
|
||||
chunks = db.fs.chunks.find()
|
||||
self.assertEquals(len(list(files)), 1)
|
||||
self.assertEquals(len(list(chunks)), 1)
|
||||
|
||||
# Deleting the docoument should delete the files
|
||||
testfile.delete()
|
||||
|
||||
files = db.fs.files.find()
|
||||
chunks = db.fs.chunks.find()
|
||||
self.assertEquals(len(list(files)), 0)
|
||||
self.assertEquals(len(list(chunks)), 0)
|
||||
|
||||
# Test case where we don't store a file in the first place
|
||||
testfile = TestFile()
|
||||
testfile.save()
|
||||
|
||||
files = db.fs.files.find()
|
||||
chunks = db.fs.chunks.find()
|
||||
self.assertEquals(len(list(files)), 0)
|
||||
self.assertEquals(len(list(chunks)), 0)
|
||||
|
||||
testfile.delete()
|
||||
|
||||
files = db.fs.files.find()
|
||||
chunks = db.fs.chunks.find()
|
||||
self.assertEquals(len(list(files)), 0)
|
||||
self.assertEquals(len(list(chunks)), 0)
|
||||
|
||||
# Test case where we overwrite the file
|
||||
testfile = TestFile()
|
||||
testfile.the_file.put(text, content_type=content_type, filename="hello")
|
||||
testfile.save()
|
||||
|
||||
text = six.b('Bonjour, World!')
|
||||
testfile.the_file.replace(text, content_type=content_type, filename="hello")
|
||||
testfile.save()
|
||||
|
||||
files = db.fs.files.find()
|
||||
chunks = db.fs.chunks.find()
|
||||
self.assertEquals(len(list(files)), 1)
|
||||
self.assertEquals(len(list(chunks)), 1)
|
||||
|
||||
testfile.delete()
|
||||
|
||||
files = db.fs.files.find()
|
||||
chunks = db.fs.chunks.find()
|
||||
self.assertEquals(len(list(files)), 0)
|
||||
self.assertEquals(len(list(chunks)), 0)
|
||||
|
||||
def test_image_field(self):
|
||||
if PY3:
|
||||
raise SkipTest('PIL does not have Python 3 support')
|
||||
if not HAS_PIL:
|
||||
raise SkipTest('PIL not installed')
|
||||
|
||||
class TestImage(Document):
|
||||
image = ImageField()
|
||||
|
||||
TestImage.drop_collection()
|
||||
|
||||
with tempfile.TemporaryFile() as f:
|
||||
f.write(six.b("Hello World!"))
|
||||
f.flush()
|
||||
|
||||
t = TestImage()
|
||||
try:
|
||||
t.image.put(f)
|
||||
self.fail("Should have raised an invalidation error")
|
||||
except ValidationError as e:
|
||||
self.assertEqual("%s" % e, "Invalid image: cannot identify image file %s" % f)
|
||||
|
||||
t = TestImage()
|
||||
t.image.put(open(TEST_IMAGE_PATH, 'rb'))
|
||||
t.save()
|
||||
@@ -264,9 +392,25 @@ class FileTest(unittest.TestCase):
|
||||
|
||||
t.image.delete()
|
||||
|
||||
def test_image_field_reassigning(self):
|
||||
if not HAS_PIL:
|
||||
raise SkipTest('PIL not installed')
|
||||
|
||||
class TestFile(Document):
|
||||
the_file = ImageField()
|
||||
TestFile.drop_collection()
|
||||
|
||||
test_file = TestFile(the_file=open(TEST_IMAGE_PATH, 'rb')).save()
|
||||
self.assertEqual(test_file.the_file.size, (371, 76))
|
||||
|
||||
test_file = TestFile.objects.first()
|
||||
test_file.the_file = open(TEST_IMAGE2_PATH, 'rb')
|
||||
test_file.save()
|
||||
self.assertEqual(test_file.the_file.size, (45, 101))
|
||||
|
||||
def test_image_field_resize(self):
|
||||
if PY3:
|
||||
raise SkipTest('PIL does not have Python 3 support')
|
||||
if not HAS_PIL:
|
||||
raise SkipTest('PIL not installed')
|
||||
|
||||
class TestImage(Document):
|
||||
image = ImageField(size=(185, 37))
|
||||
@@ -288,8 +432,8 @@ class FileTest(unittest.TestCase):
|
||||
t.image.delete()
|
||||
|
||||
def test_image_field_resize_force(self):
|
||||
if PY3:
|
||||
raise SkipTest('PIL does not have Python 3 support')
|
||||
if not HAS_PIL:
|
||||
raise SkipTest('PIL not installed')
|
||||
|
||||
class TestImage(Document):
|
||||
image = ImageField(size=(185, 37, True))
|
||||
@@ -311,8 +455,8 @@ class FileTest(unittest.TestCase):
|
||||
t.image.delete()
|
||||
|
||||
def test_image_field_thumbnail(self):
|
||||
if PY3:
|
||||
raise SkipTest('PIL does not have Python 3 support')
|
||||
if not HAS_PIL:
|
||||
raise SkipTest('PIL not installed')
|
||||
|
||||
class TestImage(Document):
|
||||
image = ImageField(thumbnail_size=(92, 18))
|
||||
@@ -348,16 +492,23 @@ class FileTest(unittest.TestCase):
|
||||
# First instance
|
||||
test_file = TestFile()
|
||||
test_file.name = "Hello, World!"
|
||||
test_file.the_file.put(b('Hello, World!'),
|
||||
test_file.the_file.put(six.b('Hello, World!'),
|
||||
name="hello.txt")
|
||||
test_file.save()
|
||||
|
||||
data = get_db("test_files").macumba.files.find_one()
|
||||
self.assertEqual(data.get('name'), 'hello.txt')
|
||||
|
||||
test_file = TestFile.objects.first()
|
||||
self.assertEqual(test_file.the_file.read(), six.b('Hello, World!'))
|
||||
|
||||
test_file = TestFile.objects.first()
|
||||
test_file.the_file = six.b('HELLO, WORLD!')
|
||||
test_file.save()
|
||||
|
||||
test_file = TestFile.objects.first()
|
||||
self.assertEqual(test_file.the_file.read(),
|
||||
b('Hello, World!'))
|
||||
six.b('HELLO, WORLD!'))
|
||||
|
||||
def test_copyable(self):
|
||||
class PutFile(Document):
|
||||
@@ -365,7 +516,7 @@ class FileTest(unittest.TestCase):
|
||||
|
||||
PutFile.drop_collection()
|
||||
|
||||
text = b('Hello, World!')
|
||||
text = six.b('Hello, World!')
|
||||
content_type = 'text/plain'
|
||||
|
||||
putfile = PutFile()
|
||||
@@ -378,6 +529,54 @@ class FileTest(unittest.TestCase):
|
||||
self.assertEqual(putfile, copy.copy(putfile))
|
||||
self.assertEqual(putfile, copy.deepcopy(putfile))
|
||||
|
||||
def test_get_image_by_grid_id(self):
|
||||
|
||||
if not HAS_PIL:
|
||||
raise SkipTest('PIL not installed')
|
||||
|
||||
class TestImage(Document):
|
||||
|
||||
image1 = ImageField()
|
||||
image2 = ImageField()
|
||||
|
||||
TestImage.drop_collection()
|
||||
|
||||
t = TestImage()
|
||||
t.image1.put(open(TEST_IMAGE_PATH, 'rb'))
|
||||
t.image2.put(open(TEST_IMAGE2_PATH, 'rb'))
|
||||
t.save()
|
||||
|
||||
test = TestImage.objects.first()
|
||||
grid_id = test.image1.grid_id
|
||||
|
||||
self.assertEqual(1, TestImage.objects(Q(image1=grid_id)
|
||||
or Q(image2=grid_id)).count())
|
||||
|
||||
def test_complex_field_filefield(self):
|
||||
"""Ensure you can add meta data to file"""
|
||||
|
||||
class Animal(Document):
|
||||
genus = StringField()
|
||||
family = StringField()
|
||||
photos = ListField(FileField())
|
||||
|
||||
Animal.drop_collection()
|
||||
marmot = Animal(genus='Marmota', family='Sciuridae')
|
||||
|
||||
marmot_photo = open(TEST_IMAGE_PATH, 'rb') # Retrieve a photo from disk
|
||||
|
||||
photos_field = marmot._fields['photos'].field
|
||||
new_proxy = photos_field.get_proxy_obj('photos', marmot)
|
||||
new_proxy.put(marmot_photo, content_type='image/jpeg', foo='bar')
|
||||
marmot_photo.close()
|
||||
|
||||
marmot.photos.append(new_proxy)
|
||||
marmot.save()
|
||||
|
||||
marmot = Animal.objects.get()
|
||||
self.assertEqual(marmot.photos[0].content_type, 'image/jpeg')
|
||||
self.assertEqual(marmot.photos[0].foo, 'bar')
|
||||
self.assertEqual(marmot.photos[0].get().length, 8313)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
387
tests/fields/geo.py
Normal file
387
tests/fields/geo.py
Normal file
@@ -0,0 +1,387 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine.connection import get_db
|
||||
|
||||
__all__ = ("GeoFieldTest", )
|
||||
|
||||
|
||||
class GeoFieldTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
connect(db='mongoenginetest')
|
||||
self.db = get_db()
|
||||
|
||||
def _test_for_expected_error(self, Cls, loc, expected):
|
||||
try:
|
||||
Cls(loc=loc).validate()
|
||||
self.fail('Should not validate the location {0}'.format(loc))
|
||||
except ValidationError as e:
|
||||
self.assertEqual(expected, e.to_dict()['loc'])
|
||||
|
||||
def test_geopoint_validation(self):
|
||||
class Location(Document):
|
||||
loc = GeoPointField()
|
||||
|
||||
invalid_coords = [{"x": 1, "y": 2}, 5, "a"]
|
||||
expected = 'GeoPointField can only accept tuples or lists of (x, y)'
|
||||
|
||||
for coord in invalid_coords:
|
||||
self._test_for_expected_error(Location, coord, expected)
|
||||
|
||||
invalid_coords = [[], [1], [1, 2, 3]]
|
||||
for coord in invalid_coords:
|
||||
expected = "Value (%s) must be a two-dimensional point" % repr(coord)
|
||||
self._test_for_expected_error(Location, coord, expected)
|
||||
|
||||
invalid_coords = [[{}, {}], ("a", "b")]
|
||||
for coord in invalid_coords:
|
||||
expected = "Both values (%s) in point must be float or int" % repr(coord)
|
||||
self._test_for_expected_error(Location, coord, expected)
|
||||
|
||||
def test_point_validation(self):
|
||||
class Location(Document):
|
||||
loc = PointField()
|
||||
|
||||
invalid_coords = {"x": 1, "y": 2}
|
||||
expected = 'PointField can only accept a valid GeoJson dictionary or lists of (x, y)'
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = {"type": "MadeUp", "coordinates": []}
|
||||
expected = 'PointField type must be "Point"'
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = {"type": "Point", "coordinates": [1, 2, 3]}
|
||||
expected = "Value ([1, 2, 3]) must be a two-dimensional point"
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = [5, "a"]
|
||||
expected = "PointField can only accept lists of [x, y]"
|
||||
for coord in invalid_coords:
|
||||
self._test_for_expected_error(Location, coord, expected)
|
||||
|
||||
invalid_coords = [[], [1], [1, 2, 3]]
|
||||
for coord in invalid_coords:
|
||||
expected = "Value (%s) must be a two-dimensional point" % repr(coord)
|
||||
self._test_for_expected_error(Location, coord, expected)
|
||||
|
||||
invalid_coords = [[{}, {}], ("a", "b")]
|
||||
for coord in invalid_coords:
|
||||
expected = "Both values (%s) in point must be float or int" % repr(coord)
|
||||
self._test_for_expected_error(Location, coord, expected)
|
||||
|
||||
Location(loc=[1, 2]).validate()
|
||||
Location(loc={
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
81.4471435546875,
|
||||
23.61432859499169
|
||||
]}).validate()
|
||||
|
||||
def test_linestring_validation(self):
|
||||
class Location(Document):
|
||||
loc = LineStringField()
|
||||
|
||||
invalid_coords = {"x": 1, "y": 2}
|
||||
expected = 'LineStringField can only accept a valid GeoJson dictionary or lists of (x, y)'
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = {"type": "MadeUp", "coordinates": [[]]}
|
||||
expected = 'LineStringField type must be "LineString"'
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = {"type": "LineString", "coordinates": [[1, 2, 3]]}
|
||||
expected = "Invalid LineString:\nValue ([1, 2, 3]) must be a two-dimensional point"
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = [5, "a"]
|
||||
expected = "Invalid LineString must contain at least one valid point"
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = [[1]]
|
||||
expected = "Invalid LineString:\nValue (%s) must be a two-dimensional point" % repr(invalid_coords[0])
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = [[1, 2, 3]]
|
||||
expected = "Invalid LineString:\nValue (%s) must be a two-dimensional point" % repr(invalid_coords[0])
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = [[[{}, {}]], [("a", "b")]]
|
||||
for coord in invalid_coords:
|
||||
expected = "Invalid LineString:\nBoth values (%s) in point must be float or int" % repr(coord[0])
|
||||
self._test_for_expected_error(Location, coord, expected)
|
||||
|
||||
Location(loc=[[1, 2], [3, 4], [5, 6], [1, 2]]).validate()
|
||||
|
||||
def test_polygon_validation(self):
|
||||
class Location(Document):
|
||||
loc = PolygonField()
|
||||
|
||||
invalid_coords = {"x": 1, "y": 2}
|
||||
expected = 'PolygonField can only accept a valid GeoJson dictionary or lists of (x, y)'
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = {"type": "MadeUp", "coordinates": [[]]}
|
||||
expected = 'PolygonField type must be "Polygon"'
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = {"type": "Polygon", "coordinates": [[[1, 2, 3]]]}
|
||||
expected = "Invalid Polygon:\nValue ([1, 2, 3]) must be a two-dimensional point"
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = [[[5, "a"]]]
|
||||
expected = "Invalid Polygon:\nBoth values ([5, 'a']) in point must be float or int"
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = [[[]]]
|
||||
expected = "Invalid Polygon must contain at least one valid linestring"
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = [[[1, 2, 3]]]
|
||||
expected = "Invalid Polygon:\nValue ([1, 2, 3]) must be a two-dimensional point"
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = [[[{}, {}]], [("a", "b")]]
|
||||
expected = "Invalid Polygon:\nBoth values ([{}, {}]) in point must be float or int, Both values (('a', 'b')) in point must be float or int"
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = [[[1, 2], [3, 4]]]
|
||||
expected = "Invalid Polygon:\nLineStrings must start and end at the same point"
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
Location(loc=[[[1, 2], [3, 4], [5, 6], [1, 2]]]).validate()
|
||||
|
||||
def test_multipoint_validation(self):
|
||||
class Location(Document):
|
||||
loc = MultiPointField()
|
||||
|
||||
invalid_coords = {"x": 1, "y": 2}
|
||||
expected = 'MultiPointField can only accept a valid GeoJson dictionary or lists of (x, y)'
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = {"type": "MadeUp", "coordinates": [[]]}
|
||||
expected = 'MultiPointField type must be "MultiPoint"'
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = {"type": "MultiPoint", "coordinates": [[1, 2, 3]]}
|
||||
expected = "Value ([1, 2, 3]) must be a two-dimensional point"
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = [[]]
|
||||
expected = "Invalid MultiPoint must contain at least one valid point"
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = [[[1]], [[1, 2, 3]]]
|
||||
for coord in invalid_coords:
|
||||
expected = "Value (%s) must be a two-dimensional point" % repr(coord[0])
|
||||
self._test_for_expected_error(Location, coord, expected)
|
||||
|
||||
invalid_coords = [[[{}, {}]], [("a", "b")]]
|
||||
for coord in invalid_coords:
|
||||
expected = "Both values (%s) in point must be float or int" % repr(coord[0])
|
||||
self._test_for_expected_error(Location, coord, expected)
|
||||
|
||||
Location(loc=[[1, 2]]).validate()
|
||||
Location(loc={
|
||||
"type": "MultiPoint",
|
||||
"coordinates": [
|
||||
[1, 2],
|
||||
[81.4471435546875, 23.61432859499169]
|
||||
]}).validate()
|
||||
|
||||
def test_multilinestring_validation(self):
|
||||
class Location(Document):
|
||||
loc = MultiLineStringField()
|
||||
|
||||
invalid_coords = {"x": 1, "y": 2}
|
||||
expected = 'MultiLineStringField can only accept a valid GeoJson dictionary or lists of (x, y)'
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = {"type": "MadeUp", "coordinates": [[]]}
|
||||
expected = 'MultiLineStringField type must be "MultiLineString"'
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = {"type": "MultiLineString", "coordinates": [[[1, 2, 3]]]}
|
||||
expected = "Invalid MultiLineString:\nValue ([1, 2, 3]) must be a two-dimensional point"
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = [5, "a"]
|
||||
expected = "Invalid MultiLineString must contain at least one valid linestring"
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = [[[1]]]
|
||||
expected = "Invalid MultiLineString:\nValue (%s) must be a two-dimensional point" % repr(invalid_coords[0][0])
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = [[[1, 2, 3]]]
|
||||
expected = "Invalid MultiLineString:\nValue (%s) must be a two-dimensional point" % repr(invalid_coords[0][0])
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = [[[[{}, {}]]], [[("a", "b")]]]
|
||||
for coord in invalid_coords:
|
||||
expected = "Invalid MultiLineString:\nBoth values (%s) in point must be float or int" % repr(coord[0][0])
|
||||
self._test_for_expected_error(Location, coord, expected)
|
||||
|
||||
Location(loc=[[[1, 2], [3, 4], [5, 6], [1, 2]]]).validate()
|
||||
|
||||
def test_multipolygon_validation(self):
|
||||
class Location(Document):
|
||||
loc = MultiPolygonField()
|
||||
|
||||
invalid_coords = {"x": 1, "y": 2}
|
||||
expected = 'MultiPolygonField can only accept a valid GeoJson dictionary or lists of (x, y)'
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = {"type": "MadeUp", "coordinates": [[]]}
|
||||
expected = 'MultiPolygonField type must be "MultiPolygon"'
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = {"type": "MultiPolygon", "coordinates": [[[[1, 2, 3]]]]}
|
||||
expected = "Invalid MultiPolygon:\nValue ([1, 2, 3]) must be a two-dimensional point"
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = [[[[5, "a"]]]]
|
||||
expected = "Invalid MultiPolygon:\nBoth values ([5, 'a']) in point must be float or int"
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = [[[[]]]]
|
||||
expected = "Invalid MultiPolygon must contain at least one valid Polygon"
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = [[[[1, 2, 3]]]]
|
||||
expected = "Invalid MultiPolygon:\nValue ([1, 2, 3]) must be a two-dimensional point"
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = [[[[{}, {}]]], [[("a", "b")]]]
|
||||
expected = "Invalid MultiPolygon:\nBoth values ([{}, {}]) in point must be float or int, Both values (('a', 'b')) in point must be float or int"
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
invalid_coords = [[[[1, 2], [3, 4]]]]
|
||||
expected = "Invalid MultiPolygon:\nLineStrings must start and end at the same point"
|
||||
self._test_for_expected_error(Location, invalid_coords, expected)
|
||||
|
||||
Location(loc=[[[[1, 2], [3, 4], [5, 6], [1, 2]]]]).validate()
|
||||
|
||||
def test_indexes_geopoint(self):
|
||||
"""Ensure that indexes are created automatically for GeoPointFields.
|
||||
"""
|
||||
class Event(Document):
|
||||
title = StringField()
|
||||
location = GeoPointField()
|
||||
|
||||
geo_indicies = Event._geo_indices()
|
||||
self.assertEqual(geo_indicies, [{'fields': [('location', '2d')]}])
|
||||
|
||||
def test_geopoint_embedded_indexes(self):
|
||||
"""Ensure that indexes are created automatically for GeoPointFields on
|
||||
embedded documents.
|
||||
"""
|
||||
class Venue(EmbeddedDocument):
|
||||
location = GeoPointField()
|
||||
name = StringField()
|
||||
|
||||
class Event(Document):
|
||||
title = StringField()
|
||||
venue = EmbeddedDocumentField(Venue)
|
||||
|
||||
geo_indicies = Event._geo_indices()
|
||||
self.assertEqual(geo_indicies, [{'fields': [('venue.location', '2d')]}])
|
||||
|
||||
def test_indexes_2dsphere(self):
|
||||
"""Ensure that indexes are created automatically for GeoPointFields.
|
||||
"""
|
||||
class Event(Document):
|
||||
title = StringField()
|
||||
point = PointField()
|
||||
line = LineStringField()
|
||||
polygon = PolygonField()
|
||||
|
||||
geo_indicies = Event._geo_indices()
|
||||
self.assertTrue({'fields': [('line', '2dsphere')]} in geo_indicies)
|
||||
self.assertTrue({'fields': [('polygon', '2dsphere')]} in geo_indicies)
|
||||
self.assertTrue({'fields': [('point', '2dsphere')]} in geo_indicies)
|
||||
|
||||
def test_indexes_2dsphere_embedded(self):
|
||||
"""Ensure that indexes are created automatically for GeoPointFields.
|
||||
"""
|
||||
class Venue(EmbeddedDocument):
|
||||
name = StringField()
|
||||
point = PointField()
|
||||
line = LineStringField()
|
||||
polygon = PolygonField()
|
||||
|
||||
class Event(Document):
|
||||
title = StringField()
|
||||
venue = EmbeddedDocumentField(Venue)
|
||||
|
||||
geo_indicies = Event._geo_indices()
|
||||
self.assertTrue({'fields': [('venue.line', '2dsphere')]} in geo_indicies)
|
||||
self.assertTrue({'fields': [('venue.polygon', '2dsphere')]} in geo_indicies)
|
||||
self.assertTrue({'fields': [('venue.point', '2dsphere')]} in geo_indicies)
|
||||
|
||||
def test_geo_indexes_recursion(self):
|
||||
|
||||
class Location(Document):
|
||||
name = StringField()
|
||||
location = GeoPointField()
|
||||
|
||||
class Parent(Document):
|
||||
name = StringField()
|
||||
location = ReferenceField(Location)
|
||||
|
||||
Location.drop_collection()
|
||||
Parent.drop_collection()
|
||||
|
||||
Parent(name='Berlin').save()
|
||||
info = Parent._get_collection().index_information()
|
||||
self.assertFalse('location_2d' in info)
|
||||
info = Location._get_collection().index_information()
|
||||
self.assertTrue('location_2d' in info)
|
||||
|
||||
self.assertEqual(len(Parent._geo_indices()), 0)
|
||||
self.assertEqual(len(Location._geo_indices()), 1)
|
||||
|
||||
def test_geo_indexes_auto_index(self):
|
||||
|
||||
# Test just listing the fields
|
||||
class Log(Document):
|
||||
location = PointField(auto_index=False)
|
||||
datetime = DateTimeField()
|
||||
|
||||
meta = {
|
||||
'indexes': [[("location", "2dsphere"), ("datetime", 1)]]
|
||||
}
|
||||
|
||||
self.assertEqual([], Log._geo_indices())
|
||||
|
||||
Log.drop_collection()
|
||||
Log.ensure_indexes()
|
||||
|
||||
info = Log._get_collection().index_information()
|
||||
self.assertEqual(info["location_2dsphere_datetime_1"]["key"],
|
||||
[('location', '2dsphere'), ('datetime', 1)])
|
||||
|
||||
# Test listing explicitly
|
||||
class Log(Document):
|
||||
location = PointField(auto_index=False)
|
||||
datetime = DateTimeField()
|
||||
|
||||
meta = {
|
||||
'indexes': [
|
||||
{'fields': [("location", "2dsphere"), ("datetime", 1)]}
|
||||
]
|
||||
}
|
||||
|
||||
self.assertEqual([], Log._geo_indices())
|
||||
|
||||
Log.drop_collection()
|
||||
Log.ensure_indexes()
|
||||
|
||||
info = Log._get_collection().index_information()
|
||||
self.assertEqual(info["location_2dsphere_datetime_1"]["key"],
|
||||
[('location', '2dsphere'), ('datetime', 1)])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
BIN
tests/fields/mongodb_leaf.png
Normal file
BIN
tests/fields/mongodb_leaf.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
@@ -1,6 +1,8 @@
|
||||
import pickle
|
||||
from datetime import datetime
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine import signals
|
||||
|
||||
|
||||
class PickleEmbedded(EmbeddedDocument):
|
||||
@@ -15,6 +17,41 @@ class PickleTest(Document):
|
||||
photo = FileField()
|
||||
|
||||
|
||||
class NewDocumentPickleTest(Document):
|
||||
number = IntField()
|
||||
string = StringField(choices=(('One', '1'), ('Two', '2')))
|
||||
embedded = EmbeddedDocumentField(PickleEmbedded)
|
||||
lists = ListField(StringField())
|
||||
photo = FileField()
|
||||
new_field = StringField()
|
||||
|
||||
|
||||
class PickleDynamicEmbedded(DynamicEmbeddedDocument):
|
||||
date = DateTimeField(default=datetime.now)
|
||||
|
||||
|
||||
class PickleDynamicTest(DynamicDocument):
|
||||
number = IntField()
|
||||
|
||||
|
||||
class PickleSignalsTest(Document):
|
||||
number = IntField()
|
||||
string = StringField(choices=(('One', '1'), ('Two', '2')))
|
||||
embedded = EmbeddedDocumentField(PickleEmbedded)
|
||||
lists = ListField(StringField())
|
||||
|
||||
@classmethod
|
||||
def post_save(self, sender, document, created, **kwargs):
|
||||
pickled = pickle.dumps(document)
|
||||
|
||||
@classmethod
|
||||
def post_delete(self, sender, document, **kwargs):
|
||||
pickled = pickle.dumps(document)
|
||||
|
||||
signals.post_save.connect(PickleSignalsTest.post_save, sender=PickleSignalsTest)
|
||||
signals.post_delete.connect(PickleSignalsTest.post_delete, sender=PickleSignalsTest)
|
||||
|
||||
|
||||
class Mixin(object):
|
||||
name = StringField()
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
from turn_off_inheritance import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,51 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
|
||||
from mongoengine import Document, connect
|
||||
from mongoengine.connection import get_db
|
||||
from mongoengine.fields import StringField
|
||||
|
||||
__all__ = ('ConvertToNewInheritanceModel', )
|
||||
|
||||
|
||||
class ConvertToNewInheritanceModel(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
connect(db='mongoenginetest')
|
||||
self.db = get_db()
|
||||
|
||||
def tearDown(self):
|
||||
for collection in self.db.collection_names():
|
||||
if 'system.' in collection:
|
||||
continue
|
||||
self.db.drop_collection(collection)
|
||||
|
||||
def test_how_to_convert_to_the_new_inheritance_model(self):
|
||||
"""Demonstrates migrating from 0.7 to 0.8
|
||||
"""
|
||||
|
||||
# 1. Declaration of the class
|
||||
class Animal(Document):
|
||||
name = StringField()
|
||||
meta = {
|
||||
'allow_inheritance': True,
|
||||
'indexes': ['name']
|
||||
}
|
||||
|
||||
# 2. Remove _types
|
||||
collection = Animal._get_collection()
|
||||
collection.update({}, {"$unset": {"_types": 1}}, multi=True)
|
||||
|
||||
# 3. Confirm extra data is removed
|
||||
count = collection.find({'_types': {"$exists": True}}).count()
|
||||
assert count == 0
|
||||
|
||||
# 4. Remove indexes
|
||||
info = collection.index_information()
|
||||
indexes_to_drop = [key for key, value in info.iteritems()
|
||||
if '_types' in dict(value['key'])]
|
||||
for index in indexes_to_drop:
|
||||
collection.drop_index(index)
|
||||
|
||||
# 5. Recreate indexes
|
||||
Animal.ensure_indexes()
|
||||
@@ -1,62 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
|
||||
from mongoengine import Document, connect
|
||||
from mongoengine.connection import get_db
|
||||
from mongoengine.fields import StringField
|
||||
|
||||
__all__ = ('TurnOffInheritanceTest', )
|
||||
|
||||
|
||||
class TurnOffInheritanceTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
connect(db='mongoenginetest')
|
||||
self.db = get_db()
|
||||
|
||||
def tearDown(self):
|
||||
for collection in self.db.collection_names():
|
||||
if 'system.' in collection:
|
||||
continue
|
||||
self.db.drop_collection(collection)
|
||||
|
||||
def test_how_to_turn_off_inheritance(self):
|
||||
"""Demonstrates migrating from allow_inheritance = True to False.
|
||||
"""
|
||||
|
||||
# 1. Old declaration of the class
|
||||
|
||||
class Animal(Document):
|
||||
name = StringField()
|
||||
meta = {
|
||||
'allow_inheritance': True,
|
||||
'indexes': ['name']
|
||||
}
|
||||
|
||||
# 2. Turn off inheritance
|
||||
class Animal(Document):
|
||||
name = StringField()
|
||||
meta = {
|
||||
'allow_inheritance': False,
|
||||
'indexes': ['name']
|
||||
}
|
||||
|
||||
# 3. Remove _types and _cls
|
||||
collection = Animal._get_collection()
|
||||
collection.update({}, {"$unset": {"_types": 1, "_cls": 1}}, multi=True)
|
||||
|
||||
# 3. Confirm extra data is removed
|
||||
count = collection.find({"$or": [{'_types': {"$exists": True}},
|
||||
{'_cls': {"$exists": True}}]}).count()
|
||||
assert count == 0
|
||||
|
||||
# 4. Remove indexes
|
||||
info = collection.index_information()
|
||||
indexes_to_drop = [key for key, value in info.iteritems()
|
||||
if '_types' in dict(value['key'])
|
||||
or '_cls' in dict(value['key'])]
|
||||
for index in indexes_to_drop:
|
||||
collection.drop_index(index)
|
||||
|
||||
# 5. Recreate indexes
|
||||
Animal.ensure_indexes()
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
from transform import *
|
||||
from field_list import *
|
||||
from queryset import *
|
||||
from visitor import *
|
||||
from visitor import *
|
||||
from geo import *
|
||||
from modify import *
|
||||
@@ -1,6 +1,3 @@
|
||||
import sys
|
||||
sys.path[0:0] = [""]
|
||||
|
||||
import unittest
|
||||
|
||||
from mongoengine import *
|
||||
@@ -20,47 +17,47 @@ class QueryFieldListTest(unittest.TestCase):
|
||||
|
||||
def test_include_include(self):
|
||||
q = QueryFieldList()
|
||||
q += QueryFieldList(fields=['a', 'b'], value=QueryFieldList.ONLY)
|
||||
self.assertEqual(q.as_dict(), {'a': True, 'b': True})
|
||||
q += QueryFieldList(fields=['a', 'b'], value=QueryFieldList.ONLY, _only_called=True)
|
||||
self.assertEqual(q.as_dict(), {'a': 1, 'b': 1})
|
||||
q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.ONLY)
|
||||
self.assertEqual(q.as_dict(), {'b': True})
|
||||
self.assertEqual(q.as_dict(), {'a': 1, 'b': 1, 'c': 1})
|
||||
|
||||
def test_include_exclude(self):
|
||||
q = QueryFieldList()
|
||||
q += QueryFieldList(fields=['a', 'b'], value=QueryFieldList.ONLY)
|
||||
self.assertEqual(q.as_dict(), {'a': True, 'b': True})
|
||||
self.assertEqual(q.as_dict(), {'a': 1, 'b': 1})
|
||||
q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.EXCLUDE)
|
||||
self.assertEqual(q.as_dict(), {'a': True})
|
||||
self.assertEqual(q.as_dict(), {'a': 1})
|
||||
|
||||
def test_exclude_exclude(self):
|
||||
q = QueryFieldList()
|
||||
q += QueryFieldList(fields=['a', 'b'], value=QueryFieldList.EXCLUDE)
|
||||
self.assertEqual(q.as_dict(), {'a': False, 'b': False})
|
||||
self.assertEqual(q.as_dict(), {'a': 0, 'b': 0})
|
||||
q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.EXCLUDE)
|
||||
self.assertEqual(q.as_dict(), {'a': False, 'b': False, 'c': False})
|
||||
self.assertEqual(q.as_dict(), {'a': 0, 'b': 0, 'c': 0})
|
||||
|
||||
def test_exclude_include(self):
|
||||
q = QueryFieldList()
|
||||
q += QueryFieldList(fields=['a', 'b'], value=QueryFieldList.EXCLUDE)
|
||||
self.assertEqual(q.as_dict(), {'a': False, 'b': False})
|
||||
self.assertEqual(q.as_dict(), {'a': 0, 'b': 0})
|
||||
q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.ONLY)
|
||||
self.assertEqual(q.as_dict(), {'c': True})
|
||||
self.assertEqual(q.as_dict(), {'c': 1})
|
||||
|
||||
def test_always_include(self):
|
||||
q = QueryFieldList(always_include=['x', 'y'])
|
||||
q += QueryFieldList(fields=['a', 'b', 'x'], value=QueryFieldList.EXCLUDE)
|
||||
q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.ONLY)
|
||||
self.assertEqual(q.as_dict(), {'x': True, 'y': True, 'c': True})
|
||||
self.assertEqual(q.as_dict(), {'x': 1, 'y': 1, 'c': 1})
|
||||
|
||||
def test_reset(self):
|
||||
q = QueryFieldList(always_include=['x', 'y'])
|
||||
q += QueryFieldList(fields=['a', 'b', 'x'], value=QueryFieldList.EXCLUDE)
|
||||
q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.ONLY)
|
||||
self.assertEqual(q.as_dict(), {'x': True, 'y': True, 'c': True})
|
||||
self.assertEqual(q.as_dict(), {'x': 1, 'y': 1, 'c': 1})
|
||||
q.reset()
|
||||
self.assertFalse(q)
|
||||
q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.ONLY)
|
||||
self.assertEqual(q.as_dict(), {'x': True, 'y': True, 'b': True, 'c': True})
|
||||
self.assertEqual(q.as_dict(), {'x': 1, 'y': 1, 'b': 1, 'c': 1})
|
||||
|
||||
def test_using_a_slice(self):
|
||||
q = QueryFieldList()
|
||||
@@ -95,22 +92,22 @@ class OnlyExcludeAllTest(unittest.TestCase):
|
||||
exclude = ['d', 'e']
|
||||
only = ['b', 'c']
|
||||
|
||||
qs = MyDoc.objects.fields(**dict(((i, 1) for i in include)))
|
||||
qs = MyDoc.objects.fields(**{i: 1 for i in include})
|
||||
self.assertEqual(qs._loaded_fields.as_dict(),
|
||||
{'a': 1, 'b': 1, 'c': 1, 'd': 1, 'e': 1})
|
||||
{'a': 1, 'b': 1, 'c': 1, 'd': 1, 'e': 1})
|
||||
qs = qs.only(*only)
|
||||
self.assertEqual(qs._loaded_fields.as_dict(), {'b': 1, 'c': 1})
|
||||
qs = qs.exclude(*exclude)
|
||||
self.assertEqual(qs._loaded_fields.as_dict(), {'b': 1, 'c': 1})
|
||||
|
||||
qs = MyDoc.objects.fields(**dict(((i, 1) for i in include)))
|
||||
qs = MyDoc.objects.fields(**{i: 1 for i in include})
|
||||
qs = qs.exclude(*exclude)
|
||||
self.assertEqual(qs._loaded_fields.as_dict(), {'a': 1, 'b': 1, 'c': 1})
|
||||
qs = qs.only(*only)
|
||||
self.assertEqual(qs._loaded_fields.as_dict(), {'b': 1, 'c': 1})
|
||||
|
||||
qs = MyDoc.objects.exclude(*exclude)
|
||||
qs = qs.fields(**dict(((i, 1) for i in include)))
|
||||
qs = qs.fields(**{i: 1 for i in include})
|
||||
self.assertEqual(qs._loaded_fields.as_dict(), {'a': 1, 'b': 1, 'c': 1})
|
||||
qs = qs.only(*only)
|
||||
self.assertEqual(qs._loaded_fields.as_dict(), {'b': 1, 'c': 1})
|
||||
@@ -129,20 +126,30 @@ class OnlyExcludeAllTest(unittest.TestCase):
|
||||
exclude = ['d', 'e']
|
||||
only = ['b', 'c']
|
||||
|
||||
qs = MyDoc.objects.fields(**dict(((i, 1) for i in include)))
|
||||
qs = MyDoc.objects.fields(**{i: 1 for i in include})
|
||||
qs = qs.exclude(*exclude)
|
||||
qs = qs.only(*only)
|
||||
qs = qs.fields(slice__b=5)
|
||||
self.assertEqual(qs._loaded_fields.as_dict(),
|
||||
{'b': {'$slice': 5}, 'c': 1})
|
||||
{'b': {'$slice': 5}, 'c': 1})
|
||||
|
||||
qs = qs.fields(slice__c=[5, 1])
|
||||
self.assertEqual(qs._loaded_fields.as_dict(),
|
||||
{'b': {'$slice': 5}, 'c': {'$slice': [5, 1]}})
|
||||
{'b': {'$slice': 5}, 'c': {'$slice': [5, 1]}})
|
||||
|
||||
qs = qs.exclude('c')
|
||||
self.assertEqual(qs._loaded_fields.as_dict(),
|
||||
{'b': {'$slice': 5}})
|
||||
{'b': {'$slice': 5}})
|
||||
|
||||
def test_mix_slice_with_other_fields(self):
|
||||
class MyDoc(Document):
|
||||
a = ListField()
|
||||
b = ListField()
|
||||
c = ListField()
|
||||
|
||||
qs = MyDoc.objects.fields(a=1, b=0, slice__c=2)
|
||||
self.assertEqual(qs._loaded_fields.as_dict(),
|
||||
{'c': {'$slice': 2}, 'a': 1})
|
||||
|
||||
def test_only(self):
|
||||
"""Ensure that QuerySet.only only returns the requested fields.
|
||||
@@ -162,6 +169,10 @@ class OnlyExcludeAllTest(unittest.TestCase):
|
||||
self.assertEqual(obj.name, person.name)
|
||||
self.assertEqual(obj.age, person.age)
|
||||
|
||||
obj = self.Person.objects.only(*('id', 'name',)).get()
|
||||
self.assertEqual(obj.name, person.name)
|
||||
self.assertEqual(obj.age, None)
|
||||
|
||||
# Check polymorphism still works
|
||||
class Employee(self.Person):
|
||||
salary = IntField(db_field='wage')
|
||||
@@ -186,14 +197,18 @@ class OnlyExcludeAllTest(unittest.TestCase):
|
||||
title = StringField()
|
||||
text = StringField()
|
||||
|
||||
class VariousData(EmbeddedDocument):
|
||||
some = BooleanField()
|
||||
|
||||
class BlogPost(Document):
|
||||
content = StringField()
|
||||
author = EmbeddedDocumentField(User)
|
||||
comments = ListField(EmbeddedDocumentField(Comment))
|
||||
various = MapField(field=EmbeddedDocumentField(VariousData))
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
post = BlogPost(content='Had a good coffee today...')
|
||||
post = BlogPost(content='Had a good coffee today...', various={'test_dynamic':{'some': True}})
|
||||
post.author = User(name='Test User')
|
||||
post.comments = [Comment(title='I aggree', text='Great post!'), Comment(title='Coffee', text='I hate coffee')]
|
||||
post.save()
|
||||
@@ -204,6 +219,9 @@ class OnlyExcludeAllTest(unittest.TestCase):
|
||||
self.assertEqual(obj.author.name, 'Test User')
|
||||
self.assertEqual(obj.comments, [])
|
||||
|
||||
obj = BlogPost.objects.only('various.test_dynamic.some').get()
|
||||
self.assertEqual(obj.various["test_dynamic"].some, True)
|
||||
|
||||
obj = BlogPost.objects.only('content', 'comments.title',).get()
|
||||
self.assertEqual(obj.content, 'Had a good coffee today...')
|
||||
self.assertEqual(obj.author, None)
|
||||
@@ -328,7 +346,7 @@ class OnlyExcludeAllTest(unittest.TestCase):
|
||||
|
||||
Numbers.drop_collection()
|
||||
|
||||
numbers = Numbers(n=[0,1,2,3,4,5,-5,-4,-3,-2,-1])
|
||||
numbers = Numbers(n=[0, 1, 2, 3, 4, 5, -5, -4, -3, -2, -1])
|
||||
numbers.save()
|
||||
|
||||
# first three
|
||||
@@ -368,7 +386,7 @@ class OnlyExcludeAllTest(unittest.TestCase):
|
||||
Numbers.drop_collection()
|
||||
|
||||
numbers = Numbers()
|
||||
numbers.embedded = EmbeddedNumber(n=[0,1,2,3,4,5,-5,-4,-3,-2,-1])
|
||||
numbers.embedded = EmbeddedNumber(n=[0, 1, 2, 3, 4, 5, -5, -4, -3, -2, -1])
|
||||
numbers.save()
|
||||
|
||||
# first three
|
||||
@@ -395,5 +413,28 @@ class OnlyExcludeAllTest(unittest.TestCase):
|
||||
numbers = Numbers.objects.fields(embedded__n={"$slice": [-5, 10]}).get()
|
||||
self.assertEqual(numbers.embedded.n, [-5, -4, -3, -2, -1])
|
||||
|
||||
|
||||
def test_exclude_from_subclasses_docs(self):
|
||||
|
||||
class Base(Document):
|
||||
username = StringField()
|
||||
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
class Anon(Base):
|
||||
anon = BooleanField()
|
||||
|
||||
class User(Base):
|
||||
password = StringField()
|
||||
wibble = StringField()
|
||||
|
||||
Base.drop_collection()
|
||||
User(username="mongodb", password="secret").save()
|
||||
|
||||
user = Base.objects().exclude("password", "wibble").first()
|
||||
self.assertEqual(user.password, None)
|
||||
|
||||
self.assertRaises(LookUpError, Base.objects.exclude, "made_up")
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
575
tests/queryset/geo.py
Normal file
575
tests/queryset/geo.py
Normal file
@@ -0,0 +1,575 @@
|
||||
import datetime
|
||||
import unittest
|
||||
|
||||
from mongoengine import *
|
||||
|
||||
from tests.utils import MongoDBTestCase, needs_mongodb_v3
|
||||
|
||||
|
||||
__all__ = ("GeoQueriesTest",)
|
||||
|
||||
|
||||
class GeoQueriesTest(MongoDBTestCase):
|
||||
|
||||
def _create_event_data(self, point_field_class=GeoPointField):
|
||||
"""Create some sample data re-used in many of the tests below."""
|
||||
class Event(Document):
|
||||
title = StringField()
|
||||
date = DateTimeField()
|
||||
location = point_field_class()
|
||||
|
||||
def __unicode__(self):
|
||||
return self.title
|
||||
|
||||
self.Event = Event
|
||||
|
||||
Event.drop_collection()
|
||||
|
||||
event1 = Event.objects.create(
|
||||
title="Coltrane Motion @ Double Door",
|
||||
date=datetime.datetime.now() - datetime.timedelta(days=1),
|
||||
location=[-87.677137, 41.909889])
|
||||
event2 = Event.objects.create(
|
||||
title="Coltrane Motion @ Bottom of the Hill",
|
||||
date=datetime.datetime.now() - datetime.timedelta(days=10),
|
||||
location=[-122.4194155, 37.7749295])
|
||||
event3 = Event.objects.create(
|
||||
title="Coltrane Motion @ Empty Bottle",
|
||||
date=datetime.datetime.now(),
|
||||
location=[-87.686638, 41.900474])
|
||||
|
||||
return event1, event2, event3
|
||||
|
||||
def test_near(self):
|
||||
"""Make sure the "near" operator works."""
|
||||
event1, event2, event3 = self._create_event_data()
|
||||
|
||||
# find all events "near" pitchfork office, chicago.
|
||||
# note that "near" will show the san francisco event, too,
|
||||
# although it sorts to last.
|
||||
events = self.Event.objects(location__near=[-87.67892, 41.9120459])
|
||||
self.assertEqual(events.count(), 3)
|
||||
self.assertEqual(list(events), [event1, event3, event2])
|
||||
|
||||
# ensure ordering is respected by "near"
|
||||
events = self.Event.objects(location__near=[-87.67892, 41.9120459])
|
||||
events = events.order_by("-date")
|
||||
self.assertEqual(events.count(), 3)
|
||||
self.assertEqual(list(events), [event3, event1, event2])
|
||||
|
||||
def test_near_and_max_distance(self):
|
||||
"""Ensure the "max_distance" operator works alongside the "near"
|
||||
operator.
|
||||
"""
|
||||
event1, event2, event3 = self._create_event_data()
|
||||
|
||||
# find events within 10 degrees of san francisco
|
||||
point = [-122.415579, 37.7566023]
|
||||
events = self.Event.objects(location__near=point,
|
||||
location__max_distance=10)
|
||||
self.assertEqual(events.count(), 1)
|
||||
self.assertEqual(events[0], event2)
|
||||
|
||||
# $minDistance was added in MongoDB v2.6, but continued being buggy
|
||||
# until v3.0; skip for older versions
|
||||
@needs_mongodb_v3
|
||||
def test_near_and_min_distance(self):
|
||||
"""Ensure the "min_distance" operator works alongside the "near"
|
||||
operator.
|
||||
"""
|
||||
event1, event2, event3 = self._create_event_data()
|
||||
|
||||
# find events at least 10 degrees away of san francisco
|
||||
point = [-122.415579, 37.7566023]
|
||||
events = self.Event.objects(location__near=point,
|
||||
location__min_distance=10)
|
||||
self.assertEqual(events.count(), 2)
|
||||
|
||||
def test_within_distance(self):
|
||||
"""Make sure the "within_distance" operator works."""
|
||||
event1, event2, event3 = self._create_event_data()
|
||||
|
||||
# find events within 5 degrees of pitchfork office, chicago
|
||||
point_and_distance = [[-87.67892, 41.9120459], 5]
|
||||
events = self.Event.objects(
|
||||
location__within_distance=point_and_distance)
|
||||
self.assertEqual(events.count(), 2)
|
||||
events = list(events)
|
||||
self.assertTrue(event2 not in events)
|
||||
self.assertTrue(event1 in events)
|
||||
self.assertTrue(event3 in events)
|
||||
|
||||
# find events within 10 degrees of san francisco
|
||||
point_and_distance = [[-122.415579, 37.7566023], 10]
|
||||
events = self.Event.objects(
|
||||
location__within_distance=point_and_distance)
|
||||
self.assertEqual(events.count(), 1)
|
||||
self.assertEqual(events[0], event2)
|
||||
|
||||
# find events within 1 degree of greenpoint, broolyn, nyc, ny
|
||||
point_and_distance = [[-73.9509714, 40.7237134], 1]
|
||||
events = self.Event.objects(
|
||||
location__within_distance=point_and_distance)
|
||||
self.assertEqual(events.count(), 0)
|
||||
|
||||
# ensure ordering is respected by "within_distance"
|
||||
point_and_distance = [[-87.67892, 41.9120459], 10]
|
||||
events = self.Event.objects(
|
||||
location__within_distance=point_and_distance)
|
||||
events = events.order_by("-date")
|
||||
self.assertEqual(events.count(), 2)
|
||||
self.assertEqual(events[0], event3)
|
||||
|
||||
def test_within_box(self):
|
||||
"""Ensure the "within_box" operator works."""
|
||||
event1, event2, event3 = self._create_event_data()
|
||||
|
||||
# check that within_box works
|
||||
box = [(-125.0, 35.0), (-100.0, 40.0)]
|
||||
events = self.Event.objects(location__within_box=box)
|
||||
self.assertEqual(events.count(), 1)
|
||||
self.assertEqual(events[0].id, event2.id)
|
||||
|
||||
def test_within_polygon(self):
|
||||
"""Ensure the "within_polygon" operator works."""
|
||||
event1, event2, event3 = self._create_event_data()
|
||||
|
||||
polygon = [
|
||||
(-87.694445, 41.912114),
|
||||
(-87.69084, 41.919395),
|
||||
(-87.681742, 41.927186),
|
||||
(-87.654276, 41.911731),
|
||||
(-87.656164, 41.898061),
|
||||
]
|
||||
events = self.Event.objects(location__within_polygon=polygon)
|
||||
self.assertEqual(events.count(), 1)
|
||||
self.assertEqual(events[0].id, event1.id)
|
||||
|
||||
polygon2 = [
|
||||
(-1.742249, 54.033586),
|
||||
(-1.225891, 52.792797),
|
||||
(-4.40094, 53.389881)
|
||||
]
|
||||
events = self.Event.objects(location__within_polygon=polygon2)
|
||||
self.assertEqual(events.count(), 0)
|
||||
|
||||
def test_2dsphere_near(self):
|
||||
"""Make sure the "near" operator works with a PointField, which
|
||||
corresponds to a 2dsphere index.
|
||||
"""
|
||||
event1, event2, event3 = self._create_event_data(
|
||||
point_field_class=PointField
|
||||
)
|
||||
|
||||
# find all events "near" pitchfork office, chicago.
|
||||
# note that "near" will show the san francisco event, too,
|
||||
# although it sorts to last.
|
||||
events = self.Event.objects(location__near=[-87.67892, 41.9120459])
|
||||
self.assertEqual(events.count(), 3)
|
||||
self.assertEqual(list(events), [event1, event3, event2])
|
||||
|
||||
# ensure ordering is respected by "near"
|
||||
events = self.Event.objects(location__near=[-87.67892, 41.9120459])
|
||||
events = events.order_by("-date")
|
||||
self.assertEqual(events.count(), 3)
|
||||
self.assertEqual(list(events), [event3, event1, event2])
|
||||
|
||||
def test_2dsphere_near_and_max_distance(self):
|
||||
"""Ensure the "max_distance" operator works alongside the "near"
|
||||
operator with a 2dsphere index.
|
||||
"""
|
||||
event1, event2, event3 = self._create_event_data(
|
||||
point_field_class=PointField
|
||||
)
|
||||
|
||||
# find events within 10km of san francisco
|
||||
point = [-122.415579, 37.7566023]
|
||||
events = self.Event.objects(location__near=point,
|
||||
location__max_distance=10000)
|
||||
self.assertEqual(events.count(), 1)
|
||||
self.assertEqual(events[0], event2)
|
||||
|
||||
# find events within 1km of greenpoint, broolyn, nyc, ny
|
||||
events = self.Event.objects(location__near=[-73.9509714, 40.7237134],
|
||||
location__max_distance=1000)
|
||||
self.assertEqual(events.count(), 0)
|
||||
|
||||
# ensure ordering is respected by "near"
|
||||
events = self.Event.objects(
|
||||
location__near=[-87.67892, 41.9120459],
|
||||
location__max_distance=10000
|
||||
).order_by("-date")
|
||||
self.assertEqual(events.count(), 2)
|
||||
self.assertEqual(events[0], event3)
|
||||
|
||||
def test_2dsphere_geo_within_box(self):
|
||||
"""Ensure the "geo_within_box" operator works with a 2dsphere
|
||||
index.
|
||||
"""
|
||||
event1, event2, event3 = self._create_event_data(
|
||||
point_field_class=PointField
|
||||
)
|
||||
|
||||
# check that within_box works
|
||||
box = [(-125.0, 35.0), (-100.0, 40.0)]
|
||||
events = self.Event.objects(location__geo_within_box=box)
|
||||
self.assertEqual(events.count(), 1)
|
||||
self.assertEqual(events[0].id, event2.id)
|
||||
|
||||
def test_2dsphere_geo_within_polygon(self):
|
||||
"""Ensure the "geo_within_polygon" operator works with a
|
||||
2dsphere index.
|
||||
"""
|
||||
event1, event2, event3 = self._create_event_data(
|
||||
point_field_class=PointField
|
||||
)
|
||||
|
||||
polygon = [
|
||||
(-87.694445, 41.912114),
|
||||
(-87.69084, 41.919395),
|
||||
(-87.681742, 41.927186),
|
||||
(-87.654276, 41.911731),
|
||||
(-87.656164, 41.898061),
|
||||
]
|
||||
events = self.Event.objects(location__geo_within_polygon=polygon)
|
||||
self.assertEqual(events.count(), 1)
|
||||
self.assertEqual(events[0].id, event1.id)
|
||||
|
||||
polygon2 = [
|
||||
(-1.742249, 54.033586),
|
||||
(-1.225891, 52.792797),
|
||||
(-4.40094, 53.389881)
|
||||
]
|
||||
events = self.Event.objects(location__geo_within_polygon=polygon2)
|
||||
self.assertEqual(events.count(), 0)
|
||||
|
||||
# $minDistance was added in MongoDB v2.6, but continued being buggy
|
||||
# until v3.0; skip for older versions
|
||||
@needs_mongodb_v3
|
||||
def test_2dsphere_near_and_min_max_distance(self):
|
||||
"""Ensure "min_distace" and "max_distance" operators work well
|
||||
together with the "near" operator in a 2dsphere index.
|
||||
"""
|
||||
event1, event2, event3 = self._create_event_data(
|
||||
point_field_class=PointField
|
||||
)
|
||||
|
||||
# ensure min_distance and max_distance combine well
|
||||
events = self.Event.objects(
|
||||
location__near=[-87.67892, 41.9120459],
|
||||
location__min_distance=1000,
|
||||
location__max_distance=10000
|
||||
).order_by("-date")
|
||||
self.assertEqual(events.count(), 1)
|
||||
self.assertEqual(events[0], event3)
|
||||
|
||||
# ensure ordering is respected by "near" with "min_distance"
|
||||
events = self.Event.objects(
|
||||
location__near=[-87.67892, 41.9120459],
|
||||
location__min_distance=10000
|
||||
).order_by("-date")
|
||||
self.assertEqual(events.count(), 1)
|
||||
self.assertEqual(events[0], event2)
|
||||
|
||||
def test_2dsphere_geo_within_center(self):
|
||||
"""Make sure the "geo_within_center" operator works with a
|
||||
2dsphere index.
|
||||
"""
|
||||
event1, event2, event3 = self._create_event_data(
|
||||
point_field_class=PointField
|
||||
)
|
||||
|
||||
# find events within 5 degrees of pitchfork office, chicago
|
||||
point_and_distance = [[-87.67892, 41.9120459], 2]
|
||||
events = self.Event.objects(
|
||||
location__geo_within_center=point_and_distance)
|
||||
self.assertEqual(events.count(), 2)
|
||||
events = list(events)
|
||||
self.assertTrue(event2 not in events)
|
||||
self.assertTrue(event1 in events)
|
||||
self.assertTrue(event3 in events)
|
||||
|
||||
def _test_embedded(self, point_field_class):
|
||||
"""Helper test method ensuring given point field class works
|
||||
well in an embedded document.
|
||||
"""
|
||||
class Venue(EmbeddedDocument):
|
||||
location = point_field_class()
|
||||
name = StringField()
|
||||
|
||||
class Event(Document):
|
||||
title = StringField()
|
||||
venue = EmbeddedDocumentField(Venue)
|
||||
|
||||
Event.drop_collection()
|
||||
|
||||
venue1 = Venue(name="The Rock", location=[-87.677137, 41.909889])
|
||||
venue2 = Venue(name="The Bridge", location=[-122.4194155, 37.7749295])
|
||||
|
||||
event1 = Event(title="Coltrane Motion @ Double Door",
|
||||
venue=venue1).save()
|
||||
event2 = Event(title="Coltrane Motion @ Bottom of the Hill",
|
||||
venue=venue2).save()
|
||||
event3 = Event(title="Coltrane Motion @ Empty Bottle",
|
||||
venue=venue1).save()
|
||||
|
||||
# find all events "near" pitchfork office, chicago.
|
||||
# note that "near" will show the san francisco event, too,
|
||||
# although it sorts to last.
|
||||
events = Event.objects(venue__location__near=[-87.67892, 41.9120459])
|
||||
self.assertEqual(events.count(), 3)
|
||||
self.assertEqual(list(events), [event1, event3, event2])
|
||||
|
||||
def test_geo_spatial_embedded(self):
|
||||
"""Make sure GeoPointField works properly in an embedded document."""
|
||||
self._test_embedded(point_field_class=GeoPointField)
|
||||
|
||||
def test_2dsphere_point_embedded(self):
|
||||
"""Make sure PointField works properly in an embedded document."""
|
||||
self._test_embedded(point_field_class=PointField)
|
||||
|
||||
# Needs MongoDB > 2.6.4 https://jira.mongodb.org/browse/SERVER-14039
|
||||
@needs_mongodb_v3
|
||||
def test_spherical_geospatial_operators(self):
|
||||
"""Ensure that spherical geospatial queries are working."""
|
||||
class Point(Document):
|
||||
location = GeoPointField()
|
||||
|
||||
Point.drop_collection()
|
||||
|
||||
# These points are one degree apart, which (according to Google Maps)
|
||||
# is about 110 km apart at this place on the Earth.
|
||||
north_point = Point(location=[-122, 38]).save() # Near Concord, CA
|
||||
south_point = Point(location=[-122, 37]).save() # Near Santa Cruz, CA
|
||||
|
||||
earth_radius = 6378.009 # in km (needs to be a float for dividing by)
|
||||
|
||||
# Finds both points because they are within 60 km of the reference
|
||||
# point equidistant between them.
|
||||
points = Point.objects(location__near_sphere=[-122, 37.5])
|
||||
self.assertEqual(points.count(), 2)
|
||||
|
||||
# Same behavior for _within_spherical_distance
|
||||
points = Point.objects(
|
||||
location__within_spherical_distance=[
|
||||
[-122, 37.5],
|
||||
60 / earth_radius
|
||||
]
|
||||
)
|
||||
self.assertEqual(points.count(), 2)
|
||||
|
||||
points = Point.objects(location__near_sphere=[-122, 37.5],
|
||||
location__max_distance=60 / earth_radius)
|
||||
self.assertEqual(points.count(), 2)
|
||||
|
||||
# Test query works with max_distance, being farer from one point
|
||||
points = Point.objects(location__near_sphere=[-122, 37.8],
|
||||
location__max_distance=60 / earth_radius)
|
||||
close_point = points.first()
|
||||
self.assertEqual(points.count(), 1)
|
||||
|
||||
# Test query works with min_distance, being farer from one point
|
||||
points = Point.objects(location__near_sphere=[-122, 37.8],
|
||||
location__min_distance=60 / earth_radius)
|
||||
self.assertEqual(points.count(), 1)
|
||||
far_point = points.first()
|
||||
self.assertNotEqual(close_point, far_point)
|
||||
|
||||
# Finds both points, but orders the north point first because it's
|
||||
# closer to the reference point to the north.
|
||||
points = Point.objects(location__near_sphere=[-122, 38.5])
|
||||
self.assertEqual(points.count(), 2)
|
||||
self.assertEqual(points[0].id, north_point.id)
|
||||
self.assertEqual(points[1].id, south_point.id)
|
||||
|
||||
# Finds both points, but orders the south point first because it's
|
||||
# closer to the reference point to the south.
|
||||
points = Point.objects(location__near_sphere=[-122, 36.5])
|
||||
self.assertEqual(points.count(), 2)
|
||||
self.assertEqual(points[0].id, south_point.id)
|
||||
self.assertEqual(points[1].id, north_point.id)
|
||||
|
||||
# Finds only one point because only the first point is within 60km of
|
||||
# the reference point to the south.
|
||||
points = Point.objects(
|
||||
location__within_spherical_distance=[
|
||||
[-122, 36.5],
|
||||
60 / earth_radius
|
||||
]
|
||||
)
|
||||
self.assertEqual(points.count(), 1)
|
||||
self.assertEqual(points[0].id, south_point.id)
|
||||
|
||||
def test_linestring(self):
|
||||
class Road(Document):
|
||||
name = StringField()
|
||||
line = LineStringField()
|
||||
|
||||
Road.drop_collection()
|
||||
|
||||
Road(name="66", line=[[40, 5], [41, 6]]).save()
|
||||
|
||||
# near
|
||||
point = {"type": "Point", "coordinates": [40, 5]}
|
||||
roads = Road.objects.filter(line__near=point["coordinates"]).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
roads = Road.objects.filter(line__near=point).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
roads = Road.objects.filter(line__near={"$geometry": point}).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
# Within
|
||||
polygon = {"type": "Polygon",
|
||||
"coordinates": [[[40, 5], [40, 6], [41, 6], [41, 5], [40, 5]]]}
|
||||
roads = Road.objects.filter(line__geo_within=polygon["coordinates"]).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
roads = Road.objects.filter(line__geo_within=polygon).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
roads = Road.objects.filter(line__geo_within={"$geometry": polygon}).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
# Intersects
|
||||
line = {"type": "LineString",
|
||||
"coordinates": [[40, 5], [40, 6]]}
|
||||
roads = Road.objects.filter(line__geo_intersects=line["coordinates"]).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
roads = Road.objects.filter(line__geo_intersects=line).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
roads = Road.objects.filter(line__geo_intersects={"$geometry": line}).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
polygon = {"type": "Polygon",
|
||||
"coordinates": [[[40, 5], [40, 6], [41, 6], [41, 5], [40, 5]]]}
|
||||
roads = Road.objects.filter(line__geo_intersects=polygon["coordinates"]).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
roads = Road.objects.filter(line__geo_intersects=polygon).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
roads = Road.objects.filter(line__geo_intersects={"$geometry": polygon}).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
def test_polygon(self):
|
||||
class Road(Document):
|
||||
name = StringField()
|
||||
poly = PolygonField()
|
||||
|
||||
Road.drop_collection()
|
||||
|
||||
Road(name="66", poly=[[[40, 5], [40, 6], [41, 6], [40, 5]]]).save()
|
||||
|
||||
# near
|
||||
point = {"type": "Point", "coordinates": [40, 5]}
|
||||
roads = Road.objects.filter(poly__near=point["coordinates"]).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
roads = Road.objects.filter(poly__near=point).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
roads = Road.objects.filter(poly__near={"$geometry": point}).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
# Within
|
||||
polygon = {"type": "Polygon",
|
||||
"coordinates": [[[40, 5], [40, 6], [41, 6], [41, 5], [40, 5]]]}
|
||||
roads = Road.objects.filter(poly__geo_within=polygon["coordinates"]).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
roads = Road.objects.filter(poly__geo_within=polygon).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
roads = Road.objects.filter(poly__geo_within={"$geometry": polygon}).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
# Intersects
|
||||
line = {"type": "LineString",
|
||||
"coordinates": [[40, 5], [41, 6]]}
|
||||
roads = Road.objects.filter(poly__geo_intersects=line["coordinates"]).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
roads = Road.objects.filter(poly__geo_intersects=line).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
roads = Road.objects.filter(poly__geo_intersects={"$geometry": line}).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
polygon = {"type": "Polygon",
|
||||
"coordinates": [[[40, 5], [40, 6], [41, 6], [41, 5], [40, 5]]]}
|
||||
roads = Road.objects.filter(poly__geo_intersects=polygon["coordinates"]).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
roads = Road.objects.filter(poly__geo_intersects=polygon).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
roads = Road.objects.filter(poly__geo_intersects={"$geometry": polygon}).count()
|
||||
self.assertEqual(1, roads)
|
||||
|
||||
def test_aspymongo_with_only(self):
|
||||
"""Ensure as_pymongo works with only"""
|
||||
class Place(Document):
|
||||
location = PointField()
|
||||
|
||||
Place.drop_collection()
|
||||
p = Place(location=[24.946861267089844, 60.16311983618494])
|
||||
p.save()
|
||||
qs = Place.objects().only('location')
|
||||
self.assertDictEqual(
|
||||
qs.as_pymongo()[0]['location'],
|
||||
{u'type': u'Point',
|
||||
u'coordinates': [
|
||||
24.946861267089844,
|
||||
60.16311983618494]
|
||||
}
|
||||
)
|
||||
|
||||
def test_2dsphere_point_sets_correctly(self):
|
||||
class Location(Document):
|
||||
loc = PointField()
|
||||
|
||||
Location.drop_collection()
|
||||
|
||||
Location(loc=[1,2]).save()
|
||||
loc = Location.objects.as_pymongo()[0]
|
||||
self.assertEqual(loc["loc"], {"type": "Point", "coordinates": [1, 2]})
|
||||
|
||||
Location.objects.update(set__loc=[2,1])
|
||||
loc = Location.objects.as_pymongo()[0]
|
||||
self.assertEqual(loc["loc"], {"type": "Point", "coordinates": [2, 1]})
|
||||
|
||||
def test_2dsphere_linestring_sets_correctly(self):
|
||||
class Location(Document):
|
||||
line = LineStringField()
|
||||
|
||||
Location.drop_collection()
|
||||
|
||||
Location(line=[[1, 2], [2, 2]]).save()
|
||||
loc = Location.objects.as_pymongo()[0]
|
||||
self.assertEqual(loc["line"], {"type": "LineString", "coordinates": [[1, 2], [2, 2]]})
|
||||
|
||||
Location.objects.update(set__line=[[2, 1], [1, 2]])
|
||||
loc = Location.objects.as_pymongo()[0]
|
||||
self.assertEqual(loc["line"], {"type": "LineString", "coordinates": [[2, 1], [1, 2]]})
|
||||
|
||||
def test_geojson_PolygonField(self):
|
||||
class Location(Document):
|
||||
poly = PolygonField()
|
||||
|
||||
Location.drop_collection()
|
||||
|
||||
Location(poly=[[[40, 5], [40, 6], [41, 6], [40, 5]]]).save()
|
||||
loc = Location.objects.as_pymongo()[0]
|
||||
self.assertEqual(loc["poly"], {"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]})
|
||||
|
||||
Location.objects.update(set__poly=[[[40, 4], [40, 6], [41, 6], [40, 4]]])
|
||||
loc = Location.objects.as_pymongo()[0]
|
||||
self.assertEqual(loc["poly"], {"type": "Polygon", "coordinates": [[[40, 4], [40, 6], [41, 6], [40, 4]]]})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
132
tests/queryset/modify.py
Normal file
132
tests/queryset/modify.py
Normal file
@@ -0,0 +1,132 @@
|
||||
import unittest
|
||||
|
||||
from mongoengine import connect, Document, IntField, StringField, ListField
|
||||
|
||||
from tests.utils import needs_mongodb_v26
|
||||
|
||||
__all__ = ("FindAndModifyTest",)
|
||||
|
||||
|
||||
class Doc(Document):
|
||||
id = IntField(primary_key=True)
|
||||
value = IntField()
|
||||
|
||||
|
||||
class FindAndModifyTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
connect(db="mongoenginetest")
|
||||
Doc.drop_collection()
|
||||
|
||||
def assertDbEqual(self, docs):
|
||||
self.assertEqual(list(Doc._collection.find().sort("id")), docs)
|
||||
|
||||
def test_modify(self):
|
||||
Doc(id=0, value=0).save()
|
||||
doc = Doc(id=1, value=1).save()
|
||||
|
||||
old_doc = Doc.objects(id=1).modify(set__value=-1)
|
||||
self.assertEqual(old_doc.to_json(), doc.to_json())
|
||||
self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": -1}])
|
||||
|
||||
def test_modify_with_new(self):
|
||||
Doc(id=0, value=0).save()
|
||||
doc = Doc(id=1, value=1).save()
|
||||
|
||||
new_doc = Doc.objects(id=1).modify(set__value=-1, new=True)
|
||||
doc.value = -1
|
||||
self.assertEqual(new_doc.to_json(), doc.to_json())
|
||||
self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": -1}])
|
||||
|
||||
def test_modify_not_existing(self):
|
||||
Doc(id=0, value=0).save()
|
||||
self.assertEqual(Doc.objects(id=1).modify(set__value=-1), None)
|
||||
self.assertDbEqual([{"_id": 0, "value": 0}])
|
||||
|
||||
def test_modify_with_upsert(self):
|
||||
Doc(id=0, value=0).save()
|
||||
old_doc = Doc.objects(id=1).modify(set__value=1, upsert=True)
|
||||
self.assertEqual(old_doc, None)
|
||||
self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": 1}])
|
||||
|
||||
def test_modify_with_upsert_existing(self):
|
||||
Doc(id=0, value=0).save()
|
||||
doc = Doc(id=1, value=1).save()
|
||||
|
||||
old_doc = Doc.objects(id=1).modify(set__value=-1, upsert=True)
|
||||
self.assertEqual(old_doc.to_json(), doc.to_json())
|
||||
self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": -1}])
|
||||
|
||||
def test_modify_with_upsert_with_new(self):
|
||||
Doc(id=0, value=0).save()
|
||||
new_doc = Doc.objects(id=1).modify(upsert=True, new=True, set__value=1)
|
||||
self.assertEqual(new_doc.to_mongo(), {"_id": 1, "value": 1})
|
||||
self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": 1}])
|
||||
|
||||
def test_modify_with_remove(self):
|
||||
Doc(id=0, value=0).save()
|
||||
doc = Doc(id=1, value=1).save()
|
||||
|
||||
old_doc = Doc.objects(id=1).modify(remove=True)
|
||||
self.assertEqual(old_doc.to_json(), doc.to_json())
|
||||
self.assertDbEqual([{"_id": 0, "value": 0}])
|
||||
|
||||
def test_find_and_modify_with_remove_not_existing(self):
|
||||
Doc(id=0, value=0).save()
|
||||
self.assertEqual(Doc.objects(id=1).modify(remove=True), None)
|
||||
self.assertDbEqual([{"_id": 0, "value": 0}])
|
||||
|
||||
def test_modify_with_order_by(self):
|
||||
Doc(id=0, value=3).save()
|
||||
Doc(id=1, value=2).save()
|
||||
Doc(id=2, value=1).save()
|
||||
doc = Doc(id=3, value=0).save()
|
||||
|
||||
old_doc = Doc.objects().order_by("-id").modify(set__value=-1)
|
||||
self.assertEqual(old_doc.to_json(), doc.to_json())
|
||||
self.assertDbEqual([
|
||||
{"_id": 0, "value": 3}, {"_id": 1, "value": 2},
|
||||
{"_id": 2, "value": 1}, {"_id": 3, "value": -1}])
|
||||
|
||||
def test_modify_with_fields(self):
|
||||
Doc(id=0, value=0).save()
|
||||
Doc(id=1, value=1).save()
|
||||
|
||||
old_doc = Doc.objects(id=1).only("id").modify(set__value=-1)
|
||||
self.assertEqual(old_doc.to_mongo(), {"_id": 1})
|
||||
self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": -1}])
|
||||
|
||||
@needs_mongodb_v26
|
||||
def test_modify_with_push(self):
|
||||
class BlogPost(Document):
|
||||
tags = ListField(StringField())
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
blog = BlogPost.objects.create()
|
||||
|
||||
# Push a new tag via modify with new=False (default).
|
||||
BlogPost(id=blog.id).modify(push__tags='code')
|
||||
self.assertEqual(blog.tags, [])
|
||||
blog.reload()
|
||||
self.assertEqual(blog.tags, ['code'])
|
||||
|
||||
# Push a new tag via modify with new=True.
|
||||
blog = BlogPost.objects(id=blog.id).modify(push__tags='java', new=True)
|
||||
self.assertEqual(blog.tags, ['code', 'java'])
|
||||
|
||||
# Push a new tag with a positional argument.
|
||||
blog = BlogPost.objects(id=blog.id).modify(
|
||||
push__tags__0='python',
|
||||
new=True)
|
||||
self.assertEqual(blog.tags, ['python', 'code', 'java'])
|
||||
|
||||
# Push multiple new tags with a positional argument.
|
||||
blog = BlogPost.objects(id=blog.id).modify(
|
||||
push__tags__1=['go', 'rust'],
|
||||
new=True)
|
||||
self.assertEqual(blog.tags, ['python', 'go', 'rust', 'code', 'java'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
78
tests/queryset/pickable.py
Normal file
78
tests/queryset/pickable.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import pickle
|
||||
import unittest
|
||||
from pymongo.mongo_client import MongoClient
|
||||
from mongoengine import Document, StringField, IntField
|
||||
from mongoengine.connection import connect
|
||||
|
||||
__author__ = 'stas'
|
||||
|
||||
class Person(Document):
|
||||
name = StringField()
|
||||
age = IntField()
|
||||
|
||||
class TestQuerysetPickable(unittest.TestCase):
|
||||
"""
|
||||
Test for adding pickling support for QuerySet instances
|
||||
See issue https://github.com/MongoEngine/mongoengine/issues/442
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestQuerysetPickable, self).setUp()
|
||||
|
||||
connection = connect(db="test") #type: pymongo.mongo_client.MongoClient
|
||||
|
||||
connection.drop_database("test")
|
||||
|
||||
self.john = Person.objects.create(
|
||||
name="John",
|
||||
age=21
|
||||
)
|
||||
|
||||
|
||||
def test_picke_simple_qs(self):
|
||||
|
||||
qs = Person.objects.all()
|
||||
|
||||
pickle.dumps(qs)
|
||||
|
||||
def _get_loaded(self, qs):
|
||||
s = pickle.dumps(qs)
|
||||
|
||||
return pickle.loads(s)
|
||||
|
||||
def test_unpickle(self):
|
||||
qs = Person.objects.all()
|
||||
|
||||
loadedQs = self._get_loaded(qs)
|
||||
|
||||
self.assertEqual(qs.count(), loadedQs.count())
|
||||
|
||||
#can update loadedQs
|
||||
loadedQs.update(age=23)
|
||||
|
||||
#check
|
||||
self.assertEqual(Person.objects.first().age, 23)
|
||||
|
||||
def test_pickle_support_filtration(self):
|
||||
Person.objects.create(
|
||||
name="Alice",
|
||||
age=22
|
||||
)
|
||||
|
||||
Person.objects.create(
|
||||
name="Bob",
|
||||
age=23
|
||||
)
|
||||
|
||||
qs = Person.objects.filter(age__gte=22)
|
||||
self.assertEqual(qs.count(), 2)
|
||||
|
||||
loaded = self._get_loaded(qs)
|
||||
|
||||
self.assertEqual(loaded.count(), 2)
|
||||
self.assertEqual(loaded.filter(name="Bob").first().age, 23)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,7 @@
|
||||
from __future__ import with_statement
|
||||
import sys
|
||||
sys.path[0:0] = [""]
|
||||
|
||||
import unittest
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine.queryset import Q
|
||||
from mongoengine.queryset import transform
|
||||
from mongoengine.queryset import Q, transform
|
||||
|
||||
__all__ = ("TransformTest",)
|
||||
|
||||
@@ -32,6 +27,30 @@ class TransformTest(unittest.TestCase):
|
||||
self.assertEqual(transform.query(name__exists=True),
|
||||
{'name': {'$exists': True}})
|
||||
|
||||
def test_transform_update(self):
|
||||
class DicDoc(Document):
|
||||
dictField = DictField()
|
||||
|
||||
class Doc(Document):
|
||||
pass
|
||||
|
||||
DicDoc.drop_collection()
|
||||
Doc.drop_collection()
|
||||
|
||||
DicDoc().save()
|
||||
doc = Doc().save()
|
||||
|
||||
for k, v in (("set", "$set"), ("set_on_insert", "$setOnInsert"), ("push", "$push")):
|
||||
update = transform.update(DicDoc, **{"%s__dictField__test" % k: doc})
|
||||
self.assertTrue(isinstance(update[v]["dictField.test"], dict))
|
||||
|
||||
# Update special cases
|
||||
update = transform.update(DicDoc, unset__dictField__test=doc)
|
||||
self.assertEqual(update["$unset"]["dictField.test"], 1)
|
||||
|
||||
update = transform.update(DicDoc, pull__dictField__test=doc)
|
||||
self.assertTrue(isinstance(update["$pull"]["dictField"]["test"], dict))
|
||||
|
||||
def test_query_field_name(self):
|
||||
"""Ensure that the correct field name is used when querying.
|
||||
"""
|
||||
@@ -53,14 +72,14 @@ class TransformTest(unittest.TestCase):
|
||||
BlogPost.objects(title=data['title'])._query)
|
||||
self.assertFalse('title' in
|
||||
BlogPost.objects(title=data['title'])._query)
|
||||
self.assertEqual(len(BlogPost.objects(title=data['title'])), 1)
|
||||
self.assertEqual(BlogPost.objects(title=data['title']).count(), 1)
|
||||
|
||||
self.assertTrue('_id' in BlogPost.objects(pk=post.id)._query)
|
||||
self.assertEqual(len(BlogPost.objects(pk=post.id)), 1)
|
||||
self.assertEqual(BlogPost.objects(pk=post.id).count(), 1)
|
||||
|
||||
self.assertTrue('postComments.commentContent' in
|
||||
BlogPost.objects(comments__content='test')._query)
|
||||
self.assertEqual(len(BlogPost.objects(comments__content='test')), 1)
|
||||
self.assertEqual(BlogPost.objects(comments__content='test').count(), 1)
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
@@ -79,7 +98,7 @@ class TransformTest(unittest.TestCase):
|
||||
|
||||
self.assertTrue('_id' in BlogPost.objects(pk=data['title'])._query)
|
||||
self.assertTrue('_id' in BlogPost.objects(title=data['title'])._query)
|
||||
self.assertEqual(len(BlogPost.objects(pk=data['title'])), 1)
|
||||
self.assertEqual(BlogPost.objects(pk=data['title']).count(), 1)
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
@@ -132,16 +151,95 @@ class TransformTest(unittest.TestCase):
|
||||
class Doc(Document):
|
||||
meta = {'allow_inheritance': False}
|
||||
|
||||
raw_query = Doc.objects(__raw__={'deleted': False,
|
||||
'scraped': 'yes',
|
||||
'$nor': [{'views.extracted': 'no'},
|
||||
{'attachments.views.extracted':'no'}]
|
||||
})._query
|
||||
raw_query = Doc.objects(__raw__={
|
||||
'deleted': False,
|
||||
'scraped': 'yes',
|
||||
'$nor': [
|
||||
{'views.extracted': 'no'},
|
||||
{'attachments.views.extracted': 'no'}
|
||||
]
|
||||
})._query
|
||||
|
||||
expected = {'deleted': False, 'scraped': 'yes',
|
||||
'$nor': [{'views.extracted': 'no'},
|
||||
{'attachments.views.extracted': 'no'}]}
|
||||
self.assertEqual(expected, raw_query)
|
||||
self.assertEqual(raw_query, {
|
||||
'deleted': False,
|
||||
'scraped': 'yes',
|
||||
'$nor': [
|
||||
{'views.extracted': 'no'},
|
||||
{'attachments.views.extracted': 'no'}
|
||||
]
|
||||
})
|
||||
|
||||
def test_geojson_PointField(self):
|
||||
class Location(Document):
|
||||
loc = PointField()
|
||||
|
||||
update = transform.update(Location, set__loc=[1, 2])
|
||||
self.assertEqual(update, {'$set': {'loc': {"type": "Point", "coordinates": [1, 2]}}})
|
||||
|
||||
update = transform.update(Location, set__loc={"type": "Point", "coordinates": [1, 2]})
|
||||
self.assertEqual(update, {'$set': {'loc': {"type": "Point", "coordinates": [1, 2]}}})
|
||||
|
||||
def test_geojson_LineStringField(self):
|
||||
class Location(Document):
|
||||
line = LineStringField()
|
||||
|
||||
update = transform.update(Location, set__line=[[1, 2], [2, 2]])
|
||||
self.assertEqual(update, {'$set': {'line': {"type": "LineString", "coordinates": [[1, 2], [2, 2]]}}})
|
||||
|
||||
update = transform.update(Location, set__line={"type": "LineString", "coordinates": [[1, 2], [2, 2]]})
|
||||
self.assertEqual(update, {'$set': {'line': {"type": "LineString", "coordinates": [[1, 2], [2, 2]]}}})
|
||||
|
||||
def test_geojson_PolygonField(self):
|
||||
class Location(Document):
|
||||
poly = PolygonField()
|
||||
|
||||
update = transform.update(Location, set__poly=[[[40, 5], [40, 6], [41, 6], [40, 5]]])
|
||||
self.assertEqual(update, {'$set': {'poly': {"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]}}})
|
||||
|
||||
update = transform.update(Location, set__poly={"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]})
|
||||
self.assertEqual(update, {'$set': {'poly': {"type": "Polygon", "coordinates": [[[40, 5], [40, 6], [41, 6], [40, 5]]]}}})
|
||||
|
||||
def test_type(self):
|
||||
class Doc(Document):
|
||||
df = DynamicField()
|
||||
Doc(df=True).save()
|
||||
Doc(df=7).save()
|
||||
Doc(df="df").save()
|
||||
self.assertEqual(Doc.objects(df__type=1).count(), 0) # double
|
||||
self.assertEqual(Doc.objects(df__type=8).count(), 1) # bool
|
||||
self.assertEqual(Doc.objects(df__type=2).count(), 1) # str
|
||||
self.assertEqual(Doc.objects(df__type=16).count(), 1) # int
|
||||
|
||||
def test_last_field_name_like_operator(self):
|
||||
class EmbeddedItem(EmbeddedDocument):
|
||||
type = StringField()
|
||||
name = StringField()
|
||||
|
||||
class Doc(Document):
|
||||
item = EmbeddedDocumentField(EmbeddedItem)
|
||||
|
||||
Doc.drop_collection()
|
||||
|
||||
doc = Doc(item=EmbeddedItem(type="axe", name="Heroic axe"))
|
||||
doc.save()
|
||||
|
||||
self.assertEqual(1, Doc.objects(item__type__="axe").count())
|
||||
self.assertEqual(1, Doc.objects(item__name__="Heroic axe").count())
|
||||
|
||||
Doc.objects(id=doc.id).update(set__item__type__='sword')
|
||||
self.assertEqual(1, Doc.objects(item__type__="sword").count())
|
||||
self.assertEqual(0, Doc.objects(item__type__="axe").count())
|
||||
|
||||
def test_understandable_error_raised(self):
|
||||
class Event(Document):
|
||||
title = StringField()
|
||||
location = GeoPointField()
|
||||
|
||||
box = [(35.0, -125.0), (40.0, -100.0)]
|
||||
# I *meant* to execute location__within_box=box
|
||||
events = Event.objects(location__within=box)
|
||||
with self.assertRaises(InvalidQueryError):
|
||||
events.count()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
from __future__ import with_statement
|
||||
import sys
|
||||
sys.path[0:0] = [""]
|
||||
|
||||
import datetime
|
||||
import re
|
||||
import unittest
|
||||
|
||||
from bson import ObjectId
|
||||
from datetime import datetime
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine.queryset import Q
|
||||
from mongoengine.errors import InvalidQueryError
|
||||
from mongoengine.queryset import Q
|
||||
|
||||
__all__ = ("QTest",)
|
||||
|
||||
@@ -69,11 +66,11 @@ class QTest(unittest.TestCase):
|
||||
x = IntField()
|
||||
y = StringField()
|
||||
|
||||
# Check than an error is raised when conflicting queries are anded
|
||||
def invalid_combination():
|
||||
query = Q(x__lt=7) & Q(x__lt=3)
|
||||
query.to_query(TestDoc)
|
||||
self.assertRaises(InvalidQueryError, invalid_combination)
|
||||
query = (Q(x__lt=7) & Q(x__lt=3)).to_query(TestDoc)
|
||||
self.assertEqual(query, {'$and': [{'x': {'$lt': 7}}, {'x': {'$lt': 3}}]})
|
||||
|
||||
query = (Q(y="a") & Q(x__lt=7) & Q(x__lt=3)).to_query(TestDoc)
|
||||
self.assertEqual(query, {'$and': [{'y': "a"}, {'x': {'$lt': 7}}, {'x': {'$lt': 3}}]})
|
||||
|
||||
# Check normal cases work without an error
|
||||
query = Q(x__lt=7) & Q(x__gt=3)
|
||||
@@ -133,12 +130,12 @@ class QTest(unittest.TestCase):
|
||||
TestDoc(x=10).save()
|
||||
TestDoc(y=True).save()
|
||||
|
||||
self.assertEqual(query,
|
||||
{'$and': [
|
||||
{'$or': [{'x': {'$gt': 0}}, {'x': {'$exists': False}}]},
|
||||
{'$or': [{'x': {'$lt': 100}}, {'y': True}]}
|
||||
]})
|
||||
|
||||
self.assertEqual(query, {
|
||||
'$and': [
|
||||
{'$or': [{'x': {'$gt': 0}}, {'x': {'$exists': False}}]},
|
||||
{'$or': [{'x': {'$lt': 100}}, {'y': True}]}
|
||||
]
|
||||
})
|
||||
self.assertEqual(2, TestDoc.objects(q1 & q2).count())
|
||||
|
||||
def test_or_and_or_combination(self):
|
||||
@@ -158,15 +155,14 @@ class QTest(unittest.TestCase):
|
||||
q2 = (Q(x__lt=100) & (Q(y=False) | Q(y__exists=False)))
|
||||
query = (q1 | q2).to_query(TestDoc)
|
||||
|
||||
self.assertEqual(query,
|
||||
{'$or': [
|
||||
self.assertEqual(query, {
|
||||
'$or': [
|
||||
{'$and': [{'x': {'$gt': 0}},
|
||||
{'$or': [{'y': True}, {'y': {'$exists': False}}]}]},
|
||||
{'$and': [{'x': {'$lt': 100}},
|
||||
{'$or': [{'y': False}, {'y': {'$exists': False}}]}]}
|
||||
]}
|
||||
)
|
||||
|
||||
]
|
||||
})
|
||||
self.assertEqual(2, TestDoc.objects(q1 | q2).count())
|
||||
|
||||
def test_multiple_occurence_in_field(self):
|
||||
@@ -189,7 +185,7 @@ class QTest(unittest.TestCase):
|
||||
x = IntField()
|
||||
|
||||
TestDoc.drop_collection()
|
||||
for i in xrange(1, 101):
|
||||
for i in range(1, 101):
|
||||
t = TestDoc(x=i)
|
||||
t.save()
|
||||
|
||||
@@ -216,19 +212,19 @@ class QTest(unittest.TestCase):
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
post1 = BlogPost(title='Test 1', publish_date=datetime(2010, 1, 8), published=False)
|
||||
post1 = BlogPost(title='Test 1', publish_date=datetime.datetime(2010, 1, 8), published=False)
|
||||
post1.save()
|
||||
|
||||
post2 = BlogPost(title='Test 2', publish_date=datetime(2010, 1, 15), published=True)
|
||||
post2 = BlogPost(title='Test 2', publish_date=datetime.datetime(2010, 1, 15), published=True)
|
||||
post2.save()
|
||||
|
||||
post3 = BlogPost(title='Test 3', published=True)
|
||||
post3.save()
|
||||
|
||||
post4 = BlogPost(title='Test 4', publish_date=datetime(2010, 1, 8))
|
||||
post4 = BlogPost(title='Test 4', publish_date=datetime.datetime(2010, 1, 8))
|
||||
post4.save()
|
||||
|
||||
post5 = BlogPost(title='Test 1', publish_date=datetime(2010, 1, 15))
|
||||
post5 = BlogPost(title='Test 1', publish_date=datetime.datetime(2010, 1, 15))
|
||||
post5.save()
|
||||
|
||||
post6 = BlogPost(title='Test 1', published=False)
|
||||
@@ -251,7 +247,7 @@ class QTest(unittest.TestCase):
|
||||
self.assertTrue(all(obj.id in posts for obj in published_posts))
|
||||
|
||||
# Check Q object combination
|
||||
date = datetime(2010, 1, 10)
|
||||
date = datetime.datetime(2010, 1, 10)
|
||||
q = BlogPost.objects(Q(publish_date__lte=date) | Q(published=True))
|
||||
posts = [post.id for post in q]
|
||||
|
||||
@@ -268,16 +264,17 @@ class QTest(unittest.TestCase):
|
||||
self.Person(name='user3', age=30).save()
|
||||
self.Person(name='user4', age=40).save()
|
||||
|
||||
self.assertEqual(len(self.Person.objects(Q(age__in=[20]))), 2)
|
||||
self.assertEqual(len(self.Person.objects(Q(age__in=[20, 30]))), 3)
|
||||
self.assertEqual(self.Person.objects(Q(age__in=[20])).count(), 2)
|
||||
self.assertEqual(self.Person.objects(Q(age__in=[20, 30])).count(), 3)
|
||||
|
||||
# Test invalid query objs
|
||||
def wrong_query_objs():
|
||||
with self.assertRaises(InvalidQueryError):
|
||||
self.Person.objects('user1')
|
||||
def wrong_query_objs_filter():
|
||||
self.Person.objects('user1')
|
||||
self.assertRaises(InvalidQueryError, wrong_query_objs)
|
||||
self.assertRaises(InvalidQueryError, wrong_query_objs_filter)
|
||||
|
||||
# filter should fail, too
|
||||
with self.assertRaises(InvalidQueryError):
|
||||
self.Person.objects.filter('user1')
|
||||
|
||||
|
||||
def test_q_regex(self):
|
||||
"""Ensure that Q objects can be queried using regexes.
|
||||
@@ -285,7 +282,6 @@ class QTest(unittest.TestCase):
|
||||
person = self.Person(name='Guido van Rossum')
|
||||
person.save()
|
||||
|
||||
import re
|
||||
obj = self.Person.objects(Q(name=re.compile('^Gui'))).first()
|
||||
self.assertEqual(obj, person)
|
||||
obj = self.Person.objects(Q(name=re.compile('^gui'))).first()
|
||||
@@ -311,8 +307,8 @@ class QTest(unittest.TestCase):
|
||||
BlogPost(tags=['python', 'mongo']).save()
|
||||
BlogPost(tags=['python']).save()
|
||||
|
||||
self.assertEqual(len(BlogPost.objects(Q(tags='mongo'))), 1)
|
||||
self.assertEqual(len(BlogPost.objects(Q(tags='python'))), 2)
|
||||
self.assertEqual(BlogPost.objects(Q(tags='mongo')).count(), 1)
|
||||
self.assertEqual(BlogPost.objects(Q(tags='python')).count(), 2)
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
@@ -326,10 +322,26 @@ class QTest(unittest.TestCase):
|
||||
pk = ObjectId()
|
||||
User(email='example@example.com', pk=pk).save()
|
||||
|
||||
self.assertEqual(1, User.objects.filter(
|
||||
Q(email='example@example.com') |
|
||||
Q(name='John Doe')
|
||||
).limit(2).filter(pk=pk).count())
|
||||
self.assertEqual(1, User.objects.filter(Q(email='example@example.com') |
|
||||
Q(name='John Doe')).limit(2).filter(pk=pk).count())
|
||||
|
||||
def test_chained_q_or_filtering(self):
|
||||
|
||||
class Post(EmbeddedDocument):
|
||||
name = StringField(required=True)
|
||||
|
||||
class Item(Document):
|
||||
postables = ListField(EmbeddedDocumentField(Post))
|
||||
|
||||
Item.drop_collection()
|
||||
|
||||
Item(postables=[Post(name="a"), Post(name="b")]).save()
|
||||
Item(postables=[Post(name="a"), Post(name="c")]).save()
|
||||
Item(postables=[Post(name="a"), Post(name="b"), Post(name="c")]).save()
|
||||
|
||||
self.assertEqual(Item.objects(Q(postables__name="a") & Q(postables__name="b")).count(), 2)
|
||||
self.assertEqual(Item.objects.filter(postables__name="a").filter(postables__name="b").count(), 2)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
from __future__ import with_statement
|
||||
import sys
|
||||
sys.path[0:0] = [""]
|
||||
import unittest
|
||||
import datetime
|
||||
from pymongo.errors import OperationFailure
|
||||
|
||||
try:
|
||||
import unittest2 as unittest
|
||||
except ImportError:
|
||||
import unittest
|
||||
from nose.plugins.skip import SkipTest
|
||||
|
||||
import pymongo
|
||||
from bson.tz_util import utc
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine import (
|
||||
connect, register_connection,
|
||||
Document, DateTimeField
|
||||
)
|
||||
from mongoengine.python_support import IS_PYMONGO_3
|
||||
import mongoengine.connection
|
||||
from mongoengine.connection import get_db, get_connection, ConnectionError
|
||||
from mongoengine.context_managers import switch_db
|
||||
from mongoengine.connection import (MongoEngineConnectionError, get_db,
|
||||
get_connection)
|
||||
|
||||
|
||||
def get_tz_awareness(connection):
|
||||
if not IS_PYMONGO_3:
|
||||
return connection.tz_aware
|
||||
else:
|
||||
return connection.codec_options.tz_aware
|
||||
|
||||
|
||||
class ConnectionTest(unittest.TestCase):
|
||||
@@ -21,12 +35,11 @@ class ConnectionTest(unittest.TestCase):
|
||||
mongoengine.connection._dbs = {}
|
||||
|
||||
def test_connect(self):
|
||||
"""Ensure that the connect() method works properly.
|
||||
"""
|
||||
"""Ensure that the connect() method works properly."""
|
||||
connect('mongoenginetest')
|
||||
|
||||
conn = get_connection()
|
||||
self.assertTrue(isinstance(conn, pymongo.connection.Connection))
|
||||
self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient))
|
||||
|
||||
db = get_db()
|
||||
self.assertTrue(isinstance(db, pymongo.database.Database))
|
||||
@@ -34,11 +47,105 @@ class ConnectionTest(unittest.TestCase):
|
||||
|
||||
connect('mongoenginetest2', alias='testdb')
|
||||
conn = get_connection('testdb')
|
||||
self.assertTrue(isinstance(conn, pymongo.connection.Connection))
|
||||
self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient))
|
||||
|
||||
def test_connect_in_mocking(self):
|
||||
"""Ensure that the connect() method works properly in mocking.
|
||||
"""
|
||||
try:
|
||||
import mongomock
|
||||
except ImportError:
|
||||
raise SkipTest('you need mongomock installed to run this testcase')
|
||||
|
||||
connect('mongoenginetest', host='mongomock://localhost')
|
||||
conn = get_connection()
|
||||
self.assertTrue(isinstance(conn, mongomock.MongoClient))
|
||||
|
||||
connect('mongoenginetest2', host='mongomock://localhost', alias='testdb2')
|
||||
conn = get_connection('testdb2')
|
||||
self.assertTrue(isinstance(conn, mongomock.MongoClient))
|
||||
|
||||
connect('mongoenginetest3', host='mongodb://localhost', is_mock=True, alias='testdb3')
|
||||
conn = get_connection('testdb3')
|
||||
self.assertTrue(isinstance(conn, mongomock.MongoClient))
|
||||
|
||||
connect('mongoenginetest4', is_mock=True, alias='testdb4')
|
||||
conn = get_connection('testdb4')
|
||||
self.assertTrue(isinstance(conn, mongomock.MongoClient))
|
||||
|
||||
connect(host='mongodb://localhost:27017/mongoenginetest5', is_mock=True, alias='testdb5')
|
||||
conn = get_connection('testdb5')
|
||||
self.assertTrue(isinstance(conn, mongomock.MongoClient))
|
||||
|
||||
connect(host='mongomock://localhost:27017/mongoenginetest6', alias='testdb6')
|
||||
conn = get_connection('testdb6')
|
||||
self.assertTrue(isinstance(conn, mongomock.MongoClient))
|
||||
|
||||
connect(host='mongomock://localhost:27017/mongoenginetest7', is_mock=True, alias='testdb7')
|
||||
conn = get_connection('testdb7')
|
||||
self.assertTrue(isinstance(conn, mongomock.MongoClient))
|
||||
|
||||
def test_connect_with_host_list(self):
|
||||
"""Ensure that the connect() method works when host is a list
|
||||
|
||||
Uses mongomock to test w/o needing multiple mongod/mongos processes
|
||||
"""
|
||||
try:
|
||||
import mongomock
|
||||
except ImportError:
|
||||
raise SkipTest('you need mongomock installed to run this testcase')
|
||||
|
||||
connect(host=['mongomock://localhost'])
|
||||
conn = get_connection()
|
||||
self.assertTrue(isinstance(conn, mongomock.MongoClient))
|
||||
|
||||
connect(host=['mongodb://localhost'], is_mock=True, alias='testdb2')
|
||||
conn = get_connection('testdb2')
|
||||
self.assertTrue(isinstance(conn, mongomock.MongoClient))
|
||||
|
||||
connect(host=['localhost'], is_mock=True, alias='testdb3')
|
||||
conn = get_connection('testdb3')
|
||||
self.assertTrue(isinstance(conn, mongomock.MongoClient))
|
||||
|
||||
connect(host=['mongomock://localhost:27017', 'mongomock://localhost:27018'], alias='testdb4')
|
||||
conn = get_connection('testdb4')
|
||||
self.assertTrue(isinstance(conn, mongomock.MongoClient))
|
||||
|
||||
connect(host=['mongodb://localhost:27017', 'mongodb://localhost:27018'], is_mock=True, alias='testdb5')
|
||||
conn = get_connection('testdb5')
|
||||
self.assertTrue(isinstance(conn, mongomock.MongoClient))
|
||||
|
||||
connect(host=['localhost:27017', 'localhost:27018'], is_mock=True, alias='testdb6')
|
||||
conn = get_connection('testdb6')
|
||||
self.assertTrue(isinstance(conn, mongomock.MongoClient))
|
||||
|
||||
def test_disconnect(self):
|
||||
"""Ensure that the disconnect() method works properly
|
||||
"""
|
||||
conn1 = connect('mongoenginetest')
|
||||
mongoengine.connection.disconnect()
|
||||
conn2 = connect('mongoenginetest')
|
||||
self.assertTrue(conn1 is not conn2)
|
||||
|
||||
def test_sharing_connections(self):
|
||||
"""Ensure that connections are shared when the connection settings are exactly the same
|
||||
"""
|
||||
connect('mongoenginetests', alias='testdb1')
|
||||
expected_connection = get_connection('testdb1')
|
||||
|
||||
connect('mongoenginetests', alias='testdb2')
|
||||
actual_connection = get_connection('testdb2')
|
||||
|
||||
# Handle PyMongo 3+ Async Connection
|
||||
if IS_PYMONGO_3:
|
||||
# Ensure we are connected, throws ServerSelectionTimeoutError otherwise.
|
||||
# Purposely not catching exception to fail test if thrown.
|
||||
expected_connection.server_info()
|
||||
|
||||
self.assertEqual(expected_connection, actual_connection)
|
||||
|
||||
def test_connect_uri(self):
|
||||
"""Ensure that the connect() method works properly with uri's
|
||||
"""
|
||||
"""Ensure that the connect() method works properly with URIs."""
|
||||
c = connect(db='mongoenginetest', alias='admin')
|
||||
c.admin.system.users.remove({})
|
||||
c.mongoenginetest.system.users.remove({})
|
||||
@@ -47,41 +154,202 @@ class ConnectionTest(unittest.TestCase):
|
||||
c.admin.authenticate("admin", "password")
|
||||
c.mongoenginetest.add_user("username", "password")
|
||||
|
||||
self.assertRaises(ConnectionError, connect, "testdb_uri_bad", host='mongodb://test:password@localhost')
|
||||
if not IS_PYMONGO_3:
|
||||
self.assertRaises(
|
||||
MongoEngineConnectionError, connect, 'testdb_uri_bad',
|
||||
host='mongodb://test:password@localhost'
|
||||
)
|
||||
|
||||
connect("testdb_uri", host='mongodb://username:password@localhost/mongoenginetest')
|
||||
|
||||
conn = get_connection()
|
||||
self.assertTrue(isinstance(conn, pymongo.connection.Connection))
|
||||
self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient))
|
||||
|
||||
db = get_db()
|
||||
self.assertTrue(isinstance(db, pymongo.database.Database))
|
||||
self.assertEqual(db.name, 'mongoenginetest')
|
||||
|
||||
c.admin.system.users.remove({})
|
||||
c.mongoenginetest.system.users.remove({})
|
||||
|
||||
def test_connect_uri_without_db(self):
|
||||
"""Ensure connect() method works properly if the URI doesn't
|
||||
include a database name.
|
||||
"""
|
||||
connect("mongoenginetest", host='mongodb://localhost/')
|
||||
|
||||
conn = get_connection()
|
||||
self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient))
|
||||
|
||||
db = get_db()
|
||||
self.assertTrue(isinstance(db, pymongo.database.Database))
|
||||
self.assertEqual(db.name, 'mongoenginetest')
|
||||
|
||||
def test_connect_uri_default_db(self):
|
||||
"""Ensure connect() defaults to the right database name if
|
||||
the URI and the database_name don't explicitly specify it.
|
||||
"""
|
||||
connect(host='mongodb://localhost/')
|
||||
|
||||
conn = get_connection()
|
||||
self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient))
|
||||
|
||||
db = get_db()
|
||||
self.assertTrue(isinstance(db, pymongo.database.Database))
|
||||
self.assertEqual(db.name, 'test')
|
||||
|
||||
def test_uri_without_credentials_doesnt_override_conn_settings(self):
|
||||
"""Ensure connect() uses the username & password params if the URI
|
||||
doesn't explicitly specify them.
|
||||
"""
|
||||
c = connect(host='mongodb://localhost/mongoenginetest',
|
||||
username='user',
|
||||
password='pass')
|
||||
|
||||
# OperationFailure means that mongoengine attempted authentication
|
||||
# w/ the provided username/password and failed - that's the desired
|
||||
# behavior. If the MongoDB URI would override the credentials
|
||||
self.assertRaises(OperationFailure, get_db)
|
||||
|
||||
def test_connect_uri_with_authsource(self):
|
||||
"""Ensure that the connect() method works well with `authSource`
|
||||
option in the URI.
|
||||
"""
|
||||
# Create users
|
||||
c = connect('mongoenginetest')
|
||||
c.admin.system.users.remove({})
|
||||
c.admin.add_user('username2', 'password')
|
||||
|
||||
# Authentication fails without "authSource"
|
||||
if IS_PYMONGO_3:
|
||||
test_conn = connect(
|
||||
'mongoenginetest', alias='test1',
|
||||
host='mongodb://username2:password@localhost/mongoenginetest'
|
||||
)
|
||||
self.assertRaises(OperationFailure, test_conn.server_info)
|
||||
else:
|
||||
self.assertRaises(
|
||||
MongoEngineConnectionError,
|
||||
connect, 'mongoenginetest', alias='test1',
|
||||
host='mongodb://username2:password@localhost/mongoenginetest'
|
||||
)
|
||||
self.assertRaises(MongoEngineConnectionError, get_db, 'test1')
|
||||
|
||||
# Authentication succeeds with "authSource"
|
||||
authd_conn = connect(
|
||||
'mongoenginetest', alias='test2',
|
||||
host=('mongodb://username2:password@localhost/'
|
||||
'mongoenginetest?authSource=admin')
|
||||
)
|
||||
db = get_db('test2')
|
||||
self.assertTrue(isinstance(db, pymongo.database.Database))
|
||||
self.assertEqual(db.name, 'mongoenginetest')
|
||||
|
||||
# Clear all users
|
||||
authd_conn.admin.system.users.remove({})
|
||||
|
||||
def test_register_connection(self):
|
||||
"""Ensure that connections with different aliases may be registered.
|
||||
"""
|
||||
register_connection('testdb', 'mongoenginetest2')
|
||||
|
||||
self.assertRaises(ConnectionError, get_connection)
|
||||
self.assertRaises(MongoEngineConnectionError, get_connection)
|
||||
conn = get_connection('testdb')
|
||||
self.assertTrue(isinstance(conn, pymongo.connection.Connection))
|
||||
self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient))
|
||||
|
||||
db = get_db('testdb')
|
||||
self.assertTrue(isinstance(db, pymongo.database.Database))
|
||||
self.assertEqual(db.name, 'mongoenginetest2')
|
||||
|
||||
def test_connection_kwargs(self):
|
||||
"""Ensure that connection kwargs get passed to pymongo.
|
||||
def test_register_connection_defaults(self):
|
||||
"""Ensure that defaults are used when the host and port are None.
|
||||
"""
|
||||
register_connection('testdb', 'mongoenginetest', host=None, port=None)
|
||||
|
||||
conn = get_connection('testdb')
|
||||
self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient))
|
||||
|
||||
def test_connection_kwargs(self):
|
||||
"""Ensure that connection kwargs get passed to pymongo."""
|
||||
connect('mongoenginetest', alias='t1', tz_aware=True)
|
||||
conn = get_connection('t1')
|
||||
|
||||
self.assertTrue(conn.tz_aware)
|
||||
self.assertTrue(get_tz_awareness(conn))
|
||||
|
||||
connect('mongoenginetest2', alias='t2')
|
||||
conn = get_connection('t2')
|
||||
self.assertFalse(conn.tz_aware)
|
||||
self.assertFalse(get_tz_awareness(conn))
|
||||
|
||||
def test_connection_pool_via_kwarg(self):
|
||||
"""Ensure we can specify a max connection pool size using
|
||||
a connection kwarg.
|
||||
"""
|
||||
# Use "max_pool_size" or "maxpoolsize" depending on PyMongo version
|
||||
# (former was changed to the latter as described in
|
||||
# https://jira.mongodb.org/browse/PYTHON-854).
|
||||
# TODO remove once PyMongo < 3.0 support is dropped
|
||||
if pymongo.version_tuple[0] >= 3:
|
||||
pool_size_kwargs = {'maxpoolsize': 100}
|
||||
else:
|
||||
pool_size_kwargs = {'max_pool_size': 100}
|
||||
|
||||
conn = connect('mongoenginetest', alias='max_pool_size_via_kwarg', **pool_size_kwargs)
|
||||
self.assertEqual(conn.max_pool_size, 100)
|
||||
|
||||
def test_connection_pool_via_uri(self):
|
||||
"""Ensure we can specify a max connection pool size using
|
||||
an option in a connection URI.
|
||||
"""
|
||||
if pymongo.version_tuple[0] == 2 and pymongo.version_tuple[1] < 9:
|
||||
raise SkipTest('maxpoolsize as a URI option is only supported in PyMongo v2.9+')
|
||||
|
||||
conn = connect(host='mongodb://localhost/test?maxpoolsize=100', alias='max_pool_size_via_uri')
|
||||
self.assertEqual(conn.max_pool_size, 100)
|
||||
|
||||
def test_write_concern(self):
|
||||
"""Ensure write concern can be specified in connect() via
|
||||
a kwarg or as part of the connection URI.
|
||||
"""
|
||||
conn1 = connect(alias='conn1', host='mongodb://localhost/testing?w=1&j=true')
|
||||
conn2 = connect('testing', alias='conn2', w=1, j=True)
|
||||
if IS_PYMONGO_3:
|
||||
self.assertEqual(conn1.write_concern.document, {'w': 1, 'j': True})
|
||||
self.assertEqual(conn2.write_concern.document, {'w': 1, 'j': True})
|
||||
else:
|
||||
self.assertEqual(dict(conn1.write_concern), {'w': 1, 'j': True})
|
||||
self.assertEqual(dict(conn2.write_concern), {'w': 1, 'j': True})
|
||||
|
||||
def test_connect_with_replicaset_via_uri(self):
|
||||
"""Ensure connect() works when specifying a replicaSet via the
|
||||
MongoDB URI.
|
||||
"""
|
||||
if IS_PYMONGO_3:
|
||||
c = connect(host='mongodb://localhost/test?replicaSet=local-rs')
|
||||
db = get_db()
|
||||
self.assertTrue(isinstance(db, pymongo.database.Database))
|
||||
self.assertEqual(db.name, 'test')
|
||||
else:
|
||||
# PyMongo < v3.x raises an exception:
|
||||
# "localhost:27017 is not a member of replica set local-rs"
|
||||
with self.assertRaises(MongoEngineConnectionError):
|
||||
c = connect(host='mongodb://localhost/test?replicaSet=local-rs')
|
||||
|
||||
def test_connect_with_replicaset_via_kwargs(self):
|
||||
"""Ensure connect() works when specifying a replicaSet via the
|
||||
connection kwargs
|
||||
"""
|
||||
if IS_PYMONGO_3:
|
||||
c = connect(replicaset='local-rs')
|
||||
self.assertEqual(c._MongoClient__options.replica_set_name,
|
||||
'local-rs')
|
||||
db = get_db()
|
||||
self.assertTrue(isinstance(db, pymongo.database.Database))
|
||||
self.assertEqual(db.name, 'test')
|
||||
else:
|
||||
# PyMongo < v3.x raises an exception:
|
||||
# "localhost:27017 is not a member of replica set local-rs"
|
||||
with self.assertRaises(MongoEngineConnectionError):
|
||||
c = connect(replicaset='local-rs')
|
||||
|
||||
def test_datetime(self):
|
||||
connect('mongoenginetest', tz_aware=True)
|
||||
@@ -96,6 +364,27 @@ class ConnectionTest(unittest.TestCase):
|
||||
date_doc = DateDoc.objects.first()
|
||||
self.assertEqual(d, date_doc.the_date)
|
||||
|
||||
def test_multiple_connection_settings(self):
|
||||
connect('mongoenginetest', alias='t1', host="localhost")
|
||||
|
||||
connect('mongoenginetest2', alias='t2', host="127.0.0.1")
|
||||
|
||||
mongo_connections = mongoengine.connection._connections
|
||||
self.assertEqual(len(mongo_connections.items()), 2)
|
||||
self.assertTrue('t1' in mongo_connections.keys())
|
||||
self.assertTrue('t2' in mongo_connections.keys())
|
||||
if not IS_PYMONGO_3:
|
||||
self.assertEqual(mongo_connections['t1'].host, 'localhost')
|
||||
self.assertEqual(mongo_connections['t2'].host, '127.0.0.1')
|
||||
else:
|
||||
# Handle PyMongo 3+ Async Connection
|
||||
# Ensure we are connected, throws ServerSelectionTimeoutError otherwise.
|
||||
# Purposely not catching exception to fail test if thrown.
|
||||
mongo_connections['t1'].server_info()
|
||||
mongo_connections['t2'].server_info()
|
||||
self.assertEqual(mongo_connections['t1'].address[0], 'localhost')
|
||||
self.assertEqual(mongo_connections['t2'].address[0], '127.0.0.1')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
from __future__ import with_statement
|
||||
import sys
|
||||
sys.path[0:0] = [""]
|
||||
import unittest
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine.connection import get_db
|
||||
from mongoengine.context_managers import (switch_db, switch_collection,
|
||||
no_dereference, query_counter)
|
||||
no_sub_classes, no_dereference,
|
||||
query_counter)
|
||||
|
||||
|
||||
class ContextManagersTest(unittest.TestCase):
|
||||
@@ -79,7 +77,7 @@ class ContextManagersTest(unittest.TestCase):
|
||||
User.drop_collection()
|
||||
Group.drop_collection()
|
||||
|
||||
for i in xrange(1, 51):
|
||||
for i in range(1, 51):
|
||||
User(name='user %s' % i).save()
|
||||
|
||||
user = User.objects.first()
|
||||
@@ -117,7 +115,7 @@ class ContextManagersTest(unittest.TestCase):
|
||||
User.drop_collection()
|
||||
Group.drop_collection()
|
||||
|
||||
for i in xrange(1, 51):
|
||||
for i in range(1, 51):
|
||||
User(name='user %s' % i).save()
|
||||
|
||||
user = User.objects.first()
|
||||
@@ -139,6 +137,54 @@ class ContextManagersTest(unittest.TestCase):
|
||||
self.assertTrue(isinstance(group.ref, User))
|
||||
self.assertTrue(isinstance(group.generic, User))
|
||||
|
||||
def test_no_sub_classes(self):
|
||||
class A(Document):
|
||||
x = IntField()
|
||||
y = IntField()
|
||||
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
class B(A):
|
||||
z = IntField()
|
||||
|
||||
class C(B):
|
||||
zz = IntField()
|
||||
|
||||
A.drop_collection()
|
||||
|
||||
A(x=10, y=20).save()
|
||||
A(x=15, y=30).save()
|
||||
B(x=20, y=40).save()
|
||||
B(x=30, y=50).save()
|
||||
C(x=40, y=60).save()
|
||||
|
||||
self.assertEqual(A.objects.count(), 5)
|
||||
self.assertEqual(B.objects.count(), 3)
|
||||
self.assertEqual(C.objects.count(), 1)
|
||||
|
||||
with no_sub_classes(A) as A:
|
||||
self.assertEqual(A.objects.count(), 2)
|
||||
|
||||
for obj in A.objects:
|
||||
self.assertEqual(obj.__class__, A)
|
||||
|
||||
with no_sub_classes(B) as B:
|
||||
self.assertEqual(B.objects.count(), 2)
|
||||
|
||||
for obj in B.objects:
|
||||
self.assertEqual(obj.__class__, B)
|
||||
|
||||
with no_sub_classes(C) as C:
|
||||
self.assertEqual(C.objects.count(), 1)
|
||||
|
||||
for obj in C.objects:
|
||||
self.assertEqual(obj.__class__, C)
|
||||
|
||||
# Confirm context manager exit correctly
|
||||
self.assertEqual(A.objects.count(), 5)
|
||||
self.assertEqual(B.objects.count(), 3)
|
||||
self.assertEqual(C.objects.count(), 1)
|
||||
|
||||
def test_query_counter(self):
|
||||
connect('mongoenginetest')
|
||||
db = get_db()
|
||||
@@ -147,7 +193,7 @@ class ContextManagersTest(unittest.TestCase):
|
||||
with query_counter() as q:
|
||||
self.assertEqual(0, q)
|
||||
|
||||
for i in xrange(1, 51):
|
||||
for i in range(1, 51):
|
||||
db.test.find({}).count()
|
||||
|
||||
self.assertEqual(50, q)
|
||||
|
||||
80
tests/test_datastructures.py
Normal file
80
tests/test_datastructures.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import unittest
|
||||
|
||||
from mongoengine.base.datastructures import StrictDict
|
||||
|
||||
|
||||
class TestStrictDict(unittest.TestCase):
|
||||
def strict_dict_class(self, *args, **kwargs):
|
||||
return StrictDict.create(*args, **kwargs)
|
||||
|
||||
def setUp(self):
|
||||
self.dtype = self.strict_dict_class(("a", "b", "c"))
|
||||
|
||||
def test_init(self):
|
||||
d = self.dtype(a=1, b=1, c=1)
|
||||
self.assertEqual((d.a, d.b, d.c), (1, 1, 1))
|
||||
|
||||
def test_repr(self):
|
||||
d = self.dtype(a=1, b=2, c=3)
|
||||
self.assertEqual(repr(d), '{"a": 1, "b": 2, "c": 3}')
|
||||
|
||||
# make sure quotes are escaped properly
|
||||
d = self.dtype(a='"', b="'", c="")
|
||||
self.assertEqual(repr(d), '{"a": \'"\', "b": "\'", "c": \'\'}')
|
||||
|
||||
def test_init_fails_on_nonexisting_attrs(self):
|
||||
with self.assertRaises(AttributeError):
|
||||
self.dtype(a=1, b=2, d=3)
|
||||
|
||||
def test_eq(self):
|
||||
d = self.dtype(a=1, b=1, c=1)
|
||||
dd = self.dtype(a=1, b=1, c=1)
|
||||
e = self.dtype(a=1, b=1, c=3)
|
||||
f = self.dtype(a=1, b=1)
|
||||
g = self.strict_dict_class(("a", "b", "c", "d"))(a=1, b=1, c=1, d=1)
|
||||
h = self.strict_dict_class(("a", "c", "b"))(a=1, b=1, c=1)
|
||||
i = self.strict_dict_class(("a", "c", "b"))(a=1, b=1, c=2)
|
||||
|
||||
self.assertEqual(d, dd)
|
||||
self.assertNotEqual(d, e)
|
||||
self.assertNotEqual(d, f)
|
||||
self.assertNotEqual(d, g)
|
||||
self.assertNotEqual(f, d)
|
||||
self.assertEqual(d, h)
|
||||
self.assertNotEqual(d, i)
|
||||
|
||||
def test_setattr_getattr(self):
|
||||
d = self.dtype()
|
||||
d.a = 1
|
||||
self.assertEqual(d.a, 1)
|
||||
self.assertRaises(AttributeError, getattr, d, 'b')
|
||||
|
||||
def test_setattr_raises_on_nonexisting_attr(self):
|
||||
d = self.dtype()
|
||||
with self.assertRaises(AttributeError):
|
||||
d.x = 1
|
||||
|
||||
def test_setattr_getattr_special(self):
|
||||
d = self.strict_dict_class(["items"])
|
||||
d.items = 1
|
||||
self.assertEqual(d.items, 1)
|
||||
|
||||
def test_get(self):
|
||||
d = self.dtype(a=1)
|
||||
self.assertEqual(d.get('a'), 1)
|
||||
self.assertEqual(d.get('b', 'bla'), 'bla')
|
||||
|
||||
def test_items(self):
|
||||
d = self.dtype(a=1)
|
||||
self.assertEqual(d.items(), [('a', 1)])
|
||||
d = self.dtype(a=1, b=2)
|
||||
self.assertEqual(d.items(), [('a', 1), ('b', 2)])
|
||||
|
||||
def test_mappings_protocol(self):
|
||||
d = self.dtype(a=1, b=2)
|
||||
assert dict(d) == {'a': 1, 'b': 2}
|
||||
assert dict(**d) == {'a': 1, 'b': 2}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,7 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import with_statement
|
||||
import sys
|
||||
sys.path[0:0] = [""]
|
||||
import unittest
|
||||
|
||||
from bson import DBRef, ObjectId
|
||||
@@ -13,9 +10,13 @@ from mongoengine.context_managers import query_counter
|
||||
|
||||
class FieldTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
connect(db='mongoenginetest')
|
||||
self.db = get_db()
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.db = connect(db='mongoenginetest')
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.db.drop_database('mongoenginetest')
|
||||
|
||||
def test_list_item_dereference(self):
|
||||
"""Ensure that DBRef items in ListFields are dereferenced.
|
||||
@@ -29,7 +30,7 @@ class FieldTest(unittest.TestCase):
|
||||
User.drop_collection()
|
||||
Group.drop_collection()
|
||||
|
||||
for i in xrange(1, 51):
|
||||
for i in range(1, 51):
|
||||
user = User(name='user %s' % i)
|
||||
user.save()
|
||||
|
||||
@@ -87,7 +88,7 @@ class FieldTest(unittest.TestCase):
|
||||
User.drop_collection()
|
||||
Group.drop_collection()
|
||||
|
||||
for i in xrange(1, 51):
|
||||
for i in range(1, 51):
|
||||
user = User(name='user %s' % i)
|
||||
user.save()
|
||||
|
||||
@@ -159,7 +160,7 @@ class FieldTest(unittest.TestCase):
|
||||
User.drop_collection()
|
||||
Group.drop_collection()
|
||||
|
||||
for i in xrange(1, 26):
|
||||
for i in range(1, 26):
|
||||
user = User(name='user %s' % i)
|
||||
user.save()
|
||||
|
||||
@@ -292,9 +293,38 @@ class FieldTest(unittest.TestCase):
|
||||
self.assertEqual(employee.friends, friends)
|
||||
self.assertEqual(q, 2)
|
||||
|
||||
def test_list_of_lists_of_references(self):
|
||||
|
||||
class User(Document):
|
||||
name = StringField()
|
||||
|
||||
class Post(Document):
|
||||
user_lists = ListField(ListField(ReferenceField(User)))
|
||||
|
||||
class SimpleList(Document):
|
||||
users = ListField(ReferenceField(User))
|
||||
|
||||
User.drop_collection()
|
||||
Post.drop_collection()
|
||||
SimpleList.drop_collection()
|
||||
|
||||
u1 = User.objects.create(name='u1')
|
||||
u2 = User.objects.create(name='u2')
|
||||
u3 = User.objects.create(name='u3')
|
||||
|
||||
SimpleList.objects.create(users=[u1, u2, u3])
|
||||
self.assertEqual(SimpleList.objects.all()[0].users, [u1, u2, u3])
|
||||
|
||||
Post.objects.create(user_lists=[[u1, u2], [u3]])
|
||||
self.assertEqual(Post.objects.all()[0].user_lists, [[u1, u2], [u3]])
|
||||
|
||||
def test_circular_reference(self):
|
||||
"""Ensure you can handle circular references
|
||||
"""
|
||||
class Relation(EmbeddedDocument):
|
||||
name = StringField()
|
||||
person = ReferenceField('Person')
|
||||
|
||||
class Person(Document):
|
||||
name = StringField()
|
||||
relations = ListField(EmbeddedDocumentField('Relation'))
|
||||
@@ -302,10 +332,6 @@ class FieldTest(unittest.TestCase):
|
||||
def __repr__(self):
|
||||
return "<Person: %s>" % self.name
|
||||
|
||||
class Relation(EmbeddedDocument):
|
||||
name = StringField()
|
||||
person = ReferenceField('Person')
|
||||
|
||||
Person.drop_collection()
|
||||
mother = Person(name="Mother")
|
||||
daughter = Person(name="Daughter")
|
||||
@@ -412,7 +438,7 @@ class FieldTest(unittest.TestCase):
|
||||
Group.drop_collection()
|
||||
|
||||
members = []
|
||||
for i in xrange(1, 51):
|
||||
for i in range(1, 51):
|
||||
a = UserA(name='User A %s' % i)
|
||||
a.save()
|
||||
|
||||
@@ -503,7 +529,7 @@ class FieldTest(unittest.TestCase):
|
||||
Group.drop_collection()
|
||||
|
||||
members = []
|
||||
for i in xrange(1, 51):
|
||||
for i in range(1, 51):
|
||||
a = UserA(name='User A %s' % i)
|
||||
a.save()
|
||||
|
||||
@@ -586,15 +612,15 @@ class FieldTest(unittest.TestCase):
|
||||
Group.drop_collection()
|
||||
|
||||
members = []
|
||||
for i in xrange(1, 51):
|
||||
for i in range(1, 51):
|
||||
user = User(name='user %s' % i)
|
||||
user.save()
|
||||
members.append(user)
|
||||
|
||||
group = Group(members=dict([(str(u.id), u) for u in members]))
|
||||
group = Group(members={str(u.id): u for u in members})
|
||||
group.save()
|
||||
|
||||
group = Group(members=dict([(str(u.id), u) for u in members]))
|
||||
group = Group(members={str(u.id): u for u in members})
|
||||
group.save()
|
||||
|
||||
with query_counter() as q:
|
||||
@@ -659,7 +685,7 @@ class FieldTest(unittest.TestCase):
|
||||
Group.drop_collection()
|
||||
|
||||
members = []
|
||||
for i in xrange(1, 51):
|
||||
for i in range(1, 51):
|
||||
a = UserA(name='User A %s' % i)
|
||||
a.save()
|
||||
|
||||
@@ -671,9 +697,9 @@ class FieldTest(unittest.TestCase):
|
||||
|
||||
members += [a, b, c]
|
||||
|
||||
group = Group(members=dict([(str(u.id), u) for u in members]))
|
||||
group = Group(members={str(u.id): u for u in members})
|
||||
group.save()
|
||||
group = Group(members=dict([(str(u.id), u) for u in members]))
|
||||
group = Group(members={str(u.id): u for u in members})
|
||||
group.save()
|
||||
|
||||
with query_counter() as q:
|
||||
@@ -755,16 +781,16 @@ class FieldTest(unittest.TestCase):
|
||||
Group.drop_collection()
|
||||
|
||||
members = []
|
||||
for i in xrange(1, 51):
|
||||
for i in range(1, 51):
|
||||
a = UserA(name='User A %s' % i)
|
||||
a.save()
|
||||
|
||||
members += [a]
|
||||
|
||||
group = Group(members=dict([(str(u.id), u) for u in members]))
|
||||
group = Group(members={str(u.id): u for u in members})
|
||||
group.save()
|
||||
|
||||
group = Group(members=dict([(str(u.id), u) for u in members]))
|
||||
group = Group(members={str(u.id): u for u in members})
|
||||
group.save()
|
||||
|
||||
with query_counter() as q:
|
||||
@@ -838,7 +864,7 @@ class FieldTest(unittest.TestCase):
|
||||
Group.drop_collection()
|
||||
|
||||
members = []
|
||||
for i in xrange(1, 51):
|
||||
for i in range(1, 51):
|
||||
a = UserA(name='User A %s' % i)
|
||||
a.save()
|
||||
|
||||
@@ -850,9 +876,9 @@ class FieldTest(unittest.TestCase):
|
||||
|
||||
members += [a, b, c]
|
||||
|
||||
group = Group(members=dict([(str(u.id), u) for u in members]))
|
||||
group = Group(members={str(u.id): u for u in members})
|
||||
group.save()
|
||||
group = Group(members=dict([(str(u.id), u) for u in members]))
|
||||
group = Group(members={str(u.id): u for u in members})
|
||||
group.save()
|
||||
|
||||
with query_counter() as q:
|
||||
@@ -924,6 +950,8 @@ class FieldTest(unittest.TestCase):
|
||||
|
||||
class Asset(Document):
|
||||
name = StringField(max_length=250, required=True)
|
||||
path = StringField()
|
||||
title = StringField()
|
||||
parent = GenericReferenceField(default=None)
|
||||
parents = ListField(GenericReferenceField())
|
||||
children = ListField(GenericReferenceField())
|
||||
@@ -1001,6 +1029,43 @@ class FieldTest(unittest.TestCase):
|
||||
self.assertEqual(type(foo.bar), Bar)
|
||||
self.assertEqual(type(foo.baz), Baz)
|
||||
|
||||
|
||||
def test_document_reload_reference_integrity(self):
|
||||
"""
|
||||
Ensure reloading a document with multiple similar id
|
||||
in different collections doesn't mix them.
|
||||
"""
|
||||
class Topic(Document):
|
||||
id = IntField(primary_key=True)
|
||||
class User(Document):
|
||||
id = IntField(primary_key=True)
|
||||
name = StringField()
|
||||
class Message(Document):
|
||||
id = IntField(primary_key=True)
|
||||
topic = ReferenceField(Topic)
|
||||
author = ReferenceField(User)
|
||||
|
||||
Topic.drop_collection()
|
||||
User.drop_collection()
|
||||
Message.drop_collection()
|
||||
|
||||
# All objects share the same id, but each in a different collection
|
||||
topic = Topic(id=1).save()
|
||||
user = User(id=1, name='user-name').save()
|
||||
Message(id=1, topic=topic, author=user).save()
|
||||
|
||||
concurrent_change_user = User.objects.get(id=1)
|
||||
concurrent_change_user.name = 'new-name'
|
||||
concurrent_change_user.save()
|
||||
self.assertNotEqual(user.name, 'new-name')
|
||||
|
||||
msg = Message.objects.get(id=1)
|
||||
msg.reload()
|
||||
self.assertEqual(msg.topic, topic)
|
||||
self.assertEqual(msg.author, user)
|
||||
self.assertEqual(msg.author.name, 'new-name')
|
||||
|
||||
|
||||
def test_list_lookup_not_checked_in_map(self):
|
||||
"""Ensure we dereference list data correctly
|
||||
"""
|
||||
@@ -1036,7 +1101,7 @@ class FieldTest(unittest.TestCase):
|
||||
User.drop_collection()
|
||||
Group.drop_collection()
|
||||
|
||||
for i in xrange(1, 51):
|
||||
for i in range(1, 51):
|
||||
User(name='user %s' % i).save()
|
||||
|
||||
Group(name="Test", members=User.objects).save()
|
||||
@@ -1065,7 +1130,7 @@ class FieldTest(unittest.TestCase):
|
||||
User.drop_collection()
|
||||
Group.drop_collection()
|
||||
|
||||
for i in xrange(1, 51):
|
||||
for i in range(1, 51):
|
||||
User(name='user %s' % i).save()
|
||||
|
||||
Group(name="Test", members=User.objects).save()
|
||||
@@ -1102,7 +1167,7 @@ class FieldTest(unittest.TestCase):
|
||||
Group.drop_collection()
|
||||
|
||||
members = []
|
||||
for i in xrange(1, 51):
|
||||
for i in range(1, 51):
|
||||
a = UserA(name='User A %s' % i).save()
|
||||
b = UserB(name='User B %s' % i).save()
|
||||
c = UserC(name='User C %s' % i).save()
|
||||
@@ -1122,37 +1187,32 @@ class FieldTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(q, 2)
|
||||
|
||||
def test_tuples_as_tuples(self):
|
||||
"""
|
||||
Ensure that tuples remain tuples when they are
|
||||
inside a ComplexBaseField
|
||||
"""
|
||||
from mongoengine.base import BaseField
|
||||
def test_objectid_reference_across_databases(self):
|
||||
# mongoenginetest - Is default connection alias from setUp()
|
||||
# Register Aliases
|
||||
register_connection('testdb-1', 'mongoenginetest2')
|
||||
|
||||
class EnumField(BaseField):
|
||||
class User(Document):
|
||||
name = StringField()
|
||||
meta = {"db_alias": "testdb-1"}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(EnumField, self).__init__(**kwargs)
|
||||
class Book(Document):
|
||||
name = StringField()
|
||||
author = ReferenceField(User)
|
||||
|
||||
def to_mongo(self, value):
|
||||
return value
|
||||
# Drops
|
||||
User.drop_collection()
|
||||
Book.drop_collection()
|
||||
|
||||
def to_python(self, value):
|
||||
return tuple(value)
|
||||
user = User(name="Ross").save()
|
||||
Book(name="MongoEngine for pros", author=user).save()
|
||||
|
||||
class TestDoc(Document):
|
||||
items = ListField(EnumField())
|
||||
# Can't use query_counter across databases - so test the _data object
|
||||
book = Book.objects.first()
|
||||
self.assertFalse(isinstance(book._data['author'], User))
|
||||
|
||||
TestDoc.drop_collection()
|
||||
tuples = [(100, 'Testing')]
|
||||
doc = TestDoc()
|
||||
doc.items = tuples
|
||||
doc.save()
|
||||
x = TestDoc.objects().get()
|
||||
self.assertTrue(x is not None)
|
||||
self.assertTrue(len(x.items) == 1)
|
||||
self.assertTrue(tuple(x.items[0]) in tuples)
|
||||
self.assertTrue(x.items[0] in tuples)
|
||||
book.select_related()
|
||||
self.assertTrue(isinstance(book._data['author'], User))
|
||||
|
||||
def test_non_ascii_pk(self):
|
||||
"""
|
||||
@@ -1177,6 +1237,55 @@ class FieldTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(2, len([brand for bg in brand_groups for brand in bg.brands]))
|
||||
|
||||
def test_dereferencing_embedded_listfield_referencefield(self):
|
||||
class Tag(Document):
|
||||
meta = {'collection': 'tags'}
|
||||
name = StringField()
|
||||
|
||||
class Post(EmbeddedDocument):
|
||||
body = StringField()
|
||||
tags = ListField(ReferenceField("Tag", dbref=True))
|
||||
|
||||
class Page(Document):
|
||||
meta = {'collection': 'pages'}
|
||||
tags = ListField(ReferenceField("Tag", dbref=True))
|
||||
posts = ListField(EmbeddedDocumentField(Post))
|
||||
|
||||
Tag.drop_collection()
|
||||
Page.drop_collection()
|
||||
|
||||
tag = Tag(name='test').save()
|
||||
post = Post(body='test body', tags=[tag])
|
||||
Page(tags=[tag], posts=[post]).save()
|
||||
|
||||
page = Page.objects.first()
|
||||
self.assertEqual(page.tags[0], page.posts[0].tags[0])
|
||||
|
||||
def test_select_related_follows_embedded_referencefields(self):
|
||||
|
||||
class Song(Document):
|
||||
title = StringField()
|
||||
|
||||
class PlaylistItem(EmbeddedDocument):
|
||||
song = ReferenceField("Song")
|
||||
|
||||
class Playlist(Document):
|
||||
items = ListField(EmbeddedDocumentField("PlaylistItem"))
|
||||
|
||||
Playlist.drop_collection()
|
||||
Song.drop_collection()
|
||||
|
||||
songs = [Song.objects.create(title="song %d" % i) for i in range(3)]
|
||||
items = [PlaylistItem(song=song) for song in songs]
|
||||
playlist = Playlist.objects.create(items=items)
|
||||
|
||||
with query_counter() as q:
|
||||
self.assertEqual(q, 0)
|
||||
|
||||
playlist = Playlist.objects.first().select_related()
|
||||
songs = [item.song for item in playlist.items]
|
||||
|
||||
self.assertEqual(q, 2)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
from __future__ import with_statement
|
||||
import sys
|
||||
sys.path[0:0] = [""]
|
||||
import unittest
|
||||
from nose.plugins.skip import SkipTest
|
||||
from mongoengine.python_support import PY3
|
||||
from mongoengine import *
|
||||
|
||||
try:
|
||||
from mongoengine.django.shortcuts import get_document_or_404
|
||||
|
||||
from django.http import Http404
|
||||
from django.template import Context, Template
|
||||
from django.conf import settings
|
||||
from django.core.paginator import Paginator
|
||||
|
||||
settings.configure(USE_TZ=True)
|
||||
|
||||
from django.contrib.sessions.tests import SessionTestsMixin
|
||||
from mongoengine.django.sessions import SessionStore, MongoSession
|
||||
except Exception, err:
|
||||
if PY3:
|
||||
SessionTestsMixin = type # dummy value so no error
|
||||
SessionStore = None # dummy value so no error
|
||||
else:
|
||||
raise err
|
||||
|
||||
|
||||
from datetime import tzinfo, timedelta
|
||||
ZERO = timedelta(0)
|
||||
|
||||
class FixedOffset(tzinfo):
|
||||
"""Fixed offset in minutes east from UTC."""
|
||||
|
||||
def __init__(self, offset, name):
|
||||
self.__offset = timedelta(minutes = offset)
|
||||
self.__name = name
|
||||
|
||||
def utcoffset(self, dt):
|
||||
return self.__offset
|
||||
|
||||
def tzname(self, dt):
|
||||
return self.__name
|
||||
|
||||
def dst(self, dt):
|
||||
return ZERO
|
||||
|
||||
|
||||
def activate_timezone(tz):
|
||||
"""Activate Django timezone support if it is available.
|
||||
"""
|
||||
try:
|
||||
from django.utils import timezone
|
||||
timezone.deactivate()
|
||||
timezone.activate(tz)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class QuerySetTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
if PY3:
|
||||
raise SkipTest('django does not have Python 3 support')
|
||||
connect(db='mongoenginetest')
|
||||
|
||||
class Person(Document):
|
||||
name = StringField()
|
||||
age = IntField()
|
||||
self.Person = Person
|
||||
|
||||
def test_order_by_in_django_template(self):
|
||||
"""Ensure that QuerySets are properly ordered in Django template.
|
||||
"""
|
||||
self.Person.drop_collection()
|
||||
|
||||
self.Person(name="A", age=20).save()
|
||||
self.Person(name="D", age=10).save()
|
||||
self.Person(name="B", age=40).save()
|
||||
self.Person(name="C", age=30).save()
|
||||
|
||||
t = Template("{% for o in ol %}{{ o.name }}-{{ o.age }}:{% endfor %}")
|
||||
|
||||
d = {"ol": self.Person.objects.order_by('-name')}
|
||||
self.assertEqual(t.render(Context(d)), u'D-10:C-30:B-40:A-20:')
|
||||
d = {"ol": self.Person.objects.order_by('+name')}
|
||||
self.assertEqual(t.render(Context(d)), u'A-20:B-40:C-30:D-10:')
|
||||
d = {"ol": self.Person.objects.order_by('-age')}
|
||||
self.assertEqual(t.render(Context(d)), u'B-40:C-30:A-20:D-10:')
|
||||
d = {"ol": self.Person.objects.order_by('+age')}
|
||||
self.assertEqual(t.render(Context(d)), u'D-10:A-20:C-30:B-40:')
|
||||
|
||||
self.Person.drop_collection()
|
||||
|
||||
def test_q_object_filter_in_template(self):
|
||||
|
||||
self.Person.drop_collection()
|
||||
|
||||
self.Person(name="A", age=20).save()
|
||||
self.Person(name="D", age=10).save()
|
||||
self.Person(name="B", age=40).save()
|
||||
self.Person(name="C", age=30).save()
|
||||
|
||||
t = Template("{% for o in ol %}{{ o.name }}-{{ o.age }}:{% endfor %}")
|
||||
|
||||
d = {"ol": self.Person.objects.filter(Q(age=10) | Q(name="C"))}
|
||||
self.assertEqual(t.render(Context(d)), 'D-10:C-30:')
|
||||
|
||||
# Check double rendering doesn't throw an error
|
||||
self.assertEqual(t.render(Context(d)), 'D-10:C-30:')
|
||||
|
||||
def test_get_document_or_404(self):
|
||||
p = self.Person(name="G404")
|
||||
p.save()
|
||||
|
||||
self.assertRaises(Http404, get_document_or_404, self.Person, pk='1234')
|
||||
self.assertEqual(p, get_document_or_404(self.Person, pk=p.pk))
|
||||
|
||||
def test_pagination(self):
|
||||
"""Ensure that Pagination works as expected
|
||||
"""
|
||||
class Page(Document):
|
||||
name = StringField()
|
||||
|
||||
Page.drop_collection()
|
||||
|
||||
for i in xrange(1, 11):
|
||||
Page(name=str(i)).save()
|
||||
|
||||
paginator = Paginator(Page.objects.all(), 2)
|
||||
|
||||
t = Template("{% for i in page.object_list %}{{ i.name }}:{% endfor %}")
|
||||
for p in paginator.page_range:
|
||||
d = {"page": paginator.page(p)}
|
||||
end = p * 2
|
||||
start = end - 1
|
||||
self.assertEqual(t.render(Context(d)), u'%d:%d:' % (start, end))
|
||||
|
||||
def test_nested_queryset_template_iterator(self):
|
||||
# Try iterating the same queryset twice, nested, in a Django template.
|
||||
names = ['A', 'B', 'C', 'D']
|
||||
|
||||
class User(Document):
|
||||
name = StringField()
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
User.drop_collection()
|
||||
|
||||
for name in names:
|
||||
User(name=name).save()
|
||||
|
||||
users = User.objects.all().order_by('name')
|
||||
template = Template("{% for user in users %}{{ user.name }}{% ifequal forloop.counter 2 %} {% for inner_user in users %}{{ inner_user.name }}{% endfor %} {% endifequal %}{% endfor %}")
|
||||
rendered = template.render(Context({'users': users}))
|
||||
self.assertEqual(rendered, 'AB ABCD CD')
|
||||
|
||||
class MongoDBSessionTest(SessionTestsMixin, unittest.TestCase):
|
||||
backend = SessionStore
|
||||
|
||||
def setUp(self):
|
||||
if PY3:
|
||||
raise SkipTest('django does not have Python 3 support')
|
||||
connect(db='mongoenginetest')
|
||||
MongoSession.drop_collection()
|
||||
super(MongoDBSessionTest, self).setUp()
|
||||
|
||||
def test_first_save(self):
|
||||
session = SessionStore()
|
||||
session['test'] = True
|
||||
session.save()
|
||||
self.assertTrue('test' in session)
|
||||
|
||||
def test_session_expiration_tz(self):
|
||||
activate_timezone(FixedOffset(60, 'UTC+1'))
|
||||
# create and save new session
|
||||
session = SessionStore()
|
||||
session.set_expiry(600) # expire in 600 seconds
|
||||
session['test_expire'] = True
|
||||
session.save()
|
||||
# reload session with key
|
||||
key = session.session_key
|
||||
session = SessionStore(key)
|
||||
self.assertTrue('test_expire' in session, 'Session has expired before it is expected')
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,17 +1,30 @@
|
||||
import sys
|
||||
sys.path[0:0] = [""]
|
||||
import unittest
|
||||
|
||||
import pymongo
|
||||
from pymongo import ReadPreference, ReplicaSetConnection
|
||||
from pymongo import ReadPreference
|
||||
|
||||
from mongoengine.python_support import IS_PYMONGO_3
|
||||
|
||||
if IS_PYMONGO_3:
|
||||
from pymongo import MongoClient
|
||||
CONN_CLASS = MongoClient
|
||||
READ_PREF = ReadPreference.SECONDARY
|
||||
else:
|
||||
from pymongo import ReplicaSetConnection
|
||||
CONN_CLASS = ReplicaSetConnection
|
||||
READ_PREF = ReadPreference.SECONDARY_ONLY
|
||||
|
||||
import mongoengine
|
||||
from mongoengine import *
|
||||
from mongoengine.connection import get_db, get_connection, ConnectionError
|
||||
from mongoengine.connection import MongoEngineConnectionError
|
||||
|
||||
|
||||
class ConnectionTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
mongoengine.connection._connection_settings = {}
|
||||
mongoengine.connection._connections = {}
|
||||
mongoengine.connection._dbs = {}
|
||||
|
||||
def tearDown(self):
|
||||
mongoengine.connection._connection_settings = {}
|
||||
mongoengine.connection._connections = {}
|
||||
@@ -22,14 +35,17 @@ class ConnectionTest(unittest.TestCase):
|
||||
"""
|
||||
|
||||
try:
|
||||
conn = connect(db='mongoenginetest', host="mongodb://localhost/mongoenginetest?replicaSet=rs", read_preference=ReadPreference.SECONDARY_ONLY)
|
||||
except ConnectionError, e:
|
||||
conn = connect(db='mongoenginetest',
|
||||
host="mongodb://localhost/mongoenginetest?replicaSet=rs",
|
||||
read_preference=READ_PREF)
|
||||
except MongoEngineConnectionError as e:
|
||||
return
|
||||
|
||||
if not isinstance(conn, ReplicaSetConnection):
|
||||
if not isinstance(conn, CONN_CLASS):
|
||||
# really???
|
||||
return
|
||||
|
||||
self.assertEqual(conn.read_preference, ReadPreference.SECONDARY_ONLY)
|
||||
self.assertEqual(conn.read_preference, READ_PREF)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
sys.path[0:0] = [""]
|
||||
import unittest
|
||||
|
||||
from mongoengine import *
|
||||
@@ -25,6 +23,8 @@ class SignalTests(unittest.TestCase):
|
||||
connect(db='mongoenginetest')
|
||||
|
||||
class Author(Document):
|
||||
# Make the id deterministic for easier testing
|
||||
id = SequenceField(primary_key=True)
|
||||
name = StringField()
|
||||
|
||||
def __unicode__(self):
|
||||
@@ -33,83 +33,85 @@ class SignalTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def pre_init(cls, sender, document, *args, **kwargs):
|
||||
signal_output.append('pre_init signal, %s' % cls.__name__)
|
||||
signal_output.append(str(kwargs['values']))
|
||||
signal_output.append(kwargs['values'])
|
||||
|
||||
@classmethod
|
||||
def post_init(cls, sender, document, **kwargs):
|
||||
signal_output.append('post_init signal, %s' % document)
|
||||
signal_output.append('post_init signal, %s, document._created = %s' % (document, document._created))
|
||||
|
||||
|
||||
@classmethod
|
||||
def pre_save(cls, sender, document, **kwargs):
|
||||
signal_output.append('pre_save signal, %s' % document)
|
||||
signal_output.append(kwargs)
|
||||
|
||||
@classmethod
|
||||
def pre_save_post_validation(cls, sender, document, **kwargs):
|
||||
signal_output.append('pre_save_post_validation signal, %s' % document)
|
||||
if kwargs.pop('created', False):
|
||||
signal_output.append('Is created')
|
||||
else:
|
||||
signal_output.append('Is updated')
|
||||
signal_output.append(kwargs)
|
||||
|
||||
@classmethod
|
||||
def post_save(cls, sender, document, **kwargs):
|
||||
dirty_keys = document._delta()[0].keys() + document._delta()[1].keys()
|
||||
signal_output.append('post_save signal, %s' % document)
|
||||
if 'created' in kwargs:
|
||||
if kwargs['created']:
|
||||
signal_output.append('Is created')
|
||||
else:
|
||||
signal_output.append('Is updated')
|
||||
signal_output.append('post_save dirty keys, %s' % dirty_keys)
|
||||
if kwargs.pop('created', False):
|
||||
signal_output.append('Is created')
|
||||
else:
|
||||
signal_output.append('Is updated')
|
||||
signal_output.append(kwargs)
|
||||
|
||||
@classmethod
|
||||
def pre_delete(cls, sender, document, **kwargs):
|
||||
signal_output.append('pre_delete signal, %s' % document)
|
||||
signal_output.append(kwargs)
|
||||
|
||||
@classmethod
|
||||
def post_delete(cls, sender, document, **kwargs):
|
||||
signal_output.append('post_delete signal, %s' % document)
|
||||
signal_output.append(kwargs)
|
||||
|
||||
@classmethod
|
||||
def pre_bulk_insert(cls, sender, documents, **kwargs):
|
||||
signal_output.append('pre_bulk_insert signal, %s' % documents)
|
||||
signal_output.append(kwargs)
|
||||
|
||||
@classmethod
|
||||
def post_bulk_insert(cls, sender, documents, **kwargs):
|
||||
signal_output.append('post_bulk_insert signal, %s' % documents)
|
||||
if kwargs.get('loaded', False):
|
||||
if kwargs.pop('loaded', False):
|
||||
signal_output.append('Is loaded')
|
||||
else:
|
||||
signal_output.append('Not loaded')
|
||||
signal_output.append(kwargs)
|
||||
|
||||
self.Author = Author
|
||||
Author.drop_collection()
|
||||
Author.id.set_next_value(0)
|
||||
|
||||
class Another(Document):
|
||||
|
||||
name = StringField()
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def pre_init(cls, sender, document, **kwargs):
|
||||
signal_output.append('pre_init Another signal, %s' % cls.__name__)
|
||||
signal_output.append(str(kwargs['values']))
|
||||
|
||||
@classmethod
|
||||
def post_init(cls, sender, document, **kwargs):
|
||||
signal_output.append('post_init Another signal, %s' % document)
|
||||
|
||||
@classmethod
|
||||
def pre_save(cls, sender, document, **kwargs):
|
||||
signal_output.append('pre_save Another signal, %s' % document)
|
||||
|
||||
@classmethod
|
||||
def post_save(cls, sender, document, **kwargs):
|
||||
signal_output.append('post_save Another signal, %s' % document)
|
||||
if 'created' in kwargs:
|
||||
if kwargs['created']:
|
||||
signal_output.append('Is created')
|
||||
else:
|
||||
signal_output.append('Is updated')
|
||||
|
||||
@classmethod
|
||||
def pre_delete(cls, sender, document, **kwargs):
|
||||
signal_output.append('pre_delete Another signal, %s' % document)
|
||||
signal_output.append('pre_delete signal, %s' % document)
|
||||
signal_output.append(kwargs)
|
||||
|
||||
@classmethod
|
||||
def post_delete(cls, sender, document, **kwargs):
|
||||
signal_output.append('post_delete Another signal, %s' % document)
|
||||
signal_output.append('post_delete signal, %s' % document)
|
||||
signal_output.append(kwargs)
|
||||
|
||||
self.Another = Another
|
||||
Another.drop_collection()
|
||||
|
||||
class ExplicitId(Document):
|
||||
id = IntField(primary_key=True)
|
||||
@@ -123,13 +125,50 @@ class SignalTests(unittest.TestCase):
|
||||
signal_output.append('Is updated')
|
||||
|
||||
self.ExplicitId = ExplicitId
|
||||
self.ExplicitId.objects.delete()
|
||||
ExplicitId.drop_collection()
|
||||
|
||||
class Post(Document):
|
||||
title = StringField()
|
||||
content = StringField()
|
||||
active = BooleanField(default=False)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.title
|
||||
|
||||
@classmethod
|
||||
def pre_bulk_insert(cls, sender, documents, **kwargs):
|
||||
signal_output.append('pre_bulk_insert signal, %s' %
|
||||
[(doc, {'active': documents[n].active})
|
||||
for n, doc in enumerate(documents)])
|
||||
|
||||
# make changes here, this is just an example -
|
||||
# it could be anything that needs pre-validation or looks-ups before bulk bulk inserting
|
||||
for document in documents:
|
||||
if not document.active:
|
||||
document.active = True
|
||||
signal_output.append(kwargs)
|
||||
|
||||
@classmethod
|
||||
def post_bulk_insert(cls, sender, documents, **kwargs):
|
||||
signal_output.append('post_bulk_insert signal, %s' %
|
||||
[(doc, {'active': documents[n].active})
|
||||
for n, doc in enumerate(documents)])
|
||||
if kwargs.pop('loaded', False):
|
||||
signal_output.append('Is loaded')
|
||||
else:
|
||||
signal_output.append('Not loaded')
|
||||
signal_output.append(kwargs)
|
||||
|
||||
self.Post = Post
|
||||
Post.drop_collection()
|
||||
|
||||
# Save up the number of connected signals so that we can check at the
|
||||
# end that all the signals we register get properly unregistered
|
||||
self.pre_signals = (
|
||||
len(signals.pre_init.receivers),
|
||||
len(signals.post_init.receivers),
|
||||
len(signals.pre_save.receivers),
|
||||
len(signals.pre_save_post_validation.receivers),
|
||||
len(signals.post_save.receivers),
|
||||
len(signals.pre_delete.receivers),
|
||||
len(signals.post_delete.receivers),
|
||||
@@ -140,45 +179,46 @@ class SignalTests(unittest.TestCase):
|
||||
signals.pre_init.connect(Author.pre_init, sender=Author)
|
||||
signals.post_init.connect(Author.post_init, sender=Author)
|
||||
signals.pre_save.connect(Author.pre_save, sender=Author)
|
||||
signals.pre_save_post_validation.connect(Author.pre_save_post_validation, sender=Author)
|
||||
signals.post_save.connect(Author.post_save, sender=Author)
|
||||
signals.pre_delete.connect(Author.pre_delete, sender=Author)
|
||||
signals.post_delete.connect(Author.post_delete, sender=Author)
|
||||
signals.pre_bulk_insert.connect(Author.pre_bulk_insert, sender=Author)
|
||||
signals.post_bulk_insert.connect(Author.post_bulk_insert, sender=Author)
|
||||
|
||||
signals.pre_init.connect(Another.pre_init, sender=Another)
|
||||
signals.post_init.connect(Another.post_init, sender=Another)
|
||||
signals.pre_save.connect(Another.pre_save, sender=Another)
|
||||
signals.post_save.connect(Another.post_save, sender=Another)
|
||||
signals.pre_delete.connect(Another.pre_delete, sender=Another)
|
||||
signals.post_delete.connect(Another.post_delete, sender=Another)
|
||||
|
||||
signals.post_save.connect(ExplicitId.post_save, sender=ExplicitId)
|
||||
|
||||
signals.pre_bulk_insert.connect(Post.pre_bulk_insert, sender=Post)
|
||||
signals.post_bulk_insert.connect(Post.post_bulk_insert, sender=Post)
|
||||
|
||||
def tearDown(self):
|
||||
signals.pre_init.disconnect(self.Author.pre_init)
|
||||
signals.post_init.disconnect(self.Author.post_init)
|
||||
signals.post_delete.disconnect(self.Author.post_delete)
|
||||
signals.pre_delete.disconnect(self.Author.pre_delete)
|
||||
signals.post_save.disconnect(self.Author.post_save)
|
||||
signals.pre_save_post_validation.disconnect(self.Author.pre_save_post_validation)
|
||||
signals.pre_save.disconnect(self.Author.pre_save)
|
||||
signals.pre_bulk_insert.disconnect(self.Author.pre_bulk_insert)
|
||||
signals.post_bulk_insert.disconnect(self.Author.post_bulk_insert)
|
||||
|
||||
signals.pre_init.disconnect(self.Another.pre_init)
|
||||
signals.post_init.disconnect(self.Another.post_init)
|
||||
signals.post_delete.disconnect(self.Another.post_delete)
|
||||
signals.pre_delete.disconnect(self.Another.pre_delete)
|
||||
signals.post_save.disconnect(self.Another.post_save)
|
||||
signals.pre_save.disconnect(self.Another.pre_save)
|
||||
|
||||
signals.post_save.disconnect(self.ExplicitId.post_save)
|
||||
|
||||
signals.pre_bulk_insert.disconnect(self.Post.pre_bulk_insert)
|
||||
signals.post_bulk_insert.disconnect(self.Post.post_bulk_insert)
|
||||
|
||||
# Check that all our signals got disconnected properly.
|
||||
post_signals = (
|
||||
len(signals.pre_init.receivers),
|
||||
len(signals.post_init.receivers),
|
||||
len(signals.pre_save.receivers),
|
||||
len(signals.pre_save_post_validation.receivers),
|
||||
len(signals.post_save.receivers),
|
||||
len(signals.pre_delete.receivers),
|
||||
len(signals.post_delete.receivers),
|
||||
@@ -204,52 +244,138 @@ class SignalTests(unittest.TestCase):
|
||||
a1 = self.Author(name='Bill Shakespeare')
|
||||
self.Author.objects.insert([a1], load_bulk=False)
|
||||
|
||||
def load_existing_author():
|
||||
a = self.Author(name='Bill Shakespeare')
|
||||
a.save()
|
||||
self.get_signal_output(lambda: None) # eliminate signal output
|
||||
a1 = self.Author.objects(name='Bill Shakespeare')[0]
|
||||
|
||||
self.assertEqual(self.get_signal_output(create_author), [
|
||||
"pre_init signal, Author",
|
||||
"{'name': 'Bill Shakespeare'}",
|
||||
"post_init signal, Bill Shakespeare",
|
||||
{'name': 'Bill Shakespeare'},
|
||||
"post_init signal, Bill Shakespeare, document._created = True",
|
||||
])
|
||||
|
||||
a1 = self.Author(name='Bill Shakespeare')
|
||||
self.assertEqual(self.get_signal_output(a1.save), [
|
||||
"pre_save signal, Bill Shakespeare",
|
||||
{},
|
||||
"pre_save_post_validation signal, Bill Shakespeare",
|
||||
"Is created",
|
||||
{},
|
||||
"post_save signal, Bill Shakespeare",
|
||||
"Is created"
|
||||
"post_save dirty keys, ['name']",
|
||||
"Is created",
|
||||
{}
|
||||
])
|
||||
|
||||
a1.reload()
|
||||
a1.name = 'William Shakespeare'
|
||||
self.assertEqual(self.get_signal_output(a1.save), [
|
||||
"pre_save signal, William Shakespeare",
|
||||
{},
|
||||
"pre_save_post_validation signal, William Shakespeare",
|
||||
"Is updated",
|
||||
{},
|
||||
"post_save signal, William Shakespeare",
|
||||
"Is updated"
|
||||
"post_save dirty keys, ['name']",
|
||||
"Is updated",
|
||||
{}
|
||||
])
|
||||
|
||||
self.assertEqual(self.get_signal_output(a1.delete), [
|
||||
'pre_delete signal, William Shakespeare',
|
||||
{},
|
||||
'post_delete signal, William Shakespeare',
|
||||
{}
|
||||
])
|
||||
|
||||
signal_output = self.get_signal_output(bulk_create_author_with_load)
|
||||
self.assertEqual(self.get_signal_output(load_existing_author), [
|
||||
"pre_init signal, Author",
|
||||
{'id': 2, 'name': 'Bill Shakespeare'},
|
||||
"post_init signal, Bill Shakespeare, document._created = False"
|
||||
])
|
||||
|
||||
# The output of this signal is not entirely deterministic. The reloaded
|
||||
# object will have an object ID. Hence, we only check part of the output
|
||||
self.assertEqual(signal_output[3],
|
||||
"pre_bulk_insert signal, [<Author: Bill Shakespeare>]")
|
||||
self.assertEqual(signal_output[-2:],
|
||||
["post_bulk_insert signal, [<Author: Bill Shakespeare>]",
|
||||
"Is loaded",])
|
||||
self.assertEqual(self.get_signal_output(bulk_create_author_with_load), [
|
||||
'pre_init signal, Author',
|
||||
{'name': 'Bill Shakespeare'},
|
||||
'post_init signal, Bill Shakespeare, document._created = True',
|
||||
'pre_bulk_insert signal, [<Author: Bill Shakespeare>]',
|
||||
{},
|
||||
'pre_init signal, Author',
|
||||
{'id': 3, 'name': 'Bill Shakespeare'},
|
||||
'post_init signal, Bill Shakespeare, document._created = False',
|
||||
'post_bulk_insert signal, [<Author: Bill Shakespeare>]',
|
||||
'Is loaded',
|
||||
{}
|
||||
])
|
||||
|
||||
self.assertEqual(self.get_signal_output(bulk_create_author_without_load), [
|
||||
"pre_init signal, Author",
|
||||
"{'name': 'Bill Shakespeare'}",
|
||||
"post_init signal, Bill Shakespeare",
|
||||
{'name': 'Bill Shakespeare'},
|
||||
"post_init signal, Bill Shakespeare, document._created = True",
|
||||
"pre_bulk_insert signal, [<Author: Bill Shakespeare>]",
|
||||
{},
|
||||
"post_bulk_insert signal, [<Author: Bill Shakespeare>]",
|
||||
"Not loaded",
|
||||
{}
|
||||
])
|
||||
|
||||
self.Author.objects.delete()
|
||||
def test_signal_kwargs(self):
|
||||
""" Make sure signal_kwargs is passed to signals calls. """
|
||||
|
||||
def live_and_let_die():
|
||||
a = self.Author(name='Bill Shakespeare')
|
||||
a.save(signal_kwargs={'live': True, 'die': False})
|
||||
a.delete(signal_kwargs={'live': False, 'die': True})
|
||||
|
||||
self.assertEqual(self.get_signal_output(live_and_let_die), [
|
||||
"pre_init signal, Author",
|
||||
{'name': 'Bill Shakespeare'},
|
||||
"post_init signal, Bill Shakespeare, document._created = True",
|
||||
"pre_save signal, Bill Shakespeare",
|
||||
{'die': False, 'live': True},
|
||||
"pre_save_post_validation signal, Bill Shakespeare",
|
||||
"Is created",
|
||||
{'die': False, 'live': True},
|
||||
"post_save signal, Bill Shakespeare",
|
||||
"post_save dirty keys, ['name']",
|
||||
"Is created",
|
||||
{'die': False, 'live': True},
|
||||
'pre_delete signal, Bill Shakespeare',
|
||||
{'die': True, 'live': False},
|
||||
'post_delete signal, Bill Shakespeare',
|
||||
{'die': True, 'live': False}
|
||||
])
|
||||
|
||||
def bulk_create_author():
|
||||
a1 = self.Author(name='Bill Shakespeare')
|
||||
self.Author.objects.insert([a1], signal_kwargs={'key': True})
|
||||
|
||||
self.assertEqual(self.get_signal_output(bulk_create_author), [
|
||||
'pre_init signal, Author',
|
||||
{'name': 'Bill Shakespeare'},
|
||||
'post_init signal, Bill Shakespeare, document._created = True',
|
||||
'pre_bulk_insert signal, [<Author: Bill Shakespeare>]',
|
||||
{'key': True},
|
||||
'pre_init signal, Author',
|
||||
{'id': 2, 'name': 'Bill Shakespeare'},
|
||||
'post_init signal, Bill Shakespeare, document._created = False',
|
||||
'post_bulk_insert signal, [<Author: Bill Shakespeare>]',
|
||||
'Is loaded',
|
||||
{'key': True}
|
||||
])
|
||||
|
||||
def test_queryset_delete_signals(self):
|
||||
""" Queryset delete should throw some signals. """
|
||||
|
||||
self.Another(name='Bill Shakespeare').save()
|
||||
self.assertEqual(self.get_signal_output(self.Another.objects.delete), [
|
||||
'pre_delete signal, Bill Shakespeare',
|
||||
{},
|
||||
'post_delete signal, Bill Shakespeare',
|
||||
{}
|
||||
])
|
||||
|
||||
def test_signals_with_explicit_doc_ids(self):
|
||||
""" Model saves must have a created flag the first time."""
|
||||
@@ -260,5 +386,50 @@ class SignalTests(unittest.TestCase):
|
||||
# second time, it must be an update
|
||||
self.assertEqual(self.get_signal_output(ei.save), ['Is updated'])
|
||||
|
||||
def test_signals_with_switch_collection(self):
|
||||
ei = self.ExplicitId(id=123)
|
||||
ei.switch_collection("explicit__1")
|
||||
self.assertEqual(self.get_signal_output(ei.save), ['Is created'])
|
||||
ei.switch_collection("explicit__1")
|
||||
self.assertEqual(self.get_signal_output(ei.save), ['Is updated'])
|
||||
|
||||
ei.switch_collection("explicit__1", keep_created=False)
|
||||
self.assertEqual(self.get_signal_output(ei.save), ['Is created'])
|
||||
ei.switch_collection("explicit__1", keep_created=False)
|
||||
self.assertEqual(self.get_signal_output(ei.save), ['Is created'])
|
||||
|
||||
def test_signals_with_switch_db(self):
|
||||
connect('mongoenginetest')
|
||||
register_connection('testdb-1', 'mongoenginetest2')
|
||||
|
||||
ei = self.ExplicitId(id=123)
|
||||
ei.switch_db("testdb-1")
|
||||
self.assertEqual(self.get_signal_output(ei.save), ['Is created'])
|
||||
ei.switch_db("testdb-1")
|
||||
self.assertEqual(self.get_signal_output(ei.save), ['Is updated'])
|
||||
|
||||
ei.switch_db("testdb-1", keep_created=False)
|
||||
self.assertEqual(self.get_signal_output(ei.save), ['Is created'])
|
||||
ei.switch_db("testdb-1", keep_created=False)
|
||||
self.assertEqual(self.get_signal_output(ei.save), ['Is created'])
|
||||
|
||||
def test_signals_bulk_insert(self):
|
||||
def bulk_set_active_post():
|
||||
posts = [
|
||||
self.Post(title='Post 1'),
|
||||
self.Post(title='Post 2'),
|
||||
self.Post(title='Post 3')
|
||||
]
|
||||
self.Post.objects.insert(posts)
|
||||
|
||||
results = self.get_signal_output(bulk_set_active_post)
|
||||
self.assertEqual(results, [
|
||||
"pre_bulk_insert signal, [(<Post: Post 1>, {'active': False}), (<Post: Post 2>, {'active': False}), (<Post: Post 3>, {'active': False})]",
|
||||
{},
|
||||
"post_bulk_insert signal, [(<Post: Post 1>, {'active': True}), (<Post: Post 2>, {'active': True}), (<Post: Post 3>, {'active': True})]",
|
||||
'Is loaded',
|
||||
{}
|
||||
])
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
78
tests/utils.py
Normal file
78
tests/utils.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import unittest
|
||||
|
||||
from nose.plugins.skip import SkipTest
|
||||
|
||||
from mongoengine import connect
|
||||
from mongoengine.connection import get_db, get_connection
|
||||
from mongoengine.python_support import IS_PYMONGO_3
|
||||
|
||||
|
||||
MONGO_TEST_DB = 'mongoenginetest'
|
||||
|
||||
|
||||
class MongoDBTestCase(unittest.TestCase):
|
||||
"""Base class for tests that need a mongodb connection
|
||||
db is being dropped automatically
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls._connection = connect(db=MONGO_TEST_DB)
|
||||
cls._connection.drop_database(MONGO_TEST_DB)
|
||||
cls.db = get_db()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls._connection.drop_database(MONGO_TEST_DB)
|
||||
|
||||
|
||||
def get_mongodb_version():
|
||||
"""Return the version tuple of the MongoDB server that the default
|
||||
connection is connected to.
|
||||
"""
|
||||
return tuple(get_connection().server_info()['versionArray'])
|
||||
|
||||
def _decorated_with_ver_requirement(func, ver_tuple):
|
||||
"""Return a given function decorated with the version requirement
|
||||
for a particular MongoDB version tuple.
|
||||
"""
|
||||
def _inner(*args, **kwargs):
|
||||
mongodb_ver = get_mongodb_version()
|
||||
if mongodb_ver >= ver_tuple:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
raise SkipTest('Needs MongoDB v{}+'.format(
|
||||
'.'.join([str(v) for v in ver_tuple])
|
||||
))
|
||||
|
||||
_inner.__name__ = func.__name__
|
||||
_inner.__doc__ = func.__doc__
|
||||
|
||||
return _inner
|
||||
|
||||
def needs_mongodb_v26(func):
|
||||
"""Raise a SkipTest exception if we're working with MongoDB version
|
||||
lower than v2.6.
|
||||
"""
|
||||
return _decorated_with_ver_requirement(func, (2, 6))
|
||||
|
||||
def needs_mongodb_v3(func):
|
||||
"""Raise a SkipTest exception if we're working with MongoDB version
|
||||
lower than v3.0.
|
||||
"""
|
||||
return _decorated_with_ver_requirement(func, (3, 0))
|
||||
|
||||
def skip_pymongo3(f):
|
||||
"""Raise a SkipTest exception if we're running a test against
|
||||
PyMongo v3.x.
|
||||
"""
|
||||
def _inner(*args, **kwargs):
|
||||
if IS_PYMONGO_3:
|
||||
raise SkipTest("Useless with PyMongo 3+")
|
||||
return f(*args, **kwargs)
|
||||
|
||||
_inner.__name__ = f.__name__
|
||||
_inner.__doc__ = f.__doc__
|
||||
|
||||
return _inner
|
||||
|
||||
Reference in New Issue
Block a user