Compare commits
2175 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdd6041a5c | ||
|
|
1894003f8a | ||
|
|
220513ae42 | ||
|
|
fcbabbe357 | ||
|
|
3627969fce | ||
|
|
8807c0dbef | ||
|
|
23cc9f6ff8 | ||
|
|
e50799e9c4 | ||
|
|
b92c4844eb | ||
|
|
c306d42d08 | ||
|
|
e31558318e | ||
|
|
78a9420f26 | ||
|
|
b47c5b5bfc | ||
|
|
28a312accf | ||
|
|
611094e92e | ||
|
|
2a8579a6a5 | ||
|
|
47577f2f47 | ||
|
|
34e3e45843 | ||
|
|
364dc9ddfb | ||
|
|
23324f0f87 | ||
|
|
17fa9a3b77 | ||
|
|
424b3ca308 | ||
|
|
26e2fc8fd4 | ||
|
|
8e18484898 | ||
|
|
354cfe0f9c | ||
|
|
983474b2bd | ||
|
|
14d861bcbb | ||
|
|
f6cd349a16 | ||
|
|
8e1c4dec87 | ||
|
|
18b47e4a73 | ||
|
|
4f157f50ed | ||
|
|
f44a2f4857 | ||
|
|
c685ace327 | ||
|
|
f23b0faf41 | ||
|
|
e0e2ca7ccd | ||
|
|
83fe7f7eef | ||
|
|
1feaa8f2e9 | ||
|
|
598d6bf4c5 | ||
|
|
0afd5a40d6 | ||
|
|
26b70e9ed3 | ||
|
|
a1a93a4bdd | ||
|
|
4939a7dd7c | ||
|
|
b0148e7860 | ||
|
|
59a06a242d | ||
|
|
ffe902605d | ||
|
|
556f7e85fc | ||
|
|
45c86be402 | ||
|
|
bf34f413de | ||
|
|
9b022b187f | ||
|
|
c3409d64dc | ||
|
|
3c5c3b5026 | ||
|
|
f240f00d84 | ||
|
|
68c7764c63 | ||
|
|
adfb039ba6 | ||
|
|
89416d9856 | ||
|
|
9b6c972e0f | ||
|
|
55fc04752a | ||
|
|
96f0919633 | ||
|
|
17b140baf4 | ||
|
|
45c2151d0f | ||
|
|
1887f5b7e7 | ||
|
|
708d1c7a32 | ||
|
|
acf8c3015a | ||
|
|
f83ae5789b | ||
|
|
57ccfcfc1b | ||
|
|
dd0fdcfdd4 | ||
|
|
5c805be067 | ||
|
|
e423380d7f | ||
|
|
4d8bebc917 | ||
|
|
4314fa883f | ||
|
|
d6e39b362b | ||
|
|
f89214f9cf | ||
|
|
d17cac8210 | ||
|
|
aa49283fa9 | ||
|
|
e79ea7a2cf | ||
|
|
8a1d280f19 | ||
|
|
6a8eb9562f | ||
|
|
8f76e1e344 | ||
|
|
7b9f084e6b | ||
|
|
5b1693a908 | ||
|
|
fd7c00da49 | ||
|
|
7fc5ced3af | ||
|
|
a86092fb64 | ||
|
|
003827e916 | ||
|
|
b15673c525 | ||
|
|
00363303b1 | ||
|
|
48fbe890f8 | ||
|
|
4179877cc7 | ||
|
|
282b83ac08 | ||
|
|
193656e71b | ||
|
|
a25d127f36 | ||
|
|
cf9df548ca | ||
|
|
f29b93c762 | ||
|
|
032ace40d1 | ||
|
|
f74dd1cb3c | ||
|
|
29889d1e35 | ||
|
|
d6d19c4229 | ||
|
|
ab08e67eaf | ||
|
|
00bf6ac258 | ||
|
|
b65478e7d9 | ||
|
|
e83b529f1c | ||
|
|
408274152b | ||
|
|
8ff82996fb | ||
|
|
d59c4044b7 | ||
|
|
3574e21e4f | ||
|
|
5a091956ef | ||
|
|
14e9c58444 | ||
|
|
bfe5b03c69 | ||
|
|
f96f7f840e | ||
|
|
a3bcf26dce | ||
|
|
a7852a89cc | ||
|
|
1b0c761fc0 | ||
|
|
5e4e8d4eda | ||
|
|
bd524d2e1e | ||
|
|
60fe919992 | ||
|
|
b90063b170 | ||
|
|
d9fce49b08 | ||
|
|
5dbee2a270 | ||
|
|
4779106139 | ||
|
|
bf2de81873 | ||
|
|
28cdedc9aa | ||
|
|
7e90571404 | ||
|
|
42bbe63927 | ||
|
|
b4860de34d | ||
|
|
576f23d5fb | ||
|
|
86548fc7bf | ||
|
|
b3b4d992fe | ||
|
|
9ad959a478 | ||
|
|
cc00a321da | ||
|
|
de74273108 | ||
|
|
a7658c7573 | ||
|
|
48a85ee6e0 | ||
|
|
461b789515 | ||
|
|
b71ff6fbb8 | ||
|
|
1bcdcce93a | ||
|
|
c09bfca634 | ||
|
|
36c5f02bfb | ||
|
|
eae6e5d9a1 | ||
|
|
364813dd73 | ||
|
|
1a2b1f283b | ||
|
|
a0e5cf4ecc | ||
|
|
820f7b4d93 | ||
|
|
727866f090 | ||
|
|
3d45cdc339 | ||
|
|
02a557aa67 | ||
|
|
6da27e5976 | ||
|
|
19a6e324c4 | ||
|
|
62eadbc174 | ||
|
|
ae783d4f45 | ||
|
|
1241a902e3 | ||
|
|
fdba648afb | ||
|
|
b070e7de07 | ||
|
|
d0741946c7 | ||
|
|
080226dd72 | ||
|
|
3cb6a5cfac | ||
|
|
758971e068 | ||
|
|
8739ab9c66 | ||
|
|
e8e47c39d7 | ||
|
|
446c101018 | ||
|
|
3654591a1b | ||
|
|
7fb1c9dd35 | ||
|
|
0fffaccdf4 | ||
|
|
5902b241f9 | ||
|
|
784386fddc | ||
|
|
d424583cbf | ||
|
|
290b821a3a | ||
|
|
a0dfa8d421 | ||
|
|
ceb00f6748 | ||
|
|
9bd328e147 | ||
|
|
6fb5c312c3 | ||
|
|
3f9ff7254f | ||
|
|
f7a3acfaf4 | ||
|
|
e4451ccaf8 | ||
|
|
2adb640821 | ||
|
|
765038274c | ||
|
|
2cbdced974 | ||
|
|
fc5d9ae100 | ||
|
|
506168ab83 | ||
|
|
088fd6334b | ||
|
|
94cda90a6e | ||
|
|
78601d90c9 | ||
|
|
fa4ac95ecc | ||
|
|
dd4d4e23ad | ||
|
|
acba86993d | ||
|
|
0fc55451c2 | ||
|
|
5c0bd8a810 | ||
|
|
1aebc95145 | ||
|
|
1d3f20b666 | ||
|
|
eb2e106871 | ||
|
|
f9a887c8c6 | ||
|
|
67ab810cb2 | ||
|
|
3e0d84383e | ||
|
|
d245ea3eaa | ||
|
|
843fc03bf4 | ||
|
|
c83c635067 | ||
|
|
f605eb14e8 | ||
|
|
fd02d77c59 | ||
|
|
0da8fb379d | ||
|
|
257a43298b | ||
|
|
a2d3bcd571 | ||
|
|
d4142c2cdd | ||
|
|
e50d66b303 | ||
|
|
08b6433843 | ||
|
|
8cd536aab5 | ||
|
|
2b495c648f | ||
|
|
06048b6d71 | ||
|
|
bb22287336 | ||
|
|
a45942a966 | ||
|
|
85d621846d | ||
|
|
534acf8df2 | ||
|
|
5a6d4387ea | ||
|
|
317e844886 | ||
|
|
b1f62a2735 | ||
|
|
65e4fea4ef | ||
|
|
faca8512c5 | ||
|
|
2121387aa2 | ||
|
|
72c4444a60 | ||
|
|
2d8d2e7e6f | ||
|
|
49bff5d544 | ||
|
|
806a80cef1 | ||
|
|
c6f0d5e478 | ||
|
|
bf30aba005 | ||
|
|
727778b730 | ||
|
|
b081ffce50 | ||
|
|
e46779f87b | ||
|
|
dabe8c1bb7 | ||
|
|
4042f88bd8 | ||
|
|
a0947d0c54 | ||
|
|
a34fd9ac89 | ||
|
|
aa68322641 | ||
|
|
2d76aebb8e | ||
|
|
7cc1d23bc7 | ||
|
|
0bd2103a8c | ||
|
|
7d8916b6e9 | ||
|
|
8b5df3ca17 | ||
|
|
ffdfe99d37 | ||
|
|
7efa67e7e6 | ||
|
|
d69808c204 | ||
|
|
de360c61dd | ||
|
|
6b04ddfad1 | ||
|
|
0d854ce906 | ||
|
|
38fdf26405 | ||
|
|
6835c15d9b | ||
|
|
fa38bfd4e8 | ||
|
|
4d5c6d11ab | ||
|
|
9e80da705a | ||
|
|
9b04391f82 | ||
|
|
8f6c0796e3 | ||
|
|
326fcf4398 | ||
|
|
fdda27abd1 | ||
|
|
7e8c62104a | ||
|
|
fb213f6e74 | ||
|
|
22e75c1691 | ||
|
|
919f221be9 | ||
|
|
da7d64667e | ||
|
|
d19c6a1573 | ||
|
|
5cd23039a0 | ||
|
|
19b18d3d0a | ||
|
|
101947da8b | ||
|
|
d3c3c23630 | ||
|
|
abc14316ea | ||
|
|
b66621f9c6 | ||
|
|
aa5510531d | ||
|
|
12b846586c | ||
|
|
b705f5b743 | ||
|
|
18a5fba42b | ||
|
|
b5a3b6f86a | ||
|
|
00f2eda576 | ||
|
|
c70d252dc3 | ||
|
|
2f088ce29e | ||
|
|
ff408c604b | ||
|
|
6621c318db | ||
|
|
22a8ad2fde | ||
|
|
7674dc9b34 | ||
|
|
9e0ca51c2f | ||
|
|
961629d156 | ||
|
|
2cbebf9c99 | ||
|
|
08a4deca17 | ||
|
|
ce9ea7baad | ||
|
|
b35efb9f72 | ||
|
|
c45dfacb41 | ||
|
|
91152a7977 | ||
|
|
0ce081323f | ||
|
|
79486e3393 | ||
|
|
60758dd76b | ||
|
|
e74f659015 | ||
|
|
c1c09fa6b4 | ||
|
|
47c7cb9327 | ||
|
|
4d6256e1a1 | ||
|
|
13180d92e3 | ||
|
|
6b38ef3c9f | ||
|
|
4f5b0634ad | ||
|
|
ea25972257 | ||
|
|
b6168898ec | ||
|
|
da33cb54fe | ||
|
|
35d0458228 | ||
|
|
e6c0280b40 | ||
|
|
15451ff42b | ||
|
|
9ab856e186 | ||
|
|
6e2db1ced6 | ||
|
|
5c4ce8754e | ||
|
|
416486c370 | ||
|
|
2f075be6f8 | ||
|
|
a1494c4c93 | ||
|
|
d79ab5ffeb | ||
|
|
01526a7b37 | ||
|
|
091a02f737 | ||
|
|
aa4996ef28 | ||
|
|
2f4e2bde6b | ||
|
|
e90f6a2fa3 | ||
|
|
be8f1b9fdd | ||
|
|
ba99190f53 | ||
|
|
70088704e2 | ||
|
|
02733e6e58 | ||
|
|
44732a5dd9 | ||
|
|
5bdd35464b | ||
|
|
1eae97731f | ||
|
|
0325a62f18 | ||
|
|
3a5538813c | ||
|
|
1f1b4b95ce | ||
|
|
8c3ed57ecc | ||
|
|
dc8a64fa7d | ||
|
|
0d1e72a764 | ||
|
|
9b3fe09508 | ||
|
|
7c0cfb1da2 | ||
|
|
66429ce331 | ||
|
|
bce859569f | ||
|
|
425fb8905b | ||
|
|
4f59c7f77f | ||
|
|
21d1faa793 | ||
|
|
b9f3991d03 | ||
|
|
c4de879b20 | ||
|
|
ee5686e91a | ||
|
|
2a795e9138 | ||
|
|
9a6aa8f8c6 | ||
|
|
3794b181d5 | ||
|
|
f09256a24e | ||
|
|
34fca9d6f5 | ||
|
|
433f10ef93 | ||
|
|
9f02f71c52 | ||
|
|
3dcc9bc143 | ||
|
|
7311895894 | ||
|
|
a7cab51369 | ||
|
|
437b11af9a | ||
|
|
820b5cbb86 | ||
|
|
e6a30f899c | ||
|
|
0bc6507df3 | ||
|
|
71c3c632d7 | ||
|
|
fb00b79d19 | ||
|
|
7782aa7379 | ||
|
|
f3ee4a5dac | ||
|
|
a8d6e59a7a | ||
|
|
1d4b1870cf | ||
|
|
f63ad2dd69 | ||
|
|
6903eed4e7 | ||
|
|
b9e922c658 | ||
|
|
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 | ||
|
|
f0eaec98c7 | ||
|
|
6dcd7006d0 | ||
|
|
5de4812477 | ||
|
|
d5b28356bc | ||
|
|
76fddd0db0 | ||
|
|
1108586303 | ||
|
|
3f49923298 | ||
|
|
c277be8b6b | ||
|
|
6e083fa6a1 | ||
|
|
073091a06e | ||
|
|
03bfd01862 | ||
|
|
539f01d08e | ||
|
|
dcf3c86dce | ||
|
|
ec639cd6e9 | ||
|
|
420376d036 | ||
|
|
51e50bf0a9 | ||
|
|
c2d77f51bb | ||
|
|
b4d87d9128 | ||
|
|
4401a309ee | ||
|
|
b562e209d1 | ||
|
|
3a85422e8f | ||
|
|
e45397c975 | ||
|
|
1f9ec0c888 | ||
|
|
f8ee470e70 | ||
|
|
d02de0798f | ||
|
|
6fe074fb13 | ||
|
|
4db339c5f4 | ||
|
|
a525764359 | ||
|
|
f970d5878a | ||
|
|
cc0a2cbc6f | ||
|
|
add0b463f5 | ||
|
|
d80b1a7749 | ||
|
|
6186691259 | ||
|
|
b451cc567d | ||
|
|
757ff31661 | ||
|
|
97a98f0045 | ||
|
|
8f05896bc9 | ||
|
|
da7a8939df | ||
|
|
b6977a88ea | ||
|
|
eafbc7f20d | ||
|
|
d92f992c01 | ||
|
|
20a5d9051d | ||
|
|
c9a5710554 | ||
|
|
f10e946896 | ||
|
|
2f19b22bb2 | ||
|
|
d134e11c6d | ||
|
|
63edd16a92 | ||
|
|
37740dc010 | ||
|
|
04b85ddbf2 | ||
|
|
836dc96f67 | ||
|
|
49a7542b14 | ||
|
|
a84ffce5a0 | ||
|
|
210b3e5192 | ||
|
|
5f1d5ea056 | ||
|
|
19a7372ff9 | ||
|
|
cc5b60b004 | ||
|
|
b06f9dbf8d | ||
|
|
d9b8ee7895 | ||
|
|
e9ff655b0e | ||
|
|
d58341d7ae | ||
|
|
669d21a114 | ||
|
|
7e980a16d0 | ||
|
|
47df8deb58 | ||
|
|
dd006a502e | ||
|
|
782d48594a | ||
|
|
07d3e52e6a | ||
|
|
fc1ce6d39b | ||
|
|
32d5c0c946 | ||
|
|
dfabfce01b | ||
|
|
74f3f4eb15 | ||
|
|
20cb0285f0 | ||
|
|
faf840f924 | ||
|
|
165bea5bb9 | ||
|
|
f7515cfca8 | ||
|
|
a762a10dec | ||
|
|
a192029901 | ||
|
|
67182713d9 | ||
|
|
e9464e32db | ||
|
|
2d6ae16912 | ||
|
|
f9cd8b1841 | ||
|
|
41a698b442 | ||
|
|
9f58bc9207 | ||
|
|
d36f6e7f24 | ||
|
|
eeb672feb9 | ||
|
|
063a162ce0 | ||
|
|
3e4a900279 | ||
|
|
43327ea4e1 | ||
|
|
0d2e84b16b | ||
|
|
3c78757778 | ||
|
|
d0245bb5ba | ||
|
|
3477b0107a | ||
|
|
8df9ff90cb | ||
|
|
d6b4ca7a98 | ||
|
|
2e18199eb2 | ||
|
|
025e17701b | ||
|
|
156ca44a13 | ||
|
|
39dac7d4db | ||
|
|
9ca632d518 | ||
|
|
4177fc6df2 | ||
|
|
d90890c08e | ||
|
|
1ca098c402 | ||
|
|
3208a7f15d | ||
|
|
8eda52e8e0 | ||
|
|
5b161b7445 | ||
|
|
8c1f8e54cd | ||
|
|
03d3c26a99 | ||
|
|
0cbd3663e4 | ||
|
|
f182daa85e | ||
|
|
de2f774e85 | ||
|
|
9d9a4afee9 | ||
|
|
0ea363c7fc | ||
|
|
d7ee47ee25 | ||
|
|
eb1b6e34c7 | ||
|
|
621b2b3f72 | ||
|
|
83da08ef7d | ||
|
|
9f551121fb | ||
|
|
ba48dfb4bf | ||
|
|
ed2ea24b75 | ||
|
|
eefbd3f597 | ||
|
|
e38bf63be0 | ||
|
|
e7ba5eb160 | ||
|
|
fff27f9b87 | ||
|
|
d58f594c17 | ||
|
|
9797d7a7fb | ||
|
|
c8b65317ef | ||
|
|
3a6dc77d36 | ||
|
|
4f70c27b56 | ||
|
|
ea46edf50a | ||
|
|
e5e88d792e | ||
|
|
6d68ad735c | ||
|
|
c44b98a7e1 | ||
|
|
445f9453c4 | ||
|
|
3364e040c8 | ||
|
|
692f00864d | ||
|
|
344dc64df8 | ||
|
|
473425a36a | ||
|
|
3ba58ebaae | ||
|
|
2c7b12c022 | ||
|
|
17eeeb7536 | ||
|
|
de5fbfde2c | ||
|
|
f5d02e1b10 | ||
|
|
e508625935 | ||
|
|
0b177ec4c1 | ||
|
|
87c965edd3 | ||
|
|
72dd9daa23 | ||
|
|
a68529fba8 | ||
|
|
06681a453f | ||
|
|
5907dde4a8 | ||
|
|
8e038dd563 | ||
|
|
50905ab459 | ||
|
|
7bb9c7d47f | ||
|
|
5c45eee817 | ||
|
|
0f9e4ef352 | ||
|
|
85173d188b | ||
|
|
d9ed33d1b1 | ||
|
|
e6ac8cab53 | ||
|
|
f890ebd0f4 | ||
|
|
e537369d98 | ||
|
|
9bbd8dbe62 | ||
|
|
09a5f5c8f3 | ||
|
|
b9e0f52526 | ||
|
|
1cdf71b647 | ||
|
|
3aff461039 | ||
|
|
bf74d7537c | ||
|
|
0c2fb6807e | ||
|
|
b9c9d127a2 | ||
|
|
286beca6c5 | ||
|
|
3a1521a34e | ||
|
|
c5b047d0cd | ||
|
|
485b811bd0 | ||
|
|
f335591045 | ||
|
|
1c10f3020b | ||
|
|
3074dad293 | ||
|
|
42f506adc6 | ||
|
|
50b755db0c | ||
|
|
420c3e0073 | ||
|
|
4a57fc33e4 | ||
|
|
25cdf16cc0 | ||
|
|
7f732459a1 | ||
|
|
9cc02d4dbe | ||
|
|
c528ac09d6 | ||
|
|
1a131ff120 | ||
|
|
accdd82970 | ||
|
|
3e8f02c64b | ||
|
|
3425264077 | ||
|
|
148f8b8a3a | ||
|
|
74343841e4 | ||
|
|
3b3738b36b | ||
|
|
b15c3f6a3f | ||
|
|
2459f9b0aa | ||
|
|
6ff1bd9b3c | ||
|
|
1bc2d2ec37 | ||
|
|
d7fd6a4628 | ||
|
|
9236f365fa | ||
|
|
90d22c2a28 | ||
|
|
c9f6e6b62a | ||
|
|
260d9377f5 | ||
|
|
22d1ce6319 | ||
|
|
6997e02476 | ||
|
|
155d79ff4d | ||
|
|
452cd125fa | ||
|
|
e62c35b040 | ||
|
|
d5ec3c6a31 | ||
|
|
ad983dc279 | ||
|
|
bb15bf8d13 | ||
|
|
94adc207ad | ||
|
|
376d1c97ab | ||
|
|
4fe87b40da | ||
|
|
b10d76cf4b | ||
|
|
3bdc9a2f09 | ||
|
|
9d52e18659 | ||
|
|
f6f7c12f0e | ||
|
|
219b28c97b | ||
|
|
3598fe0fb4 | ||
|
|
f9dd051ec9 | ||
|
|
68e4a27aaf | ||
|
|
b849c719a8 | ||
|
|
59e7617e82 | ||
|
|
b5e868655e | ||
|
|
027b3d36de | ||
|
|
653c4259ee | ||
|
|
9f5ab8149f | ||
|
|
66c6d14f7a | ||
|
|
2c0fc142a3 | ||
|
|
003454573c | ||
|
|
aa5a9ff1f4 | ||
|
|
28ef54986d | ||
|
|
0da2dfd191 | ||
|
|
787fc1cd8b | ||
|
|
dfdc0d92c3 | ||
|
|
f265915aa2 | ||
|
|
4228d06934 | ||
|
|
1a93b9b226 | ||
|
|
363e50abbe | ||
|
|
b8d53a6f0d | ||
|
|
4b45c0cd14 | ||
|
|
e7c0da38c2 | ||
|
|
8706fbe461 | ||
|
|
9ca96e4e17 | ||
|
|
99fe1da345 | ||
|
|
1986e82783 | ||
|
|
7073b9d395 | ||
|
|
f2049e9c18 | ||
|
|
f0f1308465 | ||
|
|
7d90aa76ff | ||
|
|
3cc2c617fd | ||
|
|
c31488add9 | ||
|
|
3d5b6ae332 | ||
|
|
59826c8cfd | ||
|
|
6f29d12386 | ||
|
|
0a89899ad0 | ||
|
|
e4af0e361a | ||
|
|
31ec7907b5 | ||
|
|
12f3f8c694 | ||
|
|
79098e997e | ||
|
|
dc1849bad5 | ||
|
|
e2d826c412 | ||
|
|
e6d796832e | ||
|
|
6f0a6df4f6 | ||
|
|
7a877a00d5 | ||
|
|
e8604d100e | ||
|
|
1647441ce8 | ||
|
|
9f8d6b3a00 | ||
|
|
0bfc96e459 | ||
|
|
3425574ddc | ||
|
|
4b2ad25405 | ||
|
|
3ce163b1a0 | ||
|
|
7c1ee28f13 | ||
|
|
2645e43da1 | ||
|
|
59bfe551a3 | ||
|
|
6a31736644 | ||
|
|
e2c78047b1 | ||
|
|
6a4351e44f | ||
|
|
adb60ef1ac | ||
|
|
3090adac04 | ||
|
|
b9253d86cc | ||
|
|
ab4d4e6230 | ||
|
|
7cd38c56c6 | ||
|
|
864053615b | ||
|
|
db2366f112 | ||
|
|
4defc82192 | ||
|
|
5949970a95 | ||
|
|
0ea4abda81 | ||
|
|
5c6035d636 | ||
|
|
a2183e3dcc | ||
|
|
99637151b5 | ||
|
|
a8e787c120 | ||
|
|
53339c7c72 | ||
|
|
3534bf7d70 | ||
|
|
1cf3989664 | ||
|
|
fd296918da | ||
|
|
8ad1f03dc5 | ||
|
|
fe7e17dbd5 | ||
|
|
d582394a42 | ||
|
|
02ef0df019 | ||
|
|
0dfd6aa518 | ||
|
|
0b23bc9cf2 | ||
|
|
f108c4288e | ||
|
|
9b9696aefd | ||
|
|
576e198ece | ||
|
|
52f85aab18 | ||
|
|
ab60fd0490 | ||
|
|
d79ae30f31 | ||
|
|
f27debe7f9 | ||
|
|
735e043ff6 | ||
|
|
6e7f2b73cf | ||
|
|
d645ce9745 | ||
|
|
7c08c140da | ||
|
|
81d402dc17 | ||
|
|
966fa12358 | ||
|
|
87792e1921 | ||
|
|
4c8296acc6 | ||
|
|
9989da07ed | ||
|
|
1c5e6a3425 | ||
|
|
eedf908770 | ||
|
|
5c9ef41403 | ||
|
|
0bf2ad5b67 | ||
|
|
a0e3f382cd | ||
|
|
f09c39b5d7 | ||
|
|
89c67bf259 | ||
|
|
ea666d4607 | ||
|
|
b8af154439 | ||
|
|
f594ece32a | ||
|
|
03beb6852a | ||
|
|
ab9e9a3329 | ||
|
|
a4b09344af | ||
|
|
8cb8aa392c | ||
|
|
3255519792 | ||
|
|
7e64bb2503 | ||
|
|
86a78402c3 | ||
|
|
ba276452fb | ||
|
|
4ffa8d0124 | ||
|
|
4bc5082681 | ||
|
|
0e3c34e1da | ||
|
|
658b3784ae | ||
|
|
0526f577ff | ||
|
|
bb1b9bc1d3 | ||
|
|
b1eeb77ddc | ||
|
|
999d4a7676 | ||
|
|
1b80193aac | ||
|
|
be8d39a48c | ||
|
|
a2f3d70f28 | ||
|
|
676a7bf712 | ||
|
|
e990a6c70c | ||
|
|
90fa0f6c4a | ||
|
|
22010d7d95 | ||
|
|
66279bd90f | ||
|
|
19da228855 | ||
|
|
9e67941bad | ||
|
|
0454fc74e9 | ||
|
|
2f6b1c7611 | ||
|
|
f00bed6058 | ||
|
|
529c522594 | ||
|
|
2bb9493fcf | ||
|
|
839ed8a64a | ||
|
|
500eb920e4 | ||
|
|
017a31ffd0 | ||
|
|
83b961c84d | ||
|
|
fa07423ca5 | ||
|
|
dd4af2df81 | ||
|
|
44bd8cb85b | ||
|
|
52d80ac23c | ||
|
|
43a5d73e14 | ||
|
|
abc764951d | ||
|
|
9cc6164026 | ||
|
|
475488b9f2 | ||
|
|
95b1783834 | ||
|
|
12c8b5c0b9 | ||
|
|
f99b7a811b | ||
|
|
0575abab23 | ||
|
|
9eebcf7beb | ||
|
|
ed74477150 | ||
|
|
2801b38c75 | ||
|
|
dc3fea875e | ||
|
|
aab8c2b687 | ||
|
|
3577773af3 | ||
|
|
dd023edc0f | ||
|
|
8ac9e6dc19 | ||
|
|
f45d4d781d | ||
|
|
c95652d6a8 | ||
|
|
97b37f75d3 | ||
|
|
95dae48778 | ||
|
|
73635033bd | ||
|
|
c1619d2a62 | ||
|
|
b87ef982f6 | ||
|
|
91aa90ad4a | ||
|
|
4b3cea9e78 | ||
|
|
2420b5e937 | ||
|
|
f23a976bea | ||
|
|
4226cd08f1 | ||
|
|
7a230f1693 | ||
|
|
a43d0d4612 | ||
|
|
78a40a0c70 | ||
|
|
2c69d8f0b0 | ||
|
|
0018c38b83 | ||
|
|
8df81571fc | ||
|
|
d1add62a06 | ||
|
|
c419f3379a | ||
|
|
69d57209f7 | ||
|
|
7ca81d6fb8 | ||
|
|
8a046bfa5d | ||
|
|
3628a7653c | ||
|
|
48f988acd7 | ||
|
|
6526923345 | ||
|
|
24fd1acce6 | ||
|
|
cbb9235dc5 | ||
|
|
19ec2c9bc9 | ||
|
|
6459d4c0b6 | ||
|
|
1304f2721f | ||
|
|
8bde0c0e53 | ||
|
|
598ffd3e5c | ||
|
|
1a4533a9cf | ||
|
|
601f0eb168 | ||
|
|
3070e0bf5d | ||
|
|
83c11a9834 | ||
|
|
5c912b930e | ||
|
|
1b17fb0ae7 | ||
|
|
d83e67c121 | ||
|
|
ae39ed94c9 | ||
|
|
1e51180d42 | ||
|
|
87ba69d02e | ||
|
|
8879d5560b | ||
|
|
c1621ee39c | ||
|
|
b0aa98edb4 | ||
|
|
a7a2fe0216 | ||
|
|
8e50f5fa3c | ||
|
|
31793520bf | ||
|
|
0b6b0368c5 | ||
|
|
d1d30a9280 | ||
|
|
420c6f2d1e | ||
|
|
34f06c4971 | ||
|
|
9cc4bbd49d | ||
|
|
f66b312869 | ||
|
|
2405ba8708 | ||
|
|
a91b6bff8b | ||
|
|
450dc11a68 | ||
|
|
1ce2f84ce5 | ||
|
|
f55b241cfa | ||
|
|
34d08ce8ef | ||
|
|
4f5aa8c43b | ||
|
|
27b375060d | ||
|
|
cbfdc401f7 | ||
|
|
b58bf3e0ce | ||
|
|
1fff7e9aca | ||
|
|
494b981b13 | ||
|
|
dd93995bd0 | ||
|
|
b3bb4add9c | ||
|
|
d305e71c27 | ||
|
|
0d92baa670 | ||
|
|
7a1b110f62 | ||
|
|
db8df057ce | ||
|
|
5d8ffded40 | ||
|
|
07f3e5356d | ||
|
|
1ece62f960 | ||
|
|
056c604dc3 | ||
|
|
2d08eec093 | ||
|
|
614b590551 | ||
|
|
6d90ce250a | ||
|
|
ea31846a19 | ||
|
|
e6317776c1 | ||
|
|
efeaba39a4 | ||
|
|
1a97dfd479 | ||
|
|
9fecf2b303 | ||
|
|
3d0d2f48ad | ||
|
|
581605e0e2 | ||
|
|
45d3a7f6ff | ||
|
|
7ca2ea0766 | ||
|
|
89220c142b | ||
|
|
c73ce3d220 | ||
|
|
b0f127af4e | ||
|
|
766d54795f | ||
|
|
bd41c6eea4 | ||
|
|
2435786713 | ||
|
|
9e7ea64bd2 | ||
|
|
89a6eee6af | ||
|
|
2ec1476e50 | ||
|
|
2d9b581f34 | ||
|
|
5bb63f645b | ||
|
|
a856c7cc37 | ||
|
|
26db9d8a9d | ||
|
|
8060179f6d | ||
|
|
77ebd87fed | ||
|
|
e4bc92235d | ||
|
|
27a4d83ce8 | ||
|
|
ece9b902f8 | ||
|
|
65a2f8a68b | ||
|
|
9c212306b8 | ||
|
|
0b22c140c5 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -13,4 +13,8 @@ env/
|
||||
.settings
|
||||
.project
|
||||
.pydevproject
|
||||
tests/bugfix.py
|
||||
tests/test_bugfix.py
|
||||
htmlcov/
|
||||
venv
|
||||
venv3
|
||||
scratchpad
|
||||
|
||||
28
.install_mongodb_on_travis.sh
Normal file
28
.install_mongodb_on_travis.sh
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
|
||||
sudo apt-get remove mongodb-org-server
|
||||
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
|
||||
|
||||
if [ "$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
|
||||
elif [ "$MONGODB" = "3.2" ]; then
|
||||
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv EA312927
|
||||
echo "deb http://repo.mongodb.org/apt/ubuntu trusty/mongodb-org/3.2 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.2.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install mongodb-org-server=3.2.20
|
||||
# service should be started automatically
|
||||
else
|
||||
echo "Invalid MongoDB version, expected 2.6, 3.0, or 3.2"
|
||||
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
|
||||
103
.travis.yml
103
.travis.yml
@@ -1,12 +1,99 @@
|
||||
# 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.6 is currently the "main" version tested against Python v2.7,
|
||||
# v3.5, v3.6, PyPy, and PyMongo v3.x.
|
||||
# * MongoDB v3.0 & v3.2 are tested against Python v2.7, v3.5 & v3.6
|
||||
# and Pymongo v3.5 & v3.x
|
||||
# Reminder: Update README.rst if you change MongoDB versions we test.
|
||||
|
||||
language: python
|
||||
|
||||
python:
|
||||
- 2.6
|
||||
- 2.7
|
||||
- 2.7
|
||||
- 3.5
|
||||
- 3.6
|
||||
- pypy
|
||||
|
||||
env:
|
||||
- MONGODB=2.6 PYMONGO=3.x
|
||||
|
||||
matrix:
|
||||
# Finish the build as soon as one job fails
|
||||
fast_finish: true
|
||||
|
||||
include:
|
||||
- python: 2.7
|
||||
env: MONGODB=3.0 PYMONGO=3.5
|
||||
- python: 2.7
|
||||
env: MONGODB=3.2 PYMONGO=3.x
|
||||
- python: 3.5
|
||||
env: MONGODB=3.0 PYMONGO=3.5
|
||||
- python: 3.5
|
||||
env: MONGODB=3.2 PYMONGO=3.x
|
||||
- python: 3.6
|
||||
env: MONGODB=3.0 PYMONGO=3.5
|
||||
- python: 3.6
|
||||
env: MONGODB=3.2 PYMONGO=3.x
|
||||
|
||||
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:
|
||||
- sudo apt-get install zlib1g zlib1g-dev
|
||||
- sudo ln -s /usr/lib/i386-linux-gnu/libz.so /usr/lib/
|
||||
- pip install PIL --use-mirrors ; true
|
||||
- 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
|
||||
|
||||
# Only run builds on the master branch and GitHub releases (tagged as vX.Y.Z)
|
||||
branches:
|
||||
only:
|
||||
- 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.x (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.x"
|
||||
python: 2.7
|
||||
|
||||
154
AUTHORS
154
AUTHORS
@@ -8,15 +8,14 @@ Florian Schlachter <flori@n-schlachter.de>
|
||||
Steve Challis <steve@stevechallis.com>
|
||||
Wilson Júnior <wilsonpjunior@gmail.com>
|
||||
Dan Crosta https://github.com/dcrosta
|
||||
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
|
||||
@@ -24,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
|
||||
@@ -76,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
|
||||
@@ -104,4 +103,147 @@ that much better:
|
||||
* swashbuckler
|
||||
* Adam Reeve
|
||||
* Anthony Nemitz
|
||||
* deignacio
|
||||
* deignacio
|
||||
* Shaun Duncan
|
||||
* Meir Kriheli
|
||||
* Andrey Fedoseev
|
||||
* aparajita
|
||||
* Tristan Escalada
|
||||
* Alexander Koshelev
|
||||
* Jaime Irurzun
|
||||
* Alexandre González
|
||||
* Thomas Steinacher
|
||||
* Tommi Komulainen
|
||||
* Peter Landry
|
||||
* biszkoptwielki
|
||||
* Anton Kolechkin
|
||||
* Sergey Nikitin
|
||||
* psychogenic
|
||||
* Stefan Wójcik (https://github.com/wojcikstefan)
|
||||
* dimonb
|
||||
* Garry Polley
|
||||
* James Slagle
|
||||
* Adrian Scott
|
||||
* Peter Teichman
|
||||
* Jakub Kot
|
||||
* Jorge Bastida
|
||||
* Aleksandr Sorokoumov
|
||||
* Yohan Graterol
|
||||
* bool-dev
|
||||
* Russ Weeks
|
||||
* Paul Swartz
|
||||
* Sundar Raman
|
||||
* Benoit Louy
|
||||
* Loic Raucy (https://github.com/lraucy)
|
||||
* hellysmile
|
||||
* Jaepil Jeong
|
||||
* Daniil Sharou
|
||||
* Pete Campton
|
||||
* Martyn Smith
|
||||
* Marcelo Anton
|
||||
* Aleksey Porfirov (https://github.com/lexqt)
|
||||
* Nicolas Trippar
|
||||
* Manuel Hermann
|
||||
* Gustavo Gawryszewski
|
||||
* Max Countryman
|
||||
* caitifbrito
|
||||
* lcya86 刘春洋
|
||||
* Martin Alderete (https://github.com/malderete)
|
||||
* Nick Joyce
|
||||
* Jared Forsyth
|
||||
* 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)
|
||||
* Andy Yankovsky (https://github.com/werat)
|
||||
* Bastien Gérard (https://github.com/bagerard)
|
||||
|
||||
85
CONTRIBUTING.rst
Normal file
85
CONTRIBUTING.rst
Normal file
@@ -0,0 +1,85 @@
|
||||
Contributing to MongoEngine
|
||||
===========================
|
||||
|
||||
MongoEngine has a large `community
|
||||
<https://raw.github.com/MongoEngine/mongoengine/master/AUTHORS>`_ and
|
||||
contributions are always encouraged. Contributions can be as simple as
|
||||
minor tweaks to the documentation. Please read these guidelines before
|
||||
sending a pull request.
|
||||
|
||||
Bugfixes and New Features
|
||||
-------------------------
|
||||
|
||||
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
|
||||
post to the `user group <http://groups.google.com/group/mongoengine-users>`
|
||||
|
||||
Supported Interpreters
|
||||
----------------------
|
||||
|
||||
MongoEngine supports CPython 2.7 and newer. Language
|
||||
features not supported by all interpreters can not be used.
|
||||
The codebase is written in python 2 so you must be using python 2
|
||||
when developing new features. Compatibility of the library with Python 3
|
||||
relies on the 2to3 package that gets executed as part of the installation
|
||||
build. You should ensure that your code is properly converted by
|
||||
`2to3 <http://docs.python.org/library/2to3.html>`_.
|
||||
|
||||
Style Guide
|
||||
-----------
|
||||
|
||||
MongoEngine aims to follow `PEP8 <http://www.python.org/dev/peps/pep-0008/>`_
|
||||
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. 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 nosetests``
|
||||
from the cmd line to run the test suite).
|
||||
- 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
|
||||
-------------
|
||||
|
||||
To contribute to the `API documentation
|
||||
<http://docs.mongoengine.org/en/latest/apireference.html>`_
|
||||
just make your changes to the inline documentation of the appropriate
|
||||
`source code <https://github.com/MongoEngine/mongoengine>`_ or `rst file
|
||||
<https://github.com/MongoEngine/mongoengine/tree/master/docs>`_ in a
|
||||
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``.
|
||||
8
LICENSE
8
LICENSE
@@ -1,5 +1,5 @@
|
||||
Copyright (c) 2009-2010 Harry Marr
|
||||
|
||||
Copyright (c) 2009 See AUTHORS
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
@@ -8,10 +8,10 @@ copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
|
||||
114
README.rst
114
README.rst
@@ -2,40 +2,76 @@
|
||||
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
|
||||
<http://readthedocs.org/docs/mongoengine-odm/en/latest/userguide.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.6, v3.0 and v3.2. 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.4+.
|
||||
|
||||
Installation
|
||||
============
|
||||
If you have `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_
|
||||
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``.
|
||||
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``. Another option is
|
||||
`pipenv <https://docs.pipenv.org/>`_. You can then use ``pipenv install mongoengine``
|
||||
to both create the virtual environment and install the package. 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)
|
||||
@@ -62,29 +98,47 @@ Some simple examples of what MongoEngine code looks like::
|
||||
... print 'Link:', post.url
|
||||
... print
|
||||
...
|
||||
=== Using MongoEngine ===
|
||||
See the tutorial
|
||||
|
||||
=== MongoEngine Docs ===
|
||||
Link: hmarr.com/mongoengine
|
||||
|
||||
>>> 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
|
||||
=========
|
||||
@@ -92,11 +146,7 @@ Community
|
||||
<http://groups.google.com/group/mongoengine-users>`_
|
||||
- `MongoEngine Developers mailing list
|
||||
<http://groups.google.com/group/mongoengine-dev>`_
|
||||
- `#mongoengine IRC channel <irc://irc.freenode.net/mongoengine>`_
|
||||
|
||||
Contributing
|
||||
============
|
||||
The source is available on `GitHub <http://github.com/MongoEngine/mongoengine>`_ - to
|
||||
contribute to the project, fork it on GitHub and send a pull request, all
|
||||
contributions and suggestions are welcome!
|
||||
|
||||
We welcome contributions! See the `Contribution guidelines <https://github.com/MongoEngine/mongoengine/blob/master/CONTRIBUTING.rst>`_
|
||||
|
||||
227
benchmark.py
227
benchmark.py
@@ -1,120 +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
|
||||
1.1141769886
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine
|
||||
2.37724113464
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False
|
||||
1.92479610443
|
||||
|
||||
0.5.X
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - Pymongo
|
||||
1.10552310944
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine
|
||||
16.5169169903
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False
|
||||
14.9446101189
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False, cascade=False
|
||||
14.912801981
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine, force=True
|
||||
14.9617750645
|
||||
|
||||
Performance
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - Pymongo
|
||||
1.10072994232
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine
|
||||
5.27341103554
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False
|
||||
4.49365401268
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False, cascade=False
|
||||
4.43459296227
|
||||
----------------------------------------------------------------------------------------------------
|
||||
Creating 10000 dictionaries - MongoEngine, force=True
|
||||
4.40114378929
|
||||
"""
|
||||
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)
|
||||
@@ -124,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,38 +34,97 @@ 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.StringField
|
||||
.. autoclass:: mongoengine.URLField
|
||||
.. autoclass:: mongoengine.EmailField
|
||||
.. autoclass:: mongoengine.IntField
|
||||
.. autoclass:: mongoengine.FloatField
|
||||
.. autoclass:: mongoengine.DecimalField
|
||||
.. autoclass:: mongoengine.DateTimeField
|
||||
.. autoclass:: mongoengine.ComplexDateTimeField
|
||||
.. autoclass:: mongoengine.ListField
|
||||
.. autoclass:: mongoengine.SortedListField
|
||||
.. autoclass:: mongoengine.DictField
|
||||
.. autoclass:: mongoengine.MapField
|
||||
.. autoclass:: mongoengine.ObjectIdField
|
||||
.. autoclass:: mongoengine.ReferenceField
|
||||
.. autoclass:: mongoengine.GenericReferenceField
|
||||
.. autoclass:: mongoengine.EmbeddedDocumentField
|
||||
.. autoclass:: mongoengine.GenericEmbeddedDocumentField
|
||||
.. autoclass:: mongoengine.BooleanField
|
||||
.. autoclass:: mongoengine.FileField
|
||||
.. autoclass:: mongoengine.BinaryField
|
||||
.. autoclass:: mongoengine.GeoPointField
|
||||
.. autoclass:: mongoengine.SequenceField
|
||||
.. 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.LazyReferenceField
|
||||
.. autoclass:: mongoengine.fields.GenericReferenceField
|
||||
.. autoclass:: mongoengine.fields.GenericLazyReferenceField
|
||||
.. 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,6 +2,640 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
Development
|
||||
===========
|
||||
- (Fill this out as you fix issues and develop your features).
|
||||
|
||||
=================
|
||||
Changes in 0.16.2
|
||||
=================
|
||||
- Fix .save() that fails when called with write_concern=None (regression of 0.16.1) #1958
|
||||
|
||||
=================
|
||||
Changes in 0.16.1
|
||||
=================
|
||||
- Fix `_cls` that is not set properly in Document constructor (regression) #1950
|
||||
- Fix bug in _delta method - Update of a ListField depends on an unrelated dynamic field update #1733
|
||||
- Remove deprecated `save()` method and used `insert_one()` #1899
|
||||
|
||||
=================
|
||||
Changes in 0.16.0
|
||||
=================
|
||||
- Various improvements to the doc
|
||||
- Improvement to code quality
|
||||
- POTENTIAL BREAKING CHANGES:
|
||||
- EmbeddedDocumentField will no longer accept references to Document classes in its constructor #1661
|
||||
- Get rid of the `basecls` parameter from the DictField constructor (dead code) #1876
|
||||
- default value of ComplexDateTime is now None (and no longer the current datetime) #1368
|
||||
- Fix unhashable TypeError when referencing a Document with a compound key in an EmbeddedDocument #1685
|
||||
- Fix bug where an EmbeddedDocument with the same id as its parent would not be tracked for changes #1768
|
||||
- Fix the fact that bulk `insert()` was not setting primary keys of inserted documents instances #1919
|
||||
- Fix bug when referencing the abstract class in a ReferenceField #1920
|
||||
- Allow modification to the document made in pre_save_post_validation to be taken into account #1202
|
||||
- Replaced MongoDB 2.4 tests in CI by MongoDB 3.2 #1903
|
||||
- Fix side effects of using queryset.`no_dereference` on other documents #1677
|
||||
- Fix TypeError when using lazy django translation objects as translated choices #1879
|
||||
- Improve 2-3 codebase compatibility #1889
|
||||
- Fix the support for changing the default value of ComplexDateTime #1368
|
||||
- Improves error message in case an EmbeddedDocumentListField receives an EmbeddedDocument instance
|
||||
instead of a list #1877
|
||||
- Fix the Decimal operator inc/dec #1517 #1320
|
||||
- Ignore killcursors queries in `query_counter` context manager #1869
|
||||
- Fix the fact that `query_counter` was modifying the initial profiling_level in case it was != 0 #1870
|
||||
- Repaired the `no_sub_classes` context manager + fix the fact that it was swallowing exceptions #1865
|
||||
- Fix index creation error that was swallowed by hasattr under python2 #1688
|
||||
- QuerySet limit function behaviour: Passing 0 as parameter will return all the documents in the cursor #1611
|
||||
- bulk insert updates the ids of the input documents instances #1919
|
||||
- Fix an harmless bug related to GenericReferenceField where modifications in the generic-referenced document
|
||||
were tracked in the parent #1934
|
||||
- Improve validator of BinaryField #273
|
||||
- Implemented lazy regex compiling in Field classes to improve 'import mongoengine' performance #1806
|
||||
- Updated GridFSProxy.__str__ so that it would always print both the filename and grid_id #710
|
||||
- Add __repr__ to Q and QCombination #1843
|
||||
- fix bug in BaseList.__iter__ operator (was occuring when modifying a BaseList while iterating over it) #1676
|
||||
- Added field `DateField`#513
|
||||
|
||||
Changes in 0.15.3
|
||||
=================
|
||||
- Subfield resolve error in generic_emdedded_document query #1651 #1652
|
||||
- use each modifier only with $position #1673 #1675
|
||||
- Improve LazyReferenceField and GenericLazyReferenceField with nested fields #1704
|
||||
- Fix validation error instance in GenericEmbeddedDocumentField #1067
|
||||
- Update cached fields when fields argument is given #1712
|
||||
- Add a db parameter to register_connection for compatibility with connect
|
||||
- Use insert_one, insert_many in Document.insert #1491
|
||||
- Use new update_one, update_many on document/queryset update #1491
|
||||
- Use insert_one, insert_many in Document.insert #1491
|
||||
- Fix reload(fields) affect changed fields #1371
|
||||
- Fix Read-only access to database fails when trying to create indexes #1338
|
||||
|
||||
Changes in 0.15.0
|
||||
=================
|
||||
- Add LazyReferenceField and GenericLazyReferenceField to address #1230
|
||||
|
||||
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)
|
||||
- Updated inheritable objects created by upsert now contain _cls (#118)
|
||||
- Added support for creating documents with embedded documents in a single operation (#6)
|
||||
- Added to_json and from_json to Document (#1)
|
||||
- Added to_json and from_json to QuerySet (#131)
|
||||
- Updated index creation now tied to Document class (#102)
|
||||
- Added none() to queryset (#127)
|
||||
- Updated SequenceFields to allow post processing of the calculated counter value (#141)
|
||||
- Added clean method to documents for pre validation data cleaning (#60)
|
||||
- Added support setting for read prefrence at a query level (#157)
|
||||
- Added _instance to EmbeddedDocuments pointing to the parent (#139)
|
||||
- Inheritance is off by default (#122)
|
||||
- Remove _types and just use _cls for inheritance (#148)
|
||||
- Only allow QNode instances to be passed as query objects (#199)
|
||||
- Dynamic fields are now validated on save (#153) (#154)
|
||||
- Added support for multiple slices and made slicing chainable. (#170) (#190) (#191)
|
||||
- Fixed GridFSProxy __getattr__ behaviour (#196)
|
||||
- Fix Django timezone support (#151)
|
||||
- Simplified Q objects, removed QueryTreeTransformerVisitor (#98) (#171)
|
||||
- FileFields now copyable (#198)
|
||||
- Querysets now return clones and are no longer edit in place (#56)
|
||||
- Added support for $maxDistance (#179)
|
||||
- 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 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)
|
||||
- Added switch_collection context manager (#220)
|
||||
- Added switch_collection method to document instances (#220)
|
||||
- Added support for compound primary keys (#149) (#121)
|
||||
- Fixed overriding objects with custom manager (#58)
|
||||
- Added no_dereference method for querysets (#82) (#61)
|
||||
- Undefined data should not override instance methods (#49)
|
||||
- Added Django Group and Permission (#142)
|
||||
- Added Doc class and pk to Validation messages (#69)
|
||||
- Fixed Documents deleted via a queryset don't call any signals (#105)
|
||||
- Added the "get_decoded" method to the MongoSession class (#216)
|
||||
- Fixed invalid choices error bubbling (#214)
|
||||
- Updated Save so it calls $set and $unset in a single operation (#211)
|
||||
- Fixed inner queryset looping (#204)
|
||||
|
||||
Changes in 0.7.10
|
||||
=================
|
||||
- Fix UnicodeEncodeError for dbref (#278)
|
||||
- Allow construction using positional parameters (#268)
|
||||
- Updated EmailField length to support long domains (#243)
|
||||
- Added 64-bit integer support (#251)
|
||||
- Added Django sessions TTL support (#224)
|
||||
- Fixed issue with numerical keys in MapField(EmbeddedDocumentField()) (#240)
|
||||
- Fixed clearing _changed_fields for complex nested embedded documents (#237, #239, #242)
|
||||
- Added "id" back to _data dictionary (#255)
|
||||
- Only mark a field as changed if the value has changed (#258)
|
||||
- Explicitly check for Document instances when dereferencing (#261)
|
||||
- Fixed order_by chaining issue (#265)
|
||||
- Added dereference support for tuples (#250)
|
||||
- Resolve field name to db field name when using distinct(#260, #264, #269)
|
||||
- Added kwargs to doc.save to help interop with django (#223, #270)
|
||||
- Fixed cloning querysets in PY3
|
||||
- Int fields no longer unset in save when changed to 0 (#272)
|
||||
- Fixed ReferenceField query chaining bug fixed (#254)
|
||||
|
||||
Changes in 0.7.9
|
||||
================
|
||||
- Better fix handling for old style _types
|
||||
- Embedded SequenceFields follow collection naming convention
|
||||
|
||||
Changes in 0.7.8
|
||||
================
|
||||
- Fix sequence fields in embedded documents (#166)
|
||||
- Fix query chaining with .order_by() (#176)
|
||||
- Added optional encoding and collection config for Django sessions (#180, #181, #183)
|
||||
- Fixed EmailField so can add extra validation (#173, #174, #187)
|
||||
- Fixed bulk inserts can now handle custom pk's (#192)
|
||||
- Added as_pymongo method to return raw or cast results from pymongo (#193)
|
||||
|
||||
Changes in 0.7.7
|
||||
================
|
||||
- Fix handling for old style _types
|
||||
|
||||
Changes in 0.7.6
|
||||
================
|
||||
- Unicode fix for repr (#133)
|
||||
- Allow updates with match operators (#144)
|
||||
- Updated URLField - now can have a override the regex (#136)
|
||||
- Allow Django AuthenticationBackends to work with Django user (hmarr/mongoengine#573)
|
||||
- Fixed reload issue with ReferenceField where dbref=False (#138)
|
||||
|
||||
Changes in 0.7.5
|
||||
================
|
||||
- ReferenceFields with dbref=False use ObjectId instead of strings (#134)
|
||||
See ticket for upgrade notes (#134)
|
||||
|
||||
Changes in 0.7.4
|
||||
================
|
||||
- Fixed index inheritance issues - firmed up testcases (#123) (#125)
|
||||
|
||||
Changes in 0.7.3
|
||||
================
|
||||
- Reverted EmbeddedDocuments meta handling - now can turn off inheritance (#119)
|
||||
|
||||
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)
|
||||
- Fixed reloading on sharded documents (hmarr/mongoengine#569)
|
||||
- Added NotUniqueError for duplicate keys (#62)
|
||||
- Added custom collection / sequence naming for SequenceFields (#92)
|
||||
- Fixed UnboundLocalError in composite index with pk field (#88)
|
||||
- Updated ReferenceField's to optionally store ObjectId strings
|
||||
this will become the default in 0.8 (#89)
|
||||
- Added FutureWarning - save will default to `cascade=False` in 0.8
|
||||
- Added example of indexing embedded document fields (#75)
|
||||
- Fixed ImageField resizing when forcing size (#80)
|
||||
- Add flexibility for fields handling bad data (#78)
|
||||
- Embedded Documents no longer handle meta definitions
|
||||
- Use weakref proxies in base lists / dicts (#74)
|
||||
- Improved queryset filtering (hmarr/mongoengine#554)
|
||||
- Fixed Dynamic Documents and Embedded Documents (hmarr/mongoengine#561)
|
||||
- Fixed abstract classes and shard keys (#64)
|
||||
- Fixed Python 2.5 support
|
||||
- Added Python 3 support (thanks to Laine Heron)
|
||||
|
||||
Changes in 0.6.20
|
||||
=================
|
||||
- Added support for distinct and db_alias (#59)
|
||||
- Improved support for chained querysets when constraining the same fields (hmarr/mongoengine#554)
|
||||
- Fixed BinaryField lookup re (#48)
|
||||
|
||||
Changes in 0.6.19
|
||||
=================
|
||||
|
||||
- Added Binary support to UUID (#47)
|
||||
- Fixed MapField lookup for fields without declared lookups (#46)
|
||||
- Fixed BinaryField python value issue (#48)
|
||||
- Fixed SequenceField non numeric value lookup (#41)
|
||||
- Fixed queryset manager issue (#52)
|
||||
- Fixed FileField comparision (hmarr/mongoengine#547)
|
||||
|
||||
Changes in 0.6.18
|
||||
=================
|
||||
- Fixed recursion loading bug in _get_changed_fields
|
||||
|
||||
Changes in 0.6.17
|
||||
=================
|
||||
- Fixed issue with custom queryset manager expecting explict variable names
|
||||
|
||||
Changes in 0.6.16
|
||||
=================
|
||||
- Fixed issue where db_alias wasn't inherited
|
||||
|
||||
Changes in 0.6.15
|
||||
=================
|
||||
- Updated validation error messages
|
||||
- Added support for null / zero / false values in item_frequencies
|
||||
- Fixed cascade save edge case
|
||||
- Fixed geo index creation through reference fields
|
||||
- Added support for args / kwargs when using @queryset_manager
|
||||
- Deref list custom id fix
|
||||
|
||||
Changes in 0.6.14
|
||||
=================
|
||||
- Fixed error dict with nested validation
|
||||
- Fixed Int/Float fields and not equals None
|
||||
- Exclude tests from installation
|
||||
- Allow tuples for index meta
|
||||
- Fixed use of str in instance checks
|
||||
- Fixed unicode support in transform update
|
||||
- Added support for add_to_set and each
|
||||
|
||||
Changes in 0.6.13
|
||||
=================
|
||||
- Fixed EmbeddedDocument db_field validation issue
|
||||
- Fixed StringField unicode issue
|
||||
- Fixes __repr__ modifying the cursor
|
||||
|
||||
Changes in 0.6.12
|
||||
=================
|
||||
- Fixes scalar lookups for primary_key
|
||||
- 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
|
||||
- Fixed Django TestCase
|
||||
- Added cmp to Embedded Document
|
||||
- Added PULL reverse_delete_rule
|
||||
- Fixed CASCADE delete bug
|
||||
- Fixed db_field data load error
|
||||
- Fixed recursive save with FileField
|
||||
|
||||
Changes in 0.6.10
|
||||
=================
|
||||
- Fixed basedict / baselist to return super(..)
|
||||
@@ -16,7 +650,7 @@ Changes in 0.6.8
|
||||
================
|
||||
- Fixed FileField losing reference when no default set
|
||||
- Removed possible race condition from FileField (grid_file)
|
||||
- Added assignment to save, can now do: b = MyDoc(**kwargs).save()
|
||||
- Added assignment to save, can now do: `b = MyDoc(**kwargs).save()`
|
||||
- Added support for pull operations on nested EmbeddedDocuments
|
||||
- Added support for choices with GenericReferenceFields
|
||||
- Added support for choices with GenericEmbeddedDocumentFields
|
||||
@@ -71,7 +705,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
|
||||
@@ -159,8 +793,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()
|
||||
|
||||
@@ -41,26 +45,27 @@ post2.link_url = 'http://tractiondigital.com/labs/mongoengine/docs'
|
||||
post2.tags = ['mongoengine']
|
||||
post2.save()
|
||||
|
||||
print 'ALL POSTS'
|
||||
print
|
||||
print('ALL POSTS')
|
||||
print()
|
||||
for post in Post.objects:
|
||||
print post.title
|
||||
print '=' * len(post.title)
|
||||
print(post.title)
|
||||
#print '=' * post.title.count()
|
||||
print("=" * 20)
|
||||
|
||||
if isinstance(post, TextPost):
|
||||
print post.content
|
||||
print(post.content)
|
||||
|
||||
if isinstance(post, LinkPost):
|
||||
print 'Link:', post.link_url
|
||||
print('Link:', post.link_url)
|
||||
|
||||
print
|
||||
print
|
||||
print()
|
||||
print()
|
||||
|
||||
print 'POSTS TAGGED \'MONGODB\''
|
||||
print
|
||||
print('POSTS TAGGED \'MONGODB\'')
|
||||
print()
|
||||
for post in Post.objects(tags='mongodb'):
|
||||
print post.title
|
||||
print
|
||||
print(post.title)
|
||||
print()
|
||||
|
||||
num_posts = Post.objects(tags='mongodb').count()
|
||||
print 'Found %d posts with tag "mongodb"' % num_posts
|
||||
print('Found %d posts with tag "mongodb"' % num_posts)
|
||||
|
||||
31
docs/conf.py
31
docs/conf.py
@@ -13,10 +13,14 @@
|
||||
|
||||
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.
|
||||
sys.path.append(os.path.abspath('..'))
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
@@ -38,13 +42,12 @@ master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'MongoEngine'
|
||||
copyright = u'2009-2012, MongoEngine Authors'
|
||||
copyright = u'2009, MongoEngine Authors'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |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'
|
||||
|
||||
@@ -1,90 +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.
|
||||
|
||||
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'
|
||||
|
||||
.. 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,60 +6,134 @@ 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`, :attr:`password`
|
||||
and :attr:`authentication_source` arguments should be provided::
|
||||
|
||||
connect('project1', username='webapp', password='pwd123', authentication_source='admin')
|
||||
|
||||
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'
|
||||
)
|
||||
|
||||
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)
|
||||
Bar.objects(read_preference=ReadPreference.PRIMARY)
|
||||
|
||||
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'}
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Switch Database
|
||||
---------------
|
||||
The :class:`~mongoengine.context_managers.switch_db` context manager allows
|
||||
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
|
||||
|
||||
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()
|
||||
|
||||
Group(name='test').save() # Saves in the default 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
|
||||
@@ -22,11 +22,14 @@ objects** as class attributes to the document class::
|
||||
|
||||
class Page(Document):
|
||||
title = StringField(max_length=200, required=True)
|
||||
date_modified = DateTimeField(default=datetime.datetime.now)
|
||||
date_modified = DateTimeField(default=datetime.datetime.utcnow)
|
||||
|
||||
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.
|
||||
|
||||
@@ -47,10 +50,11 @@ be saved ::
|
||||
>>> Page.objects(tags='mongoengine').count()
|
||||
>>> 1
|
||||
|
||||
..note::
|
||||
.. note::
|
||||
|
||||
There is one caveat on Dynamic Documents: fields cannot start with `_`
|
||||
|
||||
Dynamic fields are stored in creation order *after* any declared fields.
|
||||
|
||||
Fields
|
||||
======
|
||||
@@ -62,28 +66,40 @@ 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.StringField`
|
||||
* :class:`~mongoengine.URLField`
|
||||
* :class:`~mongoengine.EmailField`
|
||||
* :class:`~mongoengine.IntField`
|
||||
* :class:`~mongoengine.FloatField`
|
||||
* :class:`~mongoengine.DecimalField`
|
||||
* :class:`~mongoengine.DateTimeField`
|
||||
* :class:`~mongoengine.ComplexDateTimeField`
|
||||
* :class:`~mongoengine.ListField`
|
||||
* :class:`~mongoengine.SortedListField`
|
||||
* :class:`~mongoengine.DictField`
|
||||
* :class:`~mongoengine.MapField`
|
||||
* :class:`~mongoengine.ObjectIdField`
|
||||
* :class:`~mongoengine.ReferenceField`
|
||||
* :class:`~mongoengine.GenericReferenceField`
|
||||
* :class:`~mongoengine.EmbeddedDocumentField`
|
||||
* :class:`~mongoengine.GenericEmbeddedDocumentField`
|
||||
* :class:`~mongoengine.BooleanField`
|
||||
* :class:`~mongoengine.FileField`
|
||||
* :class:`~mongoengine.BinaryField`
|
||||
* :class:`~mongoengine.GeoPointField`
|
||||
* :class:`~mongoengine.SequenceField`
|
||||
* :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.GenericLazyReferenceField`
|
||||
* :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.LazyReferenceField`
|
||||
* :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
|
||||
---------------
|
||||
@@ -93,9 +109,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
|
||||
@@ -104,10 +117,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
|
||||
@@ -122,6 +135,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
|
||||
@@ -132,10 +146,13 @@ arguments can be set on all fields:
|
||||
field, will not have two documents in the collection with the same value.
|
||||
|
||||
:attr:`primary_key` (Default: False)
|
||||
When True, use this field as a primary key for the collection.
|
||||
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
|
||||
@@ -158,18 +175,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):
|
||||
@@ -187,7 +204,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):
|
||||
@@ -199,17 +216,17 @@ 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()
|
||||
user = ReferenceField(User)
|
||||
answers = DictField()
|
||||
|
||||
survey_response = SurveyResponse(date=datetime.now(), user=request.user)
|
||||
survey_response = SurveyResponse(date=datetime.utcnow(), user=request.user)
|
||||
response_form = ResponseForm(request.POST)
|
||||
survey_response.answers = response_form.cleaned_data()
|
||||
survey_response.save()
|
||||
@@ -220,7 +237,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::
|
||||
|
||||
@@ -241,9 +258,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::
|
||||
|
||||
@@ -256,6 +273,41 @@ as the constructor's argument::
|
||||
content = StringField()
|
||||
|
||||
|
||||
.. _one-to-many-with-listfields:
|
||||
|
||||
One to Many with ListFields
|
||||
'''''''''''''''''''''''''''
|
||||
|
||||
If you are implementing a one to many relationship via a list of references,
|
||||
then the references are stored as DBRefs and to query you need to pass an
|
||||
instance of the object to the query::
|
||||
|
||||
class User(Document):
|
||||
name = StringField()
|
||||
|
||||
class Page(Document):
|
||||
content = StringField()
|
||||
authors = ListField(ReferenceField(User))
|
||||
|
||||
bob = User(name="Bob Jones").save()
|
||||
john = User(name="John Smith").save()
|
||||
|
||||
Page(content="Test Page", authors=[bob, john]).save()
|
||||
Page(content="Another Page", authors=[john]).save()
|
||||
|
||||
# Find all pages Bob authored
|
||||
Page.objects(authors__in=[bob])
|
||||
|
||||
# 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
|
||||
'''''''''''''''''''''''''''''''''''''''''''
|
||||
By default, MongoDB doesn't check the integrity of your data, so deleting
|
||||
@@ -266,12 +318,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.
|
||||
|
||||
@@ -287,8 +339,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.fields.ListField` (:class:`~mongoengine.fields.ReferenceField`).
|
||||
|
||||
|
||||
.. warning::
|
||||
@@ -307,11 +363,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.
|
||||
|
||||
|
||||
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::
|
||||
|
||||
@@ -335,18 +390,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::
|
||||
|
||||
@@ -358,7 +413,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):
|
||||
@@ -373,7 +428,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
|
||||
@@ -390,8 +445,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::
|
||||
|
||||
@@ -399,53 +456,165 @@ 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
|
||||
=======
|
||||
|
||||
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:
|
||||
If a dictionary is passed then additional options become available. Valid options include,
|
||||
but are not limited to:
|
||||
|
||||
|
||||
:attr:`fields` (Default: None)
|
||||
The fields to index. Specified in the same format as described above.
|
||||
|
||||
:attr:`types` (Default: True)
|
||||
Whether the index should have the :attr:`_types` field added automatically
|
||||
to the start of the index.
|
||||
:attr:`cls` (Default: True)
|
||||
If you have polymorphic models that inherit and have
|
||||
:attr:`allow_inheritance` turned on, you can configure whether the index
|
||||
should have the :attr:`_cls` field added automatically to the start of the
|
||||
index.
|
||||
|
||||
:attr:`sparse` (Default: False)
|
||||
Whether the index should be sparse.
|
||||
|
||||
:attr:`unique` (Default: False)
|
||||
Whether the index should be sparse.
|
||||
Whether the index should be unique.
|
||||
|
||||
.. warning::
|
||||
:attr:`expireAfterSeconds` (Optional)
|
||||
Allows you to automatically expire data from a collection by setting the
|
||||
time in seconds to expire the a field.
|
||||
|
||||
:attr:`name` (Optional)
|
||||
Allows you to specify a name for the index
|
||||
|
||||
:attr:`collation` (Optional)
|
||||
Allows to create case insensitive indexes (MongoDB v3.4+ only)
|
||||
|
||||
.. note::
|
||||
|
||||
Additional options are forwarded as **kwargs to pymongo's create_index method.
|
||||
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_opts': {},
|
||||
'index_background': True,
|
||||
'index_cls': False,
|
||||
'auto_create_index': True,
|
||||
'index_drop_dups': True,
|
||||
}
|
||||
|
||||
|
||||
Inheritance adds extra indices.
|
||||
If don't need inheritance for a document turn inheritance off - see :ref:`document-inheritance`.
|
||||
:attr:`index_opts` (Optional)
|
||||
Set any default index options - see the `full options list <https://docs.mongodb.com/manual/reference/method/db.collection.createIndex/#db.collection.createIndex>`_
|
||||
|
||||
: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:`auto_create_index` (Optional)
|
||||
When this is True (default), MongoEngine will ensure that the correct
|
||||
indexes exist in MongoDB each time a command is run. This can be disabled
|
||||
in systems where indexes are managed separately. Disabling this will improve
|
||||
performance.
|
||||
|
||||
:attr:`index_drop_dups` (Optional)
|
||||
Set the default value for if an index should drop duplicates
|
||||
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 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. ::
|
||||
|
||||
@@ -457,6 +626,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.utcnow)
|
||||
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
|
||||
@@ -501,11 +699,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()
|
||||
@@ -527,7 +725,9 @@ 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::
|
||||
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.::
|
||||
|
||||
# Stored in a collection named 'page'
|
||||
class Page(Document):
|
||||
@@ -539,25 +739,50 @@ convenient and efficient retrieval of related documents::
|
||||
class DatedPage(Page):
|
||||
date = DateTimeField()
|
||||
|
||||
.. note:: From 0.7 onwards you must declare `allow_inheritance` in the document meta.
|
||||
.. note:: From 0.8 onwards :attr:`allow_inheritance` defaults
|
||||
to False, meaning you must set it to True to use inheritance.
|
||||
|
||||
Setting :attr:`allow_inheritance` to True should also be used in
|
||||
:class:`~mongoengine.EmbeddedDocument` class in case you need to subclass it
|
||||
|
||||
Working with existing data
|
||||
--------------------------
|
||||
To enable correct retrieval of documents involved in this kind of heirarchy,
|
||||
two extra attributes are stored on each document in the database: :attr:`_cls`
|
||||
and :attr:`_types`. These are hidden from the user through the MongoEngine
|
||||
interface, but may not be present if you are trying to use MongoEngine with
|
||||
an existing database. For this reason, you may disable this inheritance
|
||||
mechansim, removing the dependency of :attr:`_cls` and :attr:`_types`, enabling
|
||||
you to work with existing databases. To disable inheritance on a document
|
||||
class, set :attr:`allow_inheritance` to ``False`` in the :attr:`meta`
|
||||
dictionary::
|
||||
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 ::
|
||||
|
||||
# Will work with data in an existing collection named 'cmsPage'
|
||||
class Page(Document):
|
||||
title = StringField(max_length=200, required=True)
|
||||
meta = {
|
||||
'collection': 'cmsPage',
|
||||
'allow_inheritance': False,
|
||||
'collection': 'cmsPage'
|
||||
}
|
||||
|
||||
If you have wildly varying schemas then using a
|
||||
:class:`~mongoengine.DynamicDocument` might be more appropriate, instead of
|
||||
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,21 +30,54 @@ 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`
|
||||
|
||||
Pre save data validation and cleaning
|
||||
-------------------------------------
|
||||
MongoEngine allows you to create custom cleaning rules for your documents when
|
||||
calling :meth:`~mongoengine.Document.save`. By providing a custom
|
||||
:meth:`~mongoengine.Document.clean` method you can do any pre validation / data
|
||||
cleaning.
|
||||
|
||||
This might be useful if you want to ensure a default value based on other
|
||||
document values for example::
|
||||
|
||||
class Essay(Document):
|
||||
status = StringField(choices=('Published', 'Draft'), required=True)
|
||||
pub_date = DateTimeField()
|
||||
|
||||
def clean(self):
|
||||
"""Ensures that only published essays have a `pub_date` and
|
||||
automatically sets `pub_date` if essay is published and `pub_date`
|
||||
is not set"""
|
||||
if self.status == 'Draft' and self.pub_date is not None:
|
||||
msg = 'Draft entries should not have a publication date.'
|
||||
raise ValidationError(msg)
|
||||
# Set the pub_date for published items if not set.
|
||||
if self.status == 'Published' and self.pub_date is None:
|
||||
self.pub_date = datetime.now()
|
||||
|
||||
.. note::
|
||||
Cleaning is only called if validation is turned on and when calling
|
||||
:meth:`~mongoengine.Document.save`.
|
||||
|
||||
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
|
||||
------------------
|
||||
@@ -81,12 +114,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,
|
||||
@@ -18,26 +18,16 @@ a document is created to store details about animals, including a photo::
|
||||
family = StringField()
|
||||
photo = FileField()
|
||||
|
||||
marmot = Animal('Marmota', 'Sciuridae')
|
||||
|
||||
marmot_photo = open('marmot.jpg', 'r') # Retrieve a photo from disk
|
||||
marmot.photo = marmot_photo # Store photo in the document
|
||||
marmot.photo.content_type = 'image/jpeg' # Store metadata
|
||||
|
||||
marmot.save()
|
||||
|
||||
Another way of writing to a :class:`~mongoengine.FileField` is to use the
|
||||
:func:`put` method. This allows for metadata to be stored in the same call as
|
||||
the file::
|
||||
|
||||
marmot.photo.put(marmot_photo, content_type='image/jpeg')
|
||||
marmot = Animal(genus='Marmota', family='Sciuridae')
|
||||
|
||||
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()
|
||||
@@ -47,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`::
|
||||
|
||||
@@ -56,14 +46,15 @@ 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
|
||||
--------
|
||||
|
||||
Deleting stored files is achieved with the :func:`delete` method::
|
||||
|
||||
marmot.photo.delete()
|
||||
marmot.photo.delete() # Deletes the GridFS document
|
||||
marmot.save() # Saves the GridFS reference (being None) contained in the marmot instance
|
||||
|
||||
.. warning::
|
||||
|
||||
@@ -80,5 +71,6 @@ 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')
|
||||
marmot.photo.replace(another_marmot, content_type='image/png')
|
||||
another_marmot = open('another_marmot.png', 'rb')
|
||||
marmot.photo.replace(another_marmot, content_type='image/png') # Replaces the GridFS document
|
||||
marmot.save() # Replaces the GridFS reference contained in marmot instance
|
||||
|
||||
@@ -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,14 +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):
|
||||
@@ -129,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::
|
||||
|
||||
@@ -144,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]
|
||||
@@ -157,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
|
||||
@@ -179,25 +259,21 @@ Retrieving unique results
|
||||
-------------------------
|
||||
To retrieve a result that should be unique in the collection, use
|
||||
:meth:`~mongoengine.queryset.QuerySet.get`. This will raise
|
||||
:class:`~mongoengine.queryset.DoesNotExist` if no document matches the query,
|
||||
and :class:`~mongoengine.queryset.MultipleObjectsReturned` if more than one
|
||||
document matched the query.
|
||||
:class:`~mongoengine.queryset.DoesNotExist` if
|
||||
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 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
|
||||
@@ -232,7 +308,7 @@ custom manager methods as you like::
|
||||
BlogPost(title='test1', published=False).save()
|
||||
BlogPost(title='test2', published=True).save()
|
||||
assert len(BlogPost.objects) == 2
|
||||
assert len(BlogPost.live_posts) == 1
|
||||
assert len(BlogPost.live_posts()) == 1
|
||||
|
||||
Custom QuerySets
|
||||
================
|
||||
@@ -240,14 +316,19 @@ 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):
|
||||
pass
|
||||
|
||||
def get_awesome(self):
|
||||
return self.filter(awesome=True)
|
||||
|
||||
class Page(Document):
|
||||
meta = {'queryset_class': AwesomerQuerySet}
|
||||
|
||||
# To call:
|
||||
Page.objects.get_awesome()
|
||||
|
||||
.. versionadded:: 0.4
|
||||
|
||||
Aggregation
|
||||
@@ -259,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
|
||||
-------------------
|
||||
@@ -310,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
|
||||
@@ -342,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
|
||||
@@ -360,8 +448,30 @@ references to the depth of 1 level. If you have more complicated documents and
|
||||
want to dereference more of the object at once then increasing the :attr:`max_depth`
|
||||
will dereference more levels of the document.
|
||||
|
||||
Turning off dereferencing
|
||||
-------------------------
|
||||
|
||||
Sometimes for performance reasons you don't want to automatically dereference
|
||||
data. To turn off dereferencing of the results of a query use
|
||||
:func:`~mongoengine.queryset.QuerySet.no_dereference` on the queryset like so::
|
||||
|
||||
post = Post.objects.no_dereference().first()
|
||||
assert(isinstance(post.author, DBRef))
|
||||
|
||||
You can also turn off all dereferencing for a fixed period by using the
|
||||
:class:`~mongoengine.context_managers.no_dereference` context manager::
|
||||
|
||||
with no_dereference(Post) as Post:
|
||||
post = Post.objects.first()
|
||||
assert(isinstance(post.author, DBRef))
|
||||
|
||||
# Outside the context manager dereferencing occurs.
|
||||
assert(isinstance(post.author, User))
|
||||
|
||||
|
||||
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
|
||||
@@ -374,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::
|
||||
|
||||
@@ -420,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.
|
||||
@@ -436,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>`_.
|
||||
@@ -478,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)::
|
||||
|
||||
@@ -505,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 most actions
|
||||
(validation, insert/update, and cascades, but not 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,5 +113,37 @@ Example usage::
|
||||
signals.pre_save.connect(Author.pre_save, sender=Author)
|
||||
signals.post_save.connect(Author.post_save, sender=Author)
|
||||
|
||||
.. warning::
|
||||
|
||||
Note that EmbeddedDocument only supports pre/post_init signals. pre/post_save, etc should be attached to Document's class only. Attaching pre_save to an EmbeddedDocument is ignored silently.
|
||||
|
||||
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::
|
||||
|
||||
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()
|
||||
|
||||
|
||||
.. _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_text('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/hmarr/mongoengine>`_ and
|
||||
contributions are always encouraged. Contributions can be as simple as
|
||||
minor tweaks to this documentation. To contribute, fork the project on
|
||||
`GitHub <http://github.com/hmarr/mongoengine>`_ and send a
|
||||
pull request.
|
||||
**Yes please!** We are always looking for contributions, additions and improvements.
|
||||
|
||||
Also, you can join the developers' `mailing list
|
||||
<http://groups.google.com/group/mongoengine-dev>`_.
|
||||
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.
|
||||
|
||||
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,21 +85,25 @@ 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::
|
||||
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):
|
||||
title = StringField(max_length=120, required=True)
|
||||
author = ReferenceField(User)
|
||||
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
class TextPost(Post):
|
||||
content = StringField()
|
||||
|
||||
@@ -100,20 +114,21 @@ this kind of modelling out of the box::
|
||||
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):
|
||||
@@ -121,21 +136,24 @@ 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
|
||||
*embedded documents* directly on a post document. An embedded document should
|
||||
be treated no differently that a regular document; it just doesn't have its own
|
||||
be treated no differently than a regular document; it just doesn't have its own
|
||||
collection in the database. Using MongoEngine, we can define the structure of
|
||||
embedded documents, along with utility methods, in exactly the same way we do
|
||||
with regular documents::
|
||||
@@ -155,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::
|
||||
|
||||
@@ -165,9 +183,9 @@ 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::
|
||||
.. note::
|
||||
MapFields and DictFields currently don't support automatic handling of
|
||||
deleted references
|
||||
|
||||
@@ -178,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
|
||||
@@ -212,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:
|
||||
@@ -238,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
|
||||
@@ -261,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
|
||||
@@ -270,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.
|
||||
|
||||
555
docs/upgrade.rst
555
docs/upgrade.rst
@@ -1,47 +1,552 @@
|
||||
=========
|
||||
#########
|
||||
Upgrading
|
||||
=========
|
||||
#########
|
||||
|
||||
0.5 to 0.6
|
||||
Development
|
||||
***********
|
||||
(Fill this out whenever you introduce breaking changes to MongoEngine)
|
||||
|
||||
URLField's constructor no longer takes `verify_exists`
|
||||
|
||||
0.15.0
|
||||
******
|
||||
|
||||
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
|
||||
==========
|
||||
|
||||
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.
|
||||
Inheritance
|
||||
-----------
|
||||
|
||||
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
|
||||
inherited classes like so: ::
|
||||
|
||||
# 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()
|
||||
|
||||
|
||||
Document Definition
|
||||
-------------------
|
||||
|
||||
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: ::
|
||||
|
||||
class Animal(Document):
|
||||
name = StringField()
|
||||
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
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: ::
|
||||
|
||||
>>> from mongoengine import *
|
||||
>>> class Animal(Document):
|
||||
... name = StringField()
|
||||
...
|
||||
>>> cat = Animal(name="kit", size="small")
|
||||
|
||||
# 0.7
|
||||
>>> cat.size
|
||||
u'small'
|
||||
|
||||
# 0.8
|
||||
>>> cat.size
|
||||
Traceback (most recent call last):
|
||||
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
|
||||
long running gotcha. If you edit your querysets inplace you will have to
|
||||
update your code like so: ::
|
||||
|
||||
# Old code:
|
||||
mammals = Animal.objects(type="mammal")
|
||||
mammals.filter(order="Carnivora") # Returns a cloned queryset that isn't assigned to anything - so this will break in 0.8
|
||||
[m for m in mammals] # This will return all mammals in 0.8 as the 2nd filter returned a new queryset
|
||||
|
||||
# Update example a) assign queryset after a change:
|
||||
mammals = Animal.objects(type="mammal")
|
||||
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.
|
||||
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
|
||||
cascading saves then either set it in the `meta` or pass
|
||||
via `save` eg ::
|
||||
|
||||
# At the class level:
|
||||
class Person(Document):
|
||||
meta = {'cascade': True}
|
||||
|
||||
# Or in code:
|
||||
my_document.save(cascade=True)
|
||||
|
||||
.. 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`
|
||||
will be raised.
|
||||
|
||||
|
||||
To explicitly continue to use DBRefs change the `dbref` flag
|
||||
to True ::
|
||||
|
||||
class Person(Document):
|
||||
groups = ListField(ReferenceField(Group, dbref=True))
|
||||
|
||||
To migrate to using strings instead of DBRefs you will have to manually
|
||||
migrate ::
|
||||
|
||||
# Step 1 - Migrate the model definition
|
||||
class Group(Document):
|
||||
author = ReferenceField(User, dbref=False)
|
||||
members = ListField(ReferenceField(User, dbref=False))
|
||||
|
||||
# Step 2 - Migrate the data
|
||||
for g in Group.objects():
|
||||
g.author = g.author
|
||||
g.members = g.members
|
||||
g.save()
|
||||
|
||||
|
||||
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
|
||||
stored in rather than as string representations. Your code may need to be
|
||||
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.
|
||||
|
||||
Reverse Delete Rules in Embedded Documents, MapFields and DictFields now throw
|
||||
an InvalidDocument error as they aren't currently supported.
|
||||
|
||||
Document._get_subclasses - Is no longer used and the class method has been removed.
|
||||
Document._get_subclasses - Is no longer used and the class method has been
|
||||
removed.
|
||||
|
||||
Document.objects.with_id - now raises an InvalidQueryError if used with a filter.
|
||||
Document.objects.with_id - now raises an InvalidQueryError if used with a
|
||||
filter.
|
||||
|
||||
FutureWarning - A future warning has been added to all inherited classes that
|
||||
don't define `allow_inheritance` in their meta.
|
||||
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.
|
||||
map reduce now requires pymongo 1.11+- The pymongo `merge_output` and
|
||||
`reduce_output` parameters, have been depreciated.
|
||||
|
||||
More methods now use map_reduce as db.eval is not supported for sharding as such
|
||||
the following have been changed:
|
||||
More methods now use map_reduce as db.eval is not supported for sharding as
|
||||
such the following have been changed:
|
||||
|
||||
* :meth:`~mongoengine.queryset.QuerySet.sum`
|
||||
* :meth:`~mongoengine.queryset.QuerySet.average`
|
||||
@@ -49,10 +554,10 @@ 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
|
||||
@@ -88,7 +593,8 @@ Alternatively, you can rename your collections eg ::
|
||||
|
||||
failure = False
|
||||
|
||||
collection_names = [d._get_collection_name() for d in _document_registry.values()]
|
||||
collection_names = [d._get_collection_name()
|
||||
for d in _document_registry.values()]
|
||||
|
||||
for new_style_name in collection_names:
|
||||
if not new_style_name: # embedded documents don't have collections
|
||||
@@ -106,10 +612,17 @@ Alternatively, you can rename your collections eg ::
|
||||
old_style_name, new_style_name)
|
||||
else:
|
||||
db[old_style_name].rename(new_style_name)
|
||||
print "Renamed: %s to %s" % (old_style_name, new_style_name)
|
||||
print "Renamed: %s to %s" % (old_style_name,
|
||||
new_style_name)
|
||||
|
||||
if failure:
|
||||
print "Upgrading collection names failed"
|
||||
else:
|
||||
print "Upgraded collection names"
|
||||
|
||||
|
||||
mongodb 1.8 > 2.0 +
|
||||
===================
|
||||
|
||||
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 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__ = (document.__all__ + fields.__all__ + connection.__all__ +
|
||||
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, 6, 10)
|
||||
|
||||
__all__ = (list(document.__all__) + list(fields.__all__) +
|
||||
list(connection.__all__) + list(queryset.__all__) +
|
||||
list(signals.__all__) + list(errors.__all__))
|
||||
|
||||
|
||||
VERSION = (0, 16, 2)
|
||||
|
||||
|
||||
def get_version():
|
||||
version = '%s.%s' % (VERSION[0], VERSION[1])
|
||||
if VERSION[2]:
|
||||
version = '%s.%s' % (version, VERSION[2])
|
||||
return version
|
||||
"""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()
|
||||
|
||||
1337
mongoengine/base.py
1337
mongoengine/base.py
File diff suppressed because it is too large
Load Diff
28
mongoengine/base/__init__.py
Normal file
28
mongoengine/base/__init__.py
Normal file
@@ -0,0 +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', 'LazyReference',
|
||||
|
||||
# document
|
||||
'BaseDocument',
|
||||
|
||||
# fields
|
||||
'BaseField', 'ComplexBaseField', 'ObjectIdField', 'GeoJsonBaseField',
|
||||
|
||||
# metaclasses
|
||||
'DocumentMetaclass', 'TopLevelDocumentMetaclass'
|
||||
)
|
||||
32
mongoengine/base/common.py
Normal file
32
mongoengine/base/common.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from mongoengine.errors import NotRegistered
|
||||
|
||||
__all__ = ('UPDATE_OPERATORS', 'get_document', '_document_registry')
|
||||
|
||||
|
||||
UPDATE_OPERATORS = {'set', 'unset', 'inc', 'dec', 'mul',
|
||||
'pop', 'push', 'push_all', 'pull',
|
||||
'pull_all', 'add_to_set', 'set_on_insert',
|
||||
'min', 'max', 'rename'}
|
||||
|
||||
|
||||
_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
|
||||
single_end = name.split('.')[-1]
|
||||
compound_end = '.%s' % single_end
|
||||
possible_match = [k for k in _document_registry
|
||||
if k.endswith(compound_end) or k == single_end]
|
||||
if len(possible_match) == 1:
|
||||
doc = _document_registry.get(possible_match.pop(), None)
|
||||
if not doc:
|
||||
raise NotRegistered("""
|
||||
`%s` has not been registered in the document registry.
|
||||
Importing the document class automatically registers it, has it
|
||||
been imported?
|
||||
""".strip() % name)
|
||||
return doc
|
||||
488
mongoengine/base/datastructures.py
Normal file
488
mongoengine/base/datastructures.py
Normal file
@@ -0,0 +1,488 @@
|
||||
import weakref
|
||||
|
||||
from bson import DBRef
|
||||
import six
|
||||
|
||||
from mongoengine.common import _import_class
|
||||
from mongoengine.errors import DoesNotExist, MultipleObjectsReturned
|
||||
|
||||
__all__ = ('BaseDict', 'BaseList', 'EmbeddedDocumentList', 'LazyReference')
|
||||
|
||||
|
||||
class BaseDict(dict):
|
||||
"""A special dict so we can watch any changes."""
|
||||
|
||||
_dereferenced = False
|
||||
_instance = None
|
||||
_name = None
|
||||
|
||||
def __init__(self, dict_items, instance, name):
|
||||
BaseDocument = _import_class('BaseDocument')
|
||||
|
||||
if isinstance(instance, BaseDocument):
|
||||
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 isinstance(value, dict) and not isinstance(value, BaseDict):
|
||||
value = BaseDict(value, None, '%s.%s' % (self._name, key))
|
||||
super(BaseDict, self).__setitem__(key, value)
|
||||
value._instance = self._instance
|
||||
elif isinstance(value, list) and not isinstance(value, BaseList):
|
||||
value = BaseList(value, None, '%s.%s' % (self._name, key))
|
||||
super(BaseDict, self).__setitem__(key, value)
|
||||
value._instance = self._instance
|
||||
return value
|
||||
|
||||
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, key, *args, **kwargs):
|
||||
self._mark_as_changed(key)
|
||||
return super(BaseDict, self).__delitem__(key)
|
||||
|
||||
def __delattr__(self, key, *args, **kwargs):
|
||||
self._mark_as_changed(key)
|
||||
return super(BaseDict, self).__delattr__(key)
|
||||
|
||||
def __getstate__(self):
|
||||
self.instance = None
|
||||
self._dereferenced = False
|
||||
return self
|
||||
|
||||
def __setstate__(self, state):
|
||||
self = state
|
||||
return self
|
||||
|
||||
def clear(self, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
return super(BaseDict, self).clear()
|
||||
|
||||
def pop(self, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
return super(BaseDict, self).pop(*args, **kwargs)
|
||||
|
||||
def popitem(self, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
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, key=None):
|
||||
if hasattr(self._instance, '_mark_as_changed'):
|
||||
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."""
|
||||
|
||||
_dereferenced = False
|
||||
_instance = None
|
||||
_name = None
|
||||
|
||||
def __init__(self, list_items, instance, name):
|
||||
BaseDocument = _import_class('BaseDocument')
|
||||
|
||||
if isinstance(instance, BaseDocument):
|
||||
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 isinstance(value, dict) and not isinstance(value, BaseDict):
|
||||
value = BaseDict(value, None, '%s.%s' % (self._name, key))
|
||||
super(BaseList, self).__setitem__(key, value)
|
||||
value._instance = self._instance
|
||||
elif isinstance(value, list) and not isinstance(value, BaseList):
|
||||
value = BaseList(value, None, '%s.%s' % (self._name, key))
|
||||
super(BaseList, self).__setitem__(key, value)
|
||||
value._instance = self._instance
|
||||
return value
|
||||
|
||||
def __iter__(self):
|
||||
for v in super(BaseList, self).__iter__():
|
||||
yield v
|
||||
|
||||
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):
|
||||
self._mark_as_changed()
|
||||
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
|
||||
self._dereferenced = False
|
||||
return self
|
||||
|
||||
def __setstate__(self, state):
|
||||
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)
|
||||
|
||||
def extend(self, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
return super(BaseList, self).extend(*args, **kwargs)
|
||||
|
||||
def insert(self, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
return super(BaseList, self).insert(*args, **kwargs)
|
||||
|
||||
def pop(self, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
return super(BaseList, self).pop(*args, **kwargs)
|
||||
|
||||
def remove(self, *args, **kwargs):
|
||||
self._mark_as_changed()
|
||||
return super(BaseList, self).remove(*args, **kwargs)
|
||||
|
||||
def reverse(self):
|
||||
self._mark_as_changed()
|
||||
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, key=None):
|
||||
if hasattr(self._instance, '_mark_as_changed'):
|
||||
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.
|
||||
|
||||
This method only supports simple comparison (e.g: .filter(name='John Doe'))
|
||||
and does not support operators like __gte, __lte, __icontains like queryset.filter does
|
||||
|
||||
: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 replacement values. This
|
||||
function does not support mongoDB update operators such as ``inc__``.
|
||||
|
||||
.. 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 = {'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]
|
||||
|
||||
|
||||
class LazyReference(DBRef):
|
||||
__slots__ = ('_cached_doc', 'passthrough', 'document_type')
|
||||
|
||||
def fetch(self, force=False):
|
||||
if not self._cached_doc or force:
|
||||
self._cached_doc = self.document_type.objects.get(pk=self.pk)
|
||||
if not self._cached_doc:
|
||||
raise DoesNotExist('Trying to dereference unknown document %s' % (self))
|
||||
return self._cached_doc
|
||||
|
||||
@property
|
||||
def pk(self):
|
||||
return self.id
|
||||
|
||||
def __init__(self, document_type, pk, cached_doc=None, passthrough=False):
|
||||
self.document_type = document_type
|
||||
self._cached_doc = cached_doc
|
||||
self.passthrough = passthrough
|
||||
super(LazyReference, self).__init__(self.document_type._get_collection_name(), pk)
|
||||
|
||||
def __getitem__(self, name):
|
||||
if not self.passthrough:
|
||||
raise KeyError()
|
||||
document = self.fetch()
|
||||
return document[name]
|
||||
|
||||
def __getattr__(self, name):
|
||||
if not object.__getattribute__(self, 'passthrough'):
|
||||
raise AttributeError()
|
||||
document = self.fetch()
|
||||
try:
|
||||
return document[name]
|
||||
except KeyError:
|
||||
raise AttributeError()
|
||||
|
||||
def __repr__(self):
|
||||
return "<LazyReference(%s, %r)>" % (self.document_type, self.pk)
|
||||
1090
mongoengine/base/document.py
Normal file
1090
mongoengine/base/document.py
Normal file
File diff suppressed because it is too large
Load Diff
647
mongoengine/base/fields.py
Normal file
647
mongoengine/base/fields.py
Normal file
@@ -0,0 +1,647 @@
|
||||
import operator
|
||||
import warnings
|
||||
import weakref
|
||||
|
||||
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
|
||||
|
||||
|
||||
__all__ = ('BaseField', 'ComplexBaseField', 'ObjectIdField',
|
||||
'GeoJsonBaseField')
|
||||
|
||||
|
||||
class BaseField(object):
|
||||
"""A base class for fields in a MongoDB document. Instances of this class
|
||||
may be added to subclasses of `Document` to define a document's schema.
|
||||
|
||||
.. versionchanged:: 0.5 - added verbose and help text
|
||||
"""
|
||||
name = None
|
||||
_geo_index = False
|
||||
_auto_gen = False # Call `generate` to generate a value
|
||||
_auto_dereference = True
|
||||
|
||||
# These track each time a Field instance is created. Used to retain order.
|
||||
# The auto_creation_counter is used for fields that MongoEngine implicitly
|
||||
# creates, creation_counter is used for all user-specified fields.
|
||||
creation_counter = 0
|
||||
auto_creation_counter = -1
|
||||
|
||||
def __init__(self, db_field=None, name=None, required=False, default=None,
|
||||
unique=False, unique_with=None, primary_key=False,
|
||||
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) If 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 = 'Field\'s "name" attribute deprecated in favour of "db_field"'
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
self.required = required or primary_key
|
||||
self.default = default
|
||||
self.unique = bool(unique or unique_with)
|
||||
self.unique_with = unique_with
|
||||
self.primary_key = primary_key
|
||||
self.validation = validation
|
||||
self.choices = choices
|
||||
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':
|
||||
self.creation_counter = BaseField.auto_creation_counter
|
||||
BaseField.auto_creation_counter -= 1
|
||||
else:
|
||||
self.creation_counter = BaseField.creation_counter
|
||||
BaseField.creation_counter += 1
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
"""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
|
||||
return instance._data.get(self.name)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
"""Descriptor for assigning a value to a field in a document.
|
||||
"""
|
||||
# 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."""
|
||||
return value
|
||||
|
||||
def to_mongo(self, value):
|
||||
"""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."""
|
||||
if op in UPDATE_OPERATORS:
|
||||
self.validate(value)
|
||||
return value
|
||||
|
||||
def validate(self, value, clean=True):
|
||||
"""Perform validation on a value."""
|
||||
pass
|
||||
|
||||
def _validate_choices(self, value):
|
||||
Document = _import_class('Document')
|
||||
EmbeddedDocument = _import_class('EmbeddedDocument')
|
||||
|
||||
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
|
||||
else:
|
||||
values = value if isinstance(value, (list, tuple)) else [value]
|
||||
if len(set(values) - set(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:
|
||||
self._validate_choices(value)
|
||||
|
||||
# check validation argument
|
||||
if self.validation is not None:
|
||||
if callable(self.validation):
|
||||
if not self.validation(value):
|
||||
self.error('Value does not match custom validation method')
|
||||
else:
|
||||
raise ValueError('validation argument for "%s" must be a '
|
||||
'callable.' % self.name)
|
||||
|
||||
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.
|
||||
|
||||
Allows for nesting of embedded documents inside complex types.
|
||||
Handles the lazy dereferencing of a queryset by lazily dereferencing all
|
||||
items in a list / dict rather than one at a time.
|
||||
|
||||
.. versionadded:: 0.5
|
||||
"""
|
||||
|
||||
field = None
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
"""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')
|
||||
|
||||
auto_dereference = instance._fields[self.name]._auto_dereference
|
||||
|
||||
dereference = (auto_dereference and
|
||||
(self.field is None or isinstance(self.field,
|
||||
(GenericReferenceField, ReferenceField))))
|
||||
|
||||
_dereference = _import_class('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
|
||||
)
|
||||
|
||||
value = super(ComplexBaseField, self).__get__(instance, owner)
|
||||
|
||||
# Convert lists / values so we can watch for any changes on them
|
||||
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 (auto_dereference and instance._initialised and
|
||||
isinstance(value, (BaseList, BaseDict)) and
|
||||
not value._dereferenced):
|
||||
value = _dereference(
|
||||
value, max_depth=1, instance=instance, name=self.name
|
||||
)
|
||||
value._dereferenced = True
|
||||
instance._data[self.name] = value
|
||||
|
||||
return value
|
||||
|
||||
def to_python(self, value):
|
||||
"""Convert a MongoDB-compatible type to a Python type."""
|
||||
if isinstance(value, six.string_types):
|
||||
return value
|
||||
|
||||
if hasattr(value, 'to_python'):
|
||||
return value.to_python()
|
||||
|
||||
BaseDocument = _import_class('BaseDocument')
|
||||
if isinstance(value, BaseDocument):
|
||||
# Something is wrong, return the value as it is
|
||||
return value
|
||||
|
||||
is_list = False
|
||||
if not hasattr(value, 'items'):
|
||||
try:
|
||||
is_list = True
|
||||
value = {idx: v for idx, v in enumerate(value)}
|
||||
except TypeError: # Not iterable return the value
|
||||
return value
|
||||
|
||||
if self.field:
|
||||
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):
|
||||
# We need the id from the saved object to create the DBRef
|
||||
if v.pk is None:
|
||||
self.error('You can only reference documents once they'
|
||||
' have been saved to the database')
|
||||
collection = v._get_collection_name()
|
||||
value_dict[k] = DBRef(collection, v.pk)
|
||||
elif hasattr(v, 'to_python'):
|
||||
value_dict[k] = v.to_python()
|
||||
else:
|
||||
value_dict[k] = self.to_python(v)
|
||||
|
||||
if is_list: # Convert back to a list
|
||||
return [v for _, v in sorted(value_dict.items(),
|
||||
key=operator.itemgetter(0))]
|
||||
return value_dict
|
||||
|
||||
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, 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(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
|
||||
|
||||
is_list = False
|
||||
if not hasattr(value, 'items'):
|
||||
try:
|
||||
is_list = True
|
||||
value = {k: v for k, v in enumerate(value)}
|
||||
except TypeError: # Not iterable return the value
|
||||
return value
|
||||
|
||||
if self.field:
|
||||
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():
|
||||
if isinstance(v, Document):
|
||||
# We need the id from the saved object to create the DBRef
|
||||
if v.pk is None:
|
||||
self.error('You can only reference documents once they'
|
||||
' have been saved to the database')
|
||||
|
||||
# If its a document that is not inheritable it won't have
|
||||
# any _cls data so make it a generic reference allows
|
||||
# us to dereference
|
||||
meta = getattr(v, '_meta', {})
|
||||
allow_inheritance = meta.get('allow_inheritance')
|
||||
if not allow_inheritance and not self.field:
|
||||
value_dict[k] = GenericReferenceField().to_mongo(v)
|
||||
else:
|
||||
collection = v._get_collection_name()
|
||||
value_dict[k] = DBRef(collection, v.pk)
|
||||
elif hasattr(v, 'to_mongo'):
|
||||
cls = v.__class__
|
||||
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, use_db_field, fields)
|
||||
|
||||
if is_list: # Convert back to a list
|
||||
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."""
|
||||
errors = {}
|
||||
if self.field:
|
||||
if hasattr(value, 'iteritems') or hasattr(value, 'items'):
|
||||
sequence = value.iteritems()
|
||||
else:
|
||||
sequence = enumerate(value)
|
||||
for k, v in sequence:
|
||||
try:
|
||||
self.field._validate(v)
|
||||
except ValidationError as error:
|
||||
errors[k] = error.errors or error
|
||||
except (ValueError, AssertionError) as error:
|
||||
errors[k] = error
|
||||
|
||||
if errors:
|
||||
field_class = self.field.__class__.__name__
|
||||
self.error('Invalid %s item (%s)' % (field_class, value),
|
||||
errors=errors)
|
||||
# Don't allow empty values if required
|
||||
if self.required and not value:
|
||||
self.error('Field is required and cannot be empty')
|
||||
|
||||
def prepare_query_value(self, op, value):
|
||||
return self.to_mongo(value)
|
||||
|
||||
def lookup_member(self, member_name):
|
||||
if self.field:
|
||||
return self.field.lookup_member(member_name)
|
||||
return None
|
||||
|
||||
def _set_owner_document(self, owner_document):
|
||||
if self.field:
|
||||
self.field.owner_document = owner_document
|
||||
self._owner_document = owner_document
|
||||
|
||||
|
||||
class ObjectIdField(BaseField):
|
||||
"""A field wrapper around MongoDB's ObjectIds."""
|
||||
|
||||
def to_python(self, 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(six.text_type(value))
|
||||
except Exception as e:
|
||||
# e.message attribute has been deprecated since Python 2.6
|
||||
self.error(six.text_type(e))
|
||||
return value
|
||||
|
||||
def prepare_query_value(self, op, value):
|
||||
return self.to_mongo(value)
|
||||
|
||||
def validate(self, value):
|
||||
try:
|
||||
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()) == {'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)])
|
||||
450
mongoengine/base/metaclasses.py
Normal file
450
mongoengine/base/metaclasses.py
Normal file
@@ -0,0 +1,450 @@
|
||||
import warnings
|
||||
|
||||
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.queryset import (DO_NOTHING, DoesNotExist,
|
||||
MultipleObjectsReturned,
|
||||
QuerySetManager)
|
||||
|
||||
|
||||
__all__ = ('DocumentMetaclass', 'TopLevelDocumentMetaclass')
|
||||
|
||||
|
||||
class DocumentMetaclass(type):
|
||||
"""Metaclass for all documents."""
|
||||
|
||||
# TODO lower complexity of this method
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
flattened_bases = mcs._get_bases(bases)
|
||||
super_new = super(DocumentMetaclass, mcs).__new__
|
||||
|
||||
# If a base class just call super
|
||||
metaclass = attrs.get('my_metaclass')
|
||||
if metaclass and issubclass(metaclass, DocumentMetaclass):
|
||||
return super_new(mcs, 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:
|
||||
attrs['_meta'] = attrs.pop('meta')
|
||||
|
||||
# EmbeddedDocuments should inherit meta data
|
||||
if '_meta' not in attrs:
|
||||
meta = MetaDict()
|
||||
for base in flattened_bases[::-1]:
|
||||
# Add any mixin metadata from plain objects
|
||||
if hasattr(base, 'meta'):
|
||||
meta.merge(base.meta)
|
||||
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
|
||||
|
||||
# Merge all fields from subclasses
|
||||
doc_fields = {}
|
||||
for base in flattened_bases[::-1]:
|
||||
if hasattr(base, '_fields'):
|
||||
doc_fields.update(base._fields)
|
||||
|
||||
# Standard object mixin - merge in any Fields
|
||||
if not hasattr(base, '_meta'):
|
||||
base_fields = {}
|
||||
for attr_name, attr_value in base.__dict__.iteritems():
|
||||
if not isinstance(attr_value, BaseField):
|
||||
continue
|
||||
attr_value.name = attr_name
|
||||
if not attr_value.db_field:
|
||||
attr_value.db_field = attr_name
|
||||
base_fields[attr_name] = attr_value
|
||||
|
||||
doc_fields.update(base_fields)
|
||||
|
||||
# Discover any document fields
|
||||
field_names = {}
|
||||
for attr_name, attr_value in attrs.iteritems():
|
||||
if not isinstance(attr_value, BaseField):
|
||||
continue
|
||||
attr_value.name = attr_name
|
||||
if not attr_value.db_field:
|
||||
attr_value.db_field = attr_name
|
||||
doc_fields[attr_name] = attr_value
|
||||
|
||||
# Count names to ensure no db_field redefinitions
|
||||
field_names[attr_value.db_field] = field_names.get(
|
||||
attr_value.db_field, 0) + 1
|
||||
|
||||
# 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))
|
||||
raise InvalidDocumentError(msg)
|
||||
|
||||
# Set _fields and db_field maps
|
||||
attrs['_fields'] = doc_fields
|
||||
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()))
|
||||
|
||||
#
|
||||
# Set document hierarchy
|
||||
#
|
||||
superclasses = ()
|
||||
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 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')
|
||||
if not allow_inheritance and not base._meta.get('abstract'):
|
||||
raise ValueError('Document %s may not be subclassed. '
|
||||
'To enable inheritance, use the "allow_inheritance" meta attribute.' %
|
||||
base.__name__)
|
||||
|
||||
# Get superclasses from last base superclass
|
||||
document_bases = [b for b in flattened_bases
|
||||
if hasattr(b, '_class_name')]
|
||||
if document_bases:
|
||||
superclasses = document_bases[0]._superclasses
|
||||
superclasses += (document_bases[0]._class_name, )
|
||||
|
||||
_cls = '.'.join(reversed(class_name))
|
||||
attrs['_class_name'] = _cls
|
||||
attrs['_superclasses'] = superclasses
|
||||
attrs['_subclasses'] = (_cls, )
|
||||
attrs['_types'] = attrs['_subclasses'] # TODO depreciate _types
|
||||
|
||||
# Create the new_class
|
||||
new_class = super_new(mcs, name, bases, attrs)
|
||||
|
||||
# Set _subclasses
|
||||
for base in document_bases:
|
||||
if _cls not in base._subclasses:
|
||||
base._subclasses += (_cls,)
|
||||
base._types = base._subclasses # TODO depreciate _types
|
||||
|
||||
(Document, EmbeddedDocument, DictField,
|
||||
CachedReferenceField) = mcs._import_classes()
|
||||
|
||||
if issubclass(new_class, Document):
|
||||
new_class._collection = None
|
||||
|
||||
# Add class to the _document_registry
|
||||
_document_registry[new_class._class_name] = new_class
|
||||
|
||||
# In Python 2, User-defined methods objects have special read-only
|
||||
# attributes 'im_func' and 'im_self' which contain the function obj
|
||||
# and class instance object respectively. With Python 3 these special
|
||||
# attributes have been replaced by __func__ and __self__. The Blinker
|
||||
# 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 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'):
|
||||
f.__dict__.update({'im_func': getattr(f, '__func__')})
|
||||
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
|
||||
|
||||
@classmethod
|
||||
def _get_bases(mcs, bases):
|
||||
if isinstance(bases, BasesTuple):
|
||||
return bases
|
||||
seen = []
|
||||
bases = mcs.__get_bases(bases)
|
||||
unique_bases = (b for b in bases if not (b in seen or seen.append(b)))
|
||||
return BasesTuple(unique_bases)
|
||||
|
||||
@classmethod
|
||||
def __get_bases(mcs, bases):
|
||||
for base in bases:
|
||||
if base is object:
|
||||
continue
|
||||
yield base
|
||||
for child_base in mcs.__get_bases(base.__bases__):
|
||||
yield child_base
|
||||
|
||||
@classmethod
|
||||
def _import_classes(mcs):
|
||||
Document = _import_class('Document')
|
||||
EmbeddedDocument = _import_class('EmbeddedDocument')
|
||||
DictField = _import_class('DictField')
|
||||
CachedReferenceField = _import_class('CachedReferenceField')
|
||||
return Document, EmbeddedDocument, DictField, CachedReferenceField
|
||||
|
||||
|
||||
class TopLevelDocumentMetaclass(DocumentMetaclass):
|
||||
"""Metaclass for top-level documents (i.e. documents that have their own
|
||||
collection in the database.
|
||||
"""
|
||||
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
flattened_bases = mcs._get_bases(bases)
|
||||
super_new = super(TopLevelDocumentMetaclass, mcs).__new__
|
||||
|
||||
# Set default _meta data if base class, otherwise get user defined meta
|
||||
if attrs.get('my_metaclass') == TopLevelDocumentMetaclass:
|
||||
# defaults
|
||||
attrs['_meta'] = {
|
||||
'abstract': True,
|
||||
'max_documents': None,
|
||||
'max_size': None,
|
||||
'ordering': [], # default ordering applied at runtime
|
||||
'indexes': [], # indexes to be ensured at runtime
|
||||
'id_field': None,
|
||||
'index_background': False,
|
||||
'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', {})
|
||||
# Explicitly set abstract to false unless set
|
||||
attrs['_meta']['abstract'] = attrs['_meta'].get('abstract', False)
|
||||
attrs['_is_base_cls'] = False
|
||||
|
||||
# Set flag marking as document class - as opposed to an object mixin
|
||||
attrs['_is_document'] = True
|
||||
|
||||
# Ensure queryset_class is inherited
|
||||
if 'objects' in attrs:
|
||||
manager = attrs['objects']
|
||||
if hasattr(manager, 'queryset_class'):
|
||||
attrs['_meta']['queryset_class'] = manager.queryset_class
|
||||
|
||||
# Clean up top level meta
|
||||
if 'meta' in attrs:
|
||||
del attrs['meta']
|
||||
|
||||
# Find the parent document class
|
||||
parent_doc_cls = [b for b in flattened_bases
|
||||
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']
|
||||
|
||||
# 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'
|
||||
raise ValueError(msg)
|
||||
return super_new(mcs, name, bases, attrs)
|
||||
|
||||
# Merge base class metas.
|
||||
# Uses a special MetaDict that handles various merging rules
|
||||
meta = MetaDict()
|
||||
for base in flattened_bases[::-1]:
|
||||
# Add any mixin metadata from plain objects
|
||||
if hasattr(base, 'meta'):
|
||||
meta.merge(base.meta)
|
||||
elif hasattr(base, '_meta'):
|
||||
meta.merge(base._meta)
|
||||
|
||||
# Set collection in the meta if its callable
|
||||
if (getattr(base, '_is_document', False) and
|
||||
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 (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'] is False and
|
||||
not meta['abstract']
|
||||
):
|
||||
raise ValueError('Only direct subclasses of Document may set '
|
||||
'"allow_inheritance" to False')
|
||||
|
||||
# Set default collection name
|
||||
if 'collection' not in meta:
|
||||
meta['collection'] = ''.join('_%s' % c if c.isupper() else c
|
||||
for c in name).strip('_').lower()
|
||||
attrs['_meta'] = meta
|
||||
|
||||
# Call super and get the new class
|
||||
new_class = super_new(mcs, name, bases, attrs)
|
||||
|
||||
meta = new_class._meta
|
||||
|
||||
# Set index specifications
|
||||
meta['index_specs'] = new_class._build_index_specs(meta['indexes'])
|
||||
|
||||
# If collection is a callable - call it and set the value
|
||||
collection = meta.get('collection')
|
||||
if callable(collection):
|
||||
new_class._meta['collection'] = collection(new_class)
|
||||
|
||||
# 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():
|
||||
if field.primary_key:
|
||||
# Ensure only one primary key is set
|
||||
current_pk = new_class._meta.get('id_field')
|
||||
if current_pk and current_pk != field_name:
|
||||
raise ValueError('Cannot override primary key field')
|
||||
|
||||
# Set primary key
|
||||
if not current_pk:
|
||||
new_class._meta['id_field'] = field_name
|
||||
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'):
|
||||
# After 0.10, find not existing names, instead of overwriting
|
||||
id_name, id_db_name = mcs.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)
|
||||
module = attrs.get('__module__')
|
||||
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,)
|
||||
# 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(mcs, 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.
|
||||
Handles the merging of set indexes
|
||||
"""
|
||||
_merge_options = ('indexes',)
|
||||
|
||||
def merge(self, new_options):
|
||||
for k, v in new_options.iteritems():
|
||||
if k in self._merge_options:
|
||||
self[k] = self.get(k, []) + v
|
||||
else:
|
||||
self[k] = v
|
||||
|
||||
|
||||
class BasesTuple(tuple):
|
||||
"""Special class to handle introspection of bases tuple in __new__"""
|
||||
pass
|
||||
22
mongoengine/base/utils.py
Normal file
22
mongoengine/base/utils.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import re
|
||||
|
||||
|
||||
class LazyRegexCompiler(object):
|
||||
"""Descriptor to allow lazy compilation of regex"""
|
||||
|
||||
def __init__(self, pattern, flags=0):
|
||||
self._pattern = pattern
|
||||
self._flags = flags
|
||||
self._compiled_regex = None
|
||||
|
||||
@property
|
||||
def compiled_regex(self):
|
||||
if self._compiled_regex is None:
|
||||
self._compiled_regex = re.compile(self._pattern, self._flags)
|
||||
return self._compiled_regex
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
return self.compiled_regex
|
||||
|
||||
def __set__(self, instance, value):
|
||||
raise AttributeError("Can not set attribute LazyRegexCompiler")
|
||||
58
mongoengine/common.py
Normal file
58
mongoengine/common.py
Normal file
@@ -0,0 +1,58 @@
|
||||
_class_registry_cache = {}
|
||||
_field_list_cache = []
|
||||
|
||||
|
||||
def _import_class(cls_name):
|
||||
"""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
|
||||
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 == '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:
|
||||
from mongoengine import fields as module
|
||||
import_classes = field_classes
|
||||
elif cls_name in queryset_classes:
|
||||
from mongoengine import queryset as module
|
||||
import_classes = queryset_classes
|
||||
elif cls_name in deref_classes:
|
||||
from mongoengine import dereference as module
|
||||
import_classes = deref_classes
|
||||
else:
|
||||
raise ValueError('No import set for: ' % cls_name)
|
||||
|
||||
for cls in import_classes:
|
||||
_class_registry_cache[cls] = getattr(module, cls)
|
||||
|
||||
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,149 +28,249 @@ _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, db=None, 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
|
||||
throughout MongoEngine
|
||||
:param name: the name of the specific database to use
|
||||
:param db: the name of the database to use, for compatibility with connect
|
||||
: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 read_preference: The read preference for the collection
|
||||
** Added pymongo 2.1
|
||||
: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 db 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']
|
||||
if IS_PYMONGO_3 and 'readpreference' in uri_options:
|
||||
read_preferences = (
|
||||
ReadPreference.NEAREST,
|
||||
ReadPreference.PRIMARY,
|
||||
ReadPreference.PRIMARY_PREFERRED,
|
||||
ReadPreference.SECONDARY,
|
||||
ReadPreference.SECONDARY_PREFERRED)
|
||||
read_pf_mode = uri_options['readpreference'].lower()
|
||||
for preference in read_preferences:
|
||||
if preference.name.lower() == read_pf_mode:
|
||||
conn_settings['read_preference'] = preference
|
||||
break
|
||||
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)
|
||||
|
||||
return get_connection(alias)
|
||||
|
||||
|
||||
# Support old naming convention
|
||||
_get_connection = get_connection
|
||||
_get_db = get_db
|
||||
|
||||
247
mongoengine/context_managers.py
Normal file
247
mongoengine/context_managers.py
Normal file
@@ -0,0 +1,247 @@
|
||||
from contextlib import contextmanager
|
||||
from pymongo.write_concern import WriteConcern
|
||||
from mongoengine.common import _import_class
|
||||
from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db
|
||||
|
||||
|
||||
__all__ = ('switch_db', 'switch_collection', 'no_dereference',
|
||||
'no_sub_classes', 'query_counter', 'set_write_concern')
|
||||
|
||||
|
||||
class switch_db(object):
|
||||
"""switch_db alias context manager.
|
||||
|
||||
Example ::
|
||||
|
||||
# Register connections
|
||||
register_connection('default', 'mongoenginetest')
|
||||
register_connection('testdb-1', 'mongoenginetest2')
|
||||
|
||||
class Group(Document):
|
||||
name = StringField()
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
def __init__(self, cls, db_alias):
|
||||
"""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
|
||||
"""
|
||||
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)
|
||||
|
||||
def __enter__(self):
|
||||
"""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
|
||||
self.cls._collection = self.collection
|
||||
|
||||
|
||||
class switch_collection(object):
|
||||
"""switch_collection alias context manager.
|
||||
|
||||
Example ::
|
||||
|
||||
class Group(Document):
|
||||
name = StringField()
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
def __init__(self, cls, collection_name):
|
||||
"""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
|
||||
"""
|
||||
self.cls = cls
|
||||
self.ori_collection = cls._get_collection()
|
||||
self.ori_get_collection_name = cls._get_collection_name
|
||||
self.collection_name = collection_name
|
||||
|
||||
def __enter__(self):
|
||||
"""Change the _get_collection_name and clear the cached collection."""
|
||||
|
||||
@classmethod
|
||||
def _get_collection_name(cls):
|
||||
return self.collection_name
|
||||
|
||||
self.cls._get_collection_name = _get_collection_name
|
||||
self.cls._collection = None
|
||||
return self.cls
|
||||
|
||||
def __exit__(self, t, value, traceback):
|
||||
"""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.
|
||||
|
||||
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.
|
||||
|
||||
:param cls: the class to turn dereferencing off on
|
||||
"""
|
||||
self.cls = cls
|
||||
|
||||
ReferenceField = _import_class('ReferenceField')
|
||||
GenericReferenceField = _import_class('GenericReferenceField')
|
||||
ComplexBaseField = _import_class('ComplexBaseField')
|
||||
|
||||
self.deref_fields = [k for k, v in self.cls._fields.iteritems()
|
||||
if isinstance(v, (ReferenceField,
|
||||
GenericReferenceField,
|
||||
ComplexBaseField))]
|
||||
|
||||
def __enter__(self):
|
||||
"""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."""
|
||||
for field in self.deref_fields:
|
||||
self.cls._fields[field]._auto_dereference = True
|
||||
return self.cls
|
||||
|
||||
|
||||
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
|
||||
self.cls_initial_subclasses = None
|
||||
|
||||
def __enter__(self):
|
||||
"""Change the objects default and _auto_dereference values."""
|
||||
self.cls_initial_subclasses = self.cls._subclasses
|
||||
self.cls._subclasses = (self.cls._class_name,)
|
||||
return self.cls
|
||||
|
||||
def __exit__(self, t, value, traceback):
|
||||
"""Reset the default and _auto_dereference values."""
|
||||
self.cls._subclasses = self.cls_initial_subclasses
|
||||
|
||||
|
||||
class query_counter(object):
|
||||
"""Query_counter context manager to get the number of queries.
|
||||
This works by updating the `profiling_level` of the database so that all queries get logged,
|
||||
resetting the db.system.profile collection at the beginnig of the context and counting the new entries.
|
||||
|
||||
This was designed for debugging purpose. In fact it is a global counter so queries issued by other threads/processes
|
||||
can interfere with it
|
||||
|
||||
Be aware that:
|
||||
- Iterating over large amount of documents (>101) makes pymongo issue `getmore` queries to fetch the next batch of
|
||||
documents (https://docs.mongodb.com/manual/tutorial/iterate-a-cursor/#cursor-batches)
|
||||
- Some queries are ignored by default by the counter (killcursors, db.system.indexes)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Construct the query_counter
|
||||
"""
|
||||
self.db = get_db()
|
||||
self.initial_profiling_level = None
|
||||
self._ctx_query_counter = 0 # number of queries issued by the context
|
||||
|
||||
self._ignored_query = {
|
||||
'ns':
|
||||
{'$ne': '%s.system.indexes' % self.db.name},
|
||||
'op': # MONGODB < 3.2
|
||||
{'$ne': 'killcursors'},
|
||||
'command.killCursors': # MONGODB >= 3.2
|
||||
{'$exists': False}
|
||||
}
|
||||
|
||||
def _turn_on_profiling(self):
|
||||
self.initial_profiling_level = self.db.profiling_level()
|
||||
self.db.set_profiling_level(0)
|
||||
self.db.system.profile.drop()
|
||||
self.db.set_profiling_level(2)
|
||||
|
||||
def _resets_profiling(self):
|
||||
self.db.set_profiling_level(self.initial_profiling_level)
|
||||
|
||||
def __enter__(self):
|
||||
self._turn_on_profiling()
|
||||
return self
|
||||
|
||||
def __exit__(self, t, value, traceback):
|
||||
self._resets_profiling()
|
||||
|
||||
def __eq__(self, value):
|
||||
counter = self._get_count()
|
||||
return value == counter
|
||||
|
||||
def __ne__(self, value):
|
||||
return not self.__eq__(value)
|
||||
|
||||
def __lt__(self, value):
|
||||
return self._get_count() < value
|
||||
|
||||
def __le__(self, value):
|
||||
return self._get_count() <= value
|
||||
|
||||
def __gt__(self, value):
|
||||
return self._get_count() > value
|
||||
|
||||
def __ge__(self, value):
|
||||
return self._get_count() >= value
|
||||
|
||||
def __int__(self):
|
||||
return self._get_count()
|
||||
|
||||
def __repr__(self):
|
||||
"""repr query_counter as the number of queries."""
|
||||
return u"%s" % self._get_count()
|
||||
|
||||
def _get_count(self):
|
||||
"""Get the number of queries by counting the current number of entries in db.system.profile
|
||||
and substracting the queries issued by this context. In fact everytime this is called, 1 query is
|
||||
issued so we need to balance that
|
||||
"""
|
||||
count = self.db.system.profile.find(self._ignored_query).count() - self._ctx_query_counter
|
||||
self._ctx_query_counter += 1 # Account for the query we just issued to gather the information
|
||||
return count
|
||||
|
||||
|
||||
@contextmanager
|
||||
def set_write_concern(collection, write_concerns):
|
||||
combined_concerns = dict(collection.write_concern.document.items())
|
||||
combined_concerns.update(write_concerns)
|
||||
yield collection.with_options(write_concern=WriteConcern(**combined_concerns))
|
||||
@@ -1,18 +1,20 @@
|
||||
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.base.datastructures import LazyReference
|
||||
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 +24,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
|
||||
@@ -31,15 +33,45 @@ class DeReference(object):
|
||||
items = [i for i in items]
|
||||
|
||||
self.max_depth = max_depth
|
||||
|
||||
doc_type = None
|
||||
if instance and instance._fields:
|
||||
doc_type = instance._fields[name].field
|
||||
|
||||
if instance and isinstance(instance, (Document, EmbeddedDocument,
|
||||
TopLevelDocumentMetaclass)):
|
||||
doc_type = instance._fields.get(name)
|
||||
while hasattr(doc_type, 'field'):
|
||||
doc_type = doc_type.field
|
||||
|
||||
if isinstance(doc_type, ReferenceField):
|
||||
field = doc_type
|
||||
doc_type = doc_type.document_type
|
||||
if all([i.__class__ == doc_type for i in items]):
|
||||
is_list = not hasattr(items, 'items')
|
||||
|
||||
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()]):
|
||||
return items
|
||||
elif not field.dbref:
|
||||
if not hasattr(items, '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 = {
|
||||
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)
|
||||
@@ -57,36 +89,42 @@ 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 hasattr(item, '_fields'):
|
||||
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, LazyReference):
|
||||
# LazyReference inherits DBRef but should not be dereferenced here !
|
||||
continue
|
||||
elif 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, LazyReference):
|
||||
# LazyReference inherits DBRef but should not be dereferenced here !
|
||||
continue
|
||||
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
|
||||
|
||||
@@ -94,31 +132,43 @@ 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 str(dbref) 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():
|
||||
|
||||
# we use getattr instead of hasattr because hasattr swallows any exception under python2
|
||||
# so it could hide nasty things without raising exceptions (cfr bug #1688))
|
||||
ref_document_cls_exists = (getattr(collection, 'objects', None) is not None)
|
||||
|
||||
if ref_document_cls_exists:
|
||||
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 doc_type and not isinstance(doc_type, (ListField, DictField, MapField,) ):
|
||||
references = doc_type._get_db()[col].find({'_id': {'$in': refs}})
|
||||
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()[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)
|
||||
''.join(x.capitalize()
|
||||
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):
|
||||
@@ -144,14 +194,23 @@ class DeReference(object):
|
||||
|
||||
if isinstance(items, (dict, SON)):
|
||||
if '_ref' in items:
|
||||
return self.object_map.get(items['_ref'].id, items)
|
||||
elif '_types' in items and '_cls' in 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)
|
||||
doc._data = self._attach_objects(doc._data, depth, doc, name)
|
||||
_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 = []
|
||||
else:
|
||||
@@ -166,27 +225,29 @@ class DeReference(object):
|
||||
else:
|
||||
data[k] = v
|
||||
|
||||
if k in self.object_map:
|
||||
if k in self.object_map and not is_list:
|
||||
data[k] = self.object_map[k]
|
||||
elif hasattr(v, '_fields'):
|
||||
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)
|
||||
elif hasattr(v, 'id'):
|
||||
data[k] = self.object_map.get(v.id, v)
|
||||
item_name = '%s.%s' % (name, k) if name else name
|
||||
data[k] = self._attach_objects(v, depth - 1, instance=instance, name=item_name)
|
||||
elif isinstance(v, DBRef) and hasattr(v, 'id'):
|
||||
data[k] = self.object_map.get((v.collection, v.id), v)
|
||||
|
||||
if instance and name:
|
||||
if is_list:
|
||||
return 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,159 +0,0 @@
|
||||
import datetime
|
||||
|
||||
from mongoengine import *
|
||||
|
||||
from django.utils.encoding import smart_str
|
||||
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)
|
||||
|
||||
|
||||
REDIRECT_FIELD_NAME = 'next'
|
||||
|
||||
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.datetime.now,
|
||||
verbose_name=_('last login'))
|
||||
date_joined = DateTimeField(default=datetime.datetime.now,
|
||||
verbose_name=_('date joined'))
|
||||
|
||||
meta = {
|
||||
'allow_inheritance': True,
|
||||
'indexes': [
|
||||
{'fields': ['username'], 'unique': 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)
|
||||
|
||||
@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.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_and_delete_messages(self):
|
||||
return []
|
||||
|
||||
|
||||
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):
|
||||
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,74 +0,0 @@
|
||||
from datetime import datetime
|
||||
|
||||
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
|
||||
|
||||
|
||||
MONGOENGINE_SESSION_DB_ALIAS = getattr(
|
||||
settings, 'MONGOENGINE_SESSION_DB_ALIAS',
|
||||
DEFAULT_CONNECTION_NAME)
|
||||
|
||||
|
||||
class MongoSession(Document):
|
||||
session_key = fields.StringField(primary_key=True, max_length=40)
|
||||
session_data = fields.StringField()
|
||||
expire_date = fields.DateTimeField()
|
||||
|
||||
meta = {'collection': 'django_session',
|
||||
'db_alias': MONGOENGINE_SESSION_DB_ALIAS,
|
||||
'allow_inheritance': False}
|
||||
|
||||
|
||||
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]
|
||||
return self.decode(force_unicode(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)
|
||||
s.session_data = self.encode(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,46 +0,0 @@
|
||||
from django.http import Http404
|
||||
from mongoengine.queryset import QuerySet
|
||||
from mongoengine.base import BaseDocument
|
||||
from mongoengine.base 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):
|
||||
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:
|
||||
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,21 +0,0 @@
|
||||
#coding: utf-8
|
||||
from django.test import TestCase
|
||||
from django.conf import settings
|
||||
|
||||
from mongoengine import connect
|
||||
|
||||
class MongoTestCase(TestCase):
|
||||
"""
|
||||
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)
|
||||
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)
|
||||
File diff suppressed because it is too large
Load Diff
146
mongoengine/errors.py
Normal file
146
mongoengine/errors.py
Normal file
@@ -0,0 +1,146 @@
|
||||
from collections import defaultdict
|
||||
|
||||
import six
|
||||
|
||||
__all__ = ('NotRegistered', 'InvalidDocumentError', 'LookUpError',
|
||||
'DoesNotExist', 'MultipleObjectsReturned', 'InvalidQueryError',
|
||||
'OperationError', 'NotUniqueError', 'FieldDoesNotExist',
|
||||
'ValidationError', 'SaveConditionError')
|
||||
|
||||
|
||||
class NotRegistered(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidDocumentError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LookUpError(AttributeError):
|
||||
pass
|
||||
|
||||
|
||||
class DoesNotExist(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MultipleObjectsReturned(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidQueryError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class OperationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
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.
|
||||
|
||||
May represent an error validating a field or a
|
||||
document containing fields with validation errors.
|
||||
|
||||
:ivar errors: A dictionary of errors for fields within this
|
||||
document or list, or None if the error is for an
|
||||
individual field.
|
||||
"""
|
||||
|
||||
errors = {}
|
||||
field_name = None
|
||||
_message = None
|
||||
|
||||
def __init__(self, message='', **kwargs):
|
||||
super(ValidationError, self).__init__(message)
|
||||
self.errors = kwargs.get('errors', {})
|
||||
self.field_name = kwargs.get('field_name')
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return six.text_type(self.message)
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%s,)' % (self.__class__.__name__, self.message)
|
||||
|
||||
def __getattribute__(self, name):
|
||||
message = super(ValidationError, self).__getattribute__(name)
|
||||
if name == 'message':
|
||||
if self.field_name:
|
||||
message = '%s' % message
|
||||
if self.errors:
|
||||
message = '%s(%s)' % (message, self._format_errors())
|
||||
return message
|
||||
|
||||
def _get_message(self):
|
||||
return self._message
|
||||
|
||||
def _set_message(self, message):
|
||||
self._message = message
|
||||
|
||||
message = property(_get_message, _set_message)
|
||||
|
||||
def to_dict(self):
|
||||
"""Returns a dictionary of all errors within a document
|
||||
|
||||
Keys are field names or list indices and values are the
|
||||
validation error messages, or a nested dictionary of
|
||||
errors for an embedded document or list.
|
||||
"""
|
||||
|
||||
def build_dict(source):
|
||||
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 six.text_type(source)
|
||||
|
||||
return errors_dict
|
||||
|
||||
if not self.errors:
|
||||
return {}
|
||||
|
||||
return build_dict(self.errors)
|
||||
|
||||
def _format_errors(self):
|
||||
"""Returns a string listing all errors within a document"""
|
||||
|
||||
def generate_key(value, prefix=''):
|
||||
if isinstance(value, list):
|
||||
value = ' '.join([generate_key(k) for k in value])
|
||||
elif isinstance(value, dict):
|
||||
value = ' '.join(
|
||||
[generate_key(v, k) for k, v in value.iteritems()])
|
||||
|
||||
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()])
|
||||
File diff suppressed because it is too large
Load Diff
28
mongoengine/python_support.py
Normal file
28
mongoengine/python_support.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Helper functions, constants, and types to aid with Python v2.7 - v3.x and
|
||||
PyMongo v2.7 - v3.x support.
|
||||
"""
|
||||
import pymongo
|
||||
import six
|
||||
|
||||
|
||||
IS_PYMONGO_3 = pymongo.version_tuple[0] >= 3
|
||||
|
||||
# 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:
|
||||
import cStringIO
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
StringIO = cStringIO.StringIO
|
||||
|
||||
|
||||
if six.PY3:
|
||||
from collections.abc import Hashable
|
||||
else:
|
||||
# raises DeprecationWarnings in Python >=3.7
|
||||
from collections import Hashable
|
||||
File diff suppressed because it is too large
Load Diff
17
mongoengine/queryset/__init__.py
Normal file
17
mongoengine/queryset/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
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 *
|
||||
|
||||
# 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',
|
||||
)
|
||||
1912
mongoengine/queryset/base.py
Normal file
1912
mongoengine/queryset/base.py
Normal file
File diff suppressed because it is too large
Load Diff
87
mongoengine/queryset/field_list.py
Normal file
87
mongoengine/queryset/field_list.py
Normal file
@@ -0,0 +1,87 @@
|
||||
__all__ = ('QueryFieldList',)
|
||||
|
||||
|
||||
class QueryFieldList(object):
|
||||
"""Object that handles combinations of .only() and .exclude() calls"""
|
||||
ONLY = 1
|
||||
EXCLUDE = 0
|
||||
|
||||
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 or [])
|
||||
self.always_include = set(always_include or [])
|
||||
self._id = None
|
||||
self._only_called = _only_called
|
||||
self.slice = {}
|
||||
|
||||
def __add__(self, f):
|
||||
if isinstance(f.value, dict):
|
||||
for field in f.fields:
|
||||
self.slice[field] = f.value
|
||||
if not self.fields:
|
||||
self.fields = f.fields
|
||||
elif not self.fields:
|
||||
self.fields = f.fields
|
||||
self.value = f.value
|
||||
self.slice = {}
|
||||
elif self.value is self.ONLY and f.value is self.ONLY:
|
||||
self._clean_slice()
|
||||
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()
|
||||
elif self.value is self.ONLY and f.value is self.EXCLUDE:
|
||||
self.fields -= f.fields
|
||||
self._clean_slice()
|
||||
elif self.value is self.EXCLUDE and f.value is self.ONLY:
|
||||
self.value = self.ONLY
|
||||
self.fields = f.fields - self.fields
|
||||
self._clean_slice()
|
||||
|
||||
if '_id' in f.fields:
|
||||
self._id = f.value
|
||||
|
||||
if self.always_include:
|
||||
if self.value is self.ONLY and self.fields:
|
||||
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 __bool__(self):
|
||||
return bool(self.fields)
|
||||
|
||||
__nonzero__ = __bool__ # For Py2 support
|
||||
|
||||
def as_dict(self):
|
||||
field_list = {field: self.value for field in self.fields}
|
||||
if self.slice:
|
||||
field_list.update(self.slice)
|
||||
if self._id is not None:
|
||||
field_list['_id'] = self._id
|
||||
return field_list
|
||||
|
||||
def reset(self):
|
||||
self.fields = set([])
|
||||
self.slice = {}
|
||||
self.value = self.ONLY
|
||||
|
||||
def _clean_slice(self):
|
||||
if self.slice:
|
||||
for field in set(self.slice.keys()) - self.fields:
|
||||
del self.slice[field]
|
||||
57
mongoengine/queryset/manager.py
Normal file
57
mongoengine/queryset/manager.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from functools import partial
|
||||
from mongoengine.queryset.queryset import QuerySet
|
||||
|
||||
__all__ = ('queryset_manager', 'QuerySetManager')
|
||||
|
||||
|
||||
class QuerySetManager(object):
|
||||
"""
|
||||
The default QuerySet Manager.
|
||||
|
||||
Custom QuerySet Manager functions can extend this class and users can
|
||||
add extra queryset functionality. Any custom manager methods must accept a
|
||||
:class:`~mongoengine.Document` class as its first argument, and a
|
||||
:class:`~mongoengine.queryset.QuerySet` as its second argument.
|
||||
|
||||
The method function should return a :class:`~mongoengine.queryset.QuerySet`
|
||||
, probably the same one that was passed in, but modified in some way.
|
||||
"""
|
||||
|
||||
get_queryset = None
|
||||
default = QuerySet
|
||||
|
||||
def __init__(self, queryset_func=None):
|
||||
if queryset_func:
|
||||
self.get_queryset = queryset_func
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
"""Descriptor for instantiating a new QuerySet object when
|
||||
Document.objects is accessed.
|
||||
"""
|
||||
if instance is not None:
|
||||
# Document object being used rather than a document class
|
||||
return self
|
||||
|
||||
# owner is the document that contains the QuerySetManager
|
||||
queryset_class = owner._meta.get('queryset_class', self.default)
|
||||
queryset = queryset_class(owner, owner._get_collection())
|
||||
if self.get_queryset:
|
||||
arg_count = self.get_queryset.__code__.co_argcount
|
||||
if arg_count == 1:
|
||||
queryset = self.get_queryset(queryset)
|
||||
elif arg_count == 2:
|
||||
queryset = self.get_queryset(owner, queryset)
|
||||
else:
|
||||
queryset = partial(self.get_queryset, owner, queryset)
|
||||
return queryset
|
||||
|
||||
|
||||
def queryset_manager(func):
|
||||
"""Decorator that allows you to define custom QuerySet managers on
|
||||
:class:`~mongoengine.Document` classes. The manager must be a function that
|
||||
accepts a :class:`~mongoengine.Document` class as its first argument, and a
|
||||
:class:`~mongoengine.queryset.QuerySet` as its second argument. The method
|
||||
function should return a :class:`~mongoengine.queryset.QuerySet`, probably
|
||||
the same one that was passed in, but modified in some way.
|
||||
"""
|
||||
return QuerySetManager(func)
|
||||
188
mongoengine/queryset/queryset.py
Normal file
188
mongoengine/queryset/queryset.py
Normal file
@@ -0,0 +1,188 @@
|
||||
import six
|
||||
|
||||
from mongoengine.errors import OperationError
|
||||
from mongoengine.queryset.base import (BaseQuerySet, CASCADE, DENY, DO_NOTHING,
|
||||
NULLIFY, PULL)
|
||||
|
||||
__all__ = ('QuerySet', 'QuerySetNoCache', 'DO_NOTHING', 'NULLIFY', 'CASCADE',
|
||||
'DENY', 'PULL')
|
||||
|
||||
# The maximum number of items to display in a QuerySet.__repr__
|
||||
REPR_OUTPUT_SIZE = 20
|
||||
ITER_CHUNK_SIZE = 100
|
||||
|
||||
|
||||
class QuerySet(BaseQuerySet):
|
||||
"""The default queryset, that builds queries and handles a set of results
|
||||
returned from a query.
|
||||
|
||||
Wraps a MongoDB cursor, providing :class:`~mongoengine.Document` objects as
|
||||
the results.
|
||||
"""
|
||||
|
||||
_has_more = True
|
||||
_len = None
|
||||
_result_cache = None
|
||||
|
||||
def __iter__(self):
|
||||
"""Iteration utilises a results cache which iterates the cursor
|
||||
in batches of ``ITER_CHUNK_SIZE``.
|
||||
|
||||
If ``self._has_more`` the cursor hasn't been exhausted so cache then
|
||||
batch. Otherwise iterate the result_cache.
|
||||
"""
|
||||
self._iter = True
|
||||
|
||||
if self._has_more:
|
||||
return self._iter_results()
|
||||
|
||||
# iterating over the cache.
|
||||
return iter(self._result_cache)
|
||||
|
||||
def __len__(self):
|
||||
"""Since __len__ is called quite frequently (for example, as part of
|
||||
list(qs)), we populate the result cache and cache the length.
|
||||
"""
|
||||
if self._len is not None:
|
||||
return self._len
|
||||
|
||||
# Populate the result cache with *all* of the docs in the cursor
|
||||
if self._has_more:
|
||||
list(self._iter_results())
|
||||
|
||||
# Cache the length of the complete result cache and return it
|
||||
self._len = len(self._result_cache)
|
||||
return self._len
|
||||
|
||||
def __repr__(self):
|
||||
"""Provide a string representation of the QuerySet"""
|
||||
if self._iter:
|
||||
return '.. queryset mid-iteration ..'
|
||||
|
||||
self._populate_cache()
|
||||
data = self._result_cache[:REPR_OUTPUT_SIZE + 1]
|
||||
if len(data) > REPR_OUTPUT_SIZE:
|
||||
data[-1] = '...(remaining elements truncated)...'
|
||||
return repr(data)
|
||||
|
||||
def _iter_results(self):
|
||||
"""A generator for iterating over the result cache.
|
||||
|
||||
Also populates the cache if there are more possible results to
|
||||
yield. Raises StopIteration when there are no more results.
|
||||
"""
|
||||
if self._result_cache is None:
|
||||
self._result_cache = []
|
||||
|
||||
pos = 0
|
||||
while True:
|
||||
|
||||
# For all positions lower than the length of the current result
|
||||
# cache, serve the docs straight from the cache w/o hitting the
|
||||
# database.
|
||||
# XXX it's VERY important to compute the len within the `while`
|
||||
# condition because the result cache might expand mid-iteration
|
||||
# (e.g. if we call len(qs) inside a loop that iterates over the
|
||||
# queryset). Fortunately len(list) is O(1) in Python, so this
|
||||
# doesn't cause performance issues.
|
||||
while pos < len(self._result_cache):
|
||||
yield self._result_cache[pos]
|
||||
pos += 1
|
||||
|
||||
# return if we already established there were no more
|
||||
# docs in the db cursor.
|
||||
if not self._has_more:
|
||||
return
|
||||
|
||||
# Otherwise, populate more of the cache and repeat.
|
||||
if len(self._result_cache) <= pos:
|
||||
self._populate_cache()
|
||||
|
||||
def _populate_cache(self):
|
||||
"""
|
||||
Populates the result cache with ``ITER_CHUNK_SIZE`` more entries
|
||||
(until the cursor is exhausted).
|
||||
"""
|
||||
if self._result_cache is None:
|
||||
self._result_cache = []
|
||||
|
||||
# Skip populating the cache if we already established there are no
|
||||
# more docs to pull from the database.
|
||||
if not self._has_more:
|
||||
return
|
||||
|
||||
# Pull in ITER_CHUNK_SIZE docs from the database and store them in
|
||||
# the result cache.
|
||||
try:
|
||||
for _ in six.moves.range(ITER_CHUNK_SIZE):
|
||||
self._result_cache.append(six.next(self))
|
||||
except StopIteration:
|
||||
# Getting this exception means there are no more docs in the
|
||||
# db cursor. Set _has_more to False so that we can use that
|
||||
# information in other places.
|
||||
self._has_more = False
|
||||
|
||||
def count(self, with_limit_and_skip=False):
|
||||
"""Count the selected elements in the query.
|
||||
|
||||
:param with_limit_and_skip (optional): take any :meth:`limit` or
|
||||
:meth:`skip` that has been applied to this cursor into account when
|
||||
getting the count
|
||||
"""
|
||||
if with_limit_and_skip is False:
|
||||
return super(QuerySet, self).count(with_limit_and_skip)
|
||||
|
||||
if self._len is None:
|
||||
self._len = super(QuerySet, self).count(with_limit_and_skip)
|
||||
|
||||
return self._len
|
||||
|
||||
def no_cache(self):
|
||||
"""Convert to a non-caching queryset
|
||||
|
||||
.. versionadded:: 0.8.3 Convert to non caching queryset
|
||||
"""
|
||||
if self._result_cache is not None:
|
||||
raise OperationError('QuerySet already cached')
|
||||
|
||||
return self._clone_into(QuerySetNoCache(self._document,
|
||||
self._collection))
|
||||
|
||||
|
||||
class QuerySetNoCache(BaseQuerySet):
|
||||
"""A non caching QuerySet"""
|
||||
|
||||
def cache(self):
|
||||
"""Convert to a caching queryset
|
||||
|
||||
.. versionadded:: 0.8.3 Convert to caching queryset
|
||||
"""
|
||||
return self._clone_into(QuerySet(self._document, self._collection))
|
||||
|
||||
def __repr__(self):
|
||||
"""Provides the string representation of the QuerySet
|
||||
|
||||
.. versionchanged:: 0.6.13 Now doesnt modify the cursor
|
||||
"""
|
||||
if self._iter:
|
||||
return '.. queryset mid-iteration ..'
|
||||
|
||||
data = []
|
||||
for _ in six.moves.range(REPR_OUTPUT_SIZE + 1):
|
||||
try:
|
||||
data.append(six.next(self))
|
||||
except StopIteration:
|
||||
break
|
||||
|
||||
if len(data) > REPR_OUTPUT_SIZE:
|
||||
data[-1] = '...(remaining elements truncated)...'
|
||||
|
||||
self.rewind()
|
||||
return repr(data)
|
||||
|
||||
def __iter__(self):
|
||||
queryset = self
|
||||
if queryset._iter:
|
||||
queryset = self.clone()
|
||||
queryset.rewind()
|
||||
return queryset
|
||||
471
mongoengine/queryset/transform.py
Normal file
471
mongoengine/queryset/transform.py
Normal file
@@ -0,0 +1,471 @@
|
||||
from collections import defaultdict
|
||||
|
||||
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.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', '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)
|
||||
|
||||
|
||||
# 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(kwargs.items()):
|
||||
if key == '__raw__':
|
||||
mongo_query.update(value)
|
||||
continue
|
||||
|
||||
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 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 len(parts) > 1 and parts[-1] == 'not':
|
||||
parts.pop()
|
||||
negate = True
|
||||
|
||||
if _doc_cls:
|
||||
# Switch field names to proper names [set in Field(name='foo')]
|
||||
try:
|
||||
fields = _doc_cls._lookup_field(parts)
|
||||
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, 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)
|
||||
|
||||
# Convert value to proper value
|
||||
field = cleaned_fields[-1]
|
||||
|
||||
singular_ops = [None, 'ne', 'gt', 'gte', 'lt', 'lte', 'not']
|
||||
singular_ops += STRING_OPERATORS
|
||||
if op in singular_ops:
|
||||
if isinstance(field, six.string_types):
|
||||
if (op in STRING_OPERATORS and
|
||||
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)
|
||||
|
||||
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.
|
||||
value = _prepare_query_for_iterable(field, op, 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:
|
||||
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:
|
||||
value = field.prepare_query_value(op, value)
|
||||
value = {'$elemMatch': value}
|
||||
elif op in CUSTOM_OPERATORS:
|
||||
NotImplementedError('Custom method "%s" has not '
|
||||
'been implemented' % op)
|
||||
elif op not in STRING_OPERATORS:
|
||||
value = {'$' + op: value}
|
||||
|
||||
if negate:
|
||||
value = {'$not': value}
|
||||
|
||||
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 isinstance(mongo_query[key], dict) and isinstance(value, dict):
|
||||
mongo_query[key].update(value)
|
||||
# $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' or k == '$minDistance':
|
||||
continue
|
||||
value_son[k] = v
|
||||
# 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
|
||||
merge_query[key].append(value)
|
||||
|
||||
# The queryset has been filter in such a way we must manually merge
|
||||
for k, v in merge_query.items():
|
||||
merge_query[k].append(mongo_query[k])
|
||||
del mongo_query[k]
|
||||
if isinstance(v, list):
|
||||
value = [{k: val} for val in v]
|
||||
if '$and' in mongo_query.keys():
|
||||
mongo_query['$and'].extend(value)
|
||||
else:
|
||||
mongo_query['$and'] = value
|
||||
|
||||
return mongo_query
|
||||
|
||||
|
||||
def update(_doc_cls=None, **update):
|
||||
"""Transform an update spec from Django-style format to Mongo
|
||||
format.
|
||||
"""
|
||||
mongo_update = {}
|
||||
|
||||
for key, value in update.items():
|
||||
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:
|
||||
op = parts.pop(0)
|
||||
# Convert Pythonic names to Mongo equivalents
|
||||
operator_map = {
|
||||
'push_all': 'pushAll',
|
||||
'pull_all': 'pullAll',
|
||||
'dec': 'inc',
|
||||
'add_to_set': 'addToSet',
|
||||
'set_on_insert': 'setOnInsert'
|
||||
}
|
||||
if op == 'dec':
|
||||
# Support decrement by flipping a positive value's sign
|
||||
# and using 'inc'
|
||||
value = -value
|
||||
# If the operator doesn't found from operator map, the op value
|
||||
# will stay unchanged
|
||||
op = operator_map.get(op, op)
|
||||
|
||||
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 as e:
|
||||
raise InvalidQueryError(e)
|
||||
parts = []
|
||||
|
||||
cleaned_fields = []
|
||||
appended_sub_field = False
|
||||
for field in fields:
|
||||
append_field = True
|
||||
if isinstance(field, six.string_types):
|
||||
# Convert the S operator to $
|
||||
if field == 'S':
|
||||
field = '$'
|
||||
parts.append(field)
|
||||
append_field = False
|
||||
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
|
||||
if appended_sub_field:
|
||||
field = cleaned_fields[-2]
|
||||
else:
|
||||
field = cleaned_fields[-1]
|
||||
|
||||
GeoJsonBaseField = _import_class('GeoJsonBaseField')
|
||||
if isinstance(field, GeoJsonBaseField):
|
||||
value = field.to_mongo(value)
|
||||
|
||||
if op == 'pull':
|
||||
if field.required or value is not None:
|
||||
if match == 'in' and not isinstance(value, dict):
|
||||
value = _prepare_query_for_iterable(field, op, value)
|
||||
else:
|
||||
value = field.prepare_query_value(op, value)
|
||||
elif 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'):
|
||||
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 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
|
||||
elif op == 'inc':
|
||||
value = field.prepare_query_value(op, value)
|
||||
|
||||
if match:
|
||||
match = '$' + match
|
||||
value = {match: value}
|
||||
|
||||
key = '.'.join(parts)
|
||||
|
||||
if not op:
|
||||
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
|
||||
# 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')
|
||||
|
||||
# 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')
|
||||
EmbeddedDocumentListField = _import_class('EmbeddedDocumentListField')
|
||||
if ListField in field_classes or EmbeddedDocumentListField in field_classes:
|
||||
# Join all fields via dot notation to the last ListField or EmbeddedDocumentListField
|
||||
# Then process as normal
|
||||
if ListField in field_classes:
|
||||
_check_field = ListField
|
||||
else:
|
||||
_check_field = EmbeddedDocumentListField
|
||||
|
||||
last_listField = len(
|
||||
cleaned_fields) - field_classes.index(_check_field)
|
||||
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}}
|
||||
elif op in ('push', 'pushAll'):
|
||||
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}}
|
||||
else:
|
||||
if op == 'pushAll':
|
||||
op = 'push' # convert to non-deprecated keyword
|
||||
if not isinstance(value, (set, tuple, list)):
|
||||
value = [value]
|
||||
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?
|
||||
|
||||
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)')
|
||||
|
||||
|
||||
def _prepare_query_for_iterable(field, op, value):
|
||||
# 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]).")
|
||||
|
||||
if not hasattr(value, '__iter__'):
|
||||
raise TypeError("The `in`, `nin`, `all`, or "
|
||||
"`near`-operators must be applied to an "
|
||||
"iterable (e.g. a list).")
|
||||
|
||||
return [field.prepare_query_value(op, v) for v in value]
|
||||
166
mongoengine/queryset/visitor.py
Normal file
166
mongoengine/queryset/visitor.py
Normal file
@@ -0,0 +1,166 @@
|
||||
import copy
|
||||
|
||||
from mongoengine.errors import InvalidQueryError
|
||||
from mongoengine.queryset import transform
|
||||
|
||||
__all__ = ('Q', 'QNode')
|
||||
|
||||
|
||||
class QNodeVisitor(object):
|
||||
"""Base visitor class for visiting Q-object nodes in a query tree.
|
||||
"""
|
||||
|
||||
def visit_combination(self, combination):
|
||||
"""Called by QCombination objects.
|
||||
"""
|
||||
return combination
|
||||
|
||||
def visit_query(self, query):
|
||||
"""Called by (New)Q objects.
|
||||
"""
|
||||
return query
|
||||
|
||||
|
||||
class DuplicateQueryConditionsError(InvalidQueryError):
|
||||
pass
|
||||
|
||||
|
||||
class SimplificationVisitor(QNodeVisitor):
|
||||
"""Simplifies query trees by combining unnecessary 'and' connection nodes
|
||||
into a single Q-object.
|
||||
"""
|
||||
|
||||
def visit_combination(self, combination):
|
||||
if combination.operation == combination.AND:
|
||||
# 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]
|
||||
try:
|
||||
return Q(**self._query_conjunction(queries))
|
||||
except DuplicateQueryConditionsError:
|
||||
# Cannot be simplified
|
||||
pass
|
||||
return combination
|
||||
|
||||
def _query_conjunction(self, queries):
|
||||
"""Merges query dicts - effectively &ing them together.
|
||||
"""
|
||||
query_ops = set()
|
||||
combined_query = {}
|
||||
for query in queries:
|
||||
ops = set(query.keys())
|
||||
# Make sure that the same operation isn't applied more than once
|
||||
# to a single field
|
||||
intersection = ops.intersection(query_ops)
|
||||
if intersection:
|
||||
raise DuplicateQueryConditionsError()
|
||||
|
||||
query_ops.update(ops)
|
||||
combined_query.update(copy.deepcopy(query))
|
||||
return combined_query
|
||||
|
||||
|
||||
class QueryCompilerVisitor(QNodeVisitor):
|
||||
"""Compiles the nodes in a query tree to a PyMongo-compatible query
|
||||
dictionary.
|
||||
"""
|
||||
|
||||
def __init__(self, document):
|
||||
self.document = document
|
||||
|
||||
def visit_combination(self, combination):
|
||||
operator = '$and'
|
||||
if combination.operation == combination.OR:
|
||||
operator = '$or'
|
||||
return {operator: combination.children}
|
||||
|
||||
def visit_query(self, query):
|
||||
return transform.query(self.document, **query.query)
|
||||
|
||||
|
||||
class QNode(object):
|
||||
"""Base class for nodes in query trees."""
|
||||
|
||||
AND = 0
|
||||
OR = 1
|
||||
|
||||
def to_query(self, document):
|
||||
query = self.accept(SimplificationVisitor())
|
||||
query = query.accept(QueryCompilerVisitor(document))
|
||||
return query
|
||||
|
||||
def accept(self, visitor):
|
||||
raise NotImplementedError
|
||||
|
||||
def _combine(self, other, operation):
|
||||
"""Combine this node with another node into a QCombination
|
||||
object.
|
||||
"""
|
||||
if getattr(other, 'empty', True):
|
||||
return self
|
||||
|
||||
if self.empty:
|
||||
return other
|
||||
|
||||
return QCombination(operation, [self, other])
|
||||
|
||||
@property
|
||||
def empty(self):
|
||||
return False
|
||||
|
||||
def __or__(self, other):
|
||||
return self._combine(other, self.OR)
|
||||
|
||||
def __and__(self, other):
|
||||
return self._combine(other, self.AND)
|
||||
|
||||
|
||||
class QCombination(QNode):
|
||||
"""Represents the combination of several conditions by a given
|
||||
logical operator.
|
||||
"""
|
||||
|
||||
def __init__(self, operation, children):
|
||||
self.operation = operation
|
||||
self.children = []
|
||||
for node in children:
|
||||
# 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
|
||||
else:
|
||||
self.children.append(node)
|
||||
|
||||
def __repr__(self):
|
||||
op = ' & ' if self.operation is self.AND else ' | '
|
||||
return '(%s)' % op.join([repr(node) for node in self.children])
|
||||
|
||||
def accept(self, visitor):
|
||||
for i in range(len(self.children)):
|
||||
if isinstance(self.children[i], QNode):
|
||||
self.children[i] = self.children[i].accept(visitor)
|
||||
|
||||
return visitor.visit_combination(self)
|
||||
|
||||
@property
|
||||
def empty(self):
|
||||
return not bool(self.children)
|
||||
|
||||
|
||||
class Q(QNode):
|
||||
"""A simple query object, used in a query tree to build up more complex
|
||||
query structures.
|
||||
"""
|
||||
|
||||
def __init__(self, **query):
|
||||
self.query = query
|
||||
|
||||
def __repr__(self):
|
||||
return 'Q(**%s)' % repr(self.query)
|
||||
|
||||
def accept(self, visitor):
|
||||
return visitor.visit_query(self)
|
||||
|
||||
@property
|
||||
def empty(self):
|
||||
return not bool(self.query)
|
||||
@@ -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')
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
from mongoengine.connection import get_db
|
||||
|
||||
|
||||
class query_counter(object):
|
||||
""" Query_counter contextmanager to get the number of queries. """
|
||||
|
||||
def __init__(self):
|
||||
""" 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. """
|
||||
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. """
|
||||
self.db.set_profiling_level(0)
|
||||
|
||||
def __eq__(self, value):
|
||||
""" == Compare querycounter. """
|
||||
return value == self._get_count()
|
||||
|
||||
def __ne__(self, value):
|
||||
""" != Compare querycounter. """
|
||||
return not self.__eq__(value)
|
||||
|
||||
def __lt__(self, value):
|
||||
""" < Compare querycounter. """
|
||||
return self._get_count() < value
|
||||
|
||||
def __le__(self, value):
|
||||
""" <= Compare querycounter. """
|
||||
return self._get_count() <= value
|
||||
|
||||
def __gt__(self, value):
|
||||
""" > Compare querycounter. """
|
||||
return self._get_count() > value
|
||||
|
||||
def __ge__(self, value):
|
||||
""" >= Compare querycounter. """
|
||||
return self._get_count() >= value
|
||||
|
||||
def __int__(self):
|
||||
""" int representation. """
|
||||
return self._get_count()
|
||||
|
||||
def __repr__(self):
|
||||
""" 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
|
||||
self.counter += 1
|
||||
return count
|
||||
@@ -5,7 +5,7 @@
|
||||
%define srcname mongoengine
|
||||
|
||||
Name: python-%{srcname}
|
||||
Version: 0.6.10
|
||||
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
|
||||
|
||||
11
setup.cfg
Normal file
11
setup.cfg
Normal file
@@ -0,0 +1,11 @@
|
||||
[nosetests]
|
||||
verbosity=2
|
||||
detailed-errors=1
|
||||
#tests=tests
|
||||
cover-package=mongoengine
|
||||
|
||||
[flake8]
|
||||
ignore=E501,F401,F403,F405,I201,I202,W504, W605
|
||||
exclude=build,dist,docs,venv,venv3,.tox,.eggs,tests
|
||||
max-complexity=47
|
||||
application-import-names=mongoengine,tests
|
||||
91
setup.py
91
setup.py
@@ -1,27 +1,39 @@
|
||||
from setuptools import setup, find_packages
|
||||
import os
|
||||
import sys
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
DESCRIPTION = "A Python Document-Object Mapper for working with MongoDB"
|
||||
|
||||
LONG_DESCRIPTION = None
|
||||
# Hack to silence atexit traceback in newer python versions
|
||||
try:
|
||||
LONG_DESCRIPTION = open('README.rst').read()
|
||||
except:
|
||||
import multiprocessing
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
DESCRIPTION = (
|
||||
'MongoEngine is a Python Object-Document '
|
||||
'Mapper for working with MongoDB.'
|
||||
)
|
||||
|
||||
try:
|
||||
with open('README.rst') as fin:
|
||||
LONG_DESCRIPTION = fin.read()
|
||||
except Exception:
|
||||
LONG_DESCRIPTION = None
|
||||
|
||||
|
||||
def get_version(version_tuple):
|
||||
version = '%s.%s' % (version_tuple[0], version_tuple[1])
|
||||
if version_tuple[2]:
|
||||
version = '%s.%s' % (version, version_tuple[2])
|
||||
return version
|
||||
"""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
|
||||
init = os.path.join(os.path.dirname(__file__), 'mongoengine', '__init__.py')
|
||||
version_line = filter(lambda l: l.startswith('VERSION'), open(init))[0]
|
||||
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',
|
||||
@@ -29,25 +41,46 @@ CLASSIFIERS = [
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
"Programming Language :: Python :: 2",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
'Topic :: Database',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
]
|
||||
|
||||
setup(name='mongoengine',
|
||||
version=VERSION,
|
||||
packages=find_packages(),
|
||||
author='Harry Marr',
|
||||
author_email='harry.marr@{nospam}gmail.com',
|
||||
maintainer="Ross Lawley",
|
||||
maintainer_email="ross.lawley@{nospam}gmail.com",
|
||||
url='http://mongoengine.org/',
|
||||
license='MIT',
|
||||
include_package_data=True,
|
||||
description=DESCRIPTION,
|
||||
long_description=LONG_DESCRIPTION,
|
||||
platforms=['any'],
|
||||
classifiers=CLASSIFIERS,
|
||||
install_requires=['pymongo'],
|
||||
test_suite='tests',
|
||||
tests_require=['blinker', 'django>=1.3', 'PIL']
|
||||
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
|
||||
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'] += ['python-dateutil']
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from .all_warnings import AllWarnings
|
||||
from .document import *
|
||||
from .queryset import *
|
||||
from .fields import *
|
||||
|
||||
42
tests/all_warnings/__init__.py
Normal file
42
tests/all_warnings/__init__.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
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 unittest
|
||||
import warnings
|
||||
|
||||
from mongoengine import *
|
||||
|
||||
|
||||
__all__ = ('AllWarnings', )
|
||||
|
||||
|
||||
class AllWarnings(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
connect(db='mongoenginetest')
|
||||
self.warning_list = []
|
||||
self.showwarning_default = warnings.showwarning
|
||||
warnings.showwarning = self.append_to_warning_list
|
||||
|
||||
def append_to_warning_list(self, message, category, *args):
|
||||
self.warning_list.append({"message": message,
|
||||
"category": category})
|
||||
|
||||
def tearDown(self):
|
||||
# restore default handling of warnings
|
||||
warnings.showwarning = self.showwarning_default
|
||||
|
||||
def test_document_collection_syntax_warning(self):
|
||||
|
||||
class NonAbstractBase(Document):
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
class InheritedDocumentFailTest(NonAbstractBase):
|
||||
meta = {'collection': 'fail'}
|
||||
|
||||
warning = self.warning_list[0]
|
||||
self.assertEqual(SyntaxWarning, warning["category"])
|
||||
self.assertEqual('non_abstract_base',
|
||||
InheritedDocumentFailTest._get_collection_name())
|
||||
@@ -1,80 +0,0 @@
|
||||
import unittest
|
||||
import pymongo
|
||||
|
||||
import mongoengine.connection
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine.connection import get_db, get_connection, ConnectionError
|
||||
|
||||
|
||||
class ConnectionTest(unittest.TestCase):
|
||||
|
||||
def tearDown(self):
|
||||
mongoengine.connection._connection_settings = {}
|
||||
mongoengine.connection._connections = {}
|
||||
mongoengine.connection._dbs = {}
|
||||
|
||||
def test_connect(self):
|
||||
"""Ensure that the connect() method works properly.
|
||||
"""
|
||||
connect('mongoenginetest')
|
||||
|
||||
conn = get_connection()
|
||||
self.assertTrue(isinstance(conn, pymongo.connection.Connection))
|
||||
|
||||
db = get_db()
|
||||
self.assertTrue(isinstance(db, pymongo.database.Database))
|
||||
self.assertEqual(db.name, 'mongoenginetest')
|
||||
|
||||
connect('mongoenginetest2', alias='testdb')
|
||||
conn = get_connection('testdb')
|
||||
self.assertTrue(isinstance(conn, pymongo.connection.Connection))
|
||||
|
||||
def test_connect_uri(self):
|
||||
"""Ensure that the connect() method works properly with uri's
|
||||
"""
|
||||
c = connect(db='mongoenginetest', alias='admin')
|
||||
c.admin.system.users.remove({})
|
||||
c.mongoenginetest.system.users.remove({})
|
||||
|
||||
c.admin.add_user("admin", "password")
|
||||
c.admin.authenticate("admin", "password")
|
||||
c.mongoenginetest.add_user("username", "password")
|
||||
|
||||
self.assertRaises(ConnectionError, 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))
|
||||
|
||||
db = get_db()
|
||||
self.assertTrue(isinstance(db, pymongo.database.Database))
|
||||
self.assertEqual(db.name, 'mongoenginetest')
|
||||
|
||||
def test_register_connection(self):
|
||||
"""Ensure that connections with different aliases may be registered.
|
||||
"""
|
||||
register_connection('testdb', 'mongoenginetest2')
|
||||
|
||||
self.assertRaises(ConnectionError, get_connection)
|
||||
conn = get_connection('testdb')
|
||||
self.assertTrue(isinstance(conn, pymongo.connection.Connection))
|
||||
|
||||
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.
|
||||
"""
|
||||
connect('mongoenginetest', alias='t1', tz_aware=True)
|
||||
conn = get_connection('t1')
|
||||
self.assertTrue(conn.tz_aware)
|
||||
|
||||
connect('mongoenginetest2', alias='t2')
|
||||
conn = get_connection('t2')
|
||||
self.assertFalse(conn.tz_aware)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,110 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import unittest
|
||||
|
||||
from mongoengine import *
|
||||
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()
|
||||
|
||||
from django.contrib.sessions.tests import SessionTestsMixin
|
||||
from mongoengine.django.sessions import SessionStore, MongoSession
|
||||
|
||||
|
||||
class QuerySetTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
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))
|
||||
|
||||
|
||||
|
||||
class MongoDBSessionTest(SessionTestsMixin, unittest.TestCase):
|
||||
backend = SessionStore
|
||||
|
||||
def setUp(self):
|
||||
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)
|
||||
3008
tests/document.py
3008
tests/document.py
File diff suppressed because it is too large
Load Diff
13
tests/document/__init__.py
Normal file
13
tests/document/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import unittest
|
||||
|
||||
from .class_methods import *
|
||||
from .delta import *
|
||||
from .dynamic import *
|
||||
from .indexes import *
|
||||
from .inheritance import *
|
||||
from .instance import *
|
||||
from .json_serialisation import *
|
||||
from .validation import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
371
tests/document/class_methods.py
Normal file
371
tests/document/class_methods.py
Normal file
@@ -0,0 +1,371 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
|
||||
from mongoengine import *
|
||||
|
||||
from mongoengine.queryset import NULLIFY, PULL
|
||||
from mongoengine.connection import get_db
|
||||
from tests.utils import requires_mongodb_gte_26
|
||||
|
||||
__all__ = ("ClassMethodsTest", )
|
||||
|
||||
|
||||
class ClassMethodsTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
connect(db='mongoenginetest')
|
||||
self.db = get_db()
|
||||
|
||||
class Person(Document):
|
||||
name = StringField()
|
||||
age = IntField()
|
||||
|
||||
non_field = True
|
||||
|
||||
meta = {"allow_inheritance": True}
|
||||
|
||||
self.Person = Person
|
||||
|
||||
def tearDown(self):
|
||||
for collection in self.db.collection_names():
|
||||
if 'system.' in collection:
|
||||
continue
|
||||
self.db.drop_collection(collection)
|
||||
|
||||
def test_definition(self):
|
||||
"""Ensure that document may be defined using fields.
|
||||
"""
|
||||
self.assertEqual(['_cls', 'age', 'id', 'name'],
|
||||
sorted(self.Person._fields.keys()))
|
||||
self.assertEqual(["IntField", "ObjectIdField", "StringField", "StringField"],
|
||||
sorted([x.__class__.__name__ for x in
|
||||
self.Person._fields.values()]))
|
||||
|
||||
def test_get_db(self):
|
||||
"""Ensure that get_db returns the expected db.
|
||||
"""
|
||||
db = self.Person._get_db()
|
||||
self.assertEqual(self.db, db)
|
||||
|
||||
def test_get_collection_name(self):
|
||||
"""Ensure that get_collection_name returns the expected collection
|
||||
name.
|
||||
"""
|
||||
collection_name = 'person'
|
||||
self.assertEqual(collection_name, self.Person._get_collection_name())
|
||||
|
||||
def test_get_collection(self):
|
||||
"""Ensure that get_collection returns the expected collection.
|
||||
"""
|
||||
collection_name = 'person'
|
||||
collection = self.Person._get_collection()
|
||||
self.assertEqual(self.db[collection_name], collection)
|
||||
|
||||
def test_drop_collection(self):
|
||||
"""Ensure that the collection may be dropped from the database.
|
||||
"""
|
||||
collection_name = 'person'
|
||||
self.Person(name='Test').save()
|
||||
self.assertIn(collection_name, self.db.collection_names())
|
||||
|
||||
self.Person.drop_collection()
|
||||
self.assertNotIn(collection_name, self.db.collection_names())
|
||||
|
||||
def test_register_delete_rule(self):
|
||||
"""Ensure that register delete rule adds a delete rule to the document
|
||||
meta.
|
||||
"""
|
||||
class Job(Document):
|
||||
employee = ReferenceField(self.Person)
|
||||
|
||||
self.assertEqual(self.Person._meta.get('delete_rules'), None)
|
||||
|
||||
self.Person.register_delete_rule(Job, 'employee', NULLIFY)
|
||||
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': [] })
|
||||
|
||||
@requires_mongodb_gte_26
|
||||
def test_compare_indexes_for_text_indexes(self):
|
||||
""" Ensure that compare_indexes behaves correctly for text indexes """
|
||||
|
||||
class Doc(Document):
|
||||
a = StringField()
|
||||
b = StringField()
|
||||
meta = {'indexes': [
|
||||
{'fields': ['$a', "$b"],
|
||||
'default_language': 'english',
|
||||
'weights': {'a': 10, 'b': 2}
|
||||
}
|
||||
]}
|
||||
|
||||
Doc.drop_collection()
|
||||
Doc.ensure_indexes()
|
||||
actual = Doc.compare_indexes()
|
||||
expected = {'missing': [], 'extra': []}
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
class DefaultNamingTest(Document):
|
||||
pass
|
||||
self.assertEqual('default_naming_test',
|
||||
DefaultNamingTest._get_collection_name())
|
||||
|
||||
class CustomNamingTest(Document):
|
||||
meta = {'collection': 'pimp_my_collection'}
|
||||
|
||||
self.assertEqual('pimp_my_collection',
|
||||
CustomNamingTest._get_collection_name())
|
||||
|
||||
class DynamicNamingTest(Document):
|
||||
meta = {'collection': lambda c: "DYNAMO"}
|
||||
self.assertEqual('DYNAMO', DynamicNamingTest._get_collection_name())
|
||||
|
||||
# Use Abstract class to handle backwards compatibility
|
||||
class BaseDocument(Document):
|
||||
meta = {
|
||||
'abstract': True,
|
||||
'collection': lambda c: c.__name__.lower()
|
||||
}
|
||||
|
||||
class OldNamingConvention(BaseDocument):
|
||||
pass
|
||||
self.assertEqual('oldnamingconvention',
|
||||
OldNamingConvention._get_collection_name())
|
||||
|
||||
class InheritedAbstractNamingTest(BaseDocument):
|
||||
meta = {'collection': 'wibble'}
|
||||
self.assertEqual('wibble',
|
||||
InheritedAbstractNamingTest._get_collection_name())
|
||||
|
||||
# Mixin tests
|
||||
class BaseMixin(object):
|
||||
meta = {
|
||||
'collection': lambda c: c.__name__.lower()
|
||||
}
|
||||
|
||||
class OldMixinNamingConvention(Document, BaseMixin):
|
||||
pass
|
||||
self.assertEqual('oldmixinnamingconvention',
|
||||
OldMixinNamingConvention._get_collection_name())
|
||||
|
||||
class BaseMixin(object):
|
||||
meta = {
|
||||
'collection': lambda c: c.__name__.lower()
|
||||
}
|
||||
|
||||
class BaseDocument(Document, BaseMixin):
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
class MyDocument(BaseDocument):
|
||||
pass
|
||||
|
||||
self.assertEqual('basedocument', MyDocument._get_collection_name())
|
||||
|
||||
def test_custom_collection_name_operations(self):
|
||||
"""Ensure that a collection with a specified name is used as expected.
|
||||
"""
|
||||
collection_name = 'personCollTest'
|
||||
|
||||
class Person(Document):
|
||||
name = StringField()
|
||||
meta = {'collection': collection_name}
|
||||
|
||||
Person(name="Test User").save()
|
||||
self.assertIn(collection_name, self.db.collection_names())
|
||||
|
||||
user_obj = self.db[collection_name].find_one()
|
||||
self.assertEqual(user_obj['name'], "Test User")
|
||||
|
||||
user_obj = Person.objects[0]
|
||||
self.assertEqual(user_obj.name, "Test User")
|
||||
|
||||
Person.drop_collection()
|
||||
self.assertNotIn(collection_name, self.db.collection_names())
|
||||
|
||||
def test_collection_name_and_primary(self):
|
||||
"""Ensure that a collection with a specified name may be used.
|
||||
"""
|
||||
|
||||
class Person(Document):
|
||||
name = StringField(primary_key=True)
|
||||
meta = {'collection': 'app'}
|
||||
|
||||
Person(name="Test User").save()
|
||||
|
||||
user_obj = Person.objects.first()
|
||||
self.assertEqual(user_obj.name, "Test User")
|
||||
|
||||
Person.drop_collection()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
867
tests/document/delta.py
Normal file
867
tests/document/delta.py
Normal file
@@ -0,0 +1,867 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
|
||||
from bson import SON
|
||||
from mongoengine import *
|
||||
from mongoengine.connection import get_db
|
||||
|
||||
__all__ = ("DeltaTest",)
|
||||
|
||||
|
||||
class DeltaTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
connect(db='mongoenginetest')
|
||||
self.db = get_db()
|
||||
|
||||
class Person(Document):
|
||||
name = StringField()
|
||||
age = IntField()
|
||||
|
||||
non_field = True
|
||||
|
||||
meta = {"allow_inheritance": True}
|
||||
|
||||
self.Person = Person
|
||||
|
||||
def tearDown(self):
|
||||
for collection in self.db.collection_names():
|
||||
if 'system.' in collection:
|
||||
continue
|
||||
self.db.drop_collection(collection)
|
||||
|
||||
def test_delta(self):
|
||||
self.delta(Document)
|
||||
self.delta(DynamicDocument)
|
||||
|
||||
def delta(self, DocClass):
|
||||
|
||||
class Doc(DocClass):
|
||||
string_field = StringField()
|
||||
int_field = IntField()
|
||||
dict_field = DictField()
|
||||
list_field = ListField()
|
||||
|
||||
Doc.drop_collection()
|
||||
doc = Doc()
|
||||
doc.save()
|
||||
|
||||
doc = Doc.objects.first()
|
||||
self.assertEqual(doc._get_changed_fields(), [])
|
||||
self.assertEqual(doc._delta(), ({}, {}))
|
||||
|
||||
doc.string_field = 'hello'
|
||||
self.assertEqual(doc._get_changed_fields(), ['string_field'])
|
||||
self.assertEqual(doc._delta(), ({'string_field': 'hello'}, {}))
|
||||
|
||||
doc._changed_fields = []
|
||||
doc.int_field = 1
|
||||
self.assertEqual(doc._get_changed_fields(), ['int_field'])
|
||||
self.assertEqual(doc._delta(), ({'int_field': 1}, {}))
|
||||
|
||||
doc._changed_fields = []
|
||||
dict_value = {'hello': 'world', 'ping': 'pong'}
|
||||
doc.dict_field = dict_value
|
||||
self.assertEqual(doc._get_changed_fields(), ['dict_field'])
|
||||
self.assertEqual(doc._delta(), ({'dict_field': dict_value}, {}))
|
||||
|
||||
doc._changed_fields = []
|
||||
list_value = ['1', 2, {'hello': 'world'}]
|
||||
doc.list_field = list_value
|
||||
self.assertEqual(doc._get_changed_fields(), ['list_field'])
|
||||
self.assertEqual(doc._delta(), ({'list_field': list_value}, {}))
|
||||
|
||||
# Test unsetting
|
||||
doc._changed_fields = []
|
||||
doc.dict_field = {}
|
||||
self.assertEqual(doc._get_changed_fields(), ['dict_field'])
|
||||
self.assertEqual(doc._delta(), ({}, {'dict_field': 1}))
|
||||
|
||||
doc._changed_fields = []
|
||||
doc.list_field = []
|
||||
self.assertEqual(doc._get_changed_fields(), ['list_field'])
|
||||
self.assertEqual(doc._delta(), ({}, {'list_field': 1}))
|
||||
|
||||
def test_delta_recursive(self):
|
||||
self.delta_recursive(Document, EmbeddedDocument)
|
||||
self.delta_recursive(DynamicDocument, EmbeddedDocument)
|
||||
self.delta_recursive(Document, DynamicEmbeddedDocument)
|
||||
self.delta_recursive(DynamicDocument, DynamicEmbeddedDocument)
|
||||
|
||||
def delta_recursive(self, DocClass, EmbeddedClass):
|
||||
|
||||
class Embedded(EmbeddedClass):
|
||||
id = StringField()
|
||||
string_field = StringField()
|
||||
int_field = IntField()
|
||||
dict_field = DictField()
|
||||
list_field = ListField()
|
||||
|
||||
class Doc(DocClass):
|
||||
string_field = StringField()
|
||||
int_field = IntField()
|
||||
dict_field = DictField()
|
||||
list_field = ListField()
|
||||
embedded_field = EmbeddedDocumentField(Embedded)
|
||||
|
||||
Doc.drop_collection()
|
||||
doc = Doc()
|
||||
doc.save()
|
||||
|
||||
doc = Doc.objects.first()
|
||||
self.assertEqual(doc._get_changed_fields(), [])
|
||||
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'}
|
||||
embedded_1.list_field = ['1', 2, {'hello': 'world'}]
|
||||
doc.embedded_field = embedded_1
|
||||
|
||||
self.assertEqual(doc._get_changed_fields(), ['embedded_field'])
|
||||
|
||||
embedded_delta = {
|
||||
'id': "010101",
|
||||
'string_field': 'hello',
|
||||
'int_field': 1,
|
||||
'dict_field': {'hello': 'world'},
|
||||
'list_field': ['1', 2, {'hello': 'world'}]
|
||||
}
|
||||
self.assertEqual(doc.embedded_field._delta(), (embedded_delta, {}))
|
||||
self.assertEqual(doc._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'])
|
||||
self.assertEqual(doc.embedded_field._delta(), ({}, {'dict_field': 1}))
|
||||
self.assertEqual(doc._delta(), ({}, {'embedded_field.dict_field': 1}))
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
self.assertEqual(doc.embedded_field.dict_field, {})
|
||||
|
||||
doc.embedded_field.list_field = []
|
||||
self.assertEqual(doc._get_changed_fields(),
|
||||
['embedded_field.list_field'])
|
||||
self.assertEqual(doc.embedded_field._delta(), ({}, {'list_field': 1}))
|
||||
self.assertEqual(doc._delta(), ({}, {'embedded_field.list_field': 1}))
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
self.assertEqual(doc.embedded_field.list_field, [])
|
||||
|
||||
embedded_2 = Embedded()
|
||||
embedded_2.string_field = 'hello'
|
||||
embedded_2.int_field = 1
|
||||
embedded_2.dict_field = {'hello': 'world'}
|
||||
embedded_2.list_field = ['1', 2, {'hello': 'world'}]
|
||||
|
||||
doc.embedded_field.list_field = ['1', 2, embedded_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',
|
||||
'dict_field': {'hello': 'world'},
|
||||
'int_field': 1,
|
||||
'list_field': ['1', 2, {'hello': 'world'}],
|
||||
}]
|
||||
}, {}))
|
||||
|
||||
self.assertEqual(doc._delta(), ({
|
||||
'embedded_field.list_field': ['1', 2, {
|
||||
'_cls': 'Embedded',
|
||||
'string_field': 'hello',
|
||||
'dict_field': {'hello': 'world'},
|
||||
'int_field': 1,
|
||||
'list_field': ['1', 2, {'hello': 'world'}],
|
||||
}]
|
||||
}, {}))
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
|
||||
self.assertEqual(doc.embedded_field.list_field[0], '1')
|
||||
self.assertEqual(doc.embedded_field.list_field[1], 2)
|
||||
for k in doc.embedded_field.list_field[2]._fields:
|
||||
self.assertEqual(doc.embedded_field.list_field[2][k],
|
||||
embedded_2[k])
|
||||
|
||||
doc.embedded_field.list_field[2].string_field = 'world'
|
||||
self.assertEqual(doc._get_changed_fields(),
|
||||
['embedded_field.list_field.2.string_field'])
|
||||
self.assertEqual(doc.embedded_field._delta(),
|
||||
({'list_field.2.string_field': 'world'}, {}))
|
||||
self.assertEqual(doc._delta(),
|
||||
({'embedded_field.list_field.2.string_field': 'world'}, {}))
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
self.assertEqual(doc.embedded_field.list_field[2].string_field,
|
||||
'world')
|
||||
|
||||
# Test multiple assignments
|
||||
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.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')
|
||||
|
||||
# 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'}]}, {}))
|
||||
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]}, {}))
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
self.assertEqual(doc.embedded_field.list_field[2].list_field,
|
||||
[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'}])
|
||||
|
||||
del doc.embedded_field.list_field[2].list_field[2]['hello']
|
||||
self.assertEqual(doc._delta(),
|
||||
({}, {'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
|
||||
self.assertEqual(doc._delta(),
|
||||
({}, {'embedded_field.list_field.2.list_field': 1}))
|
||||
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
|
||||
doc.dict_field['Embedded'] = embedded_1
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
|
||||
doc.dict_field['Embedded'].string_field = 'Hello World'
|
||||
self.assertEqual(doc._get_changed_fields(),
|
||||
['dict_field.Embedded.string_field'])
|
||||
self.assertEqual(doc._delta(),
|
||||
({'dict_field.Embedded.string_field': 'Hello World'}, {}))
|
||||
|
||||
def test_circular_reference_deltas(self):
|
||||
self.circular_reference_deltas(Document, Document)
|
||||
self.circular_reference_deltas(Document, DynamicDocument)
|
||||
self.circular_reference_deltas(DynamicDocument, Document)
|
||||
self.circular_reference_deltas(DynamicDocument, DynamicDocument)
|
||||
|
||||
def circular_reference_deltas(self, DocClass1, DocClass2):
|
||||
|
||||
class Person(DocClass1):
|
||||
name = StringField()
|
||||
owns = ListField(ReferenceField('Organization'))
|
||||
|
||||
class Organization(DocClass2):
|
||||
name = StringField()
|
||||
owner = ReferenceField('Person')
|
||||
|
||||
Person.drop_collection()
|
||||
Organization.drop_collection()
|
||||
|
||||
person = Person(name="owner").save()
|
||||
organization = Organization(name="company").save()
|
||||
|
||||
person.owns.append(organization)
|
||||
organization.owner = person
|
||||
|
||||
person.save()
|
||||
organization.save()
|
||||
|
||||
p = Person.objects[0].select_related()
|
||||
o = Organization.objects.first()
|
||||
self.assertEqual(p.owns[0], o)
|
||||
self.assertEqual(o.owner, p)
|
||||
|
||||
def test_circular_reference_deltas_2(self):
|
||||
self.circular_reference_deltas_2(Document, Document)
|
||||
self.circular_reference_deltas_2(Document, DynamicDocument)
|
||||
self.circular_reference_deltas_2(DynamicDocument, Document)
|
||||
self.circular_reference_deltas_2(DynamicDocument, DynamicDocument)
|
||||
|
||||
def circular_reference_deltas_2(self, DocClass1, DocClass2, dbref=True):
|
||||
|
||||
class Person(DocClass1):
|
||||
name = StringField()
|
||||
owns = ListField(ReferenceField('Organization', dbref=dbref))
|
||||
employer = ReferenceField('Organization', dbref=dbref)
|
||||
|
||||
class Organization(DocClass2):
|
||||
name = StringField()
|
||||
owner = ReferenceField('Person', dbref=dbref)
|
||||
employees = ListField(ReferenceField('Person', dbref=dbref))
|
||||
|
||||
Person.drop_collection()
|
||||
Organization.drop_collection()
|
||||
|
||||
person = Person(name="owner").save()
|
||||
employee = Person(name="employee").save()
|
||||
organization = Organization(name="company").save()
|
||||
|
||||
person.owns.append(organization)
|
||||
organization.owner = person
|
||||
|
||||
organization.employees.append(employee)
|
||||
employee.employer = organization
|
||||
|
||||
person.save()
|
||||
organization.save()
|
||||
employee.save()
|
||||
|
||||
p = Person.objects.get(name="owner")
|
||||
e = Person.objects.get(name="employee")
|
||||
o = Organization.objects.first()
|
||||
|
||||
self.assertEqual(p.owns[0], o)
|
||||
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)
|
||||
|
||||
def delta_db_field(self, DocClass):
|
||||
|
||||
class Doc(DocClass):
|
||||
string_field = StringField(db_field='db_string_field')
|
||||
int_field = IntField(db_field='db_int_field')
|
||||
dict_field = DictField(db_field='db_dict_field')
|
||||
list_field = ListField(db_field='db_list_field')
|
||||
|
||||
Doc.drop_collection()
|
||||
doc = Doc()
|
||||
doc.save()
|
||||
|
||||
doc = Doc.objects.first()
|
||||
self.assertEqual(doc._get_changed_fields(), [])
|
||||
self.assertEqual(doc._delta(), ({}, {}))
|
||||
|
||||
doc.string_field = 'hello'
|
||||
self.assertEqual(doc._get_changed_fields(), ['db_string_field'])
|
||||
self.assertEqual(doc._delta(), ({'db_string_field': 'hello'}, {}))
|
||||
|
||||
doc._changed_fields = []
|
||||
doc.int_field = 1
|
||||
self.assertEqual(doc._get_changed_fields(), ['db_int_field'])
|
||||
self.assertEqual(doc._delta(), ({'db_int_field': 1}, {}))
|
||||
|
||||
doc._changed_fields = []
|
||||
dict_value = {'hello': 'world', 'ping': 'pong'}
|
||||
doc.dict_field = dict_value
|
||||
self.assertEqual(doc._get_changed_fields(), ['db_dict_field'])
|
||||
self.assertEqual(doc._delta(), ({'db_dict_field': dict_value}, {}))
|
||||
|
||||
doc._changed_fields = []
|
||||
list_value = ['1', 2, {'hello': 'world'}]
|
||||
doc.list_field = list_value
|
||||
self.assertEqual(doc._get_changed_fields(), ['db_list_field'])
|
||||
self.assertEqual(doc._delta(), ({'db_list_field': list_value}, {}))
|
||||
|
||||
# Test unsetting
|
||||
doc._changed_fields = []
|
||||
doc.dict_field = {}
|
||||
self.assertEqual(doc._get_changed_fields(), ['db_dict_field'])
|
||||
self.assertEqual(doc._delta(), ({}, {'db_dict_field': 1}))
|
||||
|
||||
doc._changed_fields = []
|
||||
doc.list_field = []
|
||||
self.assertEqual(doc._get_changed_fields(), ['db_list_field'])
|
||||
self.assertEqual(doc._delta(), ({}, {'db_list_field': 1}))
|
||||
|
||||
# Test it saves that data
|
||||
doc = Doc()
|
||||
doc.save()
|
||||
|
||||
doc.string_field = 'hello'
|
||||
doc.int_field = 1
|
||||
doc.dict_field = {'hello': 'world'}
|
||||
doc.list_field = ['1', 2, {'hello': 'world'}]
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
|
||||
self.assertEqual(doc.string_field, 'hello')
|
||||
self.assertEqual(doc.int_field, 1)
|
||||
self.assertEqual(doc.dict_field, {'hello': 'world'})
|
||||
self.assertEqual(doc.list_field, ['1', 2, {'hello': 'world'}])
|
||||
|
||||
def test_delta_recursive_db_field(self):
|
||||
self.delta_recursive_db_field(Document, EmbeddedDocument)
|
||||
self.delta_recursive_db_field(Document, DynamicEmbeddedDocument)
|
||||
self.delta_recursive_db_field(DynamicDocument, EmbeddedDocument)
|
||||
self.delta_recursive_db_field(DynamicDocument, DynamicEmbeddedDocument)
|
||||
|
||||
def delta_recursive_db_field(self, DocClass, EmbeddedClass):
|
||||
|
||||
class Embedded(EmbeddedClass):
|
||||
string_field = StringField(db_field='db_string_field')
|
||||
int_field = IntField(db_field='db_int_field')
|
||||
dict_field = DictField(db_field='db_dict_field')
|
||||
list_field = ListField(db_field='db_list_field')
|
||||
|
||||
class Doc(DocClass):
|
||||
string_field = StringField(db_field='db_string_field')
|
||||
int_field = IntField(db_field='db_int_field')
|
||||
dict_field = DictField(db_field='db_dict_field')
|
||||
list_field = ListField(db_field='db_list_field')
|
||||
embedded_field = EmbeddedDocumentField(Embedded,
|
||||
db_field='db_embedded_field')
|
||||
|
||||
Doc.drop_collection()
|
||||
doc = Doc()
|
||||
doc.save()
|
||||
|
||||
doc = Doc.objects.first()
|
||||
self.assertEqual(doc._get_changed_fields(), [])
|
||||
self.assertEqual(doc._delta(), ({}, {}))
|
||||
|
||||
embedded_1 = Embedded()
|
||||
embedded_1.string_field = 'hello'
|
||||
embedded_1.int_field = 1
|
||||
embedded_1.dict_field = {'hello': 'world'}
|
||||
embedded_1.list_field = ['1', 2, {'hello': 'world'}]
|
||||
doc.embedded_field = embedded_1
|
||||
|
||||
self.assertEqual(doc._get_changed_fields(), ['db_embedded_field'])
|
||||
|
||||
embedded_delta = {
|
||||
'db_string_field': 'hello',
|
||||
'db_int_field': 1,
|
||||
'db_dict_field': {'hello': 'world'},
|
||||
'db_list_field': ['1', 2, {'hello': 'world'}]
|
||||
}
|
||||
self.assertEqual(doc.embedded_field._delta(), (embedded_delta, {}))
|
||||
self.assertEqual(doc._delta(),
|
||||
({'db_embedded_field': embedded_delta}, {}))
|
||||
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
|
||||
doc.embedded_field.dict_field = {}
|
||||
self.assertEqual(doc._get_changed_fields(),
|
||||
['db_embedded_field.db_dict_field'])
|
||||
self.assertEqual(doc.embedded_field._delta(),
|
||||
({}, {'db_dict_field': 1}))
|
||||
self.assertEqual(doc._delta(),
|
||||
({}, {'db_embedded_field.db_dict_field': 1}))
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
self.assertEqual(doc.embedded_field.dict_field, {})
|
||||
|
||||
doc.embedded_field.list_field = []
|
||||
self.assertEqual(doc._get_changed_fields(),
|
||||
['db_embedded_field.db_list_field'])
|
||||
self.assertEqual(doc.embedded_field._delta(),
|
||||
({}, {'db_list_field': 1}))
|
||||
self.assertEqual(doc._delta(),
|
||||
({}, {'db_embedded_field.db_list_field': 1}))
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
self.assertEqual(doc.embedded_field.list_field, [])
|
||||
|
||||
embedded_2 = Embedded()
|
||||
embedded_2.string_field = 'hello'
|
||||
embedded_2.int_field = 1
|
||||
embedded_2.dict_field = {'hello': 'world'}
|
||||
embedded_2.list_field = ['1', 2, {'hello': 'world'}]
|
||||
|
||||
doc.embedded_field.list_field = ['1', 2, embedded_2]
|
||||
self.assertEqual(doc._get_changed_fields(),
|
||||
['db_embedded_field.db_list_field'])
|
||||
self.assertEqual(doc.embedded_field._delta(), ({
|
||||
'db_list_field': ['1', 2, {
|
||||
'_cls': 'Embedded',
|
||||
'db_string_field': 'hello',
|
||||
'db_dict_field': {'hello': 'world'},
|
||||
'db_int_field': 1,
|
||||
'db_list_field': ['1', 2, {'hello': 'world'}],
|
||||
}]
|
||||
}, {}))
|
||||
|
||||
self.assertEqual(doc._delta(), ({
|
||||
'db_embedded_field.db_list_field': ['1', 2, {
|
||||
'_cls': 'Embedded',
|
||||
'db_string_field': 'hello',
|
||||
'db_dict_field': {'hello': 'world'},
|
||||
'db_int_field': 1,
|
||||
'db_list_field': ['1', 2, {'hello': 'world'}],
|
||||
}]
|
||||
}, {}))
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
|
||||
self.assertEqual(doc.embedded_field.list_field[0], '1')
|
||||
self.assertEqual(doc.embedded_field.list_field[1], 2)
|
||||
for k in doc.embedded_field.list_field[2]._fields:
|
||||
self.assertEqual(doc.embedded_field.list_field[2][k],
|
||||
embedded_2[k])
|
||||
|
||||
doc.embedded_field.list_field[2].string_field = 'world'
|
||||
self.assertEqual(doc._get_changed_fields(),
|
||||
['db_embedded_field.db_list_field.2.db_string_field'])
|
||||
self.assertEqual(doc.embedded_field._delta(),
|
||||
({'db_list_field.2.db_string_field': 'world'}, {}))
|
||||
self.assertEqual(doc._delta(),
|
||||
({'db_embedded_field.db_list_field.2.db_string_field': 'world'},
|
||||
{}))
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
self.assertEqual(doc.embedded_field.list_field[2].string_field,
|
||||
'world')
|
||||
|
||||
# Test multiple assignments
|
||||
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.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'}}}, {}))
|
||||
self.assertEqual(doc._delta(), ({
|
||||
'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,
|
||||
'hello world')
|
||||
|
||||
# Test list native methods
|
||||
doc.embedded_field.list_field[2].list_field.pop(0)
|
||||
self.assertEqual(doc._delta(),
|
||||
({'db_embedded_field.db_list_field.2.db_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(),
|
||||
({'db_embedded_field.db_list_field.2.db_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])
|
||||
|
||||
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'}])
|
||||
|
||||
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.2.hello': 1}))
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
|
||||
del doc.embedded_field.list_field[2].list_field
|
||||
self.assertEqual(doc._delta(), ({},
|
||||
{'db_embedded_field.db_list_field.2.db_list_field': 1}))
|
||||
|
||||
def test_delta_for_dynamic_documents(self):
|
||||
class Person(DynamicDocument):
|
||||
name = StringField()
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
Person.drop_collection()
|
||||
|
||||
p = Person(name="James", age=34)
|
||||
self.assertEqual(p._delta(), (
|
||||
SON([('_cls', 'Person'), ('name', 'James'), ('age', 34)]), {}))
|
||||
|
||||
p.doc = 123
|
||||
del p.doc
|
||||
self.assertEqual(p._delta(), (
|
||||
SON([('_cls', 'Person'), ('name', 'James'), ('age', 34)]), {}))
|
||||
|
||||
p = Person()
|
||||
p.name = "Dean"
|
||||
p.age = 22
|
||||
p.save()
|
||||
|
||||
p.age = 24
|
||||
self.assertEqual(p.age, 24)
|
||||
self.assertEqual(p._get_changed_fields(), ['age'])
|
||||
self.assertEqual(p._delta(), ({'age': 24}, {}))
|
||||
|
||||
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, Person.objects(age=24).count())
|
||||
|
||||
def test_dynamic_delta(self):
|
||||
|
||||
class Doc(DynamicDocument):
|
||||
pass
|
||||
|
||||
Doc.drop_collection()
|
||||
doc = Doc()
|
||||
doc.save()
|
||||
|
||||
doc = Doc.objects.first()
|
||||
self.assertEqual(doc._get_changed_fields(), [])
|
||||
self.assertEqual(doc._delta(), ({}, {}))
|
||||
|
||||
doc.string_field = 'hello'
|
||||
self.assertEqual(doc._get_changed_fields(), ['string_field'])
|
||||
self.assertEqual(doc._delta(), ({'string_field': 'hello'}, {}))
|
||||
|
||||
doc._changed_fields = []
|
||||
doc.int_field = 1
|
||||
self.assertEqual(doc._get_changed_fields(), ['int_field'])
|
||||
self.assertEqual(doc._delta(), ({'int_field': 1}, {}))
|
||||
|
||||
doc._changed_fields = []
|
||||
dict_value = {'hello': 'world', 'ping': 'pong'}
|
||||
doc.dict_field = dict_value
|
||||
self.assertEqual(doc._get_changed_fields(), ['dict_field'])
|
||||
self.assertEqual(doc._delta(), ({'dict_field': dict_value}, {}))
|
||||
|
||||
doc._changed_fields = []
|
||||
list_value = ['1', 2, {'hello': 'world'}]
|
||||
doc.list_field = list_value
|
||||
self.assertEqual(doc._get_changed_fields(), ['list_field'])
|
||||
self.assertEqual(doc._delta(), ({'list_field': list_value}, {}))
|
||||
|
||||
# Test unsetting
|
||||
doc._changed_fields = []
|
||||
doc.dict_field = {}
|
||||
self.assertEqual(doc._get_changed_fields(), ['dict_field'])
|
||||
self.assertEqual(doc._delta(), ({}, {'dict_field': 1}))
|
||||
|
||||
doc._changed_fields = []
|
||||
doc.list_field = []
|
||||
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.assertIn('employees', 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.assertIn('employees', 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()
|
||||
373
tests/document/dynamic.py
Normal file
373
tests/document/dynamic.py
Normal file
@@ -0,0 +1,373 @@
|
||||
import unittest
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine.connection import get_db
|
||||
|
||||
__all__ = ("DynamicTest", )
|
||||
|
||||
|
||||
class DynamicTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
connect(db='mongoenginetest')
|
||||
self.db = get_db()
|
||||
|
||||
class Person(DynamicDocument):
|
||||
name = StringField()
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
Person.drop_collection()
|
||||
|
||||
self.Person = Person
|
||||
|
||||
def test_simple_dynamic_document(self):
|
||||
"""Ensures simple dynamic documents are saved correctly"""
|
||||
|
||||
p = self.Person()
|
||||
p.name = "James"
|
||||
p.age = 34
|
||||
|
||||
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)
|
||||
|
||||
# Confirm no changes to self.Person
|
||||
self.assertFalse(hasattr(self.Person, 'age'))
|
||||
|
||||
def test_change_scope_of_variable(self):
|
||||
"""Test changing the scope of a dynamic field has no adverse effects"""
|
||||
p = self.Person()
|
||||
p.name = "Dean"
|
||||
p.misc = 22
|
||||
p.save()
|
||||
|
||||
p = self.Person.objects.get()
|
||||
p.misc = {'hello': 'world'}
|
||||
p.save()
|
||||
|
||||
p = self.Person.objects.get()
|
||||
self.assertEqual(p.misc, {'hello': 'world'})
|
||||
|
||||
def test_delete_dynamic_field(self):
|
||||
"""Test deleting a dynamic field works"""
|
||||
self.Person.drop_collection()
|
||||
p = self.Person()
|
||||
p.name = "Dean"
|
||||
p.misc = 22
|
||||
p.save()
|
||||
|
||||
p = self.Person.objects.get()
|
||||
p.misc = {'hello': 'world'}
|
||||
p.save()
|
||||
|
||||
p = self.Person.objects.get()
|
||||
self.assertEqual(p.misc, {'hello': 'world'})
|
||||
collection = self.db[self.Person._get_collection_name()]
|
||||
obj = collection.find_one()
|
||||
self.assertEqual(sorted(obj.keys()), ['_cls', '_id', 'misc', 'name'])
|
||||
|
||||
del p.misc
|
||||
p.save()
|
||||
|
||||
p = self.Person.objects.get()
|
||||
self.assertFalse(hasattr(p, 'misc'))
|
||||
|
||||
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()
|
||||
p.name = "Dean"
|
||||
p.age = 22
|
||||
p.save()
|
||||
|
||||
self.assertEqual(1, self.Person.objects(age=22).count())
|
||||
p = self.Person.objects(age=22)
|
||||
p = p.get()
|
||||
self.assertEqual(22, p.age)
|
||||
|
||||
def test_complex_dynamic_document_queries(self):
|
||||
class Person(DynamicDocument):
|
||||
name = StringField()
|
||||
|
||||
Person.drop_collection()
|
||||
|
||||
p = Person(name="test")
|
||||
p.age = "ten"
|
||||
p.save()
|
||||
|
||||
p1 = Person(name="test1")
|
||||
p1.age = "less then ten and a half"
|
||||
p1.save()
|
||||
|
||||
p2 = Person(name="test2")
|
||||
p2.age = 10
|
||||
p2.save()
|
||||
|
||||
self.assertEqual(Person.objects(age__icontains='ten').count(), 2)
|
||||
self.assertEqual(Person.objects(age__gte=10).count(), 1)
|
||||
|
||||
def test_complex_data_lookups(self):
|
||||
"""Ensure you can query dynamic document dynamic fields"""
|
||||
p = self.Person()
|
||||
p.misc = {'hello': 'world'}
|
||||
p.save()
|
||||
|
||||
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):
|
||||
content = URLField()
|
||||
|
||||
class Doc(DynamicDocument):
|
||||
pass
|
||||
|
||||
Doc.drop_collection()
|
||||
doc = Doc()
|
||||
|
||||
embedded_doc_1 = Embedded(content='http://mongoengine.org')
|
||||
embedded_doc_1.validate()
|
||||
|
||||
embedded_doc_2 = Embedded(content='this is not a url')
|
||||
self.assertRaises(ValidationError, embedded_doc_2.validate)
|
||||
|
||||
doc.embedded_field_1 = embedded_doc_1
|
||||
doc.embedded_field_2 = embedded_doc_2
|
||||
self.assertRaises(ValidationError, doc.validate)
|
||||
|
||||
def test_inheritance(self):
|
||||
"""Ensure that dynamic document plays nice with inheritance"""
|
||||
class Employee(self.Person):
|
||||
salary = IntField()
|
||||
|
||||
Employee.drop_collection()
|
||||
|
||||
self.assertIn('name', Employee._fields)
|
||||
self.assertIn('salary', Employee._fields)
|
||||
self.assertEqual(Employee._get_collection_name(),
|
||||
self.Person._get_collection_name())
|
||||
|
||||
joe_bloggs = Employee()
|
||||
joe_bloggs.name = "Joe Bloggs"
|
||||
joe_bloggs.salary = 10
|
||||
joe_bloggs.age = 20
|
||||
joe_bloggs.save()
|
||||
|
||||
self.assertEqual(1, self.Person.objects(age=20).count())
|
||||
self.assertEqual(1, Employee.objects(age=20).count())
|
||||
|
||||
joe_bloggs = self.Person.objects.first()
|
||||
self.assertIsInstance(joe_bloggs, Employee)
|
||||
|
||||
def test_embedded_dynamic_document(self):
|
||||
"""Test dynamic embedded documents"""
|
||||
class Embedded(DynamicEmbeddedDocument):
|
||||
pass
|
||||
|
||||
class Doc(DynamicDocument):
|
||||
pass
|
||||
|
||||
Doc.drop_collection()
|
||||
doc = Doc()
|
||||
|
||||
embedded_1 = Embedded()
|
||||
embedded_1.string_field = 'hello'
|
||||
embedded_1.int_field = 1
|
||||
embedded_1.dict_field = {'hello': 'world'}
|
||||
embedded_1.list_field = ['1', 2, {'hello': 'world'}]
|
||||
doc.embedded_field = embedded_1
|
||||
|
||||
self.assertEqual(doc.to_mongo(), {
|
||||
"embedded_field": {
|
||||
"_cls": "Embedded",
|
||||
"string_field": "hello",
|
||||
"int_field": 1,
|
||||
"dict_field": {"hello": "world"},
|
||||
"list_field": ['1', 2, {'hello': 'world'}]
|
||||
}
|
||||
})
|
||||
doc.save()
|
||||
|
||||
doc = Doc.objects.first()
|
||||
self.assertEqual(doc.embedded_field.__class__, Embedded)
|
||||
self.assertEqual(doc.embedded_field.string_field, "hello")
|
||||
self.assertEqual(doc.embedded_field.int_field, 1)
|
||||
self.assertEqual(doc.embedded_field.dict_field, {'hello': 'world'})
|
||||
self.assertEqual(doc.embedded_field.list_field,
|
||||
['1', 2, {'hello': 'world'}])
|
||||
|
||||
def test_complex_embedded_documents(self):
|
||||
"""Test complex dynamic embedded documents setups"""
|
||||
class Embedded(DynamicEmbeddedDocument):
|
||||
pass
|
||||
|
||||
class Doc(DynamicDocument):
|
||||
pass
|
||||
|
||||
Doc.drop_collection()
|
||||
doc = Doc()
|
||||
|
||||
embedded_1 = Embedded()
|
||||
embedded_1.string_field = 'hello'
|
||||
embedded_1.int_field = 1
|
||||
embedded_1.dict_field = {'hello': 'world'}
|
||||
|
||||
embedded_2 = Embedded()
|
||||
embedded_2.string_field = 'hello'
|
||||
embedded_2.int_field = 1
|
||||
embedded_2.dict_field = {'hello': 'world'}
|
||||
embedded_2.list_field = ['1', 2, {'hello': 'world'}]
|
||||
|
||||
embedded_1.list_field = ['1', 2, embedded_2]
|
||||
doc.embedded_field = embedded_1
|
||||
|
||||
self.assertEqual(doc.to_mongo(), {
|
||||
"embedded_field": {
|
||||
"_cls": "Embedded",
|
||||
"string_field": "hello",
|
||||
"int_field": 1,
|
||||
"dict_field": {"hello": "world"},
|
||||
"list_field": ['1', 2,
|
||||
{"_cls": "Embedded",
|
||||
"string_field": "hello",
|
||||
"int_field": 1,
|
||||
"dict_field": {"hello": "world"},
|
||||
"list_field": ['1', 2, {'hello': 'world'}]}
|
||||
]
|
||||
}
|
||||
})
|
||||
doc.save()
|
||||
doc = Doc.objects.first()
|
||||
self.assertEqual(doc.embedded_field.__class__, Embedded)
|
||||
self.assertEqual(doc.embedded_field.string_field, "hello")
|
||||
self.assertEqual(doc.embedded_field.int_field, 1)
|
||||
self.assertEqual(doc.embedded_field.dict_field, {'hello': 'world'})
|
||||
self.assertEqual(doc.embedded_field.list_field[0], '1')
|
||||
self.assertEqual(doc.embedded_field.list_field[1], 2)
|
||||
|
||||
embedded_field = doc.embedded_field.list_field[2]
|
||||
|
||||
self.assertEqual(embedded_field.__class__, Embedded)
|
||||
self.assertEqual(embedded_field.string_field, "hello")
|
||||
self.assertEqual(embedded_field.int_field, 1)
|
||||
self.assertEqual(embedded_field.dict_field, {'hello': 'world'})
|
||||
self.assertEqual(embedded_field.list_field, ['1', 2,
|
||||
{'hello': 'world'}])
|
||||
|
||||
def test_dynamic_and_embedded(self):
|
||||
"""Ensure embedded documents play nicely"""
|
||||
|
||||
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.address.city = "Lundenne"
|
||||
person.save()
|
||||
|
||||
self.assertEqual(Person.objects.first().address.city, "Lundenne")
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
1025
tests/document/indexes.py
Normal file
1025
tests/document/indexes.py
Normal file
File diff suppressed because it is too large
Load Diff
567
tests/document/inheritance.py
Normal file
567
tests/document/inheritance.py
Normal file
@@ -0,0 +1,567 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
import warnings
|
||||
|
||||
from mongoengine import (BooleanField, Document, EmbeddedDocument,
|
||||
EmbeddedDocumentField, GenericReferenceField,
|
||||
IntField, ReferenceField, StringField, connect)
|
||||
from mongoengine.connection import get_db
|
||||
from tests.fixtures import Base
|
||||
|
||||
__all__ = ('InheritanceTest', )
|
||||
|
||||
|
||||
class InheritanceTest(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_constructor_cls(self):
|
||||
# Ensures _cls is properly set during construction
|
||||
# and when object gets reloaded (prevent regression of #1950)
|
||||
class EmbedData(EmbeddedDocument):
|
||||
data = StringField()
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
class DataDoc(Document):
|
||||
name = StringField()
|
||||
embed = EmbeddedDocumentField(EmbedData)
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
test_doc = DataDoc(name='test', embed=EmbedData(data='data'))
|
||||
assert test_doc._cls == 'DataDoc'
|
||||
assert test_doc.embed._cls == 'EmbedData'
|
||||
test_doc.save()
|
||||
saved_doc = DataDoc.objects.with_id(test_doc.id)
|
||||
assert test_doc._cls == saved_doc._cls
|
||||
assert test_doc.embed._cls == saved_doc.embed._cls
|
||||
test_doc.delete()
|
||||
|
||||
def test_superclasses(self):
|
||||
"""Ensure that the correct list of superclasses is assembled.
|
||||
"""
|
||||
class Animal(Document):
|
||||
meta = {'allow_inheritance': True}
|
||||
class Fish(Animal): pass
|
||||
class Guppy(Fish): pass
|
||||
class Mammal(Animal): pass
|
||||
class Dog(Mammal): pass
|
||||
class Human(Mammal): pass
|
||||
|
||||
self.assertEqual(Animal._superclasses, ())
|
||||
self.assertEqual(Fish._superclasses, ('Animal',))
|
||||
self.assertEqual(Guppy._superclasses, ('Animal', 'Animal.Fish'))
|
||||
self.assertEqual(Mammal._superclasses, ('Animal',))
|
||||
self.assertEqual(Dog._superclasses, ('Animal', 'Animal.Mammal'))
|
||||
self.assertEqual(Human._superclasses, ('Animal', 'Animal.Mammal'))
|
||||
|
||||
def test_external_superclasses(self):
|
||||
"""Ensure that the correct list of super classes is assembled when
|
||||
importing part of the model.
|
||||
"""
|
||||
class Animal(Base): pass
|
||||
class Fish(Animal): pass
|
||||
class Guppy(Fish): pass
|
||||
class Mammal(Animal): pass
|
||||
class Dog(Mammal): pass
|
||||
class Human(Mammal): pass
|
||||
|
||||
self.assertEqual(Animal._superclasses, ('Base', ))
|
||||
self.assertEqual(Fish._superclasses, ('Base', 'Base.Animal',))
|
||||
self.assertEqual(Guppy._superclasses, ('Base', 'Base.Animal',
|
||||
'Base.Animal.Fish'))
|
||||
self.assertEqual(Mammal._superclasses, ('Base', 'Base.Animal',))
|
||||
self.assertEqual(Dog._superclasses, ('Base', 'Base.Animal',
|
||||
'Base.Animal.Mammal'))
|
||||
self.assertEqual(Human._superclasses, ('Base', 'Base.Animal',
|
||||
'Base.Animal.Mammal'))
|
||||
|
||||
def test_subclasses(self):
|
||||
"""Ensure that the correct list of _subclasses (subclasses) is
|
||||
assembled.
|
||||
"""
|
||||
class Animal(Document):
|
||||
meta = {'allow_inheritance': True}
|
||||
class Fish(Animal): pass
|
||||
class Guppy(Fish): pass
|
||||
class Mammal(Animal): pass
|
||||
class Dog(Mammal): pass
|
||||
class Human(Mammal): pass
|
||||
|
||||
self.assertEqual(Animal._subclasses, ('Animal',
|
||||
'Animal.Fish',
|
||||
'Animal.Fish.Guppy',
|
||||
'Animal.Mammal',
|
||||
'Animal.Mammal.Dog',
|
||||
'Animal.Mammal.Human'))
|
||||
self.assertEqual(Fish._subclasses, ('Animal.Fish',
|
||||
'Animal.Fish.Guppy',))
|
||||
self.assertEqual(Guppy._subclasses, ('Animal.Fish.Guppy',))
|
||||
self.assertEqual(Mammal._subclasses, ('Animal.Mammal',
|
||||
'Animal.Mammal.Dog',
|
||||
'Animal.Mammal.Human'))
|
||||
self.assertEqual(Human._subclasses, ('Animal.Mammal.Human',))
|
||||
|
||||
def test_external_subclasses(self):
|
||||
"""Ensure that the correct list of _subclasses (subclasses) is
|
||||
assembled when importing part of the model.
|
||||
"""
|
||||
class Animal(Base): pass
|
||||
class Fish(Animal): pass
|
||||
class Guppy(Fish): pass
|
||||
class Mammal(Animal): pass
|
||||
class Dog(Mammal): pass
|
||||
class Human(Mammal): pass
|
||||
|
||||
self.assertEqual(Animal._subclasses, ('Base.Animal',
|
||||
'Base.Animal.Fish',
|
||||
'Base.Animal.Fish.Guppy',
|
||||
'Base.Animal.Mammal',
|
||||
'Base.Animal.Mammal.Dog',
|
||||
'Base.Animal.Mammal.Human'))
|
||||
self.assertEqual(Fish._subclasses, ('Base.Animal.Fish',
|
||||
'Base.Animal.Fish.Guppy',))
|
||||
self.assertEqual(Guppy._subclasses, ('Base.Animal.Fish.Guppy',))
|
||||
self.assertEqual(Mammal._subclasses, ('Base.Animal.Mammal',
|
||||
'Base.Animal.Mammal.Dog',
|
||||
'Base.Animal.Mammal.Human'))
|
||||
self.assertEqual(Human._subclasses, ('Base.Animal.Mammal.Human',))
|
||||
|
||||
def test_dynamic_declarations(self):
|
||||
"""Test that declaring an extra class updates meta data"""
|
||||
|
||||
class Animal(Document):
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
self.assertEqual(Animal._superclasses, ())
|
||||
self.assertEqual(Animal._subclasses, ('Animal',))
|
||||
|
||||
# Test dynamically adding a class changes the meta data
|
||||
class Fish(Animal):
|
||||
pass
|
||||
|
||||
self.assertEqual(Animal._superclasses, ())
|
||||
self.assertEqual(Animal._subclasses, ('Animal', 'Animal.Fish'))
|
||||
|
||||
self.assertEqual(Fish._superclasses, ('Animal', ))
|
||||
self.assertEqual(Fish._subclasses, ('Animal.Fish',))
|
||||
|
||||
# Test dynamically adding an inherited class changes the meta data
|
||||
class Pike(Fish):
|
||||
pass
|
||||
|
||||
self.assertEqual(Animal._superclasses, ())
|
||||
self.assertEqual(Animal._subclasses, ('Animal', 'Animal.Fish',
|
||||
'Animal.Fish.Pike'))
|
||||
|
||||
self.assertEqual(Fish._superclasses, ('Animal', ))
|
||||
self.assertEqual(Fish._subclasses, ('Animal.Fish', 'Animal.Fish.Pike'))
|
||||
|
||||
self.assertEqual(Pike._superclasses, ('Animal', 'Animal.Fish'))
|
||||
self.assertEqual(Pike._subclasses, ('Animal.Fish.Pike',))
|
||||
|
||||
def test_inheritance_meta_data(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(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
|
||||
"""
|
||||
|
||||
class Animal(Document):
|
||||
meta = {'allow_inheritance': True}
|
||||
class Fish(Animal): pass
|
||||
class Mammal(Animal): pass
|
||||
class Dog(Mammal): pass
|
||||
class Human(Mammal): pass
|
||||
|
||||
Animal.drop_collection()
|
||||
|
||||
Animal().save()
|
||||
Fish().save()
|
||||
Mammal().save()
|
||||
Dog().save()
|
||||
Human().save()
|
||||
|
||||
classes = [obj.__class__ for obj in Animal.objects]
|
||||
self.assertEqual(classes, [Animal, Fish, Mammal, Dog, Human])
|
||||
|
||||
classes = [obj.__class__ for obj in Mammal.objects]
|
||||
self.assertEqual(classes, [Mammal, Dog, Human])
|
||||
|
||||
classes = [obj.__class__ for obj in Human.objects]
|
||||
self.assertEqual(classes, [Human])
|
||||
|
||||
def test_allow_inheritance(self):
|
||||
"""Ensure that inheritance is disabled by default on simple
|
||||
classes and that _cls will not be used.
|
||||
"""
|
||||
class Animal(Document):
|
||||
name = StringField()
|
||||
|
||||
# can't inherit because Animal didn't explicitly allow inheritance
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
class Dog(Animal):
|
||||
pass
|
||||
self.assertIn("Document Animal may not be subclassed", str(cm.exception))
|
||||
|
||||
# Check that _cls etc aren't present on simple documents
|
||||
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.assertNotIn('_cls', obj)
|
||||
|
||||
def test_cant_turn_off_inheritance_on_subclass(self):
|
||||
"""Ensure if inheritance is on in a subclass you cant turn it off.
|
||||
"""
|
||||
class Animal(Document):
|
||||
name = StringField()
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
class Mammal(Animal):
|
||||
meta = {'allow_inheritance': False}
|
||||
self.assertEqual(str(cm.exception), 'Only direct subclasses of Document may set "allow_inheritance" to False')
|
||||
|
||||
def test_allow_inheritance_abstract_document(self):
|
||||
"""Ensure that abstract documents can set inheritance rules and that
|
||||
_cls will not be used.
|
||||
"""
|
||||
class FinalDocument(Document):
|
||||
meta = {'abstract': True,
|
||||
'allow_inheritance': False}
|
||||
|
||||
class Animal(FinalDocument):
|
||||
name = StringField()
|
||||
|
||||
with self.assertRaises(ValueError) as cm:
|
||||
class Mammal(Animal):
|
||||
pass
|
||||
|
||||
# Check that _cls isn't present in simple documents
|
||||
doc = Animal(name='dog')
|
||||
self.assertNotIn('_cls', doc.to_mongo())
|
||||
|
||||
def test_using_abstract_class_in_reference_field(self):
|
||||
# Ensures no regression of #1920
|
||||
class AbstractHuman(Document):
|
||||
meta = {'abstract': True}
|
||||
|
||||
class Dad(AbstractHuman):
|
||||
name = StringField()
|
||||
|
||||
class Home(Document):
|
||||
dad = ReferenceField(AbstractHuman) # Referencing the abstract class
|
||||
address = StringField()
|
||||
|
||||
dad = Dad(name='5').save()
|
||||
Home(dad=dad, address='street').save()
|
||||
|
||||
home = Home.objects.first()
|
||||
home.address = 'garbage'
|
||||
home.save() # Was failing with ValidationError
|
||||
|
||||
def test_abstract_class_referencing_self(self):
|
||||
# Ensures no regression of #1920
|
||||
class Human(Document):
|
||||
meta = {'abstract': True}
|
||||
creator = ReferenceField('self', dbref=True)
|
||||
|
||||
class User(Human):
|
||||
name = StringField()
|
||||
|
||||
user = User(name='John').save()
|
||||
user2 = User(name='Foo', creator=user).save()
|
||||
|
||||
user2 = User.objects.with_id(user2.id)
|
||||
user2.name = 'Bar'
|
||||
user2.save() # Was failing with ValidationError
|
||||
|
||||
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}
|
||||
|
||||
city = City(continent='asia')
|
||||
self.assertEqual(None, city.pk)
|
||||
# TODO: expected error? Shouldn't we create a new error type?
|
||||
with self.assertRaises(KeyError):
|
||||
setattr(city, 'pk', 1)
|
||||
|
||||
def test_allow_inheritance_embedded_document(self):
|
||||
"""Ensure embedded documents respect inheritance."""
|
||||
class Comment(EmbeddedDocument):
|
||||
content = StringField()
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
class SpecialComment(Comment):
|
||||
pass
|
||||
|
||||
doc = Comment(content='test')
|
||||
self.assertNotIn('_cls', doc.to_mongo())
|
||||
|
||||
class Comment(EmbeddedDocument):
|
||||
content = StringField()
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
doc = Comment(content='test')
|
||||
self.assertIn('_cls', doc.to_mongo())
|
||||
|
||||
def test_document_inheritance(self):
|
||||
"""Ensure mutliple inheritance of abstract documents
|
||||
"""
|
||||
class DateCreatedDocument(Document):
|
||||
meta = {
|
||||
'allow_inheritance': True,
|
||||
'abstract': True,
|
||||
}
|
||||
|
||||
class DateUpdatedDocument(Document):
|
||||
meta = {
|
||||
'allow_inheritance': True,
|
||||
'abstract': True,
|
||||
}
|
||||
|
||||
try:
|
||||
class MyDocument(DateCreatedDocument, DateUpdatedDocument):
|
||||
pass
|
||||
except Exception:
|
||||
self.assertTrue(False, "Couldn't create MyDocument class")
|
||||
|
||||
def test_abstract_documents(self):
|
||||
"""Ensure that a document superclass can be marked as abstract
|
||||
thereby not using it as the name for the collection."""
|
||||
|
||||
defaults = {'index_background': True,
|
||||
'index_drop_dups': True,
|
||||
'index_opts': {'hello': 'world'},
|
||||
'allow_inheritance': True,
|
||||
'queryset_class': 'QuerySet',
|
||||
'db_alias': 'myDB',
|
||||
'shard_key': ('hello', 'world')}
|
||||
|
||||
meta_settings = {'abstract': True}
|
||||
meta_settings.update(defaults)
|
||||
|
||||
class Animal(Document):
|
||||
name = StringField()
|
||||
meta = meta_settings
|
||||
|
||||
class Fish(Animal): pass
|
||||
class Guppy(Fish): pass
|
||||
|
||||
class Mammal(Animal):
|
||||
meta = {'abstract': True}
|
||||
class Human(Mammal): pass
|
||||
|
||||
for k, v in defaults.iteritems():
|
||||
for cls in [Animal, Fish, Guppy]:
|
||||
self.assertEqual(cls._meta[k], v)
|
||||
|
||||
self.assertNotIn('collection', Animal._meta)
|
||||
self.assertNotIn('collection', Mammal._meta)
|
||||
|
||||
self.assertEqual(Animal._get_collection_name(), None)
|
||||
self.assertEqual(Mammal._get_collection_name(), None)
|
||||
|
||||
self.assertEqual(Fish._get_collection_name(), 'fish')
|
||||
self.assertEqual(Guppy._get_collection_name(), 'fish')
|
||||
self.assertEqual(Human._get_collection_name(), 'human')
|
||||
|
||||
# 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}
|
||||
|
||||
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'
|
||||
collections
|
||||
"""
|
||||
|
||||
class Drink(Document):
|
||||
name = StringField()
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
class Drinker(Document):
|
||||
drink = GenericReferenceField()
|
||||
|
||||
try:
|
||||
warnings.simplefilter("error")
|
||||
|
||||
class AcloholicDrink(Drink):
|
||||
meta = {'collection': 'booze'}
|
||||
|
||||
except SyntaxWarning:
|
||||
warnings.simplefilter("ignore")
|
||||
|
||||
class AlcoholicDrink(Drink):
|
||||
meta = {'collection': 'booze'}
|
||||
|
||||
else:
|
||||
raise AssertionError("SyntaxWarning should be triggered")
|
||||
|
||||
warnings.resetwarnings()
|
||||
|
||||
Drink.drop_collection()
|
||||
AlcoholicDrink.drop_collection()
|
||||
Drinker.drop_collection()
|
||||
|
||||
red_bull = Drink(name='Red Bull')
|
||||
red_bull.save()
|
||||
|
||||
programmer = Drinker(drink=red_bull)
|
||||
programmer.save()
|
||||
|
||||
beer = AlcoholicDrink(name='Beer')
|
||||
beer.save()
|
||||
real_person = Drinker(drink=beer)
|
||||
real_person.save()
|
||||
|
||||
self.assertEqual(Drinker.objects[0].drink.name, red_bull.name)
|
||||
self.assertEqual(Drinker.objects[1].drink.name, beer.name)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
3375
tests/document/instance.py
Normal file
3375
tests/document/instance.py
Normal file
File diff suppressed because it is too large
Load Diff
112
tests/document/json_serialisation.py
Normal file
112
tests/document/json_serialisation.py
Normal file
@@ -0,0 +1,112 @@
|
||||
import unittest
|
||||
import uuid
|
||||
|
||||
from nose.plugins.skip import SkipTest
|
||||
from datetime import datetime
|
||||
from bson import ObjectId
|
||||
|
||||
import pymongo
|
||||
|
||||
from mongoengine import *
|
||||
|
||||
__all__ = ("TestJson",)
|
||||
|
||||
|
||||
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):
|
||||
string = StringField()
|
||||
|
||||
class Doc(Document):
|
||||
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):
|
||||
|
||||
if pymongo.version_tuple[0] <= 2 and pymongo.version_tuple[1] <= 3:
|
||||
raise SkipTest("Need pymongo 2.4 as has a fix for DBRefs")
|
||||
|
||||
class EmbeddedDoc(EmbeddedDocument):
|
||||
pass
|
||||
|
||||
class Simple(Document):
|
||||
pass
|
||||
|
||||
class Doc(Document):
|
||||
string_field = StringField(default='1')
|
||||
int_field = IntField(default=1)
|
||||
float_field = FloatField(default=1.1)
|
||||
boolean_field = BooleanField(default=True)
|
||||
datetime_field = DateTimeField(default=datetime.now)
|
||||
embedded_document_field = EmbeddedDocumentField(EmbeddedDoc,
|
||||
default=lambda: EmbeddedDoc())
|
||||
list_field = ListField(default=lambda: [1, 2, 3])
|
||||
dict_field = DictField(default=lambda: {"hello": "world"})
|
||||
objectid_field = ObjectIdField(default=ObjectId)
|
||||
reference_field = ReferenceField(Simple, default=lambda:
|
||||
Simple().save())
|
||||
map_field = MapField(IntField(), default=lambda: {"simple": 1})
|
||||
decimal_field = DecimalField(default=1.0)
|
||||
complex_datetime_field = ComplexDateTimeField(default=datetime.now)
|
||||
url_field = URLField(default="http://mongoengine.org")
|
||||
dynamic_field = DynamicField(default=1)
|
||||
generic_reference_field = GenericReferenceField(
|
||||
default=lambda: Simple().save())
|
||||
sorted_list_field = SortedListField(IntField(),
|
||||
default=lambda: [1, 2, 3])
|
||||
email_field = EmailField(default="ross@example.com")
|
||||
geo_point_field = GeoPointField(default=lambda: [1, 2])
|
||||
sequence_field = SequenceField()
|
||||
uuid_field = UUIDField(default=uuid.uuid4)
|
||||
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()))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
214
tests/document/validation.py
Normal file
214
tests/document/validation.py
Normal file
@@ -0,0 +1,214 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
|
||||
from mongoengine import *
|
||||
|
||||
__all__ = ("ValidatorErrorTest",)
|
||||
|
||||
|
||||
class ValidatorErrorTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
connect(db='mongoenginetest')
|
||||
|
||||
def test_to_dict(self):
|
||||
"""Ensure a ValidationError handles error to_dict correctly.
|
||||
"""
|
||||
error = ValidationError('root')
|
||||
self.assertEqual(error.to_dict(), {})
|
||||
|
||||
# 1st level error schema
|
||||
error.errors = {'1st': ValidationError('bad 1st'), }
|
||||
self.assertIn('1st', error.to_dict())
|
||||
self.assertEqual(error.to_dict()['1st'], 'bad 1st')
|
||||
|
||||
# 2nd level error schema
|
||||
error.errors = {'1st': ValidationError('bad 1st', errors={
|
||||
'2nd': ValidationError('bad 2nd'),
|
||||
})}
|
||||
self.assertIn('1st', error.to_dict())
|
||||
self.assertIsInstance(error.to_dict()['1st'], dict)
|
||||
self.assertIn('2nd', error.to_dict()['1st'])
|
||||
self.assertEqual(error.to_dict()['1st']['2nd'], 'bad 2nd')
|
||||
|
||||
# moar levels
|
||||
error.errors = {'1st': ValidationError('bad 1st', errors={
|
||||
'2nd': ValidationError('bad 2nd', errors={
|
||||
'3rd': ValidationError('bad 3rd', errors={
|
||||
'4th': ValidationError('Inception'),
|
||||
}),
|
||||
}),
|
||||
})}
|
||||
self.assertIn('1st', error.to_dict())
|
||||
self.assertIn('2nd', error.to_dict()['1st'])
|
||||
self.assertIn('3rd', error.to_dict()['1st']['2nd'])
|
||||
self.assertIn('4th', error.to_dict()['1st']['2nd']['3rd'])
|
||||
self.assertEqual(error.to_dict()['1st']['2nd']['3rd']['4th'],
|
||||
'Inception')
|
||||
|
||||
self.assertEqual(error.message, "root(2nd.3rd.4th.Inception: ['1st'])")
|
||||
|
||||
def test_model_validation(self):
|
||||
|
||||
class User(Document):
|
||||
username = StringField(primary_key=True)
|
||||
name = StringField(required=True)
|
||||
|
||||
try:
|
||||
User().validate()
|
||||
except ValidationError as e:
|
||||
self.assertIn("User:None", e.message)
|
||||
self.assertEqual(e.to_dict(), {
|
||||
'username': 'Field is required',
|
||||
'name': 'Field is required'})
|
||||
|
||||
user = User(username="RossC0", name="Ross").save()
|
||||
user.name = None
|
||||
try:
|
||||
user.save()
|
||||
except ValidationError as e:
|
||||
self.assertIn("User:RossC0", e.message)
|
||||
self.assertEqual(e.to_dict(), {
|
||||
'name': 'Field is required'})
|
||||
|
||||
def test_fields_rewrite(self):
|
||||
class BasePerson(Document):
|
||||
name = StringField()
|
||||
age = IntField()
|
||||
meta = {'abstract': True}
|
||||
|
||||
class Person(BasePerson):
|
||||
name = StringField(required=True)
|
||||
|
||||
p = Person(age=15)
|
||||
self.assertRaises(ValidationError, p.validate)
|
||||
|
||||
def test_embedded_document_validation(self):
|
||||
"""Ensure that embedded documents may be validated.
|
||||
"""
|
||||
class Comment(EmbeddedDocument):
|
||||
date = DateTimeField()
|
||||
content = StringField(required=True)
|
||||
|
||||
comment = Comment()
|
||||
self.assertRaises(ValidationError, comment.validate)
|
||||
|
||||
comment.content = 'test'
|
||||
comment.validate()
|
||||
|
||||
comment.date = 4
|
||||
self.assertRaises(ValidationError, comment.validate)
|
||||
|
||||
comment.date = datetime.now()
|
||||
comment.validate()
|
||||
self.assertEqual(comment._instance, None)
|
||||
|
||||
def test_embedded_db_field_validate(self):
|
||||
|
||||
class SubDoc(EmbeddedDocument):
|
||||
val = IntField(required=True)
|
||||
|
||||
class Doc(Document):
|
||||
id = StringField(primary_key=True)
|
||||
e = EmbeddedDocumentField(SubDoc, db_field='eb')
|
||||
|
||||
try:
|
||||
Doc(id="bad").validate()
|
||||
except ValidationError as e:
|
||||
self.assertIn("SubDoc:None", e.message)
|
||||
self.assertEqual(e.to_dict(), {
|
||||
"e": {'val': 'OK could not be converted to int'}})
|
||||
|
||||
Doc.drop_collection()
|
||||
|
||||
Doc(id="test", e=SubDoc(val=15)).save()
|
||||
|
||||
doc = Doc.objects.first()
|
||||
keys = doc._data.keys()
|
||||
self.assertEqual(2, len(keys))
|
||||
self.assertIn('e', keys)
|
||||
self.assertIn('id', keys)
|
||||
|
||||
doc.e.val = "OK"
|
||||
try:
|
||||
doc.save()
|
||||
except ValidationError as e:
|
||||
self.assertIn("Doc:test", 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,502 +0,0 @@
|
||||
import unittest
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine.connection import get_db
|
||||
|
||||
|
||||
class DynamicDocTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
connect(db='mongoenginetest')
|
||||
self.db = get_db()
|
||||
|
||||
class Person(DynamicDocument):
|
||||
name = StringField()
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
Person.drop_collection()
|
||||
|
||||
self.Person = Person
|
||||
|
||||
def test_simple_dynamic_document(self):
|
||||
"""Ensures simple dynamic documents are saved correctly"""
|
||||
|
||||
p = self.Person()
|
||||
p.name = "James"
|
||||
p.age = 34
|
||||
|
||||
self.assertEquals(p.to_mongo(),
|
||||
{"_types": ["Person"], "_cls": "Person",
|
||||
"name": "James", "age": 34}
|
||||
)
|
||||
|
||||
p.save()
|
||||
|
||||
self.assertEquals(self.Person.objects.first().age, 34)
|
||||
|
||||
# Confirm no changes to self.Person
|
||||
self.assertFalse(hasattr(self.Person, 'age'))
|
||||
|
||||
def test_dynamic_document_delta(self):
|
||||
"""Ensures simple dynamic documents can delta correctly"""
|
||||
p = self.Person(name="James", age=34)
|
||||
self.assertEquals(p._delta(), ({'_types': ['Person'], 'age': 34, 'name': 'James', '_cls': 'Person'}, {}))
|
||||
|
||||
p.doc = 123
|
||||
del(p.doc)
|
||||
self.assertEquals(p._delta(), ({'_types': ['Person'], 'age': 34, 'name': 'James', '_cls': 'Person'}, {'doc': 1}))
|
||||
|
||||
def test_change_scope_of_variable(self):
|
||||
"""Test changing the scope of a dynamic field has no adverse effects"""
|
||||
p = self.Person()
|
||||
p.name = "Dean"
|
||||
p.misc = 22
|
||||
p.save()
|
||||
|
||||
p = self.Person.objects.get()
|
||||
p.misc = {'hello': 'world'}
|
||||
p.save()
|
||||
|
||||
p = self.Person.objects.get()
|
||||
self.assertEquals(p.misc, {'hello': 'world'})
|
||||
|
||||
def test_delete_dynamic_field(self):
|
||||
"""Test deleting a dynamic field works"""
|
||||
self.Person.drop_collection()
|
||||
p = self.Person()
|
||||
p.name = "Dean"
|
||||
p.misc = 22
|
||||
p.save()
|
||||
|
||||
p = self.Person.objects.get()
|
||||
p.misc = {'hello': 'world'}
|
||||
p.save()
|
||||
|
||||
p = self.Person.objects.get()
|
||||
self.assertEquals(p.misc, {'hello': 'world'})
|
||||
collection = self.db[self.Person._get_collection_name()]
|
||||
obj = collection.find_one()
|
||||
self.assertEquals(sorted(obj.keys()), ['_cls', '_id', '_types', 'misc', 'name'])
|
||||
|
||||
del(p.misc)
|
||||
p.save()
|
||||
|
||||
p = self.Person.objects.get()
|
||||
self.assertFalse(hasattr(p, 'misc'))
|
||||
|
||||
obj = collection.find_one()
|
||||
self.assertEquals(sorted(obj.keys()), ['_cls', '_id', '_types', 'name'])
|
||||
|
||||
def test_dynamic_document_queries(self):
|
||||
"""Ensure we can query dynamic fields"""
|
||||
p = self.Person()
|
||||
p.name = "Dean"
|
||||
p.age = 22
|
||||
p.save()
|
||||
|
||||
self.assertEquals(1, self.Person.objects(age=22).count())
|
||||
p = self.Person.objects(age=22)
|
||||
p = p.get()
|
||||
self.assertEquals(22, p.age)
|
||||
|
||||
def test_complex_dynamic_document_queries(self):
|
||||
class Person(DynamicDocument):
|
||||
name = StringField()
|
||||
|
||||
Person.drop_collection()
|
||||
|
||||
p = Person(name="test")
|
||||
p.age = "ten"
|
||||
p.save()
|
||||
|
||||
p1 = Person(name="test1")
|
||||
p1.age = "less then ten and a half"
|
||||
p1.save()
|
||||
|
||||
p2 = Person(name="test2")
|
||||
p2.age = 10
|
||||
p2.save()
|
||||
|
||||
self.assertEquals(Person.objects(age__icontains='ten').count(), 2)
|
||||
self.assertEquals(Person.objects(age__gte=10).count(), 1)
|
||||
|
||||
def test_complex_data_lookups(self):
|
||||
"""Ensure you can query dynamic document dynamic fields"""
|
||||
p = self.Person()
|
||||
p.misc = {'hello': 'world'}
|
||||
p.save()
|
||||
|
||||
self.assertEquals(1, self.Person.objects(misc__hello='world').count())
|
||||
|
||||
def test_inheritance(self):
|
||||
"""Ensure that dynamic document plays nice with inheritance"""
|
||||
class Employee(self.Person):
|
||||
salary = IntField()
|
||||
|
||||
Employee.drop_collection()
|
||||
|
||||
self.assertTrue('name' in Employee._fields)
|
||||
self.assertTrue('salary' in Employee._fields)
|
||||
self.assertEqual(Employee._get_collection_name(),
|
||||
self.Person._get_collection_name())
|
||||
|
||||
joe_bloggs = Employee()
|
||||
joe_bloggs.name = "Joe Bloggs"
|
||||
joe_bloggs.salary = 10
|
||||
joe_bloggs.age = 20
|
||||
joe_bloggs.save()
|
||||
|
||||
self.assertEquals(1, self.Person.objects(age=20).count())
|
||||
self.assertEquals(1, Employee.objects(age=20).count())
|
||||
|
||||
joe_bloggs = self.Person.objects.first()
|
||||
self.assertTrue(isinstance(joe_bloggs, Employee))
|
||||
|
||||
def test_embedded_dynamic_document(self):
|
||||
"""Test dynamic embedded documents"""
|
||||
class Embedded(DynamicEmbeddedDocument):
|
||||
pass
|
||||
|
||||
class Doc(DynamicDocument):
|
||||
pass
|
||||
|
||||
Doc.drop_collection()
|
||||
doc = Doc()
|
||||
|
||||
embedded_1 = Embedded()
|
||||
embedded_1.string_field = 'hello'
|
||||
embedded_1.int_field = 1
|
||||
embedded_1.dict_field = {'hello': 'world'}
|
||||
embedded_1.list_field = ['1', 2, {'hello': 'world'}]
|
||||
doc.embedded_field = embedded_1
|
||||
|
||||
self.assertEquals(doc.to_mongo(), {"_types": ['Doc'], "_cls": "Doc",
|
||||
"embedded_field": {
|
||||
"_types": ['Embedded'], "_cls": "Embedded",
|
||||
"string_field": "hello",
|
||||
"int_field": 1,
|
||||
"dict_field": {"hello": "world"},
|
||||
"list_field": ['1', 2, {'hello': 'world'}]
|
||||
}
|
||||
})
|
||||
doc.save()
|
||||
|
||||
doc = Doc.objects.first()
|
||||
self.assertEquals(doc.embedded_field.__class__, Embedded)
|
||||
self.assertEquals(doc.embedded_field.string_field, "hello")
|
||||
self.assertEquals(doc.embedded_field.int_field, 1)
|
||||
self.assertEquals(doc.embedded_field.dict_field, {'hello': 'world'})
|
||||
self.assertEquals(doc.embedded_field.list_field, ['1', 2, {'hello': 'world'}])
|
||||
|
||||
def test_complex_embedded_documents(self):
|
||||
"""Test complex dynamic embedded documents setups"""
|
||||
class Embedded(DynamicEmbeddedDocument):
|
||||
pass
|
||||
|
||||
class Doc(DynamicDocument):
|
||||
pass
|
||||
|
||||
Doc.drop_collection()
|
||||
doc = Doc()
|
||||
|
||||
embedded_1 = Embedded()
|
||||
embedded_1.string_field = 'hello'
|
||||
embedded_1.int_field = 1
|
||||
embedded_1.dict_field = {'hello': 'world'}
|
||||
|
||||
embedded_2 = Embedded()
|
||||
embedded_2.string_field = 'hello'
|
||||
embedded_2.int_field = 1
|
||||
embedded_2.dict_field = {'hello': 'world'}
|
||||
embedded_2.list_field = ['1', 2, {'hello': 'world'}]
|
||||
|
||||
embedded_1.list_field = ['1', 2, embedded_2]
|
||||
doc.embedded_field = embedded_1
|
||||
|
||||
self.assertEquals(doc.to_mongo(), {"_types": ['Doc'], "_cls": "Doc",
|
||||
"embedded_field": {
|
||||
"_types": ['Embedded'], "_cls": "Embedded",
|
||||
"string_field": "hello",
|
||||
"int_field": 1,
|
||||
"dict_field": {"hello": "world"},
|
||||
"list_field": ['1', 2,
|
||||
{"_types": ['Embedded'], "_cls": "Embedded",
|
||||
"string_field": "hello",
|
||||
"int_field": 1,
|
||||
"dict_field": {"hello": "world"},
|
||||
"list_field": ['1', 2, {'hello': 'world'}]}
|
||||
]
|
||||
}
|
||||
})
|
||||
doc.save()
|
||||
doc = Doc.objects.first()
|
||||
self.assertEquals(doc.embedded_field.__class__, Embedded)
|
||||
self.assertEquals(doc.embedded_field.string_field, "hello")
|
||||
self.assertEquals(doc.embedded_field.int_field, 1)
|
||||
self.assertEquals(doc.embedded_field.dict_field, {'hello': 'world'})
|
||||
self.assertEquals(doc.embedded_field.list_field[0], '1')
|
||||
self.assertEquals(doc.embedded_field.list_field[1], 2)
|
||||
|
||||
embedded_field = doc.embedded_field.list_field[2]
|
||||
|
||||
self.assertEquals(embedded_field.__class__, Embedded)
|
||||
self.assertEquals(embedded_field.string_field, "hello")
|
||||
self.assertEquals(embedded_field.int_field, 1)
|
||||
self.assertEquals(embedded_field.dict_field, {'hello': 'world'})
|
||||
self.assertEquals(embedded_field.list_field, ['1', 2, {'hello': 'world'}])
|
||||
|
||||
def test_delta_for_dynamic_documents(self):
|
||||
p = self.Person()
|
||||
p.name = "Dean"
|
||||
p.age = 22
|
||||
p.save()
|
||||
|
||||
p.age = 24
|
||||
self.assertEquals(p.age, 24)
|
||||
self.assertEquals(p._get_changed_fields(), ['age'])
|
||||
self.assertEquals(p._delta(), ({'age': 24}, {}))
|
||||
|
||||
p = self.Person.objects(age=22).get()
|
||||
p.age = 24
|
||||
self.assertEquals(p.age, 24)
|
||||
self.assertEquals(p._get_changed_fields(), ['age'])
|
||||
self.assertEquals(p._delta(), ({'age': 24}, {}))
|
||||
|
||||
p.save()
|
||||
self.assertEquals(1, self.Person.objects(age=24).count())
|
||||
|
||||
def test_delta(self):
|
||||
|
||||
class Doc(DynamicDocument):
|
||||
pass
|
||||
|
||||
Doc.drop_collection()
|
||||
doc = Doc()
|
||||
doc.save()
|
||||
|
||||
doc = Doc.objects.first()
|
||||
self.assertEquals(doc._get_changed_fields(), [])
|
||||
self.assertEquals(doc._delta(), ({}, {}))
|
||||
|
||||
doc.string_field = 'hello'
|
||||
self.assertEquals(doc._get_changed_fields(), ['string_field'])
|
||||
self.assertEquals(doc._delta(), ({'string_field': 'hello'}, {}))
|
||||
|
||||
doc._changed_fields = []
|
||||
doc.int_field = 1
|
||||
self.assertEquals(doc._get_changed_fields(), ['int_field'])
|
||||
self.assertEquals(doc._delta(), ({'int_field': 1}, {}))
|
||||
|
||||
doc._changed_fields = []
|
||||
dict_value = {'hello': 'world', 'ping': 'pong'}
|
||||
doc.dict_field = dict_value
|
||||
self.assertEquals(doc._get_changed_fields(), ['dict_field'])
|
||||
self.assertEquals(doc._delta(), ({'dict_field': dict_value}, {}))
|
||||
|
||||
doc._changed_fields = []
|
||||
list_value = ['1', 2, {'hello': 'world'}]
|
||||
doc.list_field = list_value
|
||||
self.assertEquals(doc._get_changed_fields(), ['list_field'])
|
||||
self.assertEquals(doc._delta(), ({'list_field': list_value}, {}))
|
||||
|
||||
# Test unsetting
|
||||
doc._changed_fields = []
|
||||
doc.dict_field = {}
|
||||
self.assertEquals(doc._get_changed_fields(), ['dict_field'])
|
||||
self.assertEquals(doc._delta(), ({}, {'dict_field': 1}))
|
||||
|
||||
doc._changed_fields = []
|
||||
doc.list_field = []
|
||||
self.assertEquals(doc._get_changed_fields(), ['list_field'])
|
||||
self.assertEquals(doc._delta(), ({}, {'list_field': 1}))
|
||||
|
||||
def test_delta_recursive(self):
|
||||
"""Testing deltaing works with dynamic documents"""
|
||||
class Embedded(DynamicEmbeddedDocument):
|
||||
pass
|
||||
|
||||
class Doc(DynamicDocument):
|
||||
pass
|
||||
|
||||
Doc.drop_collection()
|
||||
doc = Doc()
|
||||
doc.save()
|
||||
|
||||
doc = Doc.objects.first()
|
||||
self.assertEquals(doc._get_changed_fields(), [])
|
||||
self.assertEquals(doc._delta(), ({}, {}))
|
||||
|
||||
embedded_1 = Embedded()
|
||||
embedded_1.string_field = 'hello'
|
||||
embedded_1.int_field = 1
|
||||
embedded_1.dict_field = {'hello': 'world'}
|
||||
embedded_1.list_field = ['1', 2, {'hello': 'world'}]
|
||||
doc.embedded_field = embedded_1
|
||||
|
||||
self.assertEquals(doc._get_changed_fields(), ['embedded_field'])
|
||||
|
||||
embedded_delta = {
|
||||
'string_field': 'hello',
|
||||
'int_field': 1,
|
||||
'dict_field': {'hello': 'world'},
|
||||
'list_field': ['1', 2, {'hello': 'world'}]
|
||||
}
|
||||
self.assertEquals(doc.embedded_field._delta(), (embedded_delta, {}))
|
||||
embedded_delta.update({
|
||||
'_types': ['Embedded'],
|
||||
'_cls': 'Embedded',
|
||||
})
|
||||
self.assertEquals(doc._delta(), ({'embedded_field': embedded_delta}, {}))
|
||||
|
||||
doc.save()
|
||||
doc.reload()
|
||||
|
||||
doc.embedded_field.dict_field = {}
|
||||
self.assertEquals(doc._get_changed_fields(), ['embedded_field.dict_field'])
|
||||
self.assertEquals(doc.embedded_field._delta(), ({}, {'dict_field': 1}))
|
||||
|
||||
self.assertEquals(doc._delta(), ({}, {'embedded_field.dict_field': 1}))
|
||||
doc.save()
|
||||
doc.reload()
|
||||
|
||||
doc.embedded_field.list_field = []
|
||||
self.assertEquals(doc._get_changed_fields(), ['embedded_field.list_field'])
|
||||
self.assertEquals(doc.embedded_field._delta(), ({}, {'list_field': 1}))
|
||||
self.assertEquals(doc._delta(), ({}, {'embedded_field.list_field': 1}))
|
||||
doc.save()
|
||||
doc.reload()
|
||||
|
||||
embedded_2 = Embedded()
|
||||
embedded_2.string_field = 'hello'
|
||||
embedded_2.int_field = 1
|
||||
embedded_2.dict_field = {'hello': 'world'}
|
||||
embedded_2.list_field = ['1', 2, {'hello': 'world'}]
|
||||
|
||||
doc.embedded_field.list_field = ['1', 2, embedded_2]
|
||||
self.assertEquals(doc._get_changed_fields(), ['embedded_field.list_field'])
|
||||
self.assertEquals(doc.embedded_field._delta(), ({
|
||||
'list_field': ['1', 2, {
|
||||
'_cls': 'Embedded',
|
||||
'_types': ['Embedded'],
|
||||
'string_field': 'hello',
|
||||
'dict_field': {'hello': 'world'},
|
||||
'int_field': 1,
|
||||
'list_field': ['1', 2, {'hello': 'world'}],
|
||||
}]
|
||||
}, {}))
|
||||
|
||||
self.assertEquals(doc._delta(), ({
|
||||
'embedded_field.list_field': ['1', 2, {
|
||||
'_cls': 'Embedded',
|
||||
'_types': ['Embedded'],
|
||||
'string_field': 'hello',
|
||||
'dict_field': {'hello': 'world'},
|
||||
'int_field': 1,
|
||||
'list_field': ['1', 2, {'hello': 'world'}],
|
||||
}]
|
||||
}, {}))
|
||||
doc.save()
|
||||
doc.reload()
|
||||
|
||||
self.assertEquals(doc.embedded_field.list_field[2]._changed_fields, [])
|
||||
self.assertEquals(doc.embedded_field.list_field[0], '1')
|
||||
self.assertEquals(doc.embedded_field.list_field[1], 2)
|
||||
for k in doc.embedded_field.list_field[2]._fields:
|
||||
self.assertEquals(doc.embedded_field.list_field[2][k], embedded_2[k])
|
||||
|
||||
doc.embedded_field.list_field[2].string_field = 'world'
|
||||
self.assertEquals(doc._get_changed_fields(), ['embedded_field.list_field.2.string_field'])
|
||||
self.assertEquals(doc.embedded_field._delta(), ({'list_field.2.string_field': 'world'}, {}))
|
||||
self.assertEquals(doc._delta(), ({'embedded_field.list_field.2.string_field': 'world'}, {}))
|
||||
doc.save()
|
||||
doc.reload()
|
||||
self.assertEquals(doc.embedded_field.list_field[2].string_field, 'world')
|
||||
|
||||
# Test multiple assignments
|
||||
doc.embedded_field.list_field[2].string_field = 'hello world'
|
||||
doc.embedded_field.list_field[2] = doc.embedded_field.list_field[2]
|
||||
self.assertEquals(doc._get_changed_fields(), ['embedded_field.list_field'])
|
||||
self.assertEquals(doc.embedded_field._delta(), ({
|
||||
'list_field': ['1', 2, {
|
||||
'_types': ['Embedded'],
|
||||
'_cls': 'Embedded',
|
||||
'string_field': 'hello world',
|
||||
'int_field': 1,
|
||||
'list_field': ['1', 2, {'hello': 'world'}],
|
||||
'dict_field': {'hello': 'world'}}]}, {}))
|
||||
self.assertEquals(doc._delta(), ({
|
||||
'embedded_field.list_field': ['1', 2, {
|
||||
'_types': ['Embedded'],
|
||||
'_cls': 'Embedded',
|
||||
'string_field': 'hello world',
|
||||
'int_field': 1,
|
||||
'list_field': ['1', 2, {'hello': 'world'}],
|
||||
'dict_field': {'hello': 'world'}}
|
||||
]}, {}))
|
||||
doc.save()
|
||||
doc.reload()
|
||||
self.assertEquals(doc.embedded_field.list_field[2].string_field, 'hello world')
|
||||
|
||||
# Test list native methods
|
||||
doc.embedded_field.list_field[2].list_field.pop(0)
|
||||
self.assertEquals(doc._delta(), ({'embedded_field.list_field.2.list_field': [2, {'hello': 'world'}]}, {}))
|
||||
doc.save()
|
||||
doc.reload()
|
||||
|
||||
doc.embedded_field.list_field[2].list_field.append(1)
|
||||
self.assertEquals(doc._delta(), ({'embedded_field.list_field.2.list_field': [2, {'hello': 'world'}, 1]}, {}))
|
||||
doc.save()
|
||||
doc.reload()
|
||||
self.assertEquals(doc.embedded_field.list_field[2].list_field, [2, {'hello': 'world'}, 1])
|
||||
|
||||
doc.embedded_field.list_field[2].list_field.sort()
|
||||
doc.save()
|
||||
doc.reload()
|
||||
self.assertEquals(doc.embedded_field.list_field[2].list_field, [1, 2, {'hello': 'world'}])
|
||||
|
||||
del(doc.embedded_field.list_field[2].list_field[2]['hello'])
|
||||
self.assertEquals(doc._delta(), ({'embedded_field.list_field.2.list_field': [1, 2, {}]}, {}))
|
||||
doc.save()
|
||||
doc.reload()
|
||||
|
||||
del(doc.embedded_field.list_field[2].list_field)
|
||||
self.assertEquals(doc._delta(), ({}, {'embedded_field.list_field.2.list_field': 1}))
|
||||
|
||||
doc.save()
|
||||
doc.reload()
|
||||
|
||||
doc.dict_field = {'embedded': embedded_1}
|
||||
doc.save()
|
||||
doc.reload()
|
||||
|
||||
doc.dict_field['embedded'].string_field = 'Hello World'
|
||||
self.assertEquals(doc._get_changed_fields(), ['dict_field.embedded.string_field'])
|
||||
self.assertEquals(doc._delta(), ({'dict_field.embedded.string_field': 'Hello World'}, {}))
|
||||
|
||||
def test_indexes(self):
|
||||
"""Ensure that indexes are used when meta[indexes] is specified.
|
||||
"""
|
||||
class BlogPost(DynamicDocument):
|
||||
meta = {
|
||||
'indexes': [
|
||||
'-date',
|
||||
('category', '-date')
|
||||
],
|
||||
}
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
info = BlogPost.objects._collection.index_information()
|
||||
# _id, '-date', ('cat', 'date')
|
||||
# NB: there is no index on _types by itself, since
|
||||
# the indices on -date and tags will both contain
|
||||
# _types as first element in the key
|
||||
self.assertEqual(len(info), 3)
|
||||
|
||||
# Indexes are lazy so use list() to perform query
|
||||
list(BlogPost.objects)
|
||||
info = BlogPost.objects._collection.index_information()
|
||||
info = [value['key'] for key, value in info.iteritems()]
|
||||
self.assertTrue([('_types', 1), ('category', 1), ('date', -1)]
|
||||
in info)
|
||||
self.assertTrue([('_types', 1), ('date', -1)] in info)
|
||||
2078
tests/fields.py
2078
tests/fields.py
File diff suppressed because it is too large
Load Diff
3
tests/fields/__init__.py
Normal file
3
tests/fields/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .fields import *
|
||||
from .file_tests import *
|
||||
from .geo import *
|
||||
5530
tests/fields/fields.py
Normal file
5530
tests/fields/fields.py
Normal file
File diff suppressed because it is too large
Load Diff
582
tests/fields/file_tests.py
Normal file
582
tests/fields/file_tests.py
Normal file
@@ -0,0 +1,582 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
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 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(MongoDBTestCase):
|
||||
|
||||
def tearDown(self):
|
||||
self.db.drop_collection('fs.files')
|
||||
self.db.drop_collection('fs.chunks')
|
||||
|
||||
def test_file_field_optional(self):
|
||||
# Make sure FileField is optional and not required
|
||||
class DemoFile(Document):
|
||||
the_file = FileField()
|
||||
DemoFile.objects.create()
|
||||
|
||||
def test_file_fields(self):
|
||||
"""Ensure that file fields can be written to and their data retrieved
|
||||
"""
|
||||
|
||||
class PutFile(Document):
|
||||
the_file = FileField()
|
||||
|
||||
PutFile.drop_collection()
|
||||
|
||||
text = six.b('Hello, World!')
|
||||
content_type = 'text/plain'
|
||||
|
||||
putfile = PutFile()
|
||||
putfile.the_file.put(text, content_type=content_type, filename="hello")
|
||||
putfile.save()
|
||||
|
||||
result = PutFile.objects.first()
|
||||
self.assertEqual(putfile, result)
|
||||
self.assertEqual("%s" % result.the_file, "<GridFSProxy: hello (%s)>" % result.the_file.grid_id)
|
||||
self.assertEqual(result.the_file.read(), text)
|
||||
self.assertEqual(result.the_file.content_type, content_type)
|
||||
result.the_file.delete() # Remove file from GridFS
|
||||
PutFile.objects.delete()
|
||||
|
||||
# Ensure file-like objects are stored
|
||||
PutFile.drop_collection()
|
||||
|
||||
putfile = PutFile()
|
||||
putstring = StringIO()
|
||||
putstring.write(text)
|
||||
putstring.seek(0)
|
||||
putfile.the_file.put(putstring, content_type=content_type)
|
||||
putfile.save()
|
||||
|
||||
result = PutFile.objects.first()
|
||||
self.assertEqual(putfile, result)
|
||||
self.assertEqual(result.the_file.read(), text)
|
||||
self.assertEqual(result.the_file.content_type, content_type)
|
||||
result.the_file.delete()
|
||||
|
||||
def test_file_fields_stream(self):
|
||||
"""Ensure that file fields can be written to and their data retrieved
|
||||
"""
|
||||
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.the_file.new_file(content_type=content_type)
|
||||
streamfile.the_file.write(text)
|
||||
streamfile.the_file.write(more_text)
|
||||
streamfile.the_file.close()
|
||||
streamfile.save()
|
||||
|
||||
result = StreamFile.objects.first()
|
||||
self.assertEqual(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_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.assertEqual(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 = six.b('Hello, World!')
|
||||
more_text = six.b('Foo Bar')
|
||||
|
||||
SetFile.drop_collection()
|
||||
|
||||
setfile = SetFile()
|
||||
setfile.the_file = text
|
||||
setfile.save()
|
||||
|
||||
result = SetFile.objects.first()
|
||||
self.assertEqual(setfile, result)
|
||||
self.assertEqual(result.the_file.read(), text)
|
||||
|
||||
# Try replacing file with new one
|
||||
result.the_file.replace(more_text)
|
||||
result.save()
|
||||
|
||||
result = SetFile.objects.first()
|
||||
self.assertEqual(setfile, result)
|
||||
self.assertEqual(result.the_file.read(), more_text)
|
||||
result.the_file.delete()
|
||||
|
||||
def test_file_field_no_default(self):
|
||||
|
||||
class GridDocument(Document):
|
||||
the_file = FileField()
|
||||
|
||||
GridDocument.drop_collection()
|
||||
|
||||
with tempfile.TemporaryFile() as f:
|
||||
f.write(six.b("Hello World!"))
|
||||
f.flush()
|
||||
|
||||
# Test without default
|
||||
doc_a = GridDocument()
|
||||
doc_a.save()
|
||||
|
||||
doc_b = GridDocument.objects.with_id(doc_a.id)
|
||||
doc_b.the_file.replace(f, filename='doc_b')
|
||||
doc_b.save()
|
||||
self.assertNotEqual(doc_b.the_file.grid_id, None)
|
||||
|
||||
# Test it matches
|
||||
doc_c = GridDocument.objects.with_id(doc_b.id)
|
||||
self.assertEqual(doc_b.the_file.grid_id, doc_c.the_file.grid_id)
|
||||
|
||||
# Test with default
|
||||
doc_d = GridDocument(the_file=six.b(''))
|
||||
doc_d.save()
|
||||
|
||||
doc_e = GridDocument.objects.with_id(doc_d.id)
|
||||
self.assertEqual(doc_d.the_file.grid_id, doc_e.the_file.grid_id)
|
||||
|
||||
doc_e.the_file.replace(f, filename='doc_e')
|
||||
doc_e.save()
|
||||
|
||||
doc_f = GridDocument.objects.with_id(doc_e.id)
|
||||
self.assertEqual(doc_e.the_file.grid_id, doc_f.the_file.grid_id)
|
||||
|
||||
db = GridDocument._get_db()
|
||||
grid_fs = gridfs.GridFS(db)
|
||||
self.assertEqual(['doc_b', 'doc_e'], grid_fs.list())
|
||||
|
||||
def test_file_uniqueness(self):
|
||||
"""Ensure that each instance of a FileField is unique
|
||||
"""
|
||||
class TestFile(Document):
|
||||
name = StringField()
|
||||
the_file = FileField()
|
||||
|
||||
# First instance
|
||||
test_file = TestFile()
|
||||
test_file.name = "Hello, World!"
|
||||
test_file.the_file.put(six.b('Hello, World!'))
|
||||
test_file.save()
|
||||
|
||||
# Second instance
|
||||
test_file_dupe = TestFile()
|
||||
data = test_file_dupe.the_file.read() # Should be None
|
||||
|
||||
self.assertNotEqual(test_file.name, test_file_dupe.name)
|
||||
self.assertNotEqual(test_file.the_file.read(), data)
|
||||
|
||||
TestFile.drop_collection()
|
||||
|
||||
def test_file_saving(self):
|
||||
"""Ensure you can add meta data to file"""
|
||||
|
||||
class Animal(Document):
|
||||
genus = StringField()
|
||||
family = StringField()
|
||||
photo = FileField()
|
||||
|
||||
Animal.drop_collection()
|
||||
marmot = Animal(genus='Marmota', family='Sciuridae')
|
||||
|
||||
marmot_photo = open(TEST_IMAGE_PATH, 'rb') # Retrieve a photo from disk
|
||||
marmot.photo.put(marmot_photo, content_type='image/jpeg', foo='bar')
|
||||
marmot.photo.close()
|
||||
marmot.save()
|
||||
|
||||
marmot = Animal.objects.get()
|
||||
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
|
||||
"""
|
||||
class TestFile(Document):
|
||||
the_file = FileField()
|
||||
TestFile.drop_collection()
|
||||
|
||||
test_file = TestFile()
|
||||
self.assertFalse(bool(test_file.the_file))
|
||||
test_file.the_file.put(six.b('Hello, World!'), content_type='text/plain')
|
||||
test_file.save()
|
||||
self.assertTrue(bool(test_file.the_file))
|
||||
|
||||
test_file = TestFile.objects.first()
|
||||
self.assertEqual(test_file.the_file.content_type, "text/plain")
|
||||
|
||||
def test_file_cmp(self):
|
||||
"""Test comparing against other types"""
|
||||
class TestFile(Document):
|
||||
the_file = FileField()
|
||||
|
||||
test_file = TestFile()
|
||||
self.assertNotIn(test_file.the_file, [{"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 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()
|
||||
|
||||
t = TestImage.objects.first()
|
||||
|
||||
self.assertEqual(t.image.format, 'PNG')
|
||||
|
||||
w, h = t.image.size
|
||||
self.assertEqual(w, 371)
|
||||
self.assertEqual(h, 76)
|
||||
|
||||
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 not HAS_PIL:
|
||||
raise SkipTest('PIL not installed')
|
||||
|
||||
class TestImage(Document):
|
||||
image = ImageField(size=(185, 37))
|
||||
|
||||
TestImage.drop_collection()
|
||||
|
||||
t = TestImage()
|
||||
t.image.put(open(TEST_IMAGE_PATH, 'rb'))
|
||||
t.save()
|
||||
|
||||
t = TestImage.objects.first()
|
||||
|
||||
self.assertEqual(t.image.format, 'PNG')
|
||||
w, h = t.image.size
|
||||
|
||||
self.assertEqual(w, 185)
|
||||
self.assertEqual(h, 37)
|
||||
|
||||
t.image.delete()
|
||||
|
||||
def test_image_field_resize_force(self):
|
||||
if not HAS_PIL:
|
||||
raise SkipTest('PIL not installed')
|
||||
|
||||
class TestImage(Document):
|
||||
image = ImageField(size=(185, 37, True))
|
||||
|
||||
TestImage.drop_collection()
|
||||
|
||||
t = TestImage()
|
||||
t.image.put(open(TEST_IMAGE_PATH, 'rb'))
|
||||
t.save()
|
||||
|
||||
t = TestImage.objects.first()
|
||||
|
||||
self.assertEqual(t.image.format, 'PNG')
|
||||
w, h = t.image.size
|
||||
|
||||
self.assertEqual(w, 185)
|
||||
self.assertEqual(h, 37)
|
||||
|
||||
t.image.delete()
|
||||
|
||||
def test_image_field_thumbnail(self):
|
||||
if not HAS_PIL:
|
||||
raise SkipTest('PIL not installed')
|
||||
|
||||
class TestImage(Document):
|
||||
image = ImageField(thumbnail_size=(92, 18))
|
||||
|
||||
TestImage.drop_collection()
|
||||
|
||||
t = TestImage()
|
||||
t.image.put(open(TEST_IMAGE_PATH, 'rb'))
|
||||
t.save()
|
||||
|
||||
t = TestImage.objects.first()
|
||||
|
||||
self.assertEqual(t.image.thumbnail.format, 'PNG')
|
||||
self.assertEqual(t.image.thumbnail.width, 92)
|
||||
self.assertEqual(t.image.thumbnail.height, 18)
|
||||
|
||||
t.image.delete()
|
||||
|
||||
def test_file_multidb(self):
|
||||
register_connection('test_files', 'test_files')
|
||||
|
||||
class TestFile(Document):
|
||||
name = StringField()
|
||||
the_file = FileField(db_alias="test_files",
|
||||
collection_name="macumba")
|
||||
|
||||
TestFile.drop_collection()
|
||||
|
||||
# delete old filesystem
|
||||
get_db("test_files").macumba.files.drop()
|
||||
get_db("test_files").macumba.chunks.drop()
|
||||
|
||||
# First instance
|
||||
test_file = TestFile()
|
||||
test_file.name = "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(),
|
||||
six.b('HELLO, WORLD!'))
|
||||
|
||||
def test_copyable(self):
|
||||
class PutFile(Document):
|
||||
the_file = FileField()
|
||||
|
||||
PutFile.drop_collection()
|
||||
|
||||
text = six.b('Hello, World!')
|
||||
content_type = 'text/plain'
|
||||
|
||||
putfile = PutFile()
|
||||
putfile.the_file.put(text, content_type=content_type)
|
||||
putfile.save()
|
||||
|
||||
class TestFile(Document):
|
||||
name = StringField()
|
||||
|
||||
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.assertIn({'fields': [('line', '2dsphere')]}, geo_indicies)
|
||||
self.assertIn({'fields': [('polygon', '2dsphere')]}, geo_indicies)
|
||||
self.assertIn({'fields': [('point', '2dsphere')]}, 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.assertIn({'fields': [('venue.line', '2dsphere')]}, geo_indicies)
|
||||
self.assertIn({'fields': [('venue.polygon', '2dsphere')]}, geo_indicies)
|
||||
self.assertIn({'fields': [('venue.point', '2dsphere')]}, 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.assertNotIn('location_2d', info)
|
||||
info = Location._get_collection().index_information()
|
||||
self.assertIn('location_2d', 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 |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 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()
|
||||
|
||||
|
||||
3271
tests/queryset.py
3271
tests/queryset.py
File diff suppressed because it is too large
Load Diff
6
tests/queryset/__init__.py
Normal file
6
tests/queryset/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .transform import *
|
||||
from .field_list import *
|
||||
from .queryset import *
|
||||
from .visitor import *
|
||||
from .geo import *
|
||||
from .modify import *
|
||||
440
tests/queryset/field_list.py
Normal file
440
tests/queryset/field_list.py
Normal file
@@ -0,0 +1,440 @@
|
||||
import unittest
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine.queryset import QueryFieldList
|
||||
|
||||
__all__ = ("QueryFieldListTest", "OnlyExcludeAllTest")
|
||||
|
||||
|
||||
class QueryFieldListTest(unittest.TestCase):
|
||||
|
||||
def test_empty(self):
|
||||
q = QueryFieldList()
|
||||
self.assertFalse(q)
|
||||
|
||||
q = QueryFieldList(always_include=['_cls'])
|
||||
self.assertFalse(q)
|
||||
|
||||
def test_include_include(self):
|
||||
q = QueryFieldList()
|
||||
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(), {'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': 1, 'b': 1})
|
||||
q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.EXCLUDE)
|
||||
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': 0, 'b': 0})
|
||||
q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.EXCLUDE)
|
||||
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': 0, 'b': 0})
|
||||
q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.ONLY)
|
||||
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': 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': 1, 'y': 1, 'c': 1})
|
||||
q.reset()
|
||||
self.assertFalse(q)
|
||||
q += QueryFieldList(fields=['b', 'c'], value=QueryFieldList.ONLY)
|
||||
self.assertEqual(q.as_dict(), {'x': 1, 'y': 1, 'b': 1, 'c': 1})
|
||||
|
||||
def test_using_a_slice(self):
|
||||
q = QueryFieldList()
|
||||
q += QueryFieldList(fields=['a'], value={"$slice": 5})
|
||||
self.assertEqual(q.as_dict(), {'a': {"$slice": 5}})
|
||||
|
||||
|
||||
class OnlyExcludeAllTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
connect(db='mongoenginetest')
|
||||
|
||||
class Person(Document):
|
||||
name = StringField()
|
||||
age = IntField()
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
Person.drop_collection()
|
||||
self.Person = Person
|
||||
|
||||
def test_mixing_only_exclude(self):
|
||||
|
||||
class MyDoc(Document):
|
||||
a = StringField()
|
||||
b = StringField()
|
||||
c = StringField()
|
||||
d = StringField()
|
||||
e = StringField()
|
||||
f = StringField()
|
||||
|
||||
include = ['a', 'b', 'c', 'd', 'e']
|
||||
exclude = ['d', 'e']
|
||||
only = ['b', 'c']
|
||||
|
||||
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})
|
||||
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(**{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(**{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})
|
||||
|
||||
def test_slicing(self):
|
||||
|
||||
class MyDoc(Document):
|
||||
a = ListField()
|
||||
b = ListField()
|
||||
c = ListField()
|
||||
d = ListField()
|
||||
e = ListField()
|
||||
f = ListField()
|
||||
|
||||
include = ['a', 'b', 'c', 'd', 'e']
|
||||
exclude = ['d', 'e']
|
||||
only = ['b', 'c']
|
||||
|
||||
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})
|
||||
|
||||
qs = qs.fields(slice__c=[5, 1])
|
||||
self.assertEqual(qs._loaded_fields.as_dict(),
|
||||
{'b': {'$slice': 5}, 'c': {'$slice': [5, 1]}})
|
||||
|
||||
qs = qs.exclude('c')
|
||||
self.assertEqual(qs._loaded_fields.as_dict(),
|
||||
{'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.
|
||||
"""
|
||||
person = self.Person(name='test', age=25)
|
||||
person.save()
|
||||
|
||||
obj = self.Person.objects.only('name').get()
|
||||
self.assertEqual(obj.name, person.name)
|
||||
self.assertEqual(obj.age, None)
|
||||
|
||||
obj = self.Person.objects.only('age').get()
|
||||
self.assertEqual(obj.name, None)
|
||||
self.assertEqual(obj.age, person.age)
|
||||
|
||||
obj = self.Person.objects.only('name', 'age').get()
|
||||
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')
|
||||
|
||||
employee = Employee(name='test employee', age=40, salary=30000)
|
||||
employee.save()
|
||||
|
||||
obj = self.Person.objects(id=employee.id).only('age').get()
|
||||
self.assertIsInstance(obj, Employee)
|
||||
|
||||
# Check field names are looked up properly
|
||||
obj = Employee.objects(id=employee.id).only('salary').get()
|
||||
self.assertEqual(obj.salary, employee.salary)
|
||||
self.assertEqual(obj.name, None)
|
||||
|
||||
def test_only_with_subfields(self):
|
||||
class User(EmbeddedDocument):
|
||||
name = StringField()
|
||||
email = StringField()
|
||||
|
||||
class Comment(EmbeddedDocument):
|
||||
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...', 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()
|
||||
|
||||
obj = BlogPost.objects.only('author.name',).get()
|
||||
self.assertEqual(obj.content, None)
|
||||
self.assertEqual(obj.author.email, None)
|
||||
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)
|
||||
self.assertEqual(obj.comments[0].title, 'I aggree')
|
||||
self.assertEqual(obj.comments[1].title, 'Coffee')
|
||||
self.assertEqual(obj.comments[0].text, None)
|
||||
self.assertEqual(obj.comments[1].text, None)
|
||||
|
||||
obj = BlogPost.objects.only('comments',).get()
|
||||
self.assertEqual(obj.content, None)
|
||||
self.assertEqual(obj.author, None)
|
||||
self.assertEqual(obj.comments[0].title, 'I aggree')
|
||||
self.assertEqual(obj.comments[1].title, 'Coffee')
|
||||
self.assertEqual(obj.comments[0].text, 'Great post!')
|
||||
self.assertEqual(obj.comments[1].text, 'I hate coffee')
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
def test_exclude(self):
|
||||
class User(EmbeddedDocument):
|
||||
name = StringField()
|
||||
email = StringField()
|
||||
|
||||
class Comment(EmbeddedDocument):
|
||||
title = StringField()
|
||||
text = StringField()
|
||||
|
||||
class BlogPost(Document):
|
||||
content = StringField()
|
||||
author = EmbeddedDocumentField(User)
|
||||
comments = ListField(EmbeddedDocumentField(Comment))
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
post = BlogPost(content='Had a good coffee today...')
|
||||
post.author = User(name='Test User')
|
||||
post.comments = [Comment(title='I aggree', text='Great post!'), Comment(title='Coffee', text='I hate coffee')]
|
||||
post.save()
|
||||
|
||||
obj = BlogPost.objects.exclude('author', 'comments.text').get()
|
||||
self.assertEqual(obj.author, None)
|
||||
self.assertEqual(obj.content, 'Had a good coffee today...')
|
||||
self.assertEqual(obj.comments[0].title, 'I aggree')
|
||||
self.assertEqual(obj.comments[0].text, None)
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
def test_exclude_only_combining(self):
|
||||
class Attachment(EmbeddedDocument):
|
||||
name = StringField()
|
||||
content = StringField()
|
||||
|
||||
class Email(Document):
|
||||
sender = StringField()
|
||||
to = StringField()
|
||||
subject = StringField()
|
||||
body = StringField()
|
||||
content_type = StringField()
|
||||
attachments = ListField(EmbeddedDocumentField(Attachment))
|
||||
|
||||
Email.drop_collection()
|
||||
email = Email(sender='me', to='you', subject='From Russia with Love', body='Hello!', content_type='text/plain')
|
||||
email.attachments = [
|
||||
Attachment(name='file1.doc', content='ABC'),
|
||||
Attachment(name='file2.doc', content='XYZ'),
|
||||
]
|
||||
email.save()
|
||||
|
||||
obj = Email.objects.exclude('content_type').exclude('body').get()
|
||||
self.assertEqual(obj.sender, 'me')
|
||||
self.assertEqual(obj.to, 'you')
|
||||
self.assertEqual(obj.subject, 'From Russia with Love')
|
||||
self.assertEqual(obj.body, None)
|
||||
self.assertEqual(obj.content_type, None)
|
||||
|
||||
obj = Email.objects.only('sender', 'to').exclude('body', 'sender').get()
|
||||
self.assertEqual(obj.sender, None)
|
||||
self.assertEqual(obj.to, 'you')
|
||||
self.assertEqual(obj.subject, None)
|
||||
self.assertEqual(obj.body, None)
|
||||
self.assertEqual(obj.content_type, None)
|
||||
|
||||
obj = Email.objects.exclude('attachments.content').exclude('body').only('to', 'attachments.name').get()
|
||||
self.assertEqual(obj.attachments[0].name, 'file1.doc')
|
||||
self.assertEqual(obj.attachments[0].content, None)
|
||||
self.assertEqual(obj.sender, None)
|
||||
self.assertEqual(obj.to, 'you')
|
||||
self.assertEqual(obj.subject, None)
|
||||
self.assertEqual(obj.body, None)
|
||||
self.assertEqual(obj.content_type, None)
|
||||
|
||||
Email.drop_collection()
|
||||
|
||||
def test_all_fields(self):
|
||||
|
||||
class Email(Document):
|
||||
sender = StringField()
|
||||
to = StringField()
|
||||
subject = StringField()
|
||||
body = StringField()
|
||||
content_type = StringField()
|
||||
|
||||
Email.drop_collection()
|
||||
|
||||
email = Email(sender='me', to='you', subject='From Russia with Love', body='Hello!', content_type='text/plain')
|
||||
email.save()
|
||||
|
||||
obj = Email.objects.exclude('content_type', 'body').only('to', 'body').all_fields().get()
|
||||
self.assertEqual(obj.sender, 'me')
|
||||
self.assertEqual(obj.to, 'you')
|
||||
self.assertEqual(obj.subject, 'From Russia with Love')
|
||||
self.assertEqual(obj.body, 'Hello!')
|
||||
self.assertEqual(obj.content_type, 'text/plain')
|
||||
|
||||
Email.drop_collection()
|
||||
|
||||
def test_slicing_fields(self):
|
||||
"""Ensure that query slicing an array works.
|
||||
"""
|
||||
class Numbers(Document):
|
||||
n = ListField(IntField())
|
||||
|
||||
Numbers.drop_collection()
|
||||
|
||||
numbers = Numbers(n=[0, 1, 2, 3, 4, 5, -5, -4, -3, -2, -1])
|
||||
numbers.save()
|
||||
|
||||
# first three
|
||||
numbers = Numbers.objects.fields(slice__n=3).get()
|
||||
self.assertEqual(numbers.n, [0, 1, 2])
|
||||
|
||||
# last three
|
||||
numbers = Numbers.objects.fields(slice__n=-3).get()
|
||||
self.assertEqual(numbers.n, [-3, -2, -1])
|
||||
|
||||
# skip 2, limit 3
|
||||
numbers = Numbers.objects.fields(slice__n=[2, 3]).get()
|
||||
self.assertEqual(numbers.n, [2, 3, 4])
|
||||
|
||||
# skip to fifth from last, limit 4
|
||||
numbers = Numbers.objects.fields(slice__n=[-5, 4]).get()
|
||||
self.assertEqual(numbers.n, [-5, -4, -3, -2])
|
||||
|
||||
# skip to fifth from last, limit 10
|
||||
numbers = Numbers.objects.fields(slice__n=[-5, 10]).get()
|
||||
self.assertEqual(numbers.n, [-5, -4, -3, -2, -1])
|
||||
|
||||
# skip to fifth from last, limit 10 dict method
|
||||
numbers = Numbers.objects.fields(n={"$slice": [-5, 10]}).get()
|
||||
self.assertEqual(numbers.n, [-5, -4, -3, -2, -1])
|
||||
|
||||
def test_slicing_nested_fields(self):
|
||||
"""Ensure that query slicing an embedded array works.
|
||||
"""
|
||||
|
||||
class EmbeddedNumber(EmbeddedDocument):
|
||||
n = ListField(IntField())
|
||||
|
||||
class Numbers(Document):
|
||||
embedded = EmbeddedDocumentField(EmbeddedNumber)
|
||||
|
||||
Numbers.drop_collection()
|
||||
|
||||
numbers = Numbers()
|
||||
numbers.embedded = EmbeddedNumber(n=[0, 1, 2, 3, 4, 5, -5, -4, -3, -2, -1])
|
||||
numbers.save()
|
||||
|
||||
# first three
|
||||
numbers = Numbers.objects.fields(slice__embedded__n=3).get()
|
||||
self.assertEqual(numbers.embedded.n, [0, 1, 2])
|
||||
|
||||
# last three
|
||||
numbers = Numbers.objects.fields(slice__embedded__n=-3).get()
|
||||
self.assertEqual(numbers.embedded.n, [-3, -2, -1])
|
||||
|
||||
# skip 2, limit 3
|
||||
numbers = Numbers.objects.fields(slice__embedded__n=[2, 3]).get()
|
||||
self.assertEqual(numbers.embedded.n, [2, 3, 4])
|
||||
|
||||
# skip to fifth from last, limit 4
|
||||
numbers = Numbers.objects.fields(slice__embedded__n=[-5, 4]).get()
|
||||
self.assertEqual(numbers.embedded.n, [-5, -4, -3, -2])
|
||||
|
||||
# skip to fifth from last, limit 10
|
||||
numbers = Numbers.objects.fields(slice__embedded__n=[-5, 10]).get()
|
||||
self.assertEqual(numbers.embedded.n, [-5, -4, -3, -2, -1])
|
||||
|
||||
# skip to fifth from last, limit 10 dict method
|
||||
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, requires_mongodb_gte_3
|
||||
|
||||
|
||||
__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
|
||||
@requires_mongodb_gte_3
|
||||
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.assertNotIn(event2, events)
|
||||
self.assertIn(event1, events)
|
||||
self.assertIn(event3, 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
|
||||
@requires_mongodb_gte_3
|
||||
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.assertNotIn(event2, events)
|
||||
self.assertIn(event1, events)
|
||||
self.assertIn(event3, 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
|
||||
@requires_mongodb_gte_3
|
||||
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 requires_mongodb_gte_26
|
||||
|
||||
__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}])
|
||||
|
||||
@requires_mongodb_gte_26
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
5421
tests/queryset/queryset.py
Normal file
5421
tests/queryset/queryset.py
Normal file
File diff suppressed because it is too large
Load Diff
288
tests/queryset/transform.py
Normal file
288
tests/queryset/transform.py
Normal file
@@ -0,0 +1,288 @@
|
||||
import unittest
|
||||
|
||||
from bson.son import SON
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine.queryset import Q, transform
|
||||
|
||||
__all__ = ("TransformTest",)
|
||||
|
||||
|
||||
class TransformTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
connect(db='mongoenginetest')
|
||||
|
||||
def test_transform_query(self):
|
||||
"""Ensure that the _transform_query function operates correctly.
|
||||
"""
|
||||
self.assertEqual(transform.query(name='test', age=30),
|
||||
{'name': 'test', 'age': 30})
|
||||
self.assertEqual(transform.query(age__lt=30),
|
||||
{'age': {'$lt': 30}})
|
||||
self.assertEqual(transform.query(age__gt=20, age__lt=50),
|
||||
{'age': {'$gt': 20, '$lt': 50}})
|
||||
self.assertEqual(transform.query(age=20, age__gt=50),
|
||||
{'$and': [{'age': {'$gt': 50}}, {'age': 20}]})
|
||||
self.assertEqual(transform.query(friend__age__gte=30),
|
||||
{'friend.age': {'$gte': 30}})
|
||||
self.assertEqual(transform.query(name__exists=True),
|
||||
{'name': {'$exists': True}})
|
||||
|
||||
def test_transform_update(self):
|
||||
class LisDoc(Document):
|
||||
foo = ListField(StringField())
|
||||
|
||||
class DicDoc(Document):
|
||||
dictField = DictField()
|
||||
|
||||
class Doc(Document):
|
||||
pass
|
||||
|
||||
LisDoc.drop_collection()
|
||||
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.assertIsInstance(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.assertIsInstance(update["$pull"]["dictField"]["test"], dict)
|
||||
|
||||
update = transform.update(LisDoc, pull__foo__in=['a'])
|
||||
self.assertEqual(update, {'$pull': {'foo': {'$in': ['a']}}})
|
||||
|
||||
def test_transform_update_push(self):
|
||||
"""Ensure the differences in behvaior between 'push' and 'push_all'"""
|
||||
class BlogPost(Document):
|
||||
tags = ListField(StringField())
|
||||
|
||||
update = transform.update(BlogPost, push__tags=['mongo', 'db'])
|
||||
self.assertEqual(update, {'$push': {'tags': ['mongo', 'db']}})
|
||||
|
||||
update = transform.update(BlogPost, push_all__tags=['mongo', 'db'])
|
||||
self.assertEqual(update, {'$push': {'tags': {'$each': ['mongo', 'db']}}})
|
||||
|
||||
def test_query_field_name(self):
|
||||
"""Ensure that the correct field name is used when querying.
|
||||
"""
|
||||
class Comment(EmbeddedDocument):
|
||||
content = StringField(db_field='commentContent')
|
||||
|
||||
class BlogPost(Document):
|
||||
title = StringField(db_field='postTitle')
|
||||
comments = ListField(EmbeddedDocumentField(Comment),
|
||||
db_field='postComments')
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
data = {'title': 'Post 1', 'comments': [Comment(content='test')]}
|
||||
post = BlogPost(**data)
|
||||
post.save()
|
||||
|
||||
self.assertIn('postTitle', BlogPost.objects(title=data['title'])._query)
|
||||
self.assertFalse('title' in
|
||||
BlogPost.objects(title=data['title'])._query)
|
||||
self.assertEqual(BlogPost.objects(title=data['title']).count(), 1)
|
||||
|
||||
self.assertIn('_id', BlogPost.objects(pk=post.id)._query)
|
||||
self.assertEqual(BlogPost.objects(pk=post.id).count(), 1)
|
||||
|
||||
self.assertIn('postComments.commentContent', BlogPost.objects(comments__content='test')._query)
|
||||
self.assertEqual(BlogPost.objects(comments__content='test').count(), 1)
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
def test_query_pk_field_name(self):
|
||||
"""Ensure that the correct "primary key" field name is used when
|
||||
querying
|
||||
"""
|
||||
class BlogPost(Document):
|
||||
title = StringField(primary_key=True, db_field='postTitle')
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
data = {'title': 'Post 1'}
|
||||
post = BlogPost(**data)
|
||||
post.save()
|
||||
|
||||
self.assertIn('_id', BlogPost.objects(pk=data['title'])._query)
|
||||
self.assertIn('_id', BlogPost.objects(title=data['title'])._query)
|
||||
self.assertEqual(BlogPost.objects(pk=data['title']).count(), 1)
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
def test_chaining(self):
|
||||
class A(Document):
|
||||
pass
|
||||
|
||||
class B(Document):
|
||||
a = ReferenceField(A)
|
||||
|
||||
A.drop_collection()
|
||||
B.drop_collection()
|
||||
|
||||
a1 = A().save()
|
||||
a2 = A().save()
|
||||
|
||||
B(a=a1).save()
|
||||
|
||||
# Works
|
||||
q1 = B.objects.filter(a__in=[a1, a2], a=a1)._query
|
||||
|
||||
# Doesn't work
|
||||
q2 = B.objects.filter(a__in=[a1, a2])
|
||||
q2 = q2.filter(a=a1)._query
|
||||
|
||||
self.assertEqual(q1, q2)
|
||||
|
||||
def test_raw_query_and_Q_objects(self):
|
||||
"""
|
||||
Test raw plays nicely
|
||||
"""
|
||||
class Foo(Document):
|
||||
name = StringField()
|
||||
a = StringField()
|
||||
b = StringField()
|
||||
c = StringField()
|
||||
|
||||
meta = {
|
||||
'allow_inheritance': False
|
||||
}
|
||||
|
||||
query = Foo.objects(__raw__={'$nor': [{'name': 'bar'}]})._query
|
||||
self.assertEqual(query, {'$nor': [{'name': 'bar'}]})
|
||||
|
||||
q1 = {'$or': [{'a': 1}, {'b': 1}]}
|
||||
query = Foo.objects(Q(__raw__=q1) & Q(c=1))._query
|
||||
self.assertEqual(query, {'$or': [{'a': 1}, {'b': 1}], 'c': 1})
|
||||
|
||||
def test_raw_and_merging(self):
|
||||
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
|
||||
|
||||
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()
|
||||
|
||||
def test_update_pull_for_list_fields(self):
|
||||
"""
|
||||
Test added to check pull operation in update for
|
||||
EmbeddedDocumentListField which is inside a EmbeddedDocumentField
|
||||
"""
|
||||
class Word(EmbeddedDocument):
|
||||
word = StringField()
|
||||
index = IntField()
|
||||
|
||||
class SubDoc(EmbeddedDocument):
|
||||
heading = ListField(StringField())
|
||||
text = EmbeddedDocumentListField(Word)
|
||||
|
||||
class MainDoc(Document):
|
||||
title = StringField()
|
||||
content = EmbeddedDocumentField(SubDoc)
|
||||
|
||||
word = Word(word='abc', index=1)
|
||||
update = transform.update(MainDoc, pull__content__text=word)
|
||||
self.assertEqual(update, {'$pull': {'content.text': SON([('word', u'abc'), ('index', 1)])}})
|
||||
|
||||
update = transform.update(MainDoc, pull__content__heading='xyz')
|
||||
self.assertEqual(update, {'$pull': {'content.heading': 'xyz'}})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
359
tests/queryset/visitor.py
Normal file
359
tests/queryset/visitor.py
Normal file
@@ -0,0 +1,359 @@
|
||||
import datetime
|
||||
import re
|
||||
import unittest
|
||||
|
||||
from bson import ObjectId
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine.errors import InvalidQueryError
|
||||
from mongoengine.queryset import Q
|
||||
|
||||
__all__ = ("QTest",)
|
||||
|
||||
|
||||
class QTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
connect(db='mongoenginetest')
|
||||
|
||||
class Person(Document):
|
||||
name = StringField()
|
||||
age = IntField()
|
||||
meta = {'allow_inheritance': True}
|
||||
|
||||
Person.drop_collection()
|
||||
self.Person = Person
|
||||
|
||||
def test_empty_q(self):
|
||||
"""Ensure that empty Q objects won't hurt.
|
||||
"""
|
||||
q1 = Q()
|
||||
q2 = Q(age__gte=18)
|
||||
q3 = Q()
|
||||
q4 = Q(name='test')
|
||||
q5 = Q()
|
||||
|
||||
class Person(Document):
|
||||
name = StringField()
|
||||
age = IntField()
|
||||
|
||||
query = {'$or': [{'age': {'$gte': 18}}, {'name': 'test'}]}
|
||||
self.assertEqual((q1 | q2 | q3 | q4 | q5).to_query(Person), query)
|
||||
|
||||
query = {'age': {'$gte': 18}, 'name': 'test'}
|
||||
self.assertEqual((q1 & q2 & q3 & q4 & q5).to_query(Person), query)
|
||||
|
||||
def test_q_with_dbref(self):
|
||||
"""Ensure Q objects handle DBRefs correctly"""
|
||||
connect(db='mongoenginetest')
|
||||
|
||||
class User(Document):
|
||||
pass
|
||||
|
||||
class Post(Document):
|
||||
created_user = ReferenceField(User)
|
||||
|
||||
user = User.objects.create()
|
||||
Post.objects.create(created_user=user)
|
||||
|
||||
self.assertEqual(Post.objects.filter(created_user=user).count(), 1)
|
||||
self.assertEqual(Post.objects.filter(Q(created_user=user)).count(), 1)
|
||||
|
||||
def test_and_combination(self):
|
||||
"""Ensure that Q-objects correctly AND together.
|
||||
"""
|
||||
class TestDoc(Document):
|
||||
x = IntField()
|
||||
y = StringField()
|
||||
|
||||
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)
|
||||
|
||||
q1 = Q(x__lt=7)
|
||||
q2 = Q(x__gt=3)
|
||||
query = (q1 & q2).to_query(TestDoc)
|
||||
self.assertEqual(query, {'x': {'$lt': 7, '$gt': 3}})
|
||||
|
||||
# More complex nested example
|
||||
query = Q(x__lt=100) & Q(y__ne='NotMyString')
|
||||
query &= Q(y__in=['a', 'b', 'c']) & Q(x__gt=-100)
|
||||
mongo_query = {
|
||||
'x': {'$lt': 100, '$gt': -100},
|
||||
'y': {'$ne': 'NotMyString', '$in': ['a', 'b', 'c']},
|
||||
}
|
||||
self.assertEqual(query.to_query(TestDoc), mongo_query)
|
||||
|
||||
def test_or_combination(self):
|
||||
"""Ensure that Q-objects correctly OR together.
|
||||
"""
|
||||
class TestDoc(Document):
|
||||
x = IntField()
|
||||
|
||||
q1 = Q(x__lt=3)
|
||||
q2 = Q(x__gt=7)
|
||||
query = (q1 | q2).to_query(TestDoc)
|
||||
self.assertEqual(query, {
|
||||
'$or': [
|
||||
{'x': {'$lt': 3}},
|
||||
{'x': {'$gt': 7}},
|
||||
]
|
||||
})
|
||||
|
||||
def test_and_or_combination(self):
|
||||
"""Ensure that Q-objects handle ANDing ORed components.
|
||||
"""
|
||||
class TestDoc(Document):
|
||||
x = IntField()
|
||||
y = BooleanField()
|
||||
|
||||
TestDoc.drop_collection()
|
||||
|
||||
query = (Q(x__gt=0) | Q(x__exists=False))
|
||||
query &= Q(x__lt=100)
|
||||
self.assertEqual(query.to_query(TestDoc), {'$and': [
|
||||
{'$or': [{'x': {'$gt': 0}},
|
||||
{'x': {'$exists': False}}]},
|
||||
{'x': {'$lt': 100}}]
|
||||
})
|
||||
|
||||
q1 = (Q(x__gt=0) | Q(x__exists=False))
|
||||
q2 = (Q(x__lt=100) | Q(y=True))
|
||||
query = (q1 & q2).to_query(TestDoc)
|
||||
|
||||
TestDoc(x=101).save()
|
||||
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(2, TestDoc.objects(q1 & q2).count())
|
||||
|
||||
def test_or_and_or_combination(self):
|
||||
"""Ensure that Q-objects handle ORing ANDed ORed components. :)
|
||||
"""
|
||||
class TestDoc(Document):
|
||||
x = IntField()
|
||||
y = BooleanField()
|
||||
|
||||
TestDoc.drop_collection()
|
||||
TestDoc(x=-1, y=True).save()
|
||||
TestDoc(x=101, y=True).save()
|
||||
TestDoc(x=99, y=False).save()
|
||||
TestDoc(x=101, y=False).save()
|
||||
|
||||
q1 = (Q(x__gt=0) & (Q(y=True) | Q(y__exists=False)))
|
||||
q2 = (Q(x__lt=100) & (Q(y=False) | Q(y__exists=False)))
|
||||
query = (q1 | q2).to_query(TestDoc)
|
||||
|
||||
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):
|
||||
class Test(Document):
|
||||
name = StringField(max_length=40)
|
||||
title = StringField(max_length=40)
|
||||
|
||||
q1 = Q(name__contains='te') | Q(title__contains='te')
|
||||
q2 = Q(name__contains='12') | Q(title__contains='12')
|
||||
|
||||
q3 = q1 & q2
|
||||
|
||||
query = q3.to_query(Test)
|
||||
self.assertEqual(query["$and"][0], q1.to_query(Test))
|
||||
self.assertEqual(query["$and"][1], q2.to_query(Test))
|
||||
|
||||
def test_q_clone(self):
|
||||
|
||||
class TestDoc(Document):
|
||||
x = IntField()
|
||||
|
||||
TestDoc.drop_collection()
|
||||
for i in range(1, 101):
|
||||
t = TestDoc(x=i)
|
||||
t.save()
|
||||
|
||||
# Check normal cases work without an error
|
||||
test = TestDoc.objects(Q(x__lt=7) & Q(x__gt=3))
|
||||
|
||||
self.assertEqual(test.count(), 3)
|
||||
|
||||
test2 = test.clone()
|
||||
self.assertEqual(test2.count(), 3)
|
||||
self.assertNotEqual(test2, test)
|
||||
|
||||
test3 = test2.filter(x=6)
|
||||
self.assertEqual(test3.count(), 1)
|
||||
self.assertEqual(test.count(), 3)
|
||||
|
||||
def test_q(self):
|
||||
"""Ensure that Q objects may be used to query for documents.
|
||||
"""
|
||||
class BlogPost(Document):
|
||||
title = StringField()
|
||||
publish_date = DateTimeField()
|
||||
published = BooleanField()
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
post1 = BlogPost(title='Test 1', publish_date=datetime.datetime(2010, 1, 8), published=False)
|
||||
post1.save()
|
||||
|
||||
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.datetime(2010, 1, 8))
|
||||
post4.save()
|
||||
|
||||
post5 = BlogPost(title='Test 1', publish_date=datetime.datetime(2010, 1, 15))
|
||||
post5.save()
|
||||
|
||||
post6 = BlogPost(title='Test 1', published=False)
|
||||
post6.save()
|
||||
|
||||
# Check ObjectId lookup works
|
||||
obj = BlogPost.objects(id=post1.id).first()
|
||||
self.assertEqual(obj, post1)
|
||||
|
||||
# Check Q object combination with one does not exist
|
||||
q = BlogPost.objects(Q(title='Test 5') | Q(published=True))
|
||||
posts = [post.id for post in q]
|
||||
|
||||
published_posts = (post2, post3)
|
||||
self.assertTrue(all(obj.id in posts for obj in published_posts))
|
||||
|
||||
q = BlogPost.objects(Q(title='Test 1') | Q(published=True))
|
||||
posts = [post.id for post in q]
|
||||
published_posts = (post1, post2, post3, post5, post6)
|
||||
self.assertTrue(all(obj.id in posts for obj in published_posts))
|
||||
|
||||
# Check Q object combination
|
||||
date = datetime.datetime(2010, 1, 10)
|
||||
q = BlogPost.objects(Q(publish_date__lte=date) | Q(published=True))
|
||||
posts = [post.id for post in q]
|
||||
|
||||
published_posts = (post1, post2, post3, post4)
|
||||
self.assertTrue(all(obj.id in posts for obj in published_posts))
|
||||
|
||||
self.assertFalse(any(obj.id in posts for obj in [post5, post6]))
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
# Check the 'in' operator
|
||||
self.Person(name='user1', age=20).save()
|
||||
self.Person(name='user2', age=20).save()
|
||||
self.Person(name='user3', age=30).save()
|
||||
self.Person(name='user4', age=40).save()
|
||||
|
||||
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
|
||||
with self.assertRaises(InvalidQueryError):
|
||||
self.Person.objects('user1')
|
||||
|
||||
# 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.
|
||||
"""
|
||||
person = self.Person(name='Guido van Rossum')
|
||||
person.save()
|
||||
|
||||
obj = self.Person.objects(Q(name=re.compile('^Gui'))).first()
|
||||
self.assertEqual(obj, person)
|
||||
obj = self.Person.objects(Q(name=re.compile('^gui'))).first()
|
||||
self.assertEqual(obj, None)
|
||||
|
||||
obj = self.Person.objects(Q(name=re.compile('^gui', re.I))).first()
|
||||
self.assertEqual(obj, person)
|
||||
|
||||
obj = self.Person.objects(Q(name__not=re.compile('^bob'))).first()
|
||||
self.assertEqual(obj, person)
|
||||
|
||||
obj = self.Person.objects(Q(name__not=re.compile('^Gui'))).first()
|
||||
self.assertEqual(obj, None)
|
||||
|
||||
def test_q_repr(self):
|
||||
self.assertEqual(repr(Q()), 'Q(**{})')
|
||||
self.assertEqual(repr(Q(name='test')), "Q(**{'name': 'test'})")
|
||||
|
||||
self.assertEqual(
|
||||
repr(Q(name='test') & Q(age__gte=18)),
|
||||
"(Q(**{'name': 'test'}) & Q(**{'age__gte': 18}))")
|
||||
|
||||
self.assertEqual(
|
||||
repr(Q(name='test') | Q(age__gte=18)),
|
||||
"(Q(**{'name': 'test'}) | Q(**{'age__gte': 18}))")
|
||||
|
||||
def test_q_lists(self):
|
||||
"""Ensure that Q objects query ListFields correctly.
|
||||
"""
|
||||
class BlogPost(Document):
|
||||
tags = ListField(StringField())
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
BlogPost(tags=['python', 'mongo']).save()
|
||||
BlogPost(tags=['python']).save()
|
||||
|
||||
self.assertEqual(BlogPost.objects(Q(tags='mongo')).count(), 1)
|
||||
self.assertEqual(BlogPost.objects(Q(tags='python')).count(), 2)
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
def test_q_merge_queries_edge_case(self):
|
||||
|
||||
class User(Document):
|
||||
email = EmailField(required=False)
|
||||
name = StringField()
|
||||
|
||||
User.drop_collection()
|
||||
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())
|
||||
|
||||
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,32 +0,0 @@
|
||||
import unittest
|
||||
import pymongo
|
||||
from pymongo import ReadPreference, ReplicaSetConnection
|
||||
|
||||
import mongoengine
|
||||
from mongoengine import *
|
||||
from mongoengine.connection import get_db, get_connection, ConnectionError
|
||||
|
||||
|
||||
class ConnectionTest(unittest.TestCase):
|
||||
|
||||
def tearDown(self):
|
||||
mongoengine.connection._connection_settings = {}
|
||||
mongoengine.connection._connections = {}
|
||||
mongoengine.connection._dbs = {}
|
||||
|
||||
def test_replicaset_uri_passes_read_preference(self):
|
||||
"""Requires a replica set called "rs" on port 27017
|
||||
"""
|
||||
|
||||
try:
|
||||
conn = connect(db='mongoenginetest', host="mongodb://localhost/mongoenginetest?replicaSet=rs", read_preference=ReadPreference.SECONDARY_ONLY)
|
||||
except ConnectionError, e:
|
||||
return
|
||||
|
||||
if not isinstance(conn, ReplicaSetConnection):
|
||||
return
|
||||
|
||||
self.assertEquals(conn.read_preference, ReadPreference.SECONDARY_ONLY)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user