# HG changeset patch # User David Douard # Date 1375209117 -7200 # Node ID ea32e964fbf84aa735e6ac1374c1c0e61c0cba0d # Parent 31ed9dd946d1a60f325a29931b86fb418f2844fe# Parent 5edd092298906a8fa4a871fd3563849268b6b4d6 merge stable in default (3.18) branch diff -r 31ed9dd946d1 -r ea32e964fbf8 .hgtags --- a/.hgtags Thu Jul 04 09:26:59 2013 +0200 +++ b/.hgtags Tue Jul 30 20:31:57 2013 +0200 @@ -30,11 +30,7 @@ c9c492787a8aa1b7916e22eb6498cba1c8fa316c cubicweb-debian-version-3_2_0-1 634c251dd032894850080c4e5aeb0a4e09f888c0 cubicweb-version-3_2_1 e784f8847a124a93e5b385d7a92a2772c050fe82 cubicweb-debian-version-3_2_1-1 -6539ce84f04357ef65ccee0896a30997b16a4ece cubicweb-version-3_2_2 -92d1a15f08f7c5fa87643ffb4273d12cb3f41c63 cubicweb-debian-version-3_2_2-1 -6539ce84f04357ef65ccee0896a30997b16a4ece cubicweb-version-3_2_2 9b21e068fef73c37bcb4e53d006a7bde485f390b cubicweb-version-3_2_2 -92d1a15f08f7c5fa87643ffb4273d12cb3f41c63 cubicweb-debian-version-3_2_2-1 0e07514264aa1b0b671226f41725ea4c066c210a cubicweb-debian-version-3_2_2-1 f60bb84b86cf371f1f25197e00c778b469297721 cubicweb-version-3_2_3 4003d24974f15f17bd03b7efd6a5047cad4e4c41 cubicweb-debian-version-3_2_3-1 @@ -43,10 +39,8 @@ a356da3e725bfcb59d8b48a89d04be05ea261fd3 3.3.1 e3aeb6e6c3bb5c18e8dcf61bae9d654beda6c036 cubicweb-version-3_3_2 bef5e74e53f9de8220451dca4b5863a24a0216fb cubicweb-debian-version-3_3_2-1 -1cf9e44e2f1f4415253b8892a0adfbd3b69e84fd cubicweb-version-3_3_3 +47b5236774a0cf3b1cfe75f6d4bd2ec989644ace cubicweb-version-3_3_3 81973c897c9e78e5e52643e03628654916473196 cubicweb-debian-version-3_3_3-1 -1cf9e44e2f1f4415253b8892a0adfbd3b69e84fd cubicweb-version-3_3_3 -47b5236774a0cf3b1cfe75f6d4bd2ec989644ace cubicweb-version-3_3_3 2ba27ce8ecd9828693ec53c517e1c8810cbbe33e cubicweb-debian-version-3_3_3-2 d46363eac5d71bc1570d69337955154dfcd8fcc8 cubicweb-version-3.3.4 7dc22caa7640bf70fcae55afb6d2326829dacced cubicweb-debian-version-3.3.4-1 @@ -82,11 +76,7 @@ 37d025b2aa7735dae4a861059014c560b45b19e6 cubicweb-debian-version-3.5.4-1 1eca47d59fd932fe23f643ca239cf2408e5b1856 cubicweb-version-3.5.5 aad818d9d9b6fdb2ffea56c0a9af718c0b69899d cubicweb-debian-version-3.5.5-1 -b79f361839a7251b35eb8378fbc0773de7c8a815 cubicweb-version-3.5.6 -e6225e8e36c6506c774e0a76acc301d8ae1c1028 cubicweb-debian-version-3.5.6-1 -b79f361839a7251b35eb8378fbc0773de7c8a815 cubicweb-version-3.5.6 4e619e97b3fd70769a0f454963193c10cb87f9d4 cubicweb-version-3.5.6 -e6225e8e36c6506c774e0a76acc301d8ae1c1028 cubicweb-debian-version-3.5.6-1 5f7c939301a1b915e17eec61c05e8e9ab8bdc182 cubicweb-debian-version-3.5.6-1 0fc300eb4746e01f2755b9eefd986d58d8366ccf cubicweb-version-3.5.7 7a96c0544c138a0c5f452e5b2428ce6e2b7cb378 cubicweb-debian-version-3.5.7-1 @@ -98,8 +88,6 @@ 4920121d41f28c8075a4f00461911677396fc566 cubicweb-debian-version-3.5.11-1 98af3d02b83e7635207781289cc3445fb0829951 cubicweb-version-3.5.12 4281e1e2d76b9a37f38c0eeb1cbdcaa2fac6533c cubicweb-debian-version-3.5.12-1 -5f957e351b0a60d5c5fff60c560b04e666c3a8c6 cubicweb-version-3.6.0 -17e88f2485d1ea1fb8a3926a274637ce19e95d69 cubicweb-debian-version-3.6.0-1 450804da3ab2476b7ede0c1f956235b4c239734f cubicweb-version-3.6.0 d2ba93fcb8da95ceab08f48f8149a480215f149c cubicweb-debian-version-3.6.0-1 4ae30c9ca11b1edad67d25b76fce672171d02023 cubicweb-version-3.6.1 @@ -107,12 +95,12 @@ 0a16f07112b90fb61d2e905855fece77e5a7e39c cubicweb-debian-version-3.6.1-2 bfebe3d14d5390492925fc294dfdafad890a7104 cubicweb-version-3.6.2 f3b4bb9121a0e7ee5961310ff79e61c890948a77 cubicweb-debian-version-3.6.2-1 +9c342fa4f1b73e06917d7dc675949baff442108b cubicweb-version-3.6.3 +f9fce56d6a0c2bc6c4b497b66039a8bbbbdc8074 cubicweb-debian-version-3.6.3-1 270aba1e6fa21dac6b070e7815e6d1291f9c87cd cubicweb-version-3.7.0 0c9ff7e496ce344b7e6bf5c9dd2847daf9034e5e cubicweb-debian-version-3.7.0-1 6b0832bbd1daf27c2ce445af5b5222e1e522fb90 cubicweb-version-3.7.1 9194740f070e64da5a89f6a9a31050a8401ebf0c cubicweb-debian-version-3.7.1-1 -9c342fa4f1b73e06917d7dc675949baff442108b cubicweb-version-3.6.3 -f9fce56d6a0c2bc6c4b497b66039a8bbbbdc8074 cubicweb-debian-version-3.6.3-1 d010f749c21d55cd85c5feb442b9cf816282953c cubicweb-version-3.7.2 8fda29a6c2191ba3cc59242c17b28b34127c75fa cubicweb-debian-version-3.7.2-1 768beb8e15f15e079f8ee6cfc35125e12b19e140 cubicweb-version-3.7.3 @@ -135,10 +123,11 @@ 5d05b08adeab1ea301e49ed8537e35ede6db92f6 cubicweb-debian-version-3.8.5-1 1a24c62aefc5e57f61be3d04affd415288e81904 cubicweb-version-3.8.6 607a90073911b6bb941a49b5ec0b0d2a9cd479af cubicweb-debian-version-3.8.6-1 +a1a334d934390043a4293a4ee42bdceb1343246e cubicweb-version-3.8.7 +1cccf88d6dfe42986e1091de4c364b7b5814c54f cubicweb-debian-version-3.8.7-1 +48f468f33704e401a8e7907e258bf1ac61eb8407 cubicweb-version-3.9.x d9936c39d478b6701a4adef17bc28888ffa011c6 cubicweb-version-3.9.0 eda4940ffef8b7d36127e68de63a52388374a489 cubicweb-debian-version-3.9.0-1 -a1a334d934390043a4293a4ee42bdceb1343246e cubicweb-version-3.8.7 -1cccf88d6dfe42986e1091de4c364b7b5814c54f cubicweb-debian-version-3.8.7-1 4d75f743ed49dd7baf8bde7b0e475244933fa08e cubicweb-version-3.9.1 9bd75af3dca36d7be5d25fc5ab1b89b34c811456 cubicweb-debian-version-3.9.1-1 e51796b9caf389c224c6f66dcb8aa75bf1b82eff cubicweb-version-3.9.2 @@ -155,6 +144,11 @@ 1c01f9dffd64d507863c9f8f68e3585b7aa24374 cubicweb-debian-version-3.9.7-1 eed788018b595d46a55805bd8d2054c401812b2b cubicweb-version-3.9.8 e4dba8ae963701a36be94ae58c790bc97ba029bb cubicweb-debian-version-3.9.8-1 +df0b2de62cec10c84a2fff5233db05852cbffe93 cubicweb-version-3.9.9 +1ba51b00fc44faa0d6d57448000aaa1fd5c6ab57 cubicweb-debian-version-3.9.9-1 +b7db1f59355832a409d2032e19c84cfffdb3b265 cubicweb-debian-version-3.9.9-2 +09c98763ae9d43616d047c1b25d82b4e41a4362f cubicweb-debian-version-3.9.9-3 +a62f24e1497e953fbaed5894f6064a64f7ac0be3 cubicweb-version-3.10.x 0793fe84651be36f8de9b4faba3781436dc07be0 cubicweb-version-3.10.0 9ef1347f8d99e7daad290738ef93aa894a2c03ce cubicweb-debian-version-3.10.0-1 6c6859a676732c845af69f92e74d4aafae12f83a cubicweb-version-3.10.1 @@ -163,15 +157,7 @@ 4a87c8af6f3ffe59c6048ebbdc1b6b204d0b9c7f cubicweb-debian-version-3.10.2-1 8eb58d00a0cedcf7b275b1c7f43b08e2165f655c cubicweb-version-3.10.3 303b150ebb7a92b2904efd52b446457999cab370 cubicweb-debian-version-3.10.3-1 -3829498510a754b1b8a40582cb8dcbca9145fc9d cubicweb-version-3.10.4 -49f1226f2fab6d9ff17eb27d5a66732a4e5b5add cubicweb-debian-version-3.10.4-1 -df0b2de62cec10c84a2fff5233db05852cbffe93 cubicweb-version-3.9.9 -1ba51b00fc44faa0d6d57448000aaa1fd5c6ab57 cubicweb-debian-version-3.9.9-1 -b7db1f59355832a409d2032e19c84cfffdb3b265 cubicweb-debian-version-3.9.9-2 -09c98763ae9d43616d047c1b25d82b4e41a4362f cubicweb-debian-version-3.9.9-3 -3829498510a754b1b8a40582cb8dcbca9145fc9d cubicweb-version-3.10.4 d73733479a3af453f06b849ed88d120784ce9224 cubicweb-version-3.10.4 -49f1226f2fab6d9ff17eb27d5a66732a4e5b5add cubicweb-debian-version-3.10.4-1 7b41930e1d32fea3989a85f6ea7281983300adb1 cubicweb-debian-version-3.10.4-1 159d0dbe07d9eb1c6ace4c5e160d1ec6e6762086 cubicweb-version-3.10.5 e2e7410e994777589aec218d31eef9ff8d893f92 cubicweb-debian-version-3.10.5-1 @@ -181,21 +167,20 @@ bf5d9a1415e3c9abe6b68ba3b24a8ad741f9de3c cubicweb-debian-version-3.10.7-1 e581a86a68f089946a98c966ebca7aee58a5718f cubicweb-version-3.10.8 132b525de25bc75ed6389c45aee77e847cb3a437 cubicweb-debian-version-3.10.8-1 -48f468f33704e401a8e7907e258bf1ac61eb8407 cubicweb-version-3.9.x 37432cede4fe55b97fc2e9be0a2dd20e8837a848 cubicweb-version-3.11.0 8daabda9f571863e8754f8ab722744c417ba3abf cubicweb-debian-version-3.11.0-1 d0410eb4d8bbf657d7f32b0c681db09b1f8119a0 cubicweb-version-3.11.1 77318f1ec4aae3523d455e884daf3708c3c79af7 cubicweb-debian-version-3.11.1-1 56ae3cd5f8553678a2b1d4121b61241598d0ca68 cubicweb-version-3.11.2 954b5b51cd9278eb45d66be1967064d01ab08453 cubicweb-debian-version-3.11.2-1 +b7a124f9aed2c7c9c86c6349ddd9f0a07023f0ca cubicweb-version-3.11.3 +b3c6702761a18a41fdbb7bc1083f92aefce07765 cubicweb-debian-version-3.11.3-1 fd502219eb76f4bfd239d838a498a1d1e8204baf cubicweb-version-3.12.0 92b56939b7c77bbf443b893c495a20f19bc30702 cubicweb-debian-version-3.12.0-1 59701627adba73ee97529f6ea0e250a0f3748e32 cubicweb-version-3.12.1 07e2c9c7df2617c5ecfa84cb819b3ee8ef91d1f2 cubicweb-debian-version-3.12.1-1 5a9b6bc5653807500c30a7eb0e95b90fd714fec3 cubicweb-version-3.12.2 6d418fb3ffed273562aae411efe323d5138b592a cubicweb-debian-version-3.12.2-1 -b7a124f9aed2c7c9c86c6349ddd9f0a07023f0ca cubicweb-version-3.11.3 -b3c6702761a18a41fdbb7bc1083f92aefce07765 cubicweb-debian-version-3.11.3-1 e712bc6f1f71684f032bfcb9bb151a066c707dec cubicweb-version-3.12.3 ba8fe4f2e408c3fdf6c297cd42c2577dcac50e71 cubicweb-debian-version-3.12.3-1 5cd0dbc26882f60e3f11ec55e7f058d94505e7ed cubicweb-version-3.12.4 @@ -204,14 +189,16 @@ 6dfe78a0797ccc34962510f8c2a57f63d65ce41e cubicweb-debian-version-3.12.5-1 a18dac758150fe9c1f9e4958d898717c32a8f679 cubicweb-version-3.12.6 105767487c7075dbcce36474f1af0485985cbf2c cubicweb-debian-version-3.12.6-1 -b661ef475260ca7d9ea5c36ba2cc86e95e5b17d3 cubicweb-version-3.13.0 -a96137858f571711678954477da6f7f435870cea cubicweb-debian-version-3.13.0-1 628fe57ce746c1dac87fb1b078b2026057df894e cubicweb-version-3.12.7 a07517985136bbbfa6610c428a1b42cd04cd530b cubicweb-debian-version-3.12.7-1 50122a47ce4fb2ecbf3cf20ed2777f4276c93609 cubicweb-version-3.12.8 cf49ed55685a810d8d73585330ad1a57cc76260d cubicweb-debian-version-3.12.8-1 cb2990aaa63cbfe593bcf3afdbb9071e4c76815a cubicweb-version-3.12.9 92464e39134c70e4ddbe6cd78a6e3338a3b88b05 cubicweb-debian-version-3.12.9-1 +074c848a3712a77737d9a1bfbb618c75f5c0cbfa cubicweb-version-3.12.10 +9dfd21fa0a8b9f121a08866ad3e2ebd1dd06790d cubicweb-debian-version-3.12.10-1 +b661ef475260ca7d9ea5c36ba2cc86e95e5b17d3 cubicweb-version-3.13.0 +a96137858f571711678954477da6f7f435870cea cubicweb-debian-version-3.13.0-1 7d84317ef185a10c5eb78e6086f2297d2f4bd1e3 cubicweb-version-3.13.1 cc0578049cbe8b1d40009728e36c17e45da1fc6b cubicweb-debian-version-3.13.1-1 f9227b9d61835f03163b8133a96da35db37a0c8d cubicweb-version-3.13.2 @@ -220,11 +207,8 @@ fb48c55cb80234bc0164c9bcc0e2cfc428836e5f cubicweb-debian-version-3.13.3-1 223ecf0620b6c87d997f8011aca0d9f0ee4750af cubicweb-version-3.13.4 52f26475d764129c5559b2d80fd57e6ea1bdd6ba cubicweb-debian-version-3.13.4-1 -a62f24e1497e953fbaed5894f6064a64f7ac0be3 cubicweb-version-3.10.x 20d9c550c57eb6f9adcb0cfab1c11b6b8793afb6 cubicweb-version-3.13.5 2e9dd7d945557c210d3b79153c65f6885e755315 cubicweb-debian-version-3.13.5-1 -074c848a3712a77737d9a1bfbb618c75f5c0cbfa cubicweb-version-3.12.10 -9dfd21fa0a8b9f121a08866ad3e2ebd1dd06790d cubicweb-debian-version-3.12.10-1 17c007ad845abbac82e12146abab32a634657574 cubicweb-version-3.13.6 8a8949ca5351d48c5cf795ccdff06c1d4aab2ce0 cubicweb-debian-version-3.13.6-1 68e8c81fa96d6bcd21cc17bc9832d388ce05a9eb cubicweb-version-3.13.7 @@ -233,10 +217,10 @@ 43f83f5d0a4d57a06e9a4990bc957fcfa691eec3 cubicweb-debian-version-3.13.8-1 07afe32945aa275052747f78ef1f55858aaf6fa9 cubicweb-version-3.13.9 0a3cb5e60d57a7a9851371b4ae487094ec2bf614 cubicweb-debian-version-3.13.9-1 +2ad4e5173c73a43804c265207bcabb8940bd42f4 cubicweb-version-3.13.10 +2eab9a5a6bf8e3b0cf706bee8cdf697759c0a33a cubicweb-debian-version-3.13.10-1 5c4390eb10c3fe76a81e6fccec109d7097dc1a8d cubicweb-version-3.14.0 0bfe22fceb383b46d62b437bf5dd0141a714afb8 cubicweb-debian-version-3.14.0-1 -2ad4e5173c73a43804c265207bcabb8940bd42f4 cubicweb-version-3.13.10 -2eab9a5a6bf8e3b0cf706bee8cdf697759c0a33a cubicweb-debian-version-3.13.10-1 793d2d327b3ebf0b82b2735cf3ccb86467d1c08a cubicweb-version-3.14.1 6928210da4fc25d086b5b8d5ff2029da41aade2e cubicweb-debian-version-3.14.1-1 049a3819f03dc79d803be054cc3bfe8425313f63 cubicweb-version-3.14.2 @@ -250,8 +234,6 @@ 55fc796ed5d5f31245ae60bd148c9e42657a1af6 cubicweb-debian-version-3.14.5-1 db021578232b885dc5e55dfca045332ce01e7f35 cubicweb-version-3.14.6 75364c0994907764715bd5011f6a59d934dbeb7d cubicweb-debian-version-3.14.6-1 -0642b2d03acaa5e065cae7590e82b388a280ca22 cubicweb-version-3.15.0 -925db25a3250c5090cf640fc2b02bde5818b9798 cubicweb-debian-version-3.15.0-1 3ba3ee5b3a89a54d1dc12ed41d5c12232eda1952 cubicweb-version-3.14.7 20ee573bd2379a00f29ff27bb88a8a3344d4cdfe cubicweb-debian-version-3.14.7-1 15fe07ff687238f8cc09d8e563a72981484085b3 cubicweb-version-3.14.8 @@ -260,6 +242,8 @@ 68c762adf2d5a2c338910ef1091df554370586f0 cubicweb-debian-version-3.14.9-1 0ff798f80138ca8f50a59f42284380ce8f6232e8 cubicweb-version-3.14.10 197bcd087c87cd3de9f21f5bf40bd6203c074f1f cubicweb-debian-version-3.14.10-1 +0642b2d03acaa5e065cae7590e82b388a280ca22 cubicweb-version-3.15.0 +925db25a3250c5090cf640fc2b02bde5818b9798 cubicweb-debian-version-3.15.0-1 783a5df54dc742e63c8a720b1582ff08366733bd cubicweb-version-3.15.1 fe5e60862b64f1beed2ccdf3a9c96502dfcd811b cubicweb-debian-version-3.15.1-1 2afc157ea9b2b92eccb0f2d704094e22ce8b5a05 cubicweb-version-3.15.2 @@ -291,19 +275,27 @@ ee860c51f56bd65c4f6ea363462c02700d1dab5a cubicweb-version-3.16.3 ee860c51f56bd65c4f6ea363462c02700d1dab5a cubicweb-debian-version-3.16.3-1 ee860c51f56bd65c4f6ea363462c02700d1dab5a cubicweb-centos-version-3.16.3-1 -cc1a0aad580cf93d26959f97d8d6638e786c1082 cubicweb-version-3.17.0 -22be40c492e9034483bfec379ca11462ea97825b cubicweb-debian-version-3.17.0-1 -09a0c7ea6c3cb97bbbeed3795b3c3715ceb9566b cubicweb-debian-version-3.17.0-2 041804bc48e91e440a5b573ceb0df5bf22863b80 cubicweb-version-3.16.4 041804bc48e91e440a5b573ceb0df5bf22863b80 cubicweb-debian-version-3.16.4-1 041804bc48e91e440a5b573ceb0df5bf22863b80 cubicweb-centos-version-3.16.4-1 810a05fba1a46ab893b6cadac109097a047f8355 cubicweb-version-3.16.5 810a05fba1a46ab893b6cadac109097a047f8355 cubicweb-debiann-version-3.16.5-1 810a05fba1a46ab893b6cadac109097a047f8355 cubicweb-centos-version-3.16.5-1 -f98d1c46ed9fd5db5262cf5be1c8e159c90efc8b cubicweb-version-3.17.1 +b4ccaf13081d2798c0414d002e743cb0bf6d81f8 cubicweb-version-3.16.6 +b4ccaf13081d2798c0414d002e743cb0bf6d81f8 cubicweb-centos-version-3.16.6-1 +b4ccaf13081d2798c0414d002e743cb0bf6d81f8 cubicweb-debian-version-3.16.6-1 +cc1a0aad580cf93d26959f97d8d6638e786c1082 cubicweb-version-3.17.0 +22be40c492e9034483bfec379ca11462ea97825b cubicweb-debian-version-3.17.0-1 +09a0c7ea6c3cb97bbbeed3795b3c3715ceb9566b cubicweb-debian-version-3.17.0-2 f98d1c46ed9fd5db5262cf5be1c8e159c90efc8b cubicweb-version-3.17.1 -73f2ad404716cd211b735e67ee16875f1fff7374 cubicweb-debian-version-3.17.1-1 f98d1c46ed9fd5db5262cf5be1c8e159c90efc8b cubicweb-debian-version-3.17.1-1 f98d1c46ed9fd5db5262cf5be1c8e159c90efc8b cubicweb-centos-version-3.17.1-1 +965f894b63cb7c4456acd82257709f563bde848f cubicweb-centos-version-3.17.1-2 195e519fe97c8d1a5ab5ccb21bf7c88e5801b657 cubicweb-version-3.17.2 195e519fe97c8d1a5ab5ccb21bf7c88e5801b657 cubicweb-debian-version-3.17.2-1 +32b4d5314fd90fe050c931886190f9a372686148 cubicweb-version-3.17.3 +32b4d5314fd90fe050c931886190f9a372686148 cubicweb-debian-version-3.17.3-1 +32b4d5314fd90fe050c931886190f9a372686148 cubicweb-centos-version-3.17.3-1 +c7ba8e5d2e45e3d1289c1403df40d7dcb5e62acb cubicweb-version-3.17.4 +c7ba8e5d2e45e3d1289c1403df40d7dcb5e62acb cubicweb-debian-version-3.17.4-1 +c7ba8e5d2e45e3d1289c1403df40d7dcb5e62acb cubicweb-centos-version-3.17.4-1 diff -r 31ed9dd946d1 -r ea32e964fbf8 __pkginfo__.py --- a/__pkginfo__.py Thu Jul 04 09:26:59 2013 +0200 +++ b/__pkginfo__.py Tue Jul 30 20:31:57 2013 +0200 @@ -22,7 +22,7 @@ modname = distname = "cubicweb" -numversion = (3, 17, 3) +numversion = (3, 17, 4) version = '.'.join(str(num) for num in numversion) description = "a repository of entities / relations for knowledge management" @@ -52,7 +52,6 @@ # XXX graphviz # server dependencies 'logilab-database': '>= 1.10', - 'pysqlite': '>= 2.5.5', # XXX install pysqlite2 'passlib': '', } diff -r 31ed9dd946d1 -r ea32e964fbf8 _exceptions.py --- a/_exceptions.py Thu Jul 04 09:26:59 2013 +0200 +++ b/_exceptions.py Tue Jul 30 20:31:57 2013 +0200 @@ -61,7 +61,7 @@ """ class AuthenticationError(ConnectionError): - """raised when when an attempt to establish a connection failed do to wrong + """raised when an attempt to establish a connection failed due to wrong connection information (login / password or other authentication token) """ diff -r 31ed9dd946d1 -r ea32e964fbf8 cubicweb.spec --- a/cubicweb.spec Thu Jul 04 09:26:59 2013 +0200 +++ b/cubicweb.spec Tue Jul 30 20:31:57 2013 +0200 @@ -7,7 +7,7 @@ %endif Name: cubicweb -Version: 3.17.3 +Version: 3.17.4 Release: logilab.1%{?dist} Summary: CubicWeb is a semantic web application framework Source0: http://download.logilab.org/pub/cubicweb/cubicweb-%{version}.tar.gz @@ -46,11 +46,13 @@ %install NO_SETUPTOOLS=1 %{__python} setup.py --quiet install --no-compile --prefix=%{_prefix} --root="$RPM_BUILD_ROOT" +mkdir -p $RPM_BUILD_ROOT/var/log/cubicweb %clean rm -rf $RPM_BUILD_ROOT %files %defattr(-, root, root) +%dir /var/log/cubicweb /* diff -r 31ed9dd946d1 -r ea32e964fbf8 dataimport.py --- a/dataimport.py Thu Jul 04 09:26:59 2013 +0200 +++ b/dataimport.py Tue Jul 30 20:31:57 2013 +0200 @@ -105,8 +105,8 @@ return i+1 def ucsvreader_pb(stream_or_path, encoding='utf-8', separator=',', quote='"', - skipfirst=False, withpb=True): - """same as ucsvreader but a progress bar is displayed as we iter on rows""" + skipfirst=False, withpb=True, skip_empty=True): + """same as :func:`ucsvreader` but a progress bar is displayed as we iter on rows""" if isinstance(stream_or_path, basestring): if not osp.exists(stream_or_path): raise Exception("file doesn't exists: %s" % stream_or_path) @@ -118,23 +118,30 @@ rowcount -= 1 if withpb: pb = shellutils.ProgressBar(rowcount, 50) - for urow in ucsvreader(stream, encoding, separator, quote, skipfirst): + for urow in ucsvreader(stream, encoding, separator, quote, + skipfirst=skipfirst, skip_empty=skip_empty): yield urow if withpb: pb.update() print ' %s rows imported' % rowcount def ucsvreader(stream, encoding='utf-8', separator=',', quote='"', - skipfirst=False, ignore_errors=False): + skipfirst=False, ignore_errors=False, skip_empty=True): """A csv reader that accepts files with any encoding and outputs unicode strings + + if skip_empty (the default), lines without any values specified (only + separators) will be skipped. This is useful for Excel exports which may be + full of such lines. """ it = iter(csv.reader(stream, delimiter=separator, quotechar=quote)) if not ignore_errors: if skipfirst: it.next() for row in it: - yield [item.decode(encoding) for item in row] + decoded = [item.decode(encoding) for item in row] + if not skip_empty or any(decoded): + yield [item.decode(encoding) for item in row] else: # Skip first line try: @@ -151,7 +158,10 @@ # Error in CSV, ignore line and continue except csv.Error: continue - yield [item.decode(encoding) for item in row] + decoded = [item.decode(encoding) for item in row] + if not skip_empty or any(decoded): + yield decoded + def callfunc_every(func, number, iterable): """yield items of `iterable` one by one and call function `func` diff -r 31ed9dd946d1 -r ea32e964fbf8 debian/changelog --- a/debian/changelog Thu Jul 04 09:26:59 2013 +0200 +++ b/debian/changelog Tue Jul 30 20:31:57 2013 +0200 @@ -1,3 +1,9 @@ +cubicweb (3.17.4-1) unstable; urgency=low + + * new upstream release + + -- David Douard Fri, 26 Jul 2013 09:44:19 +0200 + cubicweb (3.17.3-1) unstable; urgency=low * new upstream release @@ -28,6 +34,12 @@ -- Pierre-Yves David Mon, 29 Apr 2013 11:20:56 +0200 +cubicweb (3.16.6-1) unstable; urgency=low + + * new upstream release + + -- Florent Cayré Sat, 13 Jul 2013 05:10:23 +0200 + cubicweb (3.16.5-1) unstable; urgency=low * new upstream release diff -r 31ed9dd946d1 -r ea32e964fbf8 debian/control --- a/debian/control Thu Jul 04 09:26:59 2013 +0200 +++ b/debian/control Tue Jul 30 20:31:57 2013 +0200 @@ -10,7 +10,6 @@ Build-Depends: debhelper (>= 7), python (>= 2.6), - python-central (>= 0.5), python-sphinx, python-logilab-common, python-unittest2, @@ -24,7 +23,6 @@ Package: cubicweb Architecture: all -XB-Python-Version: ${python:Versions} Depends: ${misc:Depends}, ${python:Depends}, @@ -45,7 +43,6 @@ Package: cubicweb-server Architecture: all -XB-Python-Version: ${python:Versions} Conflicts: cubicweb-multisources Replaces: cubicweb-multisources Provides: cubicweb-multisources @@ -101,7 +98,6 @@ Package: cubicweb-twisted Architecture: all -XB-Python-Version: ${python:Versions} Provides: cubicweb-web-frontend Depends: ${misc:Depends}, @@ -123,7 +119,6 @@ Package: cubicweb-web Architecture: all -XB-Python-Version: ${python:Versions} Depends: ${misc:Depends}, ${python:Depends}, @@ -148,7 +143,6 @@ Package: cubicweb-common Architecture: all -XB-Python-Version: ${python:Versions} Depends: ${misc:Depends}, ${python:Depends}, @@ -173,7 +167,6 @@ Package: cubicweb-ctl Architecture: all -XB-Python-Version: ${python:Versions} Depends: ${misc:Depends}, ${python:Depends}, @@ -188,7 +181,6 @@ Package: cubicweb-dev Architecture: all -XB-Python-Version: ${python:Versions} Depends: ${misc:Depends}, ${python:Depends}, diff -r 31ed9dd946d1 -r ea32e964fbf8 debian/rules --- a/debian/rules Thu Jul 04 09:26:59 2013 +0200 +++ b/debian/rules Tue Jul 30 20:31:57 2013 +0200 @@ -18,11 +18,6 @@ # distributions and we don't want to block a new release of Cubicweb # because of documentation issues. -PYTHONPATH=$${PYTHONPATH:+$${PYTHONPATH}:}$(CURDIR)/debian/pythonpath $(MAKE) -C doc/book/en all - # squeeze has a broken combination of jquery and sphinx, fix it up so search works(ish) - if grep -q jQuery\\.className doc/html/_static/doctools.js && grep -q "jQuery JavaScript Library v1\.4\." doc/html/_static/jquery.js; then \ - echo 'Patching doctools.js for jQuery 1.4 compat'; \ - sed -i 's/jQuery\.className.has(node\.parentNode, className)/jQuery(node.parentNode).hasClass(className)/' doc/html/_static/doctools.js; \ - fi rm -rf debian/pythonpath touch build-stamp @@ -72,7 +67,7 @@ binary-indep: build install dh_testdir dh_testroot -i - dh_pycentral -i + dh_python2 -i dh_installinit -i -n --name cubicweb -u"defaults 99" dh_installlogrotate -i dh_installdocs -i -A README diff -r 31ed9dd946d1 -r ea32e964fbf8 devtools/devctl.py --- a/devtools/devctl.py Thu Jul 04 09:26:59 2013 +0200 +++ b/devtools/devctl.py Tue Jul 30 20:31:57 2013 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -130,24 +130,31 @@ w('# singular and plural forms for each entity type\n') w('\n') vregdone = set() + afss = vreg['uicfg']['autoform_section'] + aiams = vreg['uicfg']['actionbox_appearsin_addmenu'] if libconfig is not None: + # processing a cube, libconfig being a config with all its dependencies + # (cubicweb incl.) from cubicweb.cwvreg import CWRegistryStore libschema = libconfig.load_schema(remove_unused_rtypes=False) - afs = vreg['uicfg'].select('autoform_section') - appearsin_addmenu = vreg['uicfg'].select('actionbox_appearsin_addmenu') cleanup_sys_modules(libconfig) libvreg = CWRegistryStore(libconfig) libvreg.set_schema(libschema) # trigger objects registration - libafs = libvreg['uicfg'].select('autoform_section') - libappearsin_addmenu = libvreg['uicfg'].select('actionbox_appearsin_addmenu') + libafss = libvreg['uicfg']['autoform_section'] + libaiams = libvreg['uicfg']['actionbox_appearsin_addmenu'] # prefill vregdone set list(_iter_vreg_objids(libvreg, vregdone)) + + def is_in_lib(rtags, eschema, rschema, role, tschema, predicate=bool): + return any(predicate(rtag.etype_get(eschema, rschema, role, tschema)) + for rtag in rtags) else: + # processing cubicweb itself libschema = {} - afs = vreg['uicfg'].select('autoform_section') - appearsin_addmenu = vreg['uicfg'].select('actionbox_appearsin_addmenu') for cstrtype in CONSTRAINTS: add_msg(w, cstrtype) + + is_in_lib = lambda: False done = set() for eschema in sorted(schema.entities()): if eschema.type in libschema: @@ -169,32 +176,34 @@ if rschema.final: continue for tschema in targetschemas: - fsections = afs.etype_get(eschema, rschema, role, tschema) - if 'main_inlined' in fsections and \ - (libconfig is None or not - 'main_inlined' in libafs.etype_get( - eschema, rschema, role, tschema)): - add_msg(w, 'add a %s' % tschema, - 'inlined:%s.%s.%s' % (etype, rschema, role)) - add_msg(w, str(tschema), - 'inlined:%s.%s.%s' % (etype, rschema, role)) - if appearsin_addmenu.etype_get(eschema, rschema, role, tschema): - if libconfig is not None and libappearsin_addmenu.etype_get( - eschema, rschema, role, tschema): - if eschema in libschema and tschema in libschema: - continue - if role == 'subject': - label = 'add %s %s %s %s' % (eschema, rschema, - tschema, role) - label2 = "creating %s (%s %%(linkto)s %s %s)" % ( - tschema, eschema, rschema, tschema) - else: - label = 'add %s %s %s %s' % (tschema, rschema, - eschema, role) - label2 = "creating %s (%s %s %s %%(linkto)s)" % ( - tschema, tschema, rschema, eschema) - add_msg(w, label) - add_msg(w, label2) + + for afs in afss: + fsections = afs.etype_get(eschema, rschema, role, tschema) + if 'main_inlined' in fsections and not \ + is_in_lib(libafss, eschema, rschema, role, tschema, + lambda x: 'main_inlined' in x): + add_msg(w, 'add a %s' % tschema, + 'inlined:%s.%s.%s' % (etype, rschema, role)) + add_msg(w, str(tschema), + 'inlined:%s.%s.%s' % (etype, rschema, role)) + break + + for aiam in aiams: + if aiam.etype_get(eschema, rschema, role, tschema) and not \ + is_in_lib(libaiams, eschema, rschema, role, tschema): + if role == 'subject': + label = 'add %s %s %s %s' % (eschema, rschema, + tschema, role) + label2 = "creating %s (%s %%(linkto)s %s %s)" % ( + tschema, eschema, rschema, tschema) + else: + label = 'add %s %s %s %s' % (tschema, rschema, + eschema, role) + label2 = "creating %s (%s %s %s %%(linkto)s)" % ( + tschema, tschema, rschema, eschema) + add_msg(w, label) + add_msg(w, label2) + break # XXX also generate "creating ...' messages for actions in the # addrelated submenu w('# subject and object forms for each relation type\n') diff -r 31ed9dd946d1 -r ea32e964fbf8 devtools/test/data/cubes/__init__.py diff -r 31ed9dd946d1 -r ea32e964fbf8 devtools/test/data/cubes/i18ntestcube/__init__.py diff -r 31ed9dd946d1 -r ea32e964fbf8 devtools/test/data/cubes/i18ntestcube/__pkginfo__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/devtools/test/data/cubes/i18ntestcube/__pkginfo__.py Tue Jul 30 20:31:57 2013 +0200 @@ -0,0 +1,18 @@ +# pylint: disable=W0622 +"""cubicweb i18n test cube application packaging information""" + +modname = 'i18ntestcube' +distname = 'cubicweb-i18ntestcube' + +numversion = (0, 1, 0) +version = '.'.join(str(num) for num in numversion) + +license = 'LGPL' +author = 'LOGILAB S.A. (Paris, FRANCE)' +author_email = 'contact@logilab.fr' +description = 'forum' +web = 'http://www.cubicweb.org/project/%s' % distname + +__depends__ = {'cubicweb': '>= 3.16.4', + } +__recommends__ = {} diff -r 31ed9dd946d1 -r ea32e964fbf8 devtools/test/data/cubes/i18ntestcube/i18n/en.po.ref --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/devtools/test/data/cubes/i18ntestcube/i18n/en.po.ref Tue Jul 30 20:31:57 2013 +0200 @@ -0,0 +1,170 @@ +msgid "" +msgstr "" +"Project-Id-Version: cubicweb 3.16.5\n" +"PO-Revision-Date: 2008-03-28 18:14+0100\n" +"Last-Translator: Logilab Team \n" +"Language-Team: fr \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: cubicweb-devtools\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +# schema pot file, generated on 2013-07-12 16:18:12 +# +# singular and plural forms for each entity type +# subject and object forms for each relation type +# (no object form for final or symmetric relation types) +msgid "Forum" +msgstr "" + +msgid "Forum_plural" +msgstr "" + +msgid "This Forum" +msgstr "" + +msgid "New Forum" +msgstr "" + +msgctxt "inlined:Forum.in_forum.object" +msgid "add a ForumThread" +msgstr "" + +msgctxt "inlined:Forum.in_forum.object" +msgid "ForumThread" +msgstr "" + +msgid "add ForumThread in_forum Forum object" +msgstr "" + +msgid "creating ForumThread (ForumThread in_forum Forum %(linkto)s)" +msgstr "" + +msgid "ForumThread" +msgstr "" + +msgid "ForumThread_plural" +msgstr "" + +msgid "This ForumThread" +msgstr "" + +msgid "New ForumThread" +msgstr "" + +msgid "content" +msgstr "" + +msgctxt "ForumThread" +msgid "content" +msgstr "" + +msgid "content_format" +msgstr "" + +msgctxt "ForumThread" +msgid "content_format" +msgstr "" + +msgctxt "Forum" +msgid "description" +msgstr "" + +msgctxt "Forum" +msgid "description_format" +msgstr "" + +msgid "in_forum" +msgstr "" + +msgctxt "ForumThread" +msgid "in_forum" +msgstr "" + +msgctxt "Forum" +msgid "in_forum_object" +msgstr "" + +msgid "in_forum_object" +msgstr "" + +msgid "interested_in" +msgstr "" + +msgctxt "CWUser" +msgid "interested_in" +msgstr "" + +msgctxt "ForumThread" +msgid "interested_in_object" +msgstr "" + +msgctxt "Forum" +msgid "interested_in_object" +msgstr "" + +msgid "interested_in_object" +msgstr "" + +msgid "nosy_list" +msgstr "" + +msgctxt "ForumThread" +msgid "nosy_list" +msgstr "" + +msgctxt "Forum" +msgid "nosy_list" +msgstr "" + +msgctxt "CWUser" +msgid "nosy_list_object" +msgstr "" + +msgid "nosy_list_object" +msgstr "" + +msgctxt "ForumThread" +msgid "title" +msgstr "" + +msgid "topic" +msgstr "" + +msgctxt "Forum" +msgid "topic" +msgstr "" + +msgid "Topic" +msgstr "" + +msgid "Description" +msgstr "" + +msgid "Number of threads" +msgstr "" + +msgid "Last activity" +msgstr "" + +msgid "" +"a long\n" +"tranlated line\n" +"hop." +msgstr "" + +msgid "Subject" +msgstr "" + +msgid "Created" +msgstr "" + +msgid "Answers" +msgstr "" + +msgid "Last answered" +msgstr "" + +msgid "This forum does not have any thread yet." +msgstr "" diff -r 31ed9dd946d1 -r ea32e964fbf8 devtools/test/data/cubes/i18ntestcube/schema.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/devtools/test/data/cubes/i18ntestcube/schema.py Tue Jul 30 20:31:57 2013 +0200 @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# copyright 2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr -- mailto:contact@logilab.fr +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with this program. If not, see . + +"""cubicweb-forum schema""" + +from yams.buildobjs import (String, RichString, EntityType, + RelationDefinition, SubjectRelation) +from yams.reader import context + +class Forum(EntityType): + topic = String(maxsize=50, required=True, unique=True) + description = RichString() + +class ForumThread(EntityType): + __permissions__ = { + 'read': ('managers', 'users'), + 'add': ('managers', 'users'), + 'update': ('managers', 'owners'), + 'delete': ('managers', 'owners') + } + title = String(required=True, fulltextindexed=True, maxsize=256) + content = RichString(required=True, fulltextindexed=True) + in_forum = SubjectRelation('Forum', cardinality='1*', inlined=True, + composite='object') +class interested_in(RelationDefinition): + subject = 'CWUser' + object = ('ForumThread', 'Forum') + +class nosy_list(RelationDefinition): + subject = ('Forum', 'ForumThread') + object = 'CWUser' diff -r 31ed9dd946d1 -r ea32e964fbf8 devtools/test/data/cubes/i18ntestcube/views.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/devtools/test/data/cubes/i18ntestcube/views.py Tue Jul 30 20:31:57 2013 +0200 @@ -0,0 +1,64 @@ +# copyright 2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr -- mailto:contact@logilab.fr +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with this program. If not, see . + +"""cubicweb-forum views/forms/actions/components for web ui""" + +from cubicweb import view +from cubicweb.predicates import is_instance +from cubicweb.web.views import primary, baseviews, uicfg +from cubicweb.web.views.uicfg import autoform_section as afs + +class MyAFS(uicfg.AutoformSectionRelationTags): + __select__ = is_instance('ForumThread') + +_myafs = MyAFS() + +# XXX useless ASA logilab.common.registry is fixed +_myafs.__module__ = "cubes.i18ntestcube.views" + +_myafs.tag_object_of(('*', 'in_forum', 'Forum'), 'main', 'inlined') + +afs.tag_object_of(('*', 'in_forum', 'Forum'), 'main', 'inlined') + + +class ForumSameETypeListView(baseviews.SameETypeListView): + __select__ = baseviews.SameETypeListView.__select__ & is_instance('Forum') + + def call(self, **kwargs): + _ = self._cw._ + _('Topic'), _('Description') + _('Number of threads'), _('Last activity') + _('''a long +tranlated line +hop.''') + + +class ForumLastActivity(view.EntityView): + __regid__ = 'forum_last_activity' + __select__ = view.EntityView.__select__ & is_instance('Forum') + + +class ForumPrimaryView(primary.PrimaryView): + __select__ = primary.PrimaryView.__select__ & is_instance('Forum') + + def render_entity_attributes(self, entity): + _ = self._cw._ + _('Subject'), _('Created'), _('Answers'), + _('Last answered') + _('This forum does not have any thread yet.') + +class ForumThreadPrimaryView(primary.PrimaryView): + __select__ = primary.PrimaryView.__select__ & is_instance('ForumThread') diff -r 31ed9dd946d1 -r ea32e964fbf8 devtools/test/unittest_i18n.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/devtools/test/unittest_i18n.py Tue Jul 30 20:31:57 2013 +0200 @@ -0,0 +1,79 @@ +# -*- coding: iso-8859-1 -*- +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of CubicWeb. +# +# CubicWeb is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# CubicWeb is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with CubicWeb. If not, see . +"""unit tests for i18n messages generator""" + +import os, os.path as osp +import sys + +from logilab.common.testlib import TestCase, unittest_main + +from cubicweb.cwconfig import CubicWebNoAppConfiguration + +DATADIR = osp.join(osp.abspath(osp.dirname(__file__)), 'data') + +def load_po(fname): + """load a po file and return a set of encountered (msgid, msgctx)""" + msgs = set() + msgid = msgctxt = None + for line in open(fname): + if line.strip() in ('', '#'): + continue + if line.startswith('msgstr'): + assert not (msgid, msgctxt) in msgs + msgs.add( (msgid, msgctxt) ) + msgid = msgctxt = None + elif line.startswith('msgid'): + msgid = line.split(' ', 1)[1][1:-1] + elif line.startswith('msgctx'): + msgctxt = line.split(' ', 1)[1][1: -1] + elif msgid is not None: + msgid += line[1:-1] + elif msgctxt is not None: + msgctxt += line[1:-1] + return msgs + + +class cubePotGeneratorTC(TestCase): + """test case for i18n pot file generator""" + + def setUp(self): + self._CUBES_PATH = CubicWebNoAppConfiguration.CUBES_PATH[:] + CubicWebNoAppConfiguration.CUBES_PATH.append(osp.join(DATADIR, 'cubes')) + CubicWebNoAppConfiguration.cls_adjust_sys_path() + + def tearDown(self): + CubicWebNoAppConfiguration.CUBES_PATH[:] = self._CUBES_PATH + + def test_i18ncube(self): + # MUST import here to make, since the import statement fire + # the cube paths setup (and then must occur after the setUp) + from cubicweb.devtools.devctl import update_cube_catalogs + cube = osp.join(DATADIR, 'cubes', 'i18ntestcube') + msgs = load_po(osp.join(cube, 'i18n', 'en.po.ref')) + update_cube_catalogs(cube) + newmsgs = load_po(osp.join(cube, 'i18n', 'en.po')) + self.assertEqual(msgs, newmsgs) + +if __name__ == '__main__': + # XXX dirty hack to make this test runnable using python (works + # fine with pytest, but not with python directly if this hack is + # not present) + # XXX to remove ASA logilab.common is fixed + sys.path.append('') + unittest_main() diff -r 31ed9dd946d1 -r ea32e964fbf8 doc/book/en/devrepo/devcore/dbapi.rst --- a/doc/book/en/devrepo/devcore/dbapi.rst Thu Jul 04 09:26:59 2013 +0200 +++ b/doc/book/en/devrepo/devcore/dbapi.rst Tue Jul 30 20:31:57 2013 +0200 @@ -29,6 +29,11 @@ Also, a rollback is automatically done if an error occurs during commit. +.. note:: + + A :exc:`ValidationError` has a `entity` attribute. In CubicWeb, + this atttribute is set to the entity's eid (not a reference to the + entity itself). Executing RQL queries from a view or a hook ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff -r 31ed9dd946d1 -r ea32e964fbf8 doc/book/en/devrepo/repo/hooks.rst --- a/doc/book/en/devrepo/repo/hooks.rst Thu Jul 04 09:26:59 2013 +0200 +++ b/doc/book/en/devrepo/repo/hooks.rst Tue Jul 30 20:31:57 2013 +0200 @@ -237,7 +237,7 @@ interface. Hence its constructor is different from the default Exception constructor. It accepts, positionally: -* an entity eid, +* an entity eid (**not the entity itself**), * a dict whose keys represent attribute (or relation) names and values an end-user facing message (hence properly translated) relating the diff -r 31ed9dd946d1 -r ea32e964fbf8 doc/book/en/devrepo/repo/sessions.rst --- a/doc/book/en/devrepo/repo/sessions.rst Thu Jul 04 09:26:59 2013 +0200 +++ b/doc/book/en/devrepo/repo/sessions.rst Tue Jul 30 20:31:57 2013 +0200 @@ -59,7 +59,7 @@ other credentials elements (calling `authentication_information`), giving the request object each time - * the default retriever (oddly named `LoginPasswordRetreiver`) + * the default retriever (named `LoginPasswordRetriever`) will in turn defer login and password fetching to the request object (which, depending on the authentication mode (`cookie` or `http`), will do the appropriate things and return a login diff -r 31ed9dd946d1 -r ea32e964fbf8 doc/book/en/devweb/request.rst --- a/doc/book/en/devweb/request.rst Thu Jul 04 09:26:59 2013 +0200 +++ b/doc/book/en/devweb/request.rst Tue Jul 30 20:31:57 2013 +0200 @@ -30,11 +30,7 @@ * `Session data handling` - * `session_data()`, returns a dictionary containing all the session data - * `get_session_data(key, default=None)`, returns a value associated to the given - key or the value `default` if the key is not defined - * `set_session_data(key, value)`, assign a value to a key - * `del_session_data(key)`, suppress the value associated to a key + * `session.data` is the dictionnary of the session data; it can be manipulated like an ordinary Python dictionnary * `Edition` (utilities for edition control): diff -r 31ed9dd946d1 -r ea32e964fbf8 entity.py --- a/entity.py Thu Jul 04 09:26:59 2013 +0200 +++ b/entity.py Tue Jul 30 20:31:57 2013 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -1181,8 +1181,7 @@ if v in select.defined_vars and v in cstr.mainvars) # rewrite constraint by constraint since we want a AND between # expressions. - rewriter.rewrite(select, [(varmap, (cstr,))], select.solutions, - args, existant) + rewriter.rewrite(select, [(varmap, (cstr,))], args, existant) # insert security RQL expressions granting the permission to 'add' the # relation into the rql syntax tree, if necessary rqlexprs = rdef.get_rqlexprs('add') @@ -1194,8 +1193,7 @@ varmap = dict((v, v) for v in (searchedvar.name, evar.name) if v in select.defined_vars) # rewrite all expressions at once since we want a OR between them. - rewriter.rewrite(select, [(varmap, rqlexprs)], select.solutions, - args, existant) + rewriter.rewrite(select, [(varmap, rqlexprs)], args, existant) # ensure we have an order defined if not select.orderby: select.add_sort_var(select.defined_vars[searchedvar.name]) diff -r 31ed9dd946d1 -r ea32e964fbf8 misc/migration/bootstrapmigration_repository.py --- a/misc/migration/bootstrapmigration_repository.py Thu Jul 04 09:26:59 2013 +0200 +++ b/misc/migration/bootstrapmigration_repository.py Tue Jul 30 20:31:57 2013 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -20,6 +20,7 @@ it should only include low level schema changes """ +from cubicweb import ConfigurationError from cubicweb.server.session import hooks_control from cubicweb.server import schemaserial as ss @@ -37,19 +38,19 @@ if applcubicwebversion < (3, 17, 0) and cubicwebversion >= (3, 17, 0): try: add_cube('sioc', update_database=False) - except ImportError: + except ConfigurationError: if not confirm('In cubicweb 3.17 sioc views have been moved to the sioc ' 'cube, which is not installed. Continue anyway?'): raise try: add_cube('embed', update_database=False) - except ImportError: + except ConfigurationError: if not confirm('In cubicweb 3.17 embedding views have been moved to the embed ' 'cube, which is not installed. Continue anyway?'): raise try: add_cube('geocoding', update_database=False) - except ImportError: + except ConfigurationError: if not confirm('In cubicweb 3.17 geocoding views have been moved to the geocoding ' 'cube, which is not installed. Continue anyway?'): raise @@ -72,7 +73,7 @@ from cubicweb import ExecutionError try: add_cube('localperms', update_database=False) - except ImportError: + except ConfigurationError: raise ExecutionError('In cubicweb 3.14, CWPermission and related stuff ' 'has been moved to cube localperms. Install it first.') diff -r 31ed9dd946d1 -r ea32e964fbf8 predicates.py --- a/predicates.py Thu Jul 04 09:26:59 2013 +0200 +++ b/predicates.py Tue Jul 30 20:31:57 2013 +0200 @@ -994,7 +994,11 @@ return 0 # relation not supported if self.action: if self.target_etype is not None: - rschema = rschema.role_rdef(entity.e_schema, self.target_etype, self.role) + try: + rschema = rschema.role_rdef(entity.e_schema, + self.target_etype, self.role) + except KeyError: + return 0 if self.role == 'subject': if not rschema.has_perm(entity._cw, self.action, fromeid=entity.eid): return 0 diff -r 31ed9dd946d1 -r ea32e964fbf8 rqlrewrite.py --- a/rqlrewrite.py Thu Jul 04 09:26:59 2013 +0200 +++ b/rqlrewrite.py Tue Jul 30 20:31:57 2013 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -33,6 +33,13 @@ from cubicweb import Unauthorized +def cleanup_solutions(rqlst, solutions): + for sol in solutions: + for vname in list(sol): + if not (vname in rqlst.defined_vars or vname in rqlst.aliases): + del sol[vname] + + def add_types_restriction(schema, rqlst, newroot=None, solutions=None): if newroot is None: assert solutions is None @@ -132,10 +139,69 @@ return newsolutions +def _add_noinvariant(noinvariant, restricted, select, nbtrees): + # a variable can actually be invariant if it has not been restricted for + # security reason or if security assertion hasn't modified the possible + # solutions for the query + for vname in restricted: + try: + var = select.defined_vars[vname] + except KeyError: + # this is an alias + continue + if nbtrees != 1 or len(var.stinfo['possibletypes']) != 1: + noinvariant.add(var) + + +def _expand_selection(terms, selected, aliases, select, newselect): + for term in terms: + for vref in term.iget_nodes(n.VariableRef): + if not vref.name in selected: + select.append_selected(vref) + colalias = newselect.get_variable(vref.name, len(aliases)) + aliases.append(n.VariableRef(colalias)) + selected.add(vref.name) + +def _has_multiple_cardinality(etypes, rdef, ttypes_func, cardindex): + """return True if relation definitions from entity types (`etypes`) to + target types returned by the `ttypes_func` function all have single (1 or ?) + cardinality. + """ + for etype in etypes: + for ttype in ttypes_func(etype): + if rdef(etype, ttype).cardinality[cardindex] in '+*': + return True + return False + +def _compatible_relation(relations, stmt, sniprel): + """Search among given rql relation nodes if there is one 'compatible' with the + snippet relation, and return it if any, else None. + + A relation is compatible if it: + * belongs to the currently processed statement, + * isn't negged (i.e. direct parent is a NOT node) + * isn't optional (outer join) or similarly as the snippet relation + """ + for rel in relations: + # don't share if relation's scope is not the current statement + if rel.scope is not stmt: + continue + # don't share neged relation + if rel.neged(strict=True): + continue + # don't share optional relation, unless the snippet relation is + # similarly optional + if rel.optional and rel.optional != sniprel.optional: + continue + return rel + return None + + def iter_relations(stinfo): # this is a function so that test may return relation in a predictable order return stinfo['relations'] - stinfo['rhsrelations'] + class Unsupported(Exception): """raised when an rql expression can't be inserted in some rql query because it create an unresolvable query (eg no solutions found) @@ -164,13 +230,118 @@ if len(self.select.solutions) < len(self.solutions): raise Unsupported() - def rewrite(self, select, snippets, solutions, kwargs, existingvars=None): + def insert_local_checks(self, select, kwargs, + localchecks, restricted, noinvariant): + """ + select: the rql syntax tree Select node + kwargs: query arguments + + localchecks: {(('Var name', (rqlexpr1, rqlexpr2)), + ('Var name1', (rqlexpr1, rqlexpr23))): [solution]} + + (see querier._check_permissions docstring for more information) + + restricted: set of variable names to which an rql expression has to be + applied + + noinvariant: set of variable names that can't be considered has + invariant due to security reason (will be filed by this method) + """ + nbtrees = len(localchecks) + myunion = union = select.parent + # transform in subquery when len(localchecks)>1 and groups + if nbtrees > 1 and (select.orderby or select.groupby or + select.having or select.has_aggregat or + select.distinct or + select.limit or select.offset): + newselect = stmts.Select() + # only select variables in subqueries + origselection = select.selection + select.select_only_variables() + select.has_aggregat = False + # create subquery first so correct node are used on copy + # (eg ColumnAlias instead of Variable) + aliases = [n.VariableRef(newselect.get_variable(vref.name, i)) + for i, vref in enumerate(select.selection)] + selected = set(vref.name for vref in aliases) + # now copy original selection and groups + for term in origselection: + newselect.append_selected(term.copy(newselect)) + if select.orderby: + sortterms = [] + for sortterm in select.orderby: + sortterms.append(sortterm.copy(newselect)) + for fnode in sortterm.get_nodes(n.Function): + if fnode.name == 'FTIRANK': + # we've to fetch the has_text relation as well + var = fnode.children[0].variable + rel = iter(var.stinfo['ftirels']).next() + assert not rel.ored(), 'unsupported' + newselect.add_restriction(rel.copy(newselect)) + # remove relation from the orig select and + # cleanup variable stinfo + rel.parent.remove(rel) + var.stinfo['ftirels'].remove(rel) + var.stinfo['relations'].remove(rel) + # XXX not properly re-annotated after security insertion? + newvar = newselect.get_variable(var.name) + newvar.stinfo.setdefault('ftirels', set()).add(rel) + newvar.stinfo.setdefault('relations', set()).add(rel) + newselect.set_orderby(sortterms) + _expand_selection(select.orderby, selected, aliases, select, newselect) + select.orderby = () # XXX dereference? + if select.groupby: + newselect.set_groupby([g.copy(newselect) for g in select.groupby]) + _expand_selection(select.groupby, selected, aliases, select, newselect) + select.groupby = () # XXX dereference? + if select.having: + newselect.set_having([g.copy(newselect) for g in select.having]) + _expand_selection(select.having, selected, aliases, select, newselect) + select.having = () # XXX dereference? + if select.limit: + newselect.limit = select.limit + select.limit = None + if select.offset: + newselect.offset = select.offset + select.offset = 0 + myunion = stmts.Union() + newselect.set_with([n.SubQuery(aliases, myunion)], check=False) + newselect.distinct = select.distinct + solutions = [sol.copy() for sol in select.solutions] + cleanup_solutions(newselect, solutions) + newselect.set_possible_types(solutions) + # if some solutions doesn't need rewriting, insert original + # select as first union subquery + if () in localchecks: + myunion.append(select) + # we're done, replace original select by the new select with + # subqueries (more added in the loop below) + union.replace(select, newselect) + elif not () in localchecks: + union.remove(select) + for lcheckdef, lchecksolutions in localchecks.iteritems(): + if not lcheckdef: + continue + myrqlst = select.copy(solutions=lchecksolutions) + myunion.append(myrqlst) + # in-place rewrite + annotation / simplification + lcheckdef = [({var: 'X'}, rqlexprs) for var, rqlexprs in lcheckdef] + self.rewrite(myrqlst, lcheckdef, kwargs) + _add_noinvariant(noinvariant, restricted, myrqlst, nbtrees) + if () in localchecks: + select.set_possible_types(localchecks[()]) + add_types_restriction(self.schema, select) + _add_noinvariant(noinvariant, restricted, select, nbtrees) + self.annotate(union) + + def rewrite(self, select, snippets, kwargs, existingvars=None): """ snippets: (varmap, list of rql expression) with varmap a *tuple* (select var, snippet var) """ self.select = select - self.solutions = solutions + # remove_solutions used below require a copy + self.solutions = solutions = select.solutions[:] self.kwargs = kwargs self.u_varname = None self.removing_ambiguity = False @@ -195,6 +366,7 @@ select, solutions, newsolutions)) if len(newsolutions) > len(solutions): newsolutions = self.remove_ambiguities(snippets, newsolutions) + assert newsolutions select.solutions = newsolutions add_types_restriction(self.schema, select) @@ -235,9 +407,14 @@ subselect.solutions, self.kwargs) return if varexistsmap is None: - vi['rhs_rels'] = dict( (r.r_type, r) for r in sti['rhsrelations']) - vi['lhs_rels'] = dict( (r.r_type, r) for r in sti['relations'] - if not r in sti['rhsrelations']) + # build an index for quick access to relations + vi['rhs_rels'] = {} + for rel in sti['rhsrelations']: + vi['rhs_rels'].setdefault(rel.r_type, []).append(rel) + vi['lhs_rels'] = {} + for rel in sti['relations']: + if not rel in sti['rhsrelations']: + vi['lhs_rels'].setdefault(rel.r_type, []).append(rel) else: vi['rhs_rels'] = vi['lhs_rels'] = {} previous = None @@ -464,7 +641,6 @@ exists = var.references()[0].scope exists.add_constant_restriction(var, 'is', etype, 'etype') # recompute solutions - #select.annotated = False # avoid assertion error self.compute_solutions() # clean solutions according to initial solutions return remove_solutions(self.solutions, self.select.solutions, @@ -509,38 +685,34 @@ """if the snippet relation can be skipped to use a relation from the original query, return that relation node """ + if sniprel.neged(strict=True): + return None # no way rschema = self.schema.rschema(sniprel.r_type) stmt = self.current_statement() for vi in self.varinfos: try: if target == 'object': - orel = vi['lhs_rels'][sniprel.r_type] + orels = vi['lhs_rels'][sniprel.r_type] cardindex = 0 ttypes_func = rschema.objects rdef = rschema.rdef else: # target == 'subject': - orel = vi['rhs_rels'][sniprel.r_type] + orels = vi['rhs_rels'][sniprel.r_type] cardindex = 1 ttypes_func = rschema.subjects rdef = lambda x, y: rschema.rdef(y, x) except KeyError: # may be raised by vi['xhs_rels'][sniprel.r_type] - return None - # don't share if relation's statement is not the current statement - if orel.stmt is not stmt: - return None - # can't share neged relation or relations with different outer join - if (orel.neged(strict=True) or sniprel.neged(strict=True) - or (orel.optional and orel.optional != sniprel.optional)): - return None - # if cardinality is in '?1', we can ignore the snippet relation and use - # variable from the original query - for etype in vi['stinfo']['possibletypes']: - for ttype in ttypes_func(etype): - if rdef(etype, ttype).cardinality[cardindex] in '+*': - return None - break - return orel + continue + # if cardinality isn't in '?1', we can't ignore the snippet relation + # and use variable from the original query + if _has_multiple_cardinality(vi['stinfo']['possibletypes'], rdef, + ttypes_func, cardindex): + continue + orel = _compatible_relation(orels, stmt, sniprel) + if orel is not None: + return orel + return None def _use_orig_term(self, snippet_varname, term): key = (self.current_expr, self.varmap, snippet_varname) diff -r 31ed9dd946d1 -r ea32e964fbf8 schema.py --- a/schema.py Thu Jul 04 09:26:59 2013 +0200 +++ b/schema.py Tue Jul 30 20:31:57 2013 +0200 @@ -93,7 +93,7 @@ 'WorkflowTransition', 'BaseTransition', 'SubWorkflowExitPoint')) -INTERNAL_TYPES = set(('CWProperty', 'CWCache', 'ExternalUri', +INTERNAL_TYPES = set(('CWProperty', 'CWCache', 'ExternalUri', 'CWDataImport', 'CWSource', 'CWSourceHostConfig', 'CWSourceSchemaConfig')) @@ -739,6 +739,9 @@ return self.expression == other.expression return False + def __hash__(self): + return hash(self.expression) + def __deepcopy__(self, memo): return self.__class__(self.expression, self.mainvars) def __getstate__(self): diff -r 31ed9dd946d1 -r ea32e964fbf8 server/checkintegrity.py --- a/server/checkintegrity.py Thu Jul 04 09:26:59 2013 +0200 +++ b/server/checkintegrity.py Tue Jul 30 20:31:57 2013 +0200 @@ -57,6 +57,9 @@ pass eids[eid] = False return False + if etype not in session.vreg.schema: + eids[eid] = False + return False sqlcursor.execute('SELECT * FROM %s%s WHERE %seid=%s' % (SQL_PREFIX, etype, SQL_PREFIX, eid)) result = sqlcursor.fetchall() @@ -179,12 +182,12 @@ """check all entities registered in the repo system table""" print 'Checking entities system table' # system table but no source - msg = ' Entity with eid %s exists in the system table but in no source (autofix will delete the entity)' - cursor = session.system_sql('SELECT eid FROM entities;') + msg = ' Entity %s with eid %s exists in the system table but in no source (autofix will delete the entity)' + cursor = session.system_sql('SELECT eid,type FROM entities;') for row in cursor.fetchall(): - eid = row[0] + eid, etype = row if not has_eid(session, cursor, eid, eids): - sys.stderr.write(msg % eid) + sys.stderr.write(msg % (etype, eid)) if fix: session.system_sql('DELETE FROM entities WHERE eid=%s;' % eid) notify_fixed(fix) @@ -258,6 +261,12 @@ sys.stderr.write(msg % (rtype, target, eid)) notify_fixed(fix) +def bad_inlined_msg(rtype, parent_eid, eid, fix): + msg = (' An inlined relation %s from %s to %s exists but the latter ' + 'entity does not exist') + sys.stderr.write(msg % (rtype, parent_eid, eid)) + notify_fixed(fix) + def check_relations(schema, session, eids, fix=1): """check that eids referenced by relations are registered in the repo system @@ -271,13 +280,13 @@ for subjtype in rschema.subjects(): table = SQL_PREFIX + str(subjtype) column = SQL_PREFIX + str(rschema) - sql = 'SELECT %s FROM %s WHERE %s IS NOT NULL;' % ( + sql = 'SELECT cw_eid,%s FROM %s WHERE %s IS NOT NULL;' % ( column, table, column) cursor = session.system_sql(sql) for row in cursor.fetchall(): - eid = row[0] + parent_eid, eid = row if not has_eid(session, cursor, eid, eids): - bad_related_msg(rschema, 'object', eid, fix) + bad_inlined_msg(rschema, parent_eid, eid, fix) if fix: sql = 'UPDATE %s SET %s=NULL WHERE %s=%s;' % ( table, column, column, eid) @@ -366,6 +375,13 @@ eidcolumn = SQL_PREFIX + 'eid' msg = ' %s with eid %s has no %s (autofix will set it to now)' for etype, in cursor.fetchall(): + if etype not in session.vreg.schema: + sys.stderr.write('entities table references unknown type %s\n' % + etype) + if fix: + session.system_sql("DELETE FROM entities WHERE type = %(type)s", + {'type': etype}) + continue table = SQL_PREFIX + etype for rel, default in ( ('creation_date', datetime.now()), ('modification_date', datetime.now()), ): diff -r 31ed9dd946d1 -r ea32e964fbf8 server/edition.py --- a/server/edition.py Thu Jul 04 09:26:59 2013 +0200 +++ b/server/edition.py Tue Jul 30 20:31:57 2013 +0200 @@ -145,7 +145,7 @@ entity.e_schema.check(dict_protocol_catcher(entity), creation=creation, relations=relations) except ValidationError as ex: - ex.entity = self.entity + ex.entity = self.entity.eid raise def clone(self): diff -r 31ed9dd946d1 -r ea32e964fbf8 server/migractions.py --- a/server/migractions.py Thu Jul 04 09:26:59 2013 +0200 +++ b/server/migractions.py Tue Jul 30 20:31:57 2013 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -83,8 +83,8 @@ repo.vreg.register(ClearGroupMap) class ServerMigrationHelper(MigrationHelper): - """specific migration helper for server side migration scripts, - providind actions related to schema/data migration + """specific migration helper for server side migration scripts, + providing actions related to schema/data migration """ def __init__(self, config, schema, interactive=True, @@ -575,10 +575,24 @@ ' E eid %%(x)s,' ' %s' % ', '.join(restrictions), substs) + def possible_unique_constraint(ut): + for name in ut: + rschema = repoeschema.subjrels.get(name) + if rschema is None: + print 'dont add %s unique constraint on %s, missing %s' % ( + ','.join(ut), eschema, name) + return False + if not (rschema.final or rschema.final.inlined): + (eschema, name) + print 'dont add %s unique constraint on %s, %s is neither final nor inlined' % ( + ','.join(ut), eschema, name) + return False + return True for ut in unique_together - repo_unique_together: - rql, substs = ss.uniquetogether2rql(eschema, ut) - substs['x'] = repoeschema.eid - self.rqlexec(rql, substs) + if possible_unique_constraint(ut): + rql, substs = ss.uniquetogether2rql(eschema, ut) + substs['x'] = repoeschema.eid + self.rqlexec(rql, substs) def _synchronize_rdef_schema(self, subjtype, rtype, objtype, syncperms=True, syncprops=True): @@ -688,7 +702,10 @@ for rschema in newcubes_schema.relations(): existingschema = self.repo.schema.rschema(rschema.type) for (fromtype, totype) in rschema.rdefs: - if (fromtype, totype) in existingschema.rdefs: + # if rdef already exists or is infered from inheritance, + # don't add it + if (fromtype, totype) in existingschema.rdefs \ + or rschema.rdefs[(fromtype, totype)].infered: continue # check we should actually add the relation definition if not (fromtype in new or totype in new or rschema in new): @@ -929,6 +946,10 @@ `newname` is a string giving the name of the renamed entity type """ schema = self.repo.schema + if oldname not in schema: + print 'warning: entity type %s is unknown, skip renaming' % oldname + return + # if merging two existing entity types if newname in schema: assert oldname in ETYPE_NAME_MAP, \ '%s should be mapped to %s in ETYPE_NAME_MAP' % (oldname, @@ -1003,6 +1024,7 @@ # remove the old type: use rql to propagate deletion self.rqlexec('DELETE CWEType ET WHERE ET name %(on)s', {'on': oldname}, ask_confirm=False) + # elif simply renaming an entity type else: self.rqlexec('SET ET name %(newname)s WHERE ET is CWEType, ET name %(on)s', {'newname' : unicode(newname), 'on' : oldname}, diff -r 31ed9dd946d1 -r ea32e964fbf8 server/msplanner.py --- a/server/msplanner.py Thu Jul 04 09:26:59 2013 +0200 +++ b/server/msplanner.py Tue Jul 30 20:31:57 2013 +0200 @@ -100,8 +100,7 @@ from cubicweb import server from cubicweb.utils import make_uid -from cubicweb.rqlrewrite import add_types_restriction -from cubicweb.server.utils import cleanup_solutions +from cubicweb.rqlrewrite import add_types_restriction, cleanup_solutions from cubicweb.server.ssplanner import SSPlanner, OneFetchStep from cubicweb.server.mssteps import * diff -r 31ed9dd946d1 -r ea32e964fbf8 server/querier.py --- a/server/querier.py Thu Jul 04 09:26:59 2013 +0200 +++ b/server/querier.py Tue Jul 30 20:31:57 2013 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -24,18 +24,15 @@ from logilab.common.compat import any from rql import RQLSyntaxError, CoercionError -from rql.stmts import Union, Select -from rql.nodes import ETYPE_PYOBJ_MAP, etype_from_pyobj -from rql.nodes import (Relation, VariableRef, Constant, SubQuery, Function, - Exists, Not) +from rql.stmts import Union +from rql.nodes import ETYPE_PYOBJ_MAP, etype_from_pyobj, Relation, Exists, Not from yams import BASE_TYPES -from cubicweb import ValidationError, Unauthorized, QueryError, UnknownEid +from cubicweb import ValidationError, Unauthorized, UnknownEid from cubicweb import Binary, server from cubicweb.rset import ResultSet from cubicweb.utils import QueryCache, RepeatList -from cubicweb.server.utils import cleanup_solutions from cubicweb.server.rqlannotation import SQLGenAnnotator, set_qdata from cubicweb.server.ssplanner import READ_ONLY_RTYPES, add_types_restriction from cubicweb.server.edition import EditedEntity @@ -77,12 +74,13 @@ return session.describe(term.eval(args))[0] def check_read_access(session, rqlst, solution, args): - """check that the given user has credentials to access data read the - query + """Check that the given user has credentials to access data read by the + query and return a dict defining necessary "local checks" (i.e. rql + expression in read permission defined in the schema) where no group grants + him the permission. - return a dict defining necessary local checks (due to use of rql expression - in the schema), keys are variable names and values associated rql expression - for the associated variable with the given solution + Returned dictionary's keys are variable names and values the rql expressions + for this variable (with the given solution). """ # use `term_etype` since we've to deal with rewritten constants here, # when used as an external source by another repository. @@ -130,35 +128,6 @@ localchecks[varname] = erqlexprs return localchecks -def add_noinvariant(noinvariant, restricted, select, nbtrees): - # a variable can actually be invariant if it has not been restricted for - # security reason or if security assertion hasn't modified the possible - # solutions for the query - if nbtrees != 1: - for vname in restricted: - try: - noinvariant.add(select.defined_vars[vname]) - except KeyError: - # this is an alias - continue - else: - for vname in restricted: - try: - var = select.defined_vars[vname] - except KeyError: - # this is an alias - continue - if len(var.stinfo['possibletypes']) != 1: - noinvariant.add(var) - -def _expand_selection(terms, selected, aliases, select, newselect): - for term in terms: - for vref in term.iget_nodes(VariableRef): - if not vref.name in selected: - select.append_selected(vref) - colalias = newselect.get_variable(vref.name, len(aliases)) - aliases.append(VariableRef(colalias)) - selected.add(vref.name) # Plans ####################################################################### @@ -258,9 +227,8 @@ self.args = args cached = True else: - noinvariant = set() with self.session.security_enabled(read=False): - self._insert_security(union, noinvariant) + noinvariant = self._insert_security(union) if key is not None: self.session.transaction_data[key] = (union, self.args) else: @@ -272,121 +240,39 @@ if union.has_text_query: self.cache_key = None - def _insert_security(self, union, noinvariant): + def _insert_security(self, union): + noinvariant = set() for select in union.children[:]: for subquery in select.with_: - self._insert_security(subquery.query, noinvariant) + self._insert_security(subquery.query) localchecks, restricted = self._check_permissions(select) if any(localchecks): - rewrite = self.session.rql_rewriter.rewrite - nbtrees = len(localchecks) - myunion = union - # transform in subquery when len(localchecks)>1 and groups - if nbtrees > 1 and (select.orderby or select.groupby or - select.having or select.has_aggregat or - select.distinct or - select.limit or select.offset): - newselect = Select() - # only select variables in subqueries - origselection = select.selection - select.select_only_variables() - select.has_aggregat = False - # create subquery first so correct node are used on copy - # (eg ColumnAlias instead of Variable) - aliases = [VariableRef(newselect.get_variable(vref.name, i)) - for i, vref in enumerate(select.selection)] - selected = set(vref.name for vref in aliases) - # now copy original selection and groups - for term in origselection: - newselect.append_selected(term.copy(newselect)) - if select.orderby: - sortterms = [] - for sortterm in select.orderby: - sortterms.append(sortterm.copy(newselect)) - for fnode in sortterm.get_nodes(Function): - if fnode.name == 'FTIRANK': - # we've to fetch the has_text relation as well - var = fnode.children[0].variable - rel = iter(var.stinfo['ftirels']).next() - assert not rel.ored(), 'unsupported' - newselect.add_restriction(rel.copy(newselect)) - # remove relation from the orig select and - # cleanup variable stinfo - rel.parent.remove(rel) - var.stinfo['ftirels'].remove(rel) - var.stinfo['relations'].remove(rel) - # XXX not properly re-annotated after security insertion? - newvar = newselect.get_variable(var.name) - newvar.stinfo.setdefault('ftirels', set()).add(rel) - newvar.stinfo.setdefault('relations', set()).add(rel) - newselect.set_orderby(sortterms) - _expand_selection(select.orderby, selected, aliases, select, newselect) - select.orderby = () # XXX dereference? - if select.groupby: - newselect.set_groupby([g.copy(newselect) for g in select.groupby]) - _expand_selection(select.groupby, selected, aliases, select, newselect) - select.groupby = () # XXX dereference? - if select.having: - newselect.set_having([g.copy(newselect) for g in select.having]) - _expand_selection(select.having, selected, aliases, select, newselect) - select.having = () # XXX dereference? - if select.limit: - newselect.limit = select.limit - select.limit = None - if select.offset: - newselect.offset = select.offset - select.offset = 0 - myunion = Union() - newselect.set_with([SubQuery(aliases, myunion)], check=False) - newselect.distinct = select.distinct - solutions = [sol.copy() for sol in select.solutions] - cleanup_solutions(newselect, solutions) - newselect.set_possible_types(solutions) - # if some solutions doesn't need rewriting, insert original - # select as first union subquery - if () in localchecks: - myunion.append(select) - # we're done, replace original select by the new select with - # subqueries (more added in the loop below) - union.replace(select, newselect) - elif not () in localchecks: - union.remove(select) - for lcheckdef, lchecksolutions in localchecks.iteritems(): - if not lcheckdef: - continue - myrqlst = select.copy(solutions=lchecksolutions) - myunion.append(myrqlst) - # in-place rewrite + annotation / simplification - lcheckdef = [({var: 'X'}, rqlexprs) for var, rqlexprs in lcheckdef] - rewrite(myrqlst, lcheckdef, lchecksolutions, self.args) - add_noinvariant(noinvariant, restricted, myrqlst, nbtrees) - if () in localchecks: - select.set_possible_types(localchecks[()]) - add_types_restriction(self.schema, select) - add_noinvariant(noinvariant, restricted, select, nbtrees) - self.rqlhelper.annotate(union) + self.session.rql_rewriter.insert_local_checks( + select, self.args, localchecks, restricted, noinvariant) + return noinvariant def _check_permissions(self, rqlst): - """return a dict defining "local checks", e.g. RQLExpression defined in - the schema that should be inserted in the original query - - solutions where a variable has a type which the user can't definitly read - are removed, else if the user may read it (eg if an rql expression is - defined for the "read" permission of the related type), the local checks - dict for the solution is updated + """Return a dict defining "local checks", i.e. RQLExpression defined in + the schema that should be inserted in the original query, together with + a set of variable names which requires some security to be inserted. - return a dict with entries for each different local check necessary, - with associated solutions as value. A local check is defined by a list - of 2-uple, with variable name as first item and the necessary rql - expression as second item for each variable which has to be checked. - So solutions which don't require local checks will be associated to - the empty tuple key. + Solutions where a variable has a type which the user can't definitly + read are removed, else if the user *may* read it (i.e. if an rql + expression is defined for the "read" permission of the related type), + the local checks dict is updated. - note: rqlst should not have been simplified at this point + The local checks dict has entries for each different local check + necessary, with associated solutions as value, a local check being + defined by a list of 2-uple (variable name, rql expressions) for each + variable which has to be checked. Solutions which don't require local + checks will be associated to the empty tuple key. + + Note rqlst should not have been simplified at this point. """ session = self.session msgs = [] - neweids = session.transaction_data.get('neweids', ()) + # dict(varname: eid), allowing to check rql expression for variables + # which have a known eid varkwargs = {} if not session.transaction_data.get('security-rqlst-cache'): for var in rqlst.defined_vars.itervalues(): @@ -414,20 +300,27 @@ rqlexprs = localcheck.pop(varname) except KeyError: continue - if eid in neweids: + # if entity has been added in the current transaction, the + # user can read it whatever rql expressions are associated + # to its type + if session.added_in_transaction(eid): continue for rqlexpr in rqlexprs: if rqlexpr.check(session, eid): break else: raise Unauthorized('No read acces on %r with eid %i.' % (var, eid)) + # mark variables protected by an rql expression restricted_vars.update(localcheck) - localchecks.setdefault(tuple(localcheck.iteritems()), []).append(solution) + # turn local check into a dict key + localcheck = tuple(sorted(localcheck.iteritems())) + localchecks.setdefault(localcheck, []).append(solution) # raise Unautorized exception if the user can't access to any solution if not newsolutions: raise Unauthorized('\n'.join(msgs)) + # if there is some message, solutions have been modified and must be + # reconsidered by the syntax treee if msgs: - # (else solutions have not been modified) rqlst.set_possible_types(newsolutions) return localchecks, restricted_vars @@ -728,7 +621,7 @@ if args: # different SQL generated when some argument is None or not (IS # NULL). This should be considered when computing sql cache key - cachekey += tuple(sorted([k for k,v in args.iteritems() + cachekey += tuple(sorted([k for k, v in args.iteritems() if v is None])) # make an execution plan plan = self.plan_factory(rqlst, args, session) diff -r 31ed9dd946d1 -r ea32e964fbf8 server/repository.py --- a/server/repository.py Thu Jul 04 09:26:59 2013 +0200 +++ b/server/repository.py Tue Jul 30 20:31:57 2013 +0200 @@ -239,7 +239,7 @@ # load schema from the file system if not config.creating: self.warning("set fs instance'schema") - self.set_schema(config.load_schema()) + self.set_schema(config.load_schema(expand_cubes=True)) else: # normal start: load the instance schema from the database self.info('loading schema from the repository') @@ -352,9 +352,8 @@ except Exception as ex: import traceback traceback.print_exc() - raise Exception('Is the database initialised ? (cause: %s)' % - (ex.args and ex.args[0].strip() or 'unknown')), \ - None, sys.exc_info()[-1] + raise (Exception('Is the database initialised ? (cause: %s)' % ex), + None, sys.exc_info()[-1]) return appschema def _prepare_startup(self): @@ -794,16 +793,7 @@ # Zeroed to avoid useless overhead with pyro rset._rqlst = None return rset - except (Unauthorized, RQLSyntaxError): - raise - except ValidationError as ex: - # need ValidationError normalization here so error may pass - # through pyro - if hasattr(ex.entity, 'eid'): - ex.entity = ex.entity.eid # error raised by yams - args = list(ex.args) - args[0] = ex.entity - ex.args = tuple(args) + except (ValidationError, Unauthorized, RQLSyntaxError): raise except Exception: # FIXME: check error to catch internal errors diff -r 31ed9dd946d1 -r ea32e964fbf8 server/serverconfig.py --- a/server/serverconfig.py Thu Jul 04 09:26:59 2013 +0200 +++ b/server/serverconfig.py Tue Jul 30 20:31:57 2013 +0200 @@ -361,7 +361,7 @@ self.init_cubes(self.expand_cubes(origcubes)) schema = CubicWebSchemaLoader().load(self, **kwargs) if expand_cubes: - # restaure original value + # restore original value self._cubes = origcubes return schema diff -r 31ed9dd946d1 -r ea32e964fbf8 server/session.py --- a/server/session.py Thu Jul 04 09:26:59 2013 +0200 +++ b/server/session.py Tue Jul 30 20:31:57 2013 +0200 @@ -1259,6 +1259,9 @@ self.pending_operations[:] = processed self.debug('precommit session %s done', self.id) except BaseException: + # save exception context, it may be clutered below by + # exception in revert_* event + exc_info = sys.exc_info() # if error on [pre]commit: # # * set .failed = True on the operation causing the failure @@ -1284,7 +1287,7 @@ # read-only property. self.pending_operations[:] = processed + self.pending_operations self.rollback(free_cnxset) - raise + raise exc_info[0], exc_info[1], exc_info[2] self.cnxset.commit() self.commit_state = 'postcommit' if debug: diff -r 31ed9dd946d1 -r ea32e964fbf8 server/sources/__init__.py --- a/server/sources/__init__.py Thu Jul 04 09:26:59 2013 +0200 +++ b/server/sources/__init__.py Tue Jul 30 20:31:57 2013 +0200 @@ -21,6 +21,7 @@ import itertools from os.path import join, splitext +from time import time from datetime import datetime, timedelta from logging import getLogger @@ -37,7 +38,9 @@ def dbg_st_search(uri, union, varmap, args, cachekey=None, prefix='rql for'): if server.DEBUG & server.DBG_RQL: + global t print ' %s %s source: %s' % (prefix, uri, repr(union.as_string())) + t = time() if varmap: print ' using varmap', varmap if server.DEBUG & server.DBG_MORE: @@ -51,9 +54,10 @@ def dbg_results(results): if server.DEBUG & server.DBG_RQL: if len(results) > 10: - print ' -->', results[:10], '...', len(results) + print ' -->', results[:10], '...', len(results), else: - print ' -->', results + print ' -->', results, + print 'time: ', time() - t # return true so it can be used as assertion (and so be killed by python -O) return True diff -r 31ed9dd946d1 -r ea32e964fbf8 server/sources/datafeed.py --- a/server/sources/datafeed.py Thu Jul 04 09:26:59 2013 +0200 +++ b/server/sources/datafeed.py Tue Jul 30 20:31:57 2013 +0200 @@ -78,6 +78,12 @@ 'help': ('Time before logs from datafeed imports are deleted.'), 'group': 'datafeed-source', 'level': 2, }), + ('http-timeout', + {'type': 'time', + 'default': '1min', + 'help': ('Timeout of HTTP GET requests, when synchronizing a source.'), + 'group': 'datafeed-source', 'level': 2, + }), ) def check_config(self, source_entity): @@ -101,6 +107,7 @@ super(DataFeedSource, self).update_config(source_entity, typed_config) self.synchro_interval = timedelta(seconds=typed_config['synchronization-interval']) self.max_lock_lifetime = timedelta(seconds=typed_config['max-lock-lifetime']) + self.http_timeout = typed_config['http-timeout'] def init(self, activated, source_entity): super(DataFeedSource, self).init(activated, source_entity) @@ -438,7 +445,7 @@ if url.startswith('http'): url = self.normalize_url(url) self.source.info('GET %s', url) - stream = _OPENER.open(url) + stream = _OPENER.open(url, timeout=self.http_timeout) elif url.startswith('file://'): stream = open(url[7:]) else: @@ -454,7 +461,8 @@ def is_deleted(self, extid, etype, eid): if extid.startswith('http'): try: - _OPENER.open(self.normalize_url(extid)) # XXX HTTP HEAD request + _OPENER.open(self.normalize_url(extid), # XXX HTTP HEAD request + timeout=self.http_timeout) except urllib2.HTTPError as ex: if ex.code == 404: return True diff -r 31ed9dd946d1 -r ea32e964fbf8 server/sources/rql2sql.py --- a/server/sources/rql2sql.py Thu Jul 04 09:26:59 2013 +0200 +++ b/server/sources/rql2sql.py Tue Jul 30 20:31:57 2013 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -62,8 +62,8 @@ Not, Comparison, ColumnAlias, Relation, SubQuery, Exists) from cubicweb import QueryError +from cubicweb.rqlrewrite import cleanup_solutions from cubicweb.server.sqlutils import SQL_PREFIX -from cubicweb.server.utils import cleanup_solutions ColumnAlias._q_invariant = False # avoid to check for ColumnAlias / Variable diff -r 31ed9dd946d1 -r ea32e964fbf8 server/test/unittest_migractions.py --- a/server/test/unittest_migractions.py Thu Jul 04 09:26:59 2013 +0200 +++ b/server/test/unittest_migractions.py Tue Jul 30 20:31:57 2013 +0200 @@ -370,14 +370,14 @@ 'X description D')[0][0], 'title for this person') rinorder = [n for n, in cursor.execute( - 'Any N ORDERBY O WHERE X is CWAttribute, X relation_type RT, RT name N,' + 'Any N ORDERBY O,N WHERE X is CWAttribute, X relation_type RT, RT name N,' 'X from_entity FE, FE name "Personne",' 'X ordernum O')] expected = [u'nom', u'prenom', u'sexe', u'promo', u'ass', u'adel', u'titre', - u'web', u'tel', u'fax', u'datenaiss', u'tzdatenaiss', u'test', + u'web', u'tel', u'fax', u'datenaiss', u'test', u'tzdatenaiss', u'description', u'firstname', u'creation_date', u'cwuri', u'modification_date'] - self.assertEqual(rinorder, expected) + self.assertEqual(expected, rinorder) # test permissions synchronization #################################### # new rql expr to add note entity diff -r 31ed9dd946d1 -r ea32e964fbf8 server/test/unittest_msplanner.py --- a/server/test/unittest_msplanner.py Thu Jul 04 09:26:59 2013 +0200 +++ b/server/test/unittest_msplanner.py Tue Jul 30 20:31:57 2013 +0200 @@ -801,10 +801,8 @@ [{'C': 'Division', 'E': 'Note', 'D': 'Affaire', 'G': 'SubDivision', 'F': 'Societe', 'I': 'Affaire', 'H': 'Affaire', 'J': 'Affaire', 'X': 'Affaire'}])], None, None, [self.system], {'E': 'table0.C0'}, []), ('OneFetchStep', - [('Any X WHERE X has_text "bla", EXISTS(X owned_by %s), X is Basket' % ueid, - [{'X': 'Basket'}]), - ('Any X WHERE X has_text "bla", EXISTS(X owned_by %s), X is CWUser' % ueid, - [{'X': 'CWUser'}]), + [('Any X WHERE X has_text "bla", EXISTS(X owned_by %s), X is IN(Basket, CWUser)' % ueid, + [{'X': 'Basket'}, {'X': 'CWUser'}]), ('Any X WHERE X has_text "bla", X is IN(Card, Comment, Division, Email, EmailThread, File, Folder, Note, Personne, Societe, SubDivision, Tag)', [{'X': 'Card'}, {'X': 'Comment'}, {'X': 'Division'}, {'X': 'Email'}, {'X': 'EmailThread'}, @@ -829,10 +827,8 @@ [{'C': 'Division', 'E': 'Note', 'D': 'Affaire', 'G': 'SubDivision', 'F': 'Societe', 'I': 'Affaire', 'H': 'Affaire', 'J': 'Affaire', 'X': 'Affaire'}])], [self.system], {'E': 'table1.C0'}, {'X': 'table0.C0'}, []), ('FetchStep', - [('Any X WHERE X has_text "bla", EXISTS(X owned_by %s), X is Basket' % ueid, - [{'X': 'Basket'}]), - ('Any X WHERE X has_text "bla", EXISTS(X owned_by %s), X is CWUser' % ueid, - [{'X': 'CWUser'}]), + [('Any X WHERE X has_text "bla", EXISTS(X owned_by %s), X is IN(Basket, CWUser)' % ueid, + [{'X': 'Basket'}, {'X': 'CWUser'}]), ('Any X WHERE X has_text "bla", X is IN(Card, Comment, Division, Email, EmailThread, File, Folder, Note, Personne, Societe, SubDivision, Tag)', [{'X': 'Card'}, {'X': 'Comment'}, {'X': 'Division'}, {'X': 'Email'}, {'X': 'EmailThread'}, @@ -909,12 +905,11 @@ self._test('Any MAX(X)', [('FetchStep', [('Any E WHERE E type "X", E is Note', [{'E': 'Note'}])], [self.cards, self.system], None, {'E': 'table1.C0'}, []), - ('FetchStep', [('Any X WHERE X is CWUser', [{'X': 'CWUser'}])], + ('FetchStep', [('Any X WHERE X is IN(CWUser)', [{'X': 'CWUser'}])], [self.ldap, self.system], None, {'X': 'table2.C0'}, []), ('UnionFetchStep', [ ('FetchStep', [('Any X WHERE EXISTS(%s use_email X), X is EmailAddress' % ueid, - [{'X': 'EmailAddress'}]), - ('Any X WHERE EXISTS(X owned_by %s), X is Basket' % ueid, [{'X': 'Basket'}])], + [{'X': 'EmailAddress'}])], [self.system], {}, {'X': 'table0.C0'}, []), ('UnionFetchStep', [('FetchStep', [('Any X WHERE X is IN(Card, Note, State)', @@ -942,11 +937,17 @@ {'X': 'Workflow'}, {'X': 'WorkflowTransition'}])], [self.system], {}, {'X': 'table0.C0'}, []), ]), - ('FetchStep', [('Any X WHERE EXISTS(X owned_by %s), X is CWUser' % ueid, [{'X': 'CWUser'}])], - [self.system], {'X': 'table2.C0'}, {'X': 'table0.C0'}, []), ('FetchStep', [('Any X WHERE (EXISTS(X owned_by %(ueid)s)) OR ((((EXISTS(D concerne C?, C owned_by %(ueid)s, C type "X", X identity D, C is Division, D is Affaire)) OR (EXISTS(H concerne G?, G owned_by %(ueid)s, G type "X", X identity H, G is SubDivision, H is Affaire))) OR (EXISTS(I concerne F?, F owned_by %(ueid)s, F type "X", X identity I, F is Societe, I is Affaire))) OR (EXISTS(J concerne E?, E owned_by %(ueid)s, X identity J, E is Note, J is Affaire))), X is Affaire' % {'ueid': ueid}, [{'C': 'Division', 'E': 'Note', 'D': 'Affaire', 'G': 'SubDivision', 'F': 'Societe', 'I': 'Affaire', 'H': 'Affaire', 'J': 'Affaire', 'X': 'Affaire'}])], [self.system], {'E': 'table1.C0'}, {'X': 'table0.C0'}, []), + ('UnionFetchStep', [ + ('FetchStep', [('Any X WHERE EXISTS(X owned_by %s), X is Basket' % ueid, + [{'X': 'Basket'}])], + [self.system], {}, {'X': 'table0.C0'}, []), + ('FetchStep', [('Any X WHERE EXISTS(X owned_by %s), X is CWUser' % ueid, + [{'X': 'CWUser'}])], + [self.system], {'X': 'table2.C0'}, {'X': 'table0.C0'}, []), + ]), ]), ('OneFetchStep', [('Any MAX(X)', ALL_SOLS)], None, None, [self.system], {'X': 'table0.C0'}, []) @@ -969,23 +970,13 @@ [self.cards, self.system], None, {'X': 'table1.C0'}, []), ('FetchStep', [('Any E WHERE E type "X", E is Note', [{'E': 'Note'}])], [self.cards, self.system], None, {'E': 'table2.C0'}, []), - ('FetchStep', [('Any X WHERE X is CWUser', [{'X': 'CWUser'}])], + ('FetchStep', [('Any X WHERE X is IN(CWUser)', [{'X': 'CWUser'}])], [self.ldap, self.system], None, {'X': 'table3.C0'}, []), ('UnionFetchStep', [('FetchStep', [('Any ET,X WHERE X is ET, EXISTS(%s use_email X), ET is CWEType, X is EmailAddress' % ueid, - [{'ET': 'CWEType', 'X': 'EmailAddress'}]), ('Any ET,X WHERE X is ET, EXISTS(X owned_by %s), ET is CWEType, X is Basket' % ueid, - [{'ET': 'CWEType', 'X': 'Basket'}])], + [{'ET': 'CWEType', 'X': 'EmailAddress'}]), + ], [self.system], {}, {'ET': 'table0.C0', 'X': 'table0.C1'}, []), - ('FetchStep', [('Any ET,X WHERE X is ET, (EXISTS(X owned_by %(ueid)s)) OR ((((EXISTS(D concerne C?, C owned_by %(ueid)s, C type "X", X identity D, C is Division, D is Affaire)) OR (EXISTS(H concerne G?, G owned_by %(ueid)s, G type "X", X identity H, G is SubDivision, H is Affaire))) OR (EXISTS(I concerne F?, F owned_by %(ueid)s, F type "X", X identity I, F is Societe, I is Affaire))) OR (EXISTS(J concerne E?, E owned_by %(ueid)s, X identity J, E is Note, J is Affaire))), ET is CWEType, X is Affaire' % {'ueid': ueid}, - [{'C': 'Division', 'E': 'Note', 'D': 'Affaire', - 'G': 'SubDivision', 'F': 'Societe', 'I': 'Affaire', - 'H': 'Affaire', 'J': 'Affaire', 'X': 'Affaire', - 'ET': 'CWEType'}])], - [self.system], {'E': 'table2.C0'}, {'ET': 'table0.C0', 'X': 'table0.C1'}, - []), - ('FetchStep', [('Any ET,X WHERE X is ET, EXISTS(X owned_by %s), ET is CWEType, X is CWUser' % ueid, - [{'ET': 'CWEType', 'X': 'CWUser'}])], - [self.system], {'X': 'table3.C0'}, {'ET': 'table0.C0', 'X': 'table0.C1'}, []), # extra UnionFetchStep could be avoided but has no cost, so don't care ('UnionFetchStep', [('FetchStep', [('Any ET,X WHERE X is ET, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWSource, CWUniqueTogetherConstraint, Comment, Division, Email, EmailPart, EmailThread, ExternalUri, File, Folder, Old, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)', @@ -1018,6 +1009,22 @@ {'ET': 'CWEType', 'X': 'State'}])], [self.system], {'X': 'table1.C0'}, {'ET': 'table0.C0', 'X': 'table0.C1'}, []), ]), + + ('FetchStep', [('Any ET,X WHERE X is ET, (EXISTS(X owned_by %(ueid)s)) OR ((((EXISTS(D concerne C?, C owned_by %(ueid)s, C type "X", X identity D, C is Division, D is Affaire)) OR (EXISTS(H concerne G?, G owned_by %(ueid)s, G type "X", X identity H, G is SubDivision, H is Affaire))) OR (EXISTS(I concerne F?, F owned_by %(ueid)s, F type "X", X identity I, F is Societe, I is Affaire))) OR (EXISTS(J concerne E?, E owned_by %(ueid)s, X identity J, E is Note, J is Affaire))), ET is CWEType, X is Affaire' % {'ueid': ueid}, + [{'C': 'Division', 'E': 'Note', 'D': 'Affaire', + 'G': 'SubDivision', 'F': 'Societe', 'I': 'Affaire', + 'H': 'Affaire', 'J': 'Affaire', 'X': 'Affaire', + 'ET': 'CWEType'}])], + [self.system], {'E': 'table2.C0'}, {'ET': 'table0.C0', 'X': 'table0.C1'}, + []), + ('UnionFetchStep', [ + ('FetchStep', [('Any ET,X WHERE X is ET, EXISTS(X owned_by %s), ET is CWEType, X is Basket' % ueid, + [{'ET': 'CWEType', 'X': 'Basket'}])], + [self.system], {}, {'ET': 'table0.C0', 'X': 'table0.C1'}, []), + ('FetchStep', [('Any ET,X WHERE X is ET, EXISTS(X owned_by %s), ET is CWEType, X is CWUser' % ueid, + [{'ET': 'CWEType', 'X': 'CWUser'}])], + [self.system], {'X': 'table3.C0'}, {'ET': 'table0.C0', 'X': 'table0.C1'}, []), + ]), ]), ('OneFetchStep', [('Any ET,COUNT(X) GROUPBY ET ORDERBY ET', X_ET_ALL_SOLS)], diff -r 31ed9dd946d1 -r ea32e964fbf8 server/utils.py --- a/server/utils.py Thu Jul 04 09:26:59 2013 +0200 +++ b/server/utils.py Tue Jul 30 20:31:57 2013 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -91,13 +91,6 @@ return rloop(seqin, []) -def cleanup_solutions(rqlst, solutions): - for sol in solutions: - for vname in list(sol): - if not (vname in rqlst.defined_vars or vname in rqlst.aliases): - del sol[vname] - - def eschema_eid(session, eschema): """get eid of the CWEType entity for the given yams type. You should use this because when schema has been loaded from the file-system, not from the diff -r 31ed9dd946d1 -r ea32e964fbf8 test/data/rewrite/schema.py --- a/test/data/rewrite/schema.py Thu Jul 04 09:26:59 2013 +0200 +++ b/test/data/rewrite/schema.py Tue Jul 30 20:31:57 2013 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -38,6 +38,7 @@ 'delete': ('managers', 'owners', ERQLExpression('U login L, X nom L')), 'add': ('managers', 'users',) } + nom = String() class Division(Societe): @@ -75,3 +76,9 @@ object = 'Affaire' inlined = True cardinality = '?*' + +class responsable(RelationDefinition): + subject = 'Societe' + object = 'CWUser' + inlined = True + cardinality = '1*' diff -r 31ed9dd946d1 -r ea32e964fbf8 test/unittest_dataimport.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unittest_dataimport.py Tue Jul 30 20:31:57 2013 +0200 @@ -0,0 +1,26 @@ +from StringIO import StringIO +from logilab.common.testlib import TestCase, unittest_main +from cubicweb import dataimport +class UcsvreaderTC(TestCase): + + def test_empty_lines_skipped(self): + stream = StringIO('''a,b,c,d, +1,2,3,4, +,,,, +,,,, +''') + self.assertEqual([[u'a', u'b', u'c', u'd', u''], + [u'1', u'2', u'3', u'4', u''], + ], + list(dataimport.ucsvreader(stream))) + stream.seek(0) + self.assertEqual([[u'a', u'b', u'c', u'd', u''], + [u'1', u'2', u'3', u'4', u''], + [u'', u'', u'', u'', u''], + [u'', u'', u'', u'', u''] + ], + list(dataimport.ucsvreader(stream, skip_empty=False))) + + +if __name__ == '__main__': + unittest_main() diff -r 31ed9dd946d1 -r ea32e964fbf8 test/unittest_entity.py --- a/test/unittest_entity.py Thu Jul 04 09:26:59 2013 +0200 +++ b/test/unittest_entity.py Tue Jul 30 20:31:57 2013 +0200 @@ -643,8 +643,10 @@ e.cw_attr_cache['data_format'] = 'text/html' e.cw_attr_cache['data_encoding'] = 'ascii' e._cw.transaction_data = {} # XXX req should be a session - self.assertEqual(e.cw_adapt_to('IFTIndexable').get_words(), - {'C': ['an', 'html', 'file', 'du', 'html', 'some', 'data']}) + words = e.cw_adapt_to('IFTIndexable').get_words() + words['C'].sort() + self.assertEqual({'C': sorted(['an', 'html', 'file', 'du', 'html', 'some', 'data'])}, + words) def test_nonregr_relation_cache(self): diff -r 31ed9dd946d1 -r ea32e964fbf8 test/unittest_predicates.py --- a/test/unittest_predicates.py Thu Jul 04 09:26:59 2013 +0200 +++ b/test/unittest_predicates.py Tue Jul 30 20:31:57 2013 +0200 @@ -203,6 +203,17 @@ select=select, filtered_variable=select.defined_vars['X']) self.assertEqual(score, 1) + def test_ambiguous(self): + # Ambiguous relations are : + # (Service, fabrique_par, Personne) and (Produit, fabrique_par, Usine) + # There used to be a crash here with a bad rdef choice in the strict + # checking case. + selector = relation_possible('fabrique_par', role='object', + target_etype='Personne', strict=True) + req = self.request() + usine = req.create_entity('Usine', lieu=u'here') + score = selector(None, req, rset=usine.as_rset()) + self.assertEqual(0, score) class MatchUserGroupsTC(CubicWebTC): def test_owners_group(self): diff -r 31ed9dd946d1 -r ea32e964fbf8 test/unittest_rqlrewrite.py --- a/test/unittest_rqlrewrite.py Thu Jul 04 09:26:59 2013 +0200 +++ b/test/unittest_rqlrewrite.py Tue Jul 30 20:31:57 2013 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -23,7 +23,7 @@ from cubicweb import Unauthorized, rqlrewrite from cubicweb.schema import RRQLExpression, ERQLExpression -from cubicweb.devtools import repotest, TestServerConfiguration +from cubicweb.devtools import repotest, TestServerConfiguration, BaseApptestConfiguration def setUpModule(*args): @@ -46,7 +46,8 @@ def eid_func_map(eid): return {1: 'CWUser', - 2: 'Card'}[eid] + 2: 'Card', + 3: 'Affaire'}[eid] def rewrite(rqlst, snippets_map, kwargs, existingvars=None): class FakeVReg: @@ -72,9 +73,7 @@ for snippet in exprs] snippets.append((dict([v]), rqlexprs)) rqlhelper.compute_solutions(rqlst.children[0], {'eid': eid_func_map}, kwargs=kwargs) - solutions = rqlst.children[0].solutions - rewriter.rewrite(rqlst.children[0], snippets, solutions, kwargs, - existingvars) + rewriter.rewrite(rqlst.children[0], snippets, kwargs, existingvars) test_vrefs(rqlst.children[0]) return rewriter.rewritten @@ -202,6 +201,17 @@ 'WITH LA BEING (Any LA WHERE (EXISTS(A created_by B, LA documented_by A)) OR (EXISTS(E created_by B, LA concerne E)), ' 'B eid %(D)s, LA is Affaire)') + + def test_ambiguous_optional_same_exprs(self): + """See #3013535""" + # see test of the same name in RewriteFullTC: original problem is + # unreproducible here because it actually lies in + # RQLRewriter.insert_local_checks + rqlst = parse('Any A,AR,X,CD WHERE A concerne X?, A ref AR, A eid %(a)s, X creation_date CD') + rewrite(rqlst, {('X', 'X'): ('X created_by U',),}, {'a': 3}) + self.assertEqual(rqlst.as_string(), + u'Any A,AR,X,CD WHERE A concerne X?, A ref AR, A eid %(a)s WITH X,CD BEING (Any X,CD WHERE X creation_date CD, EXISTS(X created_by B), B eid %(A)s, X is IN(Division, Note, Societe))') + def test_optional_var_inlined(self): c1 = ('X require_permission P') c2 = ('X inlined_card O, O require_permission P') @@ -292,6 +302,7 @@ self.assertEqual(rqlst.as_string(), "Any C WHERE C in_state STATE, C is Card, " "EXISTS(STATE name 'hop'), STATE is State") + def test_relation_optimization_3_rhs(self): snippet = ('TW? subworkflow_exit X, TW name "hop"') rqlst = parse('WorkflowTransition C WHERE C subworkflow_exit EXIT') @@ -308,6 +319,7 @@ self.assertEqual(rqlst.as_string(), "Any C WHERE C in_state STATE?, C is Card, " "EXISTS(C in_state A, A name 'hop', A is State), STATE is State") + def test_relation_non_optimization_1_rhs(self): snippet = ('TW subworkflow_exit X, TW name "hop"') rqlst = parse('SubWorkflowExitPoint EXIT WHERE C? subworkflow_exit EXIT') @@ -317,6 +329,21 @@ "EXISTS(A subworkflow_exit EXIT, A name 'hop', A is WorkflowTransition), " "C is WorkflowTransition") + def test_relation_non_optimization_2(self): + """See #3024730""" + # 'X inlined_note N' must not be shared with 'C inlined_note N' + # previously inserted, else this may introduce duplicated results, as N + # will then be shared by multiple EXISTS and so at SQL generation time, + # the table will be in the FROM clause of the outermost query + rqlst = parse('Any A,C WHERE A inlined_card C') + rewrite(rqlst, {('A', 'X'): ('X inlined_card C, C inlined_note N, N owned_by U',), + ('C', 'X'): ('X inlined_note N, N owned_by U',)}, {}) + self.assertEqual(rqlst.as_string(), + 'Any A,C WHERE A inlined_card C, D eid %(E)s, ' + 'EXISTS(C inlined_note B, B owned_by D, B is Note), ' + 'EXISTS(C inlined_note F, F owned_by D, F is Note), ' + 'A is Affaire, C is Card') + def test_unsupported_constraint_1(self): # CWUser doesn't have require_permission trinfo_constraint = ('X wf_info_for Y, Y require_permission P, P name "read"') @@ -459,5 +486,55 @@ rqlst = parse('Any A, R WHERE A ref R, S is Affaire') rewrite(rqlst, {('A', 'X'): (c_ok, c_bad)}, {}) + +from cubicweb.devtools.testlib import CubicWebTC +from logilab.common.decorators import classproperty + +class RewriteFullTC(CubicWebTC): + @classproperty + def config(cls): + return BaseApptestConfiguration(apphome=cls.datapath('rewrite')) + + def process(self, rql, args=None): + if args is None: + args = {} + querier = self.repo.querier + union = querier.parse(rql) + querier.solutions(self.session, union, args) + querier._annotate(union) + plan = querier.plan_factory(union, args, self.session) + plan.preprocess(union) + return union + + def test_ambiguous_optional_same_exprs(self): + """See #3013535""" + edef1 = self.schema['Societe'] + edef2 = self.schema['Division'] + edef3 = self.schema['Note'] + with self.temporary_permissions((edef1, {'read': (ERQLExpression('X owned_by U'),)}), + (edef2, {'read': (ERQLExpression('X owned_by U'),)}), + (edef3, {'read': (ERQLExpression('X owned_by U'),)})): + union = self.process('Any A,AR,X,CD WHERE A concerne X?, A ref AR, X creation_date CD') + self.assertEqual('Any A,AR,X,CD WHERE A concerne X?, A ref AR, A is Affaire ' + 'WITH X,CD BEING (Any X,CD WHERE X creation_date CD, ' + 'EXISTS(X owned_by %(A)s), X is IN(Division, Note, Societe))', + union.as_string()) + + + def test_xxxx(self): + edef1 = self.schema['Societe'] + edef2 = self.schema['Division'] + read_expr = ERQLExpression('X responsable E, U has_read_permission E') + with self.temporary_permissions((edef1, {'read': (read_expr,)}), + (edef2, {'read': (read_expr,)})): + union = self.process('Any X,AA,AC,AD ORDERBY AD DESC ' + 'WHERE X responsable E, X nom AA, ' + 'X responsable AC?, AC modification_date AD') + self.assertEqual('Any X,AA,AC,AD ORDERBY AD DESC ' + 'WHERE X responsable E, X nom AA, ' + 'X responsable AC?, AC modification_date AD, ' + 'AC is CWUser, E is CWUser, X is IN(Division, Societe)', + union.as_string()) + if __name__ == '__main__': unittest_main() diff -r 31ed9dd946d1 -r ea32e964fbf8 test/unittest_schema.py --- a/test/unittest_schema.py Thu Jul 04 09:26:59 2013 +0200 +++ b/test/unittest_schema.py Tue Jul 30 20:31:57 2013 +0200 @@ -216,6 +216,8 @@ 'value', 'wf_info_for', 'wikiid', 'workflow_of', 'tr_count'] + if config.cube_version('file') >= (1, 14, 0): + expected_relations.append('data_sha1hex') self.assertListEqual(sorted(expected_relations), relations) diff -r 31ed9dd946d1 -r ea32e964fbf8 web/application.py --- a/web/application.py Thu Jul 04 09:26:59 2013 +0200 +++ b/web/application.py Tue Jul 30 20:31:57 2013 +0200 @@ -343,17 +343,17 @@ def main_handle_request(self, req, path): - """Process and http request + """Process an http request Arguments are: - a Request object - path of the request object - It return the content of the http response. HTTP header and status are - are set on the Request Object. + It returns the content of the http response. HTTP header and status are + set on the Request object. """ if not isinstance(req, CubicWebRequestBase): - warn('[3.15] Application entry poin arguments are now (req, path) ' + warn('[3.15] Application entry point arguments are now (req, path) ' 'not (path, req)', DeprecationWarning, 2) req, path = path, req if req.authmode == 'http': @@ -399,7 +399,7 @@ # Wrong, absent or Reseted credential except AuthenticationError: # If there is an https url configured and - # the request do not used https, redirect to login form + # the request does not use https, redirect to login form https_url = self.vreg.config['https-url'] if https_url and req.base_url() != https_url: req.status_out = httplib.SEE_OTHER diff -r 31ed9dd946d1 -r ea32e964fbf8 web/data/cubicweb.facets.js --- a/web/data/cubicweb.facets.js Thu Jul 04 09:26:59 2013 +0200 +++ b/web/data/cubicweb.facets.js Tue Jul 30 20:31:57 2013 +0200 @@ -1,14 +1,13 @@ /** filter form, aka facets, javascript functions * * :organization: Logilab - * :copyright: 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. + * :copyright: 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. * :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr */ -var SELECTED_IMG = baseuri() + "data/black-check.png"; -var UNSELECTED_IMG = baseuri() + "data/no-check-no-border.png"; -var UNSELECTED_BORDER_IMG = baseuri() + "data/black-uncheck.png"; - +var SELECTED_IMG = DATA_URL + 'black-check.png'; +var UNSELECTED_IMG = DATA_URL + 'no-check-no-border.png'; +var UNSELECTED_BORDER_IMG = DATA_URL + 'black-uncheck.png'; function copyParam(origparams, newparams, param) { var index = $.inArray(param, origparams[0]); diff -r 31ed9dd946d1 -r ea32e964fbf8 web/formfields.py --- a/web/formfields.py Thu Jul 04 09:26:59 2013 +0200 +++ b/web/formfields.py Tue Jul 30 20:31:57 2013 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -1153,12 +1153,19 @@ elif not isinstance(values, list): values = (values,) eids = set() + rschema = form._cw.vreg.schema.rschema(self.name) for eid in values: if not eid or eid == INTERNAL_FIELD_VALUE: continue typed_eid = form.actual_eid(eid) + # if entity doesn't exist yet if typed_eid is None: - form._cw.data['pendingfields'].add( (form, self) ) + # inlined relations of to-be-created **subject entities** have + # to be handled separatly + if self.role == 'object' and rschema.inlined: + form._cw.data['pending_inlined'][eid].add( (form, self) ) + else: + form._cw.data['pending_others'].add( (form, self) ) return None eids.add(typed_eid) return eids diff -r 31ed9dd946d1 -r ea32e964fbf8 web/test/data/schema.py --- a/web/test/data/schema.py Thu Jul 04 09:26:59 2013 +0200 +++ b/web/test/data/schema.py Tue Jul 30 20:31:57 2013 +0200 @@ -24,7 +24,8 @@ from yams.constraints import IntervalBoundConstraint class Salesterm(EntityType): - described_by_test = SubjectRelation('File', cardinality='1*', composite='subject') + described_by_test = SubjectRelation('File', cardinality='1*', + composite='subject', inlined=True) amount = Int(constraints=[IntervalBoundConstraint(0, 100)]) reason = String(maxsize=20, vocabulary=[u'canceled', u'sold']) diff -r 31ed9dd946d1 -r ea32e964fbf8 web/test/unittest_views_actions.py --- a/web/test/unittest_views_actions.py Thu Jul 04 09:26:59 2013 +0200 +++ b/web/test/unittest_views_actions.py Tue Jul 30 20:31:57 2013 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -15,21 +15,32 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -""" -""" from logilab.common.testlib import unittest_main from cubicweb.devtools.testlib import CubicWebTC +from cubicweb.web.views import actions, uicfg class ActionsTC(CubicWebTC): def test_view_action(self): - req = self.request(__message='bla bla bla', vid='rss', rql='CWUser X') + req = self.request(vid='rss', rql='CWUser X') rset = self.execute('CWUser X') actions = self.vreg['actions'].poss_visible_objects(req, rset=rset) vaction = [action for action in actions if action.__regid__ == 'view'][0] self.assertEqual(vaction.url(), 'http://testing.fr/cubicweb/view?rql=CWUser%20X') + def test_has_editable_relations(self): + """ensure has_editable_relation predicate used by ModifyAction + return positive score if there is only some inlined forms + """ + use_email = self.schema['use_email'].rdefs['CWUser', 'EmailAddress'] + with self.temporary_permissions((use_email, {'add': ('guests',)}), + ): + with self.login('anon'): + req = self.request() + predicate = actions.has_editable_relation() + self.assertEqual(predicate(None, req, rset=req.user.as_rset()), + 1) if __name__ == '__main__': unittest_main() diff -r 31ed9dd946d1 -r ea32e964fbf8 web/test/unittest_views_basecontrollers.py --- a/web/test/unittest_views_basecontrollers.py Thu Jul 04 09:26:59 2013 +0200 +++ b/web/test/unittest_views_basecontrollers.py Tue Jul 30 20:31:57 2013 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -39,6 +39,8 @@ from cubicweb.web.views.basecontrollers import JSonController, xhtmlize, jsonize from cubicweb.web.views.ajaxcontroller import ajaxfunc, AjaxFunction import cubicweb.transaction as tx +from cubicweb.server.hook import Hook, Operation +from cubicweb.predicates import is_instance u = unicode @@ -171,6 +173,54 @@ email = e.use_email[0] self.assertEqual(email.address, 'dima@logilab.fr') + def test_create_mandatory_inlined(self): + req = self.request() + req.form = {'eid': ['X', 'Y'], '__maineid' : 'X', + + '__type:X': 'Salesterm', + '_cw_entity_fields:X': '', + + '__type:Y': 'File', + '_cw_entity_fields:Y': 'data-subject,described_by_test-object', + 'data-subject:Y': (u'coucou.txt', Binary('coucou')), + 'described_by_test-object:Y': 'X', + } + path, params = self.expect_redirect_handle_request(req, 'edit') + self.assertTrue(path.startswith('salesterm/'), path) + eid = path.split('/')[1] + salesterm = req.entity_from_eid(eid) + # The NOT NULL constraint of mandatory relation implies that the File + # must be created before the Salesterm, otherwise Salesterm insertion + # will fail. + # NOTE: sqlite does have NOT NULL constraint, unlike Postgres so the + # insertion does not fail and we have to check dumbly that File is + # created before. + self.assertGreater(salesterm.eid, salesterm.described_by_test[0].eid) + + def test_create_mandatory_inlined2(self): + req = self.request() + req.form = {'eid': ['X', 'Y'], '__maineid' : 'X', + + '__type:X': 'Salesterm', + '_cw_entity_fields:X': 'described_by_test-subject', + 'described_by_test-subject:X': 'Y', + + '__type:Y': 'File', + '_cw_entity_fields:Y': 'data-subject', + 'data-subject:Y': (u'coucou.txt', Binary('coucou')), + } + path, params = self.expect_redirect_handle_request(req, 'edit') + self.assertTrue(path.startswith('salesterm/'), path) + eid = path.split('/')[1] + salesterm = req.entity_from_eid(eid) + # The NOT NULL constraint of mandatory relation implies that the File + # must be created before the Salesterm, otherwise Salesterm insertion + # will fail. + # NOTE: sqlite does have NOT NULL constraint, unlike Postgres so the + # insertion does not fail and we have to check dumbly that File is + # created before. + self.assertGreater(salesterm.eid, salesterm.described_by_test[0].eid) + def test_edit_multiple_linked(self): req = self.request() peid = u(self.create_user(req, 'adim').eid) @@ -263,6 +313,7 @@ self.ctrl_publish(req) cm.exception.translate(unicode) self.assertEqual(cm.exception.errors, {'amount-subject': 'value 110 must be <= 100'}) + req = self.request(rollbackfirst=True) req.form = {'eid': ['X'], '__type:X': 'Salesterm', @@ -276,6 +327,67 @@ e = self.execute('Salesterm X').get_entity(0, 0) self.assertEqual(e.amount, 10) + def test_interval_bound_constraint_validateform(self): + """Test the FormValidatorController controller on entity with + constrained attributes""" + feid = self.execute('INSERT File X: X data_name "toto.txt", X data %(data)s', + {'data': Binary('yo')})[0][0] + seid = self.request().create_entity('Salesterm', amount=0, described_by_test=feid).eid + self.commit() + + # ensure a value that violate a constraint is properly detected + req = self.request(rollbackfirst=True) + req.form = {'eid': [unicode(seid)], + '__type:%s'%seid: 'Salesterm', + '_cw_entity_fields:%s'%seid: 'amount-subject', + 'amount-subject:%s'%seid: u'-10', + } + self.assertEqual(''''''%seid, self.ctrl_publish(req, 'validateform')) + + # ensure a value that comply a constraint is properly processed + req = self.request(rollbackfirst=True) + req.form = {'eid': [unicode(seid)], + '__type:%s'%seid: 'Salesterm', + '_cw_entity_fields:%s'%seid: 'amount-subject', + 'amount-subject:%s'%seid: u'20', + } + self.assertEqual('''''', self.ctrl_publish(req, 'validateform')) + self.assertEqual(20, self.execute('Any V WHERE X amount V, X eid %(eid)s', {'eid': seid})[0][0]) + + req = self.request(rollbackfirst=True) + req.form = {'eid': ['X'], + '__type:X': 'Salesterm', + '_cw_entity_fields:X': 'amount-subject,described_by_test-subject', + 'amount-subject:X': u'0', + 'described_by_test-subject:X': u(feid), + } + + # ensure a value that is modified in an operation on a modify + # hook works as it should (see + # https://www.cubicweb.org/ticket/2509729 ) + class MyOperation(Operation): + def precommit_event(self): + self.entity.cw_set(amount=-10) + class ValidationErrorInOpAfterHook(Hook): + __regid__ = 'valerror-op-after-hook' + __select__ = Hook.__select__ & is_instance('Salesterm') + events = ('after_add_entity',) + def __call__(self): + MyOperation(self._cw, entity=self.entity) + + with self.temporary_appobjects(ValidationErrorInOpAfterHook): + self.assertEqual('''''', self.ctrl_publish(req, 'validateform')) + + self.assertEqual('''''', self.ctrl_publish(req, 'validateform')) + def test_req_pending_insert(self): """make sure req's pending insertions are taken into account""" tmpgroup = self.request().create_entity('CWGroup', name=u"test") @@ -288,7 +400,6 @@ self.assertItemsEqual(usergroups, ['managers', 'test']) self.assertEqual(get_pending_inserts(req), []) - def test_req_pending_delete(self): """make sure req's pending deletions are taken into account""" user = self.user() diff -r 31ed9dd946d1 -r ea32e964fbf8 web/uihelper.py --- a/web/uihelper.py Thu Jul 04 09:26:59 2013 +0200 +++ b/web/uihelper.py Tue Jul 30 20:31:57 2013 +0200 @@ -1,4 +1,4 @@ -# copyright 2011-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2011-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -66,8 +66,8 @@ for funcname, tag in backward_compat_funcs: msg = ('[3.16] uihelper.%(name)s is deprecated, please use ' - 'web.uicfg.%(classname)s.%(name)s' % dict( - name=funcname, classname=tag.__class__.__name__)) + 'web.views.uicfg.%(rtagid)s.%(name)s' % dict( + name=funcname, rtagid=tag.__regid__)) globals()[funcname] = deprecated(msg)(getattr(tag, funcname)) diff -r 31ed9dd946d1 -r ea32e964fbf8 web/views/actions.py --- a/web/views/actions.py Thu Jul 04 09:26:59 2013 +0200 +++ b/web/views/actions.py Tue Jul 30 20:31:57 2013 +0200 @@ -50,8 +50,9 @@ entity=entity, mainform=False) for dummy in form.editable_relations(): return 1 - editableattrs = form.editable_attributes(strict=True) - for rschema, role in editableattrs: + for dummy in form.inlined_relations(): + return 1 + for dummy in form.editable_attributes(strict=True): return 1 return 0 diff -r 31ed9dd946d1 -r ea32e964fbf8 web/views/authentication.py --- a/web/views/authentication.py Thu Jul 04 09:26:59 2013 +0200 +++ b/web/views/authentication.py Tue Jul 30 20:31:57 2013 +0200 @@ -22,6 +22,7 @@ from threading import Lock from logilab.common.decorators import clear_cache +from logilab.common.deprecation import class_renamed from cubicweb import AuthenticationError, BadConnectionId from cubicweb.view import Component @@ -32,18 +33,18 @@ class NoAuthInfo(Exception): pass -class WebAuthInfoRetreiver(Component): +class WebAuthInfoRetriever(Component): __registry__ = 'webauth' order = None __abstract__ = True def authentication_information(self, req): - """retreive authentication information from the given request, raise + """retrieve authentication information from the given request, raise NoAuthInfo if expected information is not found. """ raise NotImplementedError() - def authenticated(self, retreiver, req, cnx, login, authinfo): + def authenticated(self, retriever, req, cnx, login, authinfo): """callback when return authentication information have opened a repository connection successfully. Take care req has no session attached yet, hence req.execute isn't available. @@ -66,12 +67,14 @@ def cleanup_authentication_information(self, req): """called when the retriever has returned some authentication information but we get an authentication error when using them, so it - get a chance to cleanup things (e.g. remove cookie) + get a chance to clean things up (e.g. remove cookie) """ pass +WebAuthInfoRetreiver = class_renamed('WebAuthInfoRetreiver', WebAuthInfoRetriever) -class LoginPasswordRetreiver(WebAuthInfoRetreiver): + +class LoginPasswordRetriever(WebAuthInfoRetriever): __regid__ = 'loginpwdauth' order = 10 @@ -90,6 +93,9 @@ def revalidate_login(self, req): return req.get_authorization()[0] +LoginPasswordRetreiver = class_renamed('LoginPasswordRetreiver', LoginPasswordRetriever) + + class RepositoryAuthenticationManager(AbstractAuthenticationManager): """authenticate user associated to a request and check session validity""" diff -r 31ed9dd946d1 -r ea32e964fbf8 web/views/autoform.py --- a/web/views/autoform.py Thu Jul 04 09:26:59 2013 +0200 +++ b/web/views/autoform.py Tue Jul 30 20:31:57 2013 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -145,8 +145,11 @@ class InlinedFormField(ff.Field): def __init__(self, view=None, **kwargs): kwargs.setdefault('label', None) + # don't add eidparam=True since this field doesn't actually hold the + # relation value (the subform does) hence should not be listed in + # _cw_entity_fields super(InlinedFormField, self).__init__(name=view.rtype, role=view.role, - eidparam=True, **kwargs) + **kwargs) self.view = view def render(self, form, renderer): diff -r 31ed9dd946d1 -r ea32e964fbf8 web/views/basetemplates.py --- a/web/views/basetemplates.py Thu Jul 04 09:26:59 2013 +0200 +++ b/web/views/basetemplates.py Tue Jul 30 20:31:57 2013 +0200 @@ -162,6 +162,7 @@ self.write_doctype() # explictly close the tag to avoid IE 6 bugs while browsing DOM self._cw.html_headers.define_var('BASE_URL', self._cw.base_url()) + self._cw.html_headers.define_var('DATA_URL', self._cw.datadir_url) w(u'\n' % (content_type, self._cw.encoding)) w(u'\n'.join(additional_headers) + u'\n') diff -r 31ed9dd946d1 -r ea32e964fbf8 web/views/editcontroller.py --- a/web/views/editcontroller.py Thu Jul 04 09:26:59 2013 +0200 +++ b/web/views/editcontroller.py Tue Jul 30 20:31:57 2013 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -20,8 +20,10 @@ __docformat__ = "restructuredtext en" from warnings import warn +from collections import defaultdict from logilab.common.deprecation import deprecated +from logilab.common.graph import ordered_nodes from rql.utils import rqlvar_maker @@ -101,6 +103,15 @@ self.kwargs[var] = eid return rql + def set_attribute(self, attr, value): + self.kwargs[attr] = value + self.edited.append('X %s %%(%s)s' % (attr, attr)) + + def set_inlined(self, relation, value): + self.kwargs[relation] = value + self.edited.append('X %s %s' % (relation, relation.upper())) + self.restrictions.append('%s eid %%(%s)s' % (relation.upper(), relation)) + class EditController(basecontrollers.ViewController): __regid__ = 'edit' @@ -120,6 +131,49 @@ self._default_publish() self.reset() + def _ordered_formparams(self): + """ Return form parameters dictionaries for each edited entity. + + We ensure that entities can be created in this order accounting for + mandatory inlined relations. + """ + req = self._cw + graph = {} + get_rschema = self._cw.vreg.schema.rschema + # minparams = 2, because at least __type and eid are needed + values_by_eid = dict((eid, req.extract_entity_params(eid, minparams=2)) + for eid in req.edited_eids()) + # iterate over all the edited entities + for eid, values in values_by_eid.iteritems(): + # add eid to the dependency graph + graph.setdefault(eid, set()) + # search entity's edited fields for mandatory inlined relation + for param in values['_cw_entity_fields'].split(','): + try: + rtype, role = param.split('-') + except ValueError: + # e.g. param='__type' + continue + rschema = get_rschema(rtype) + if rschema.inlined: + for target in rschema.targets(values['__type'], role): + rdef = rschema.role_rdef(values['__type'], target, role) + # if cardinality is 1 and if the target entity is being + # simultaneously edited, the current entity must be + # created before the target one + if rdef.cardinality[0] == '1': + target_eid = values[param] + if target_eid in values_by_eid: + # add dependency from the target entity to the + # current one + if role == 'object': + graph.setdefault(target_eid, set()).add(eid) + else: + graph.setdefault(eid, set()).add(target_eid) + break + for eid in reversed(ordered_nodes(graph)): + yield values_by_eid[eid] + def _default_publish(self): req = self._cw self.errors = [] @@ -130,22 +184,27 @@ req.set_shared_data('__maineid', form['__maineid'], txdata=True) # no specific action, generic edition self._to_create = req.data['eidmap'] = {} - self._pending_fields = req.data['pendingfields'] = set() + # those two data variables are used to handle relation from/to entities + # which doesn't exist at time where the entity is edited and that + # deserves special treatment + req.data['pending_inlined'] = defaultdict(set) + req.data['pending_others'] = set() try: - for eid in req.edited_eids(): - # __type and eid - formparams = req.extract_entity_params(eid, minparams=2) + for formparams in self._ordered_formparams(): eid = self.edit_entity(formparams) except (RequestError, NothingToEdit) as ex: if '__linkto' in req.form and 'eid' in req.form: self.execute_linkto() elif not ('__delete' in req.form or '__insert' in req.form): raise ValidationError(None, {None: unicode(ex)}) - # handle relations in newly created entities - if self._pending_fields: - for form, field in self._pending_fields: - self.handle_formfield(form, field) - # execute rql to set all relations + # all pending inlined relations to newly created entities have been + # treated now (pop to ensure there are no attempt to add new ones) + pending_inlined = req.data.pop('pending_inlined') + assert not pending_inlined, pending_inlined + # handle all other remaining relations now + for form_, field in req.data.pop('pending_others'): + self.handle_formfield(form_, field) + # then execute rql to set all relations for querydef in self.relations_rql: self._cw.execute(*querydef) # XXX this processes *all* pending operations of *all* entities @@ -176,10 +235,11 @@ def edit_entity(self, formparams, multiple=False): """edit / create / copy an entity and return its eid""" + req = self._cw etype = formparams['__type'] - entity = self._cw.vreg['etypes'].etype_class(etype)(self._cw) + entity = req.vreg['etypes'].etype_class(etype)(req) entity.eid = valerror_eid(formparams['eid']) - is_main_entity = self._cw.form.get('__maineid') == formparams['eid'] + is_main_entity = req.form.get('__maineid') == formparams['eid'] # let a chance to do some entity specific stuff entity.cw_adapt_to('IEditControl').pre_web_edit() # create a rql query from parameters @@ -188,12 +248,12 @@ # this will generate less rql queries and might be useful in # a few dark corners if is_main_entity: - formid = self._cw.form.get('__form_id', 'edition') + formid = req.form.get('__form_id', 'edition') else: # XXX inlined forms formid should be saved in a different formparams entry # inbetween, use cubicweb standard formid for inlined forms formid = 'edition' - form = self._cw.vreg['forms'].select(formid, self._cw, entity=entity) + form = req.vreg['forms'].select(formid, req, entity=entity) eid = form.actual_eid(entity.eid) try: editedfields = formparams['_cw_entity_fields'] @@ -203,10 +263,14 @@ warn('[3.13] _cw_edited_fields has been renamed _cw_entity_fields', DeprecationWarning) except KeyError: - raise RequestError(self._cw._('no edited fields specified for entity %s' % entity.eid)) + raise RequestError(req._('no edited fields specified for entity %s' % entity.eid)) form.formvalues = {} # init fields value cache for field in form.iter_modified_fields(editedfields, entity): self.handle_formfield(form, field, rqlquery) + # if there are some inlined field which were waiting for this entity's + # creation, add relevant data to the rqlquery + for form_, field in req.data['pending_inlined'].pop(entity.eid, ()): + rqlquery.set_inlined(field.name, form_.edited_entity.eid) if self.errors: errors = dict((f.role_name(), unicode(ex)) for f, ex in self.errors) raise ValidationError(valerror_eid(entity.eid), errors) @@ -218,8 +282,8 @@ self.notify_edited(entity) if '__delete' in formparams: # XXX deprecate? - todelete = self._cw.list_form_param('__delete', formparams, pop=True) - autoform.delete_relations(self._cw, todelete) + todelete = req.list_form_param('__delete', formparams, pop=True) + autoform.delete_relations(req, todelete) if '__cloned_eid' in formparams: entity.copy_relations(int(formparams['__cloned_eid'])) if is_main_entity: # only execute linkto for the main entity @@ -237,8 +301,7 @@ continue rschema = self._cw.vreg.schema.rschema(field.name) if rschema.final: - rqlquery.kwargs[field.name] = value - rqlquery.edited.append('X %s %%(%s)s' % (rschema, rschema)) + rqlquery.set_attribute(field.name, value) else: if form.edited_entity.has_eid(): origvalues = set(entity.eid for entity in form.edited_entity.related(field.name, field.role, entities=True)) @@ -251,19 +314,15 @@ elif form.edited_entity.has_eid(): self.handle_relation(form, field, value, origvalues) else: - self._pending_fields.add( (form, field) ) - + form._cw.data['pending_others'].add( (form, field) ) except ProcessFormError as exc: self.errors.append((field, exc)) def handle_inlined_relation(self, form, field, values, origvalues, rqlquery): """handle edition for the (rschema, x) relation of the given entity """ - attr = field.name if values: - rqlquery.kwargs[attr] = iter(values).next() - rqlquery.edited.append('X %s %s' % (attr, attr.upper())) - rqlquery.restrictions.append('%s eid %%(%s)s' % (attr.upper(), attr)) + rqlquery.set_inlined(field.name, iter(values).next()) elif form.edited_entity.has_eid(): self.handle_relation(form, field, values, origvalues) diff -r 31ed9dd946d1 -r ea32e964fbf8 web/views/staticcontrollers.py --- a/web/views/staticcontrollers.py Thu Jul 04 09:26:59 2013 +0200 +++ b/web/views/staticcontrollers.py Tue Jul 30 20:31:57 2013 +0200 @@ -27,6 +27,7 @@ import os.path as osp import hashlib import mimetypes +import threading from time import mktime from datetime import datetime, timedelta from logging import getLogger @@ -105,6 +106,7 @@ self._resources = {} self.config = config self.logger = getLogger('cubicweb.web') + self.lock = threading.Lock() def _resource(self, path): """get the resouce""" @@ -143,21 +145,32 @@ def concat_cached_filepath(self, paths): filepath = self.build_filepath(paths) if not self._up_to_date(filepath, paths): - with open(filepath, 'wb') as f: - for path in paths: - dirpath, rid = self._resource(path) - if rid is None: - # In production mode log an error, do not return a 404 - # XXX the erroneous content is cached anyway - self.logger.error('concatenated data url error: %r file ' - 'does not exist', path) - if self.config.debugmode: - raise NotFound(path) - else: - with open(osp.join(dirpath, rid), 'rb') as source: - for line in source: - f.write(line) - f.write('\n') + tmpfile = filepath + '.tmp' + try: + with self.lock: + if self._up_to_date(filepath, paths): + # first check could have raced with some other thread + # updating the file + return filepath + with open(tmpfile, 'wb') as f: + for path in paths: + dirpath, rid = self._resource(path) + if rid is None: + # In production mode log an error, do not return a 404 + # XXX the erroneous content is cached anyway + self.logger.error('concatenated data url error: %r file ' + 'does not exist', path) + if self.config.debugmode: + raise NotFound(path) + else: + with open(osp.join(dirpath, rid), 'rb') as source: + for line in source: + f.write(line) + f.write('\n') + os.rename(tmpfile, filepath) + except: + os.remove(tmpfile) + raise return filepath diff -r 31ed9dd946d1 -r ea32e964fbf8 web/views/tableview.py --- a/web/views/tableview.py Thu Jul 04 09:26:59 2013 +0200 +++ b/web/views/tableview.py Tue Jul 30 20:31:57 2013 +0200 @@ -201,10 +201,11 @@ facetsform.render(w, vid=self.view.__regid__, cssclass=cssclass, divid=self.view.domid) actions = [] - if self.add_view_actions: - actions = self.view.table_actions() - if self.display_filter and self.hide_filter and (facetsform or not generate_form): - actions += self.show_hide_filter_actions(not generate_form) + if self.display_actions: + if self.add_view_actions: + actions = self.view.table_actions() + if self.display_filter and self.hide_filter and (facetsform or not generate_form): + actions += self.show_hide_filter_actions(not generate_form) self.render_table(w, actions, self.view.paginable) if facetsform and self.display_filter == 'bottom': cssclass = u'hidden' if self.hide_filter else u''