# HG changeset patch # User David Douard # Date 1374824993 -7200 # Node ID 1d42a6ab670fc7a4ce2276cf9722ffd7cbe56f82 # Parent 9448215c73c4fab7b5fdf85e1c49ce7c1f20f221# Parent b922929badbab8295bb558eeb15aa6cfe4a83f27 [pkg] merge centos 3.17.1-2 in stable (prepare 3.17.4) diff -r b922929badba -r 1d42a6ab670f .hgtags --- a/.hgtags Fri Jul 26 09:47:22 2013 +0200 +++ b/.hgtags Fri Jul 26 09:49:53 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,10 +275,24 @@ ee860c51f56bd65c4f6ea363462c02700d1dab5a cubicweb-version-3.16.3 ee860c51f56bd65c4f6ea363462c02700d1dab5a cubicweb-debian-version-3.16.3-1 ee860c51f56bd65c4f6ea363462c02700d1dab5a cubicweb-centos-version-3.16.3-1 +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 +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 -041804bc48e91e440a5b573ceb0df5bf22863b80 cubicweb-version-3.16.4 -041804bc48e91e440a5b573ceb0df5bf22863b80 cubicweb-debian-version-3.16.4-1 -041804bc48e91e440a5b573ceb0df5bf22863b80 cubicweb-centos-version-3.16.4-1 +f98d1c46ed9fd5db5262cf5be1c8e159c90efc8b cubicweb-version-3.17.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 diff -r b922929badba -r 1d42a6ab670f __pkginfo__.py --- a/__pkginfo__.py Fri Jul 26 09:47:22 2013 +0200 +++ b/__pkginfo__.py Fri Jul 26 09:49:53 2013 +0200 @@ -22,7 +22,7 @@ modname = distname = "cubicweb" -numversion = (3, 17, 1) +numversion = (3, 17, 3) 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 b922929badba -r 1d42a6ab670f _exceptions.py --- a/_exceptions.py Fri Jul 26 09:47:22 2013 +0200 +++ b/_exceptions.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f cubicweb.spec --- a/cubicweb.spec Fri Jul 26 09:47:22 2013 +0200 +++ b/cubicweb.spec Fri Jul 26 09:49:53 2013 +0200 @@ -7,8 +7,8 @@ %endif Name: cubicweb -Version: 3.17.1 -Release: logilab.2%{?dist} +Version: 3.17.3 +Release: logilab.1%{?dist} Summary: CubicWeb is a semantic web application framework Source0: http://download.logilab.org/pub/cubicweb/cubicweb-%{version}.tar.gz License: LGPLv2+ diff -r b922929badba -r 1d42a6ab670f cwctl.py --- a/cwctl.py Fri Jul 26 09:47:22 2013 +0200 +++ b/cwctl.py Fri Jul 26 09:49:53 2013 +0200 @@ -439,7 +439,7 @@ chown(config.appdatahome, config['uid']) print '\n-> creation done for %s\n' % repr(config.apphome)[1:-1] if not self.config.no_db_create: - helper.postcreate(self.config.automatic) + helper.postcreate(self.config.automatic, self.config.config_level) def _handle_win32(self, config, appid): if sys.platform != 'win32': diff -r b922929badba -r 1d42a6ab670f dataimport.py --- a/dataimport.py Fri Jul 26 09:47:22 2013 +0200 +++ b/dataimport.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f debian.hardy/compat --- a/debian.hardy/compat Fri Jul 26 09:47:22 2013 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -5 diff -r b922929badba -r 1d42a6ab670f debian.hardy/rules --- a/debian.hardy/rules Fri Jul 26 09:47:22 2013 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,82 +0,0 @@ -#!/usr/bin/make -f -# Sample debian/rules that uses debhelper. -# GNU copyright 1997 to 1999 by Joey Hess. - -# Uncomment this to turn on verbose mode. -#export DH_VERBOSE=1 - -PY_VERSION:=$(shell pyversions -d) - -build: build-stamp -build-stamp: - dh_testdir - # XXX doesn't work if logilab-doctools, logilab-xml are not in build depends - # and I can't get pbuilder find them in its chroot :( - # cd doc && make - # FIXME cleanup and use sphinx-build as build-depends ? - NO_SETUPTOOLS=1 python setup.py build - touch build-stamp - -clean: - dh_testdir - dh_testroot - rm -f build-stamp configure-stamp - rm -rf build - #rm -rf debian/cubicweb-*/ - find . -name "*.pyc" -delete - rm -f $(basename $(wildcard debian/*.in)) - dh_clean - -install: build $(basename $(wildcard debian/*.in)) - dh_testdir - dh_testroot - dh_clean - dh_installdirs - - NO_SETUPTOOLS=1 python setup.py -q install --no-compile --prefix=debian/tmp/usr - - # Put all the python library and data in cubicweb-common - # and scripts in cubicweb-server - dh_install -vi - # cwctl in the cubicweb-ctl package - rm -f debian/cubicweb-common/usr/share/pyshared/cubicweb/cwctl.py - - - # Remove unittests directory (should be available in cubicweb-dev only) - rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/site-packages/cubicweb/server/test - rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/site-packages/cubicweb/hooks/test - rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/site-packages/cubicweb/sobjects/test - rm -rf debian/cubicweb-web/usr/lib/${PY_VERSION}/site-packages/cubicweb/web/test - rm -rf debian/cubicweb-twisted/usr/lib/${PY_VERSION}/site-packages/cubicweb/etwist/test - rm -rf debian/cubicweb-common/usr/lib/${PY_VERSION}/site-packages/cubicweb/ext/test - rm -rf debian/cubicweb-common/usr/lib/${PY_VERSION}/site-packages/cubicweb/entities/test - - # cubes directory must be managed as a valid python module - touch debian/cubicweb-common/usr/share/cubicweb/cubes/__init__.py - -%: %.in - sed "s/PY_VERSION/${PY_VERSION}/g" < $< > $@ - -# Build architecture-independent files here. -binary-indep: build install - dh_testdir - dh_testroot -i - dh_pycentral -i - dh_installinit -i -n --name cubicweb -u"defaults 99" - dh_installlogrotate -i - dh_installdocs -i -A README - dh_installman -i - dh_installchangelogs -i - dh_link -i - dh_compress -i -X.py -X.ini -X.xml - dh_fixperms -i - dh_installdeb -i - dh_gencontrol -i - dh_md5sums -i - dh_builddeb -i - -binary-arch: - -binary: binary-indep -.PHONY: build clean binary binary-indep binary-arch - diff -r b922929badba -r 1d42a6ab670f debian/changelog --- a/debian/changelog Fri Jul 26 09:47:22 2013 +0200 +++ b/debian/changelog Fri Jul 26 09:49:53 2013 +0200 @@ -1,3 +1,15 @@ +cubicweb (3.17.3-1) unstable; urgency=low + + * new upstream release + + -- David Douard Tue, 09 Jul 2013 15:10:16 +0200 + +cubicweb (3.17.2-1) unstable; urgency=low + + * new upstream release + + -- David Douard Thu, 13 Jun 2013 17:32:18 +0200 + cubicweb (3.17.1-1) unstable; urgency=low * new upstream release @@ -16,6 +28,18 @@ -- 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 + + -- David Douard Fri, 14 Jun 2013 16:01:47 +0200 + cubicweb (3.16.4-1) unstable; urgency=low * New upstream release diff -r b922929badba -r 1d42a6ab670f debian/rules --- a/debian/rules Fri Jul 26 09:47:22 2013 +0200 +++ b/debian/rules Fri Jul 26 09:49:53 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 diff -r b922929badba -r 1d42a6ab670f devtools/devctl.py --- a/devtools/devctl.py Fri Jul 26 09:47:22 2013 +0200 +++ b/devtools/devctl.py Fri Jul 26 09:49:53 2013 +0200 @@ -130,22 +130,20 @@ w('# singular and plural forms for each entity type\n') w('\n') vregdone = set() + afss = vreg['uicfg']['autoform_section'] + appearsin_addmenus = vreg['uicfg']['actionbox_appearsin_addmenu'] if libconfig is not None: 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'] + libappearsin_addmenus = libvreg['uicfg']['actionbox_appearsin_addmenu'] # prefill vregdone set list(_iter_vreg_objids(libvreg, vregdone)) else: libschema = {} - afs = vreg['uicfg'].select('autoform_section') - appearsin_addmenu = vreg['uicfg'].select('actionbox_appearsin_addmenu') for cstrtype in CONSTRAINTS: add_msg(w, cstrtype) done = set() @@ -169,32 +167,42 @@ 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) + for libafs in libafss: + 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)) + + def isinlib(eschema, rschema, role, tschema): + if libconfig is not None: + for libappearsin_addmenu in libappearsin_addmenus: + if (libappearsin_addmenu.etype_get( + eschema, rschema, role, tschema)): + if eschema in libschema and tschema in libschema: + return True + return False + + for appearsin_addmenu in appearsin_addmenus: + if appearsin_addmenu.etype_get(eschema, rschema, role, tschema): + if not isinlib(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) # XXX also generate "creating ...' messages for actions in the # addrelated submenu w('# subject and object forms for each relation type\n') diff -r b922929badba -r 1d42a6ab670f devtools/httptest.py --- a/devtools/httptest.py Fri Jul 26 09:47:22 2013 +0200 +++ b/devtools/httptest.py Fri Jul 26 09:49:53 2013 +0200 @@ -20,6 +20,7 @@ """ __docformat__ = "restructuredtext en" +import random import threading import socket import httplib @@ -46,6 +47,8 @@ .. see:: :func:`test.test_support.bind_port` """ + ports_scan = list(ports_scan) + random.shuffle(ports_scan) # lower the chance of race condition for port in ports_scan: try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) diff -r b922929badba -r 1d42a6ab670f devtools/test/data/cubes/__init__.py diff -r b922929badba -r 1d42a6ab670f devtools/test/data/cubes/i18ntestcube/__init__.py diff -r b922929badba -r 1d42a6ab670f 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 Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f 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 Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f 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 Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f devtools/test/data/cubes/i18ntestcube/views/__init__.py diff -r b922929badba -r 1d42a6ab670f devtools/test/data/cubes/i18ntestcube/views/primary.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/devtools/test/data/cubes/i18ntestcube/views/primary.py Fri Jul 26 09:49:53 2013 +0200 @@ -0,0 +1,53 @@ +# -*- 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 views/forms/actions/components for web ui""" + +from cubicweb.predicates import is_instance + +from cubicweb import view +from cubicweb.web.views import primary, baseviews + + +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 b922929badba -r 1d42a6ab670f devtools/test/data/cubes/i18ntestcube/views/uicfg.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/devtools/test/data/cubes/i18ntestcube/views/uicfg.py Fri Jul 26 09:49:53 2013 +0200 @@ -0,0 +1,30 @@ +# -*- 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 views/forms/actions/components for web ui""" + +from cubicweb.predicates import is_instance +from cubicweb.web.views import uicfg +from cubicweb.web.views.uicfg import autoform_section as afs + +class MyAFS(uicfg.AutoformSectionRelationTags): + __select__ = is_instance('ForumThread') + +_myafs = MyAFS() +_myafs.__module__ = "cubes.i18ntestcube.views.uicfg" + +_myafs.tag_object_of(('*', 'in_forum', 'Forum'), 'main', 'inlined') diff -r b922929badba -r 1d42a6ab670f devtools/test/unittest_i18n.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/devtools/test/unittest_i18n.py Fri Jul 26 09:49:53 2013 +0200 @@ -0,0 +1,74 @@ +# -*- 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): + msgs = [] + msgid = None + msgctxt = None + for line in open(fname): + if line.strip() in ('', '#'): + continue + if line.startswith('msgstr'): + msgs.append((msgid, msgctxt)) + msgid = None + msgctxt = None + elif line.startswith('msgid'): + msgid = line.split(' ', 1)[1][1:-1] + elif line.startswith('msgctx'): + msgctxt = line.split(' ', 1)[1][1: -1] + + else: + if msgctxt is not None: + msgctxt += line[1:-1] + elif msgid is not None: + msgid += line[1:-1] + return set(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__': + unittest_main() diff -r b922929badba -r 1d42a6ab670f doc/book/en/devrepo/devcore/dbapi.rst --- a/doc/book/en/devrepo/devcore/dbapi.rst Fri Jul 26 09:47:22 2013 +0200 +++ b/doc/book/en/devrepo/devcore/dbapi.rst Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f doc/book/en/devrepo/repo/hooks.rst --- a/doc/book/en/devrepo/repo/hooks.rst Fri Jul 26 09:47:22 2013 +0200 +++ b/doc/book/en/devrepo/repo/hooks.rst Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f doc/book/en/devrepo/repo/sessions.rst --- a/doc/book/en/devrepo/repo/sessions.rst Fri Jul 26 09:47:22 2013 +0200 +++ b/doc/book/en/devrepo/repo/sessions.rst Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f doc/book/en/devweb/request.rst --- a/doc/book/en/devweb/request.rst Fri Jul 26 09:47:22 2013 +0200 +++ b/doc/book/en/devweb/request.rst Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f entities/adapters.py --- a/entities/adapters.py Fri Jul 26 09:47:22 2013 +0200 +++ b/entities/adapters.py Fri Jul 26 09:49:53 2013 +0200 @@ -379,6 +379,7 @@ class IUserFriendlyError(view.EntityAdapter): __regid__ = 'IUserFriendlyError' __abstract__ = True + def __init__(self, *args, **kwargs): self.exc = kwargs.pop('exc') super(IUserFriendlyError, self).__init__(*args, **kwargs) @@ -386,11 +387,27 @@ class IUserFriendlyUniqueTogether(IUserFriendlyError): __select__ = match_exception(UniqueTogetherError) + def raise_user_exception(self): etype, rtypes = self.exc.args - msg = self._cw._('violates unique_together constraints (%s)') % ( - ', '.join([self._cw._(rtype) for rtype in rtypes])) - raise ValidationError(self.entity.eid, dict((col, msg) for col in rtypes)) + # Because of index name size limits (e.g: postgres around 64, + # sqlserver around 128), we cannot be sure of what we got, + # especially for the rtypes part. + # Hence we will try to validate them, and handle invalid ones + # in the most user-friendly manner ... + _ = self._cw._ + schema = self.entity._cw.vreg.schema + rtypes_msg = {} + for rtype in rtypes: + if rtype in schema: + rtypes_msg[rtype] = _('%s is part of violated unicity constraint') % rtype + globalmsg = _('some relations %sviolate a unicity constraint') + if len(rtypes) != len(rtypes_msg): # we got mangled/missing rtypes + globalmsg = globalmsg % _('(not all shown here) ') + else: + globalmsg = globalmsg % '' + rtypes_msg['unicity constraint'] = globalmsg + raise ValidationError(self.entity.eid, rtypes_msg) # deprecated ################################################################### diff -r b922929badba -r 1d42a6ab670f entity.py --- a/entity.py Fri Jul 26 09:47:22 2013 +0200 +++ b/entity.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f hooks/notification.py --- a/hooks/notification.py Fri Jul 26 09:47:22 2013 +0200 +++ b/hooks/notification.py Fri Jul 26 09:49:53 2013 +0200 @@ -158,7 +158,8 @@ view = session.vreg['views'].select('notif_entity_updated', session, rset=session.eid_rset(eid), row=0) - notify_on_commit(self.session, view) + notify_on_commit(self.session, view, + viewargs={'changes': session.transaction_data['changes'][eid]}) class EntityUpdateHook(NotificationHook): diff -r b922929badba -r 1d42a6ab670f hooks/security.py --- a/hooks/security.py Fri Jul 26 09:47:22 2013 +0200 +++ b/hooks/security.py Fri Jul 26 09:49:53 2013 +0200 @@ -23,10 +23,13 @@ from logilab.common.registry import objectify_predicate +from yams import buildobjs + from cubicweb import Unauthorized from cubicweb.server import BEFORE_ADD_RELATIONS, ON_COMMIT_ADD_RELATIONS, hook +_DEFAULT_UPDATE_ATTRPERM = buildobjs.DEFAULT_ATTRPERMS['update'] def check_entity_attributes(session, entity, editedattrs=None, creation=False): eid = entity.eid eschema = entity.e_schema @@ -39,9 +42,26 @@ if attr in dontcheck: continue rdef = eschema.rdef(attr) - if rdef.final: # non final relation are checked by other hooks - # add/delete should be equivalent (XXX: unify them into 'update' ?) - if creation and not rdef.permissions.get('update'): + if rdef.final: # non final relation are checked by standard hooks + # attributes only have a specific 'update' permission + updateperm = rdef.permissions.get('update') + # comparison below works because the default update perm is: + # + # ('managers', ERQLExpression(Any X WHERE U has_update_permission X, X eid %(x)s, U eid %(u)s)) + # + # is deserialized in this order (groups first), and ERQLExpression + # implements comparison by expression. + if updateperm == _DEFAULT_UPDATE_ATTRPERM: + # The default update permission is to delegate to the entity + # update permission. This is an historical artefact but it is + # costly (in general). Hence we take this permission object as a + # marker saying "no specific" update permissions for this + # attribute. Thus we just do nothing. + continue + if creation and updateperm == (): + # That actually means an immutable attribute. We make an + # _exception_ to the `check attr update perms at entity create & + # update time` rule for this case. continue rdef.check_perm(session, 'update', eid=eid) diff -r b922929badba -r 1d42a6ab670f i18n/de.po --- a/i18n/de.po Fri Jul 26 09:47:22 2013 +0200 +++ b/i18n/de.po Fri Jul 26 09:49:53 2013 +0200 @@ -114,6 +114,10 @@ msgstr "%s Fehlerbericht" #, python-format +msgid "%s is part of violated unicity constraint" +msgstr "" + +#, python-format msgid "%s not estimated" msgstr "%s unbekannt(e)" @@ -145,6 +149,9 @@ msgid "(UNEXISTANT EID)" msgstr "(EID nicht gefunden)" +msgid "(not all shown here) " +msgstr "" + #, python-format msgid "(suppressed) entity #%d" msgstr "" @@ -3861,6 +3868,10 @@ "Eine oder mehrere frühere Transaktion(en) betreffen die Tntität. Machen Sie " "sie zuerst rückgängig." +#, python-format +msgid "some relations %sviolate a unicity constraint" +msgstr "" + msgid "sorry, the server is unable to handle this query" msgstr "Der Server kann diese Anfrage leider nicht bearbeiten." diff -r b922929badba -r 1d42a6ab670f i18n/en.po --- a/i18n/en.po Fri Jul 26 09:47:22 2013 +0200 +++ b/i18n/en.po Fri Jul 26 09:49:53 2013 +0200 @@ -106,6 +106,10 @@ msgstr "" #, python-format +msgid "%s is part of violated unicity constraint" +msgstr "" + +#, python-format msgid "%s not estimated" msgstr "" @@ -137,6 +141,9 @@ msgid "(UNEXISTANT EID)" msgstr "" +msgid "(not all shown here) " +msgstr "" + #, python-format msgid "(suppressed) entity #%d" msgstr "" @@ -3766,6 +3773,10 @@ msgid "some later transaction(s) touch entity, undo them first" msgstr "" +#, python-format +msgid "some relations %sviolate a unicity constraint" +msgstr "" + msgid "sorry, the server is unable to handle this query" msgstr "" diff -r b922929badba -r 1d42a6ab670f i18n/es.po --- a/i18n/es.po Fri Jul 26 09:47:22 2013 +0200 +++ b/i18n/es.po Fri Jul 26 09:49:53 2013 +0200 @@ -115,6 +115,10 @@ msgstr "%s reporte de errores" #, python-format +msgid "%s is part of violated unicity constraint" +msgstr "" + +#, python-format msgid "%s not estimated" msgstr "%s no estimado(s)" @@ -146,6 +150,9 @@ msgid "(UNEXISTANT EID)" msgstr "(EID INEXISTENTE" +msgid "(not all shown here) " +msgstr "" + #, python-format msgid "(suppressed) entity #%d" msgstr "" @@ -3909,6 +3916,10 @@ msgstr "" "Las transacciones más recientes modificaron esta entidad, anúlelas primero" +#, python-format +msgid "some relations %sviolate a unicity constraint" +msgstr "" + msgid "sorry, the server is unable to handle this query" msgstr "Lo sentimos, el servidor no puede manejar esta consulta" diff -r b922929badba -r 1d42a6ab670f i18n/fr.po --- a/i18n/fr.po Fri Jul 26 09:47:22 2013 +0200 +++ b/i18n/fr.po Fri Jul 26 09:49:53 2013 +0200 @@ -115,6 +115,10 @@ msgstr "%s rapport d'erreur" #, python-format +msgid "%s is part of violated unicity constraint" +msgstr "%s appartient à une contrainte d'unicité transgressée" + +#, python-format msgid "%s not estimated" msgstr "%s non estimé(s)" @@ -148,6 +152,9 @@ msgid "(UNEXISTANT EID)" msgstr "(EID INTROUVABLE)" +msgid "(not all shown here) " +msgstr "(toutes ne sont pas montrées)" + #, python-format msgid "(suppressed) entity #%d" msgstr "entité #%d (supprimée)" @@ -3922,6 +3929,10 @@ msgstr "" "des transactions plus récentes modifient cette entité, annulez les d'abord" +#, python-format +msgid "some relations %sviolate a unicity constraint" +msgstr "certaines relations %stransgressent une contrainte d'unicité" + msgid "sorry, the server is unable to handle this query" msgstr "désolé, le serveur ne peut traiter cette requête" diff -r b922929badba -r 1d42a6ab670f misc/migration/bootstrapmigration_repository.py --- a/misc/migration/bootstrapmigration_repository.py Fri Jul 26 09:47:22 2013 +0200 +++ b/misc/migration/bootstrapmigration_repository.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f predicates.py --- a/predicates.py Fri Jul 26 09:47:22 2013 +0200 +++ b/predicates.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f rqlrewrite.py --- a/rqlrewrite.py Fri Jul 26 09:47:22 2013 +0200 +++ b/rqlrewrite.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f schema.py --- a/schema.py Fri Jul 26 09:47:22 2013 +0200 +++ b/schema.py Fri Jul 26 09:49:53 2013 +0200 @@ -84,7 +84,7 @@ 'WorkflowTransition', 'BaseTransition', 'SubWorkflowExitPoint')) -INTERNAL_TYPES = set(('CWProperty', 'CWCache', 'ExternalUri', +INTERNAL_TYPES = set(('CWProperty', 'CWCache', 'ExternalUri', 'CWDataImport', 'CWSource', 'CWSourceHostConfig', 'CWSourceSchemaConfig')) @@ -711,6 +711,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 b922929badba -r 1d42a6ab670f selectors.py diff -r b922929badba -r 1d42a6ab670f server/checkintegrity.py --- a/server/checkintegrity.py Fri Jul 26 09:47:22 2013 +0200 +++ b/server/checkintegrity.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f server/edition.py --- a/server/edition.py Fri Jul 26 09:47:22 2013 +0200 +++ b/server/edition.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f server/migractions.py --- a/server/migractions.py Fri Jul 26 09:47:22 2013 +0200 +++ b/server/migractions.py Fri Jul 26 09:49:53 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, @@ -688,7 +688,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 +932,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 +1010,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 b922929badba -r 1d42a6ab670f server/msplanner.py --- a/server/msplanner.py Fri Jul 26 09:47:22 2013 +0200 +++ b/server/msplanner.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f server/querier.py --- a/server/querier.py Fri Jul 26 09:47:22 2013 +0200 +++ b/server/querier.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f server/repository.py --- a/server/repository.py Fri Jul 26 09:47:22 2013 +0200 +++ b/server/repository.py Fri Jul 26 09:49:53 2013 +0200 @@ -167,6 +167,9 @@ self.pyro_registered = False self.pyro_uri = None + # every pyro client is handled in its own thread; map these threads to + # the session we opened for them so we can clean up when they go away + self._pyro_sessions = {} self.app_instances_bus = NullEventBus() self.info('starting repository from %s', self.config.apphome) # dictionary of opened sessions @@ -236,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') @@ -349,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): @@ -756,6 +758,12 @@ # try to get a user object user = self.authenticate_user(session, login, **kwargs) session = Session(user, self, cnxprops) + if threading.currentThread() in self._pyro_sessions: + # assume no pyro client does one get_repository followed by + # multiple repo.connect + assert self._pyro_sessions[threading.currentThread()] == None + self.debug('record session %s', session) + self._pyro_sessions[threading.currentThread()] = session user._cw = user.cw_rset.req = session user.cw_clear_relation_cache() self._sessions[session.id] = session @@ -785,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 @@ -876,6 +875,8 @@ # during `session_close` hooks session.commit() session.close() + if threading.currentThread() in self._pyro_sessions: + self._pyro_sessions[threading.currentThread()] = None del self._sessions[sessionid] self.info('closed session %s for user %s', sessionid, session.user.login) @@ -1637,22 +1638,23 @@ # into the pyro name server if self._use_pyrons(): self.looping_task(60*10, self._ensure_pyro_ns) + pyro_sessions = self._pyro_sessions # install hacky function to free cnxset - self.looping_task(60, self._cleanup_pyro) + def handleConnection(conn, tcpserver, sessions=pyro_sessions): + sessions[threading.currentThread()] = None + return tcpserver.getAdapter().__class__.handleConnection(tcpserver.getAdapter(), conn, tcpserver) + daemon.getAdapter().handleConnection = handleConnection + def removeConnection(conn, sessions=pyro_sessions): + daemon.__class__.removeConnection(daemon, conn) + session = sessions.pop(threading.currentThread(), None) + if session is None: + # client was not yet connected to the repo + return + if not session.closed: + self.close(session.id) + daemon.removeConnection = removeConnection return daemon - def _cleanup_pyro(self): - """Very hacky function to cleanup session left by dead Pyro thread. - - There is no clean pyro callback to detect this. - """ - for session in self._sessions.values(): - for thread, cnxset in session._threads_in_transaction.copy(): - if not thread.isAlive(): - self.warning('Freeing cnxset used by dead pyro threads: %', - thread) - session._free_thread_cnxset(thread, cnxset) - def _ensure_pyro_ns(self): if not self._use_pyrons(): return diff -r b922929badba -r 1d42a6ab670f server/serverconfig.py --- a/server/serverconfig.py Fri Jul 26 09:47:22 2013 +0200 +++ b/server/serverconfig.py Fri Jul 26 09:49:53 2013 +0200 @@ -239,14 +239,14 @@ {'type' : 'csv', 'default' : None, 'help': ('List of ZMQ addresses to subscribe to (requires pyzmq) ' - '(of the form `zmqpickle-tcp://:`)'), + '(of the form `tcp://:`)'), 'group': 'zmq', 'level': 1, }), ('zmq-address-pub', {'type' : 'string', 'default' : None, 'help': ('ZMQ address to use for publishing (requires pyzmq) ' - '(of the form `zmqpickle-tcp://:`)'), + '(of the form `tcp://:`)'), 'group': 'zmq', 'level': 1, }), ) + CubicWebConfiguration.options) @@ -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 b922929badba -r 1d42a6ab670f server/session.py --- a/server/session.py Fri Jul 26 09:47:22 2013 +0200 +++ b/server/session.py Fri Jul 26 09:49:53 2013 +0200 @@ -720,14 +720,13 @@ class Session(RequestSessionBase): """Repository user session - This tie all together: + This ties all together: * session id, * user, * connections set, * other session data. - About session storage / transactions - ------------------------------------ + **About session storage / transactions** Here is a description of internal session attributes. Besides :attr:`data` and :attr:`transaction_data`, you should not have to use attributes diff -r b922929badba -r 1d42a6ab670f server/sources/__init__.py --- a/server/sources/__init__.py Fri Jul 26 09:47:22 2013 +0200 +++ b/server/sources/__init__.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f server/sources/datafeed.py --- a/server/sources/datafeed.py Fri Jul 26 09:47:22 2013 +0200 +++ b/server/sources/datafeed.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f server/sources/native.py --- a/server/sources/native.py Fri Jul 26 09:47:22 2013 +0200 +++ b/server/sources/native.py Fri Jul 26 09:49:53 2013 +0200 @@ -410,14 +410,14 @@ def init(self, activated, source_entity): - super(NativeSQLSource, self).init(activated, source_entity) - self.init_creating(source_entity._cw.cnxset) try: # test if 'asource' column exists query = self.dbhelper.sql_add_limit_offset('SELECT asource FROM entities', 1) source_entity._cw.system_sql(query) except Exception as ex: self.eid_type_source = self.eid_type_source_pre_131 + super(NativeSQLSource, self).init(activated, source_entity) + self.init_creating(source_entity._cw.cnxset) def shutdown(self): if self._eid_creation_cnx: @@ -757,15 +757,15 @@ if ex.__class__.__name__ == 'IntegrityError': # need string comparison because of various backends for arg in ex.args: - mo = re.search('unique_cw_[^ ]+_idx', arg) + # postgres and sqlserver + mo = re.search('"unique_cw_[^ ]+"', arg) if mo is not None: - index_name = mo.group(0) - # right-chop '_idx' postfix - # (garanteed to be there, see regexp above) - elements = index_name[:-4].split('_cw_')[1:] + index_name = mo.group(0)[1:-1] # eat the surrounding " pair + elements = index_name.split('_cw_')[1:] etype = elements[0] rtypes = elements[1:] raise UniqueTogetherError(etype, rtypes) + # sqlite mo = re.search('columns (.*) are not unique', arg) if mo is not None: # sqlite in use # we left chop the 'cw_' prefix of attribute names diff -r b922929badba -r 1d42a6ab670f server/sources/rql2sql.py --- a/server/sources/rql2sql.py Fri Jul 26 09:47:22 2013 +0200 +++ b/server/sources/rql2sql.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f server/test/unittest_ldapsource.py --- a/server/test/unittest_ldapsource.py Fri Jul 26 09:47:22 2013 +0200 +++ b/server/test/unittest_ldapsource.py Fri Jul 26 09:49:53 2013 +0200 @@ -143,16 +143,26 @@ return self._pull(self.session) def setup_database(self): - if self.test_db_id == 'ldap-feed': - with self.session.repo.internal_session(safe=True) as session: - session.execute('DELETE Any E WHERE E cw_source S, S name "ldap"') - session.commit() - if self.test_db_id == 'ldap-feed': - src = self.sexecute('CWSource S WHERE S name "ldap"').get_entity(0,0) - src.cw_set(config=CONFIG_LDAPFEED) - self.session.commit() + with self.session.repo.internal_session(safe=True) as session: + session.execute('DELETE Any E WHERE E cw_source S, S name "ldap"') + session.execute('SET S config %(conf)s, S url %(url)s ' + 'WHERE S is CWSource, S name "ldap"', + {"conf": CONFIG_LDAPFEED, 'url': URL} ) + session.commit() self.pull() + def add_ldap_entry(self, dn, mods): + """ + add an LDAP entity + """ + modcmd = ['dn: %s'%dn, 'changetype: add'] + for key, values in mods.iteritems(): + if isinstance(values, basestring): + values = [values] + for value in values: + modcmd.append('%s: %s'%(key, value)) + self._ldapmodify(modcmd) + def delete_ldap_entry(self, dn): """ delete an LDAP entity @@ -329,9 +339,23 @@ 'deactivated') # check that it doesn't choke self.pull() - # reset the ldap database - self.tearDownClass() - self.setUpClass() + # reinsert syt + self.add_ldap_entry('uid=syt,ou=People,dc=cubicweb,dc=test', + { 'objectClass': ['OpenLDAPperson','posixAccount','top','shadowAccount'], + 'cn': 'Sylvain Thenault', + 'sn': 'Thenault', + 'gidNumber': '1004', + 'uid': 'syt', + 'homeDirectory': '/home/syt', + 'shadowFlag': '134538764', + 'uidNumber': '1004', + 'givenName': 'Sylvain', + 'telephoneNumber': '106', + 'displayName': 'sthenault', + 'gecos': 'Sylvain Thenault', + 'mail': ['sylvain.thenault@logilab.fr','syt@logilab.fr'], + 'userPassword': 'syt', + }) self.pull() self.assertEqual(self.execute('Any N WHERE U login "syt", ' 'U in_state S, S name N').rows[0][0], @@ -441,6 +465,18 @@ # XXX keep it there session.execute('CWUser U') + def setup_database(self): + # XXX a traceback may appear in the logs of the test due to + # the _init_repo method that may fail to connect to the ldap + # source if its URI has changed (from what is stored in the + # database). This TB is NOT a failure or so. + with self.session.repo.internal_session(safe=True) as session: + session.execute('SET S url %(url)s, S config %(conf)s ' + 'WHERE S is CWSource, S name "ldap"', + {"conf": CONFIG_LDAPUSER, 'url': URL} ) + session.commit() + self.pull() + def assertMetadata(self, entity): self.assertEqual(entity.creation_date, None) self.assertEqual(entity.modification_date, None) diff -r b922929badba -r 1d42a6ab670f server/test/unittest_migractions.py --- a/server/test/unittest_migractions.py Fri Jul 26 09:47:22 2013 +0200 +++ b/server/test/unittest_migractions.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f server/test/unittest_msplanner.py --- a/server/test/unittest_msplanner.py Fri Jul 26 09:47:22 2013 +0200 +++ b/server/test/unittest_msplanner.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f server/test/unittest_repository.py --- a/server/test/unittest_repository.py Fri Jul 26 09:47:22 2013 +0200 +++ b/server/test/unittest_repository.py Fri Jul 26 09:49:53 2013 +0200 @@ -52,16 +52,18 @@ and relation """ - def test_uniquetogether(self): + def test_unique_together_constraint(self): self.execute('INSERT Societe S: S nom "Logilab", S type "SSLL", S cp "75013"') with self.assertRaises(ValidationError) as wraperr: self.execute('INSERT Societe S: S nom "Logilab", S type "SSLL", S cp "75013"') - self.assertEqual({'nom': u'violates unique_together constraints (cp, nom, type)', - 'cp': u'violates unique_together constraints (cp, nom, type)', - 'type': u'violates unique_together constraints (cp, nom, type)'}, - wraperr.exception.args[1]) + self.assertEqual( + {'cp': u'cp is part of violated unicity constraint', + 'nom': u'nom is part of violated unicity constraint', + 'type': u'type is part of violated unicity constraint', + 'unicity constraint': u'some relations violate a unicity constraint'}, + wraperr.exception.args[1]) - def test_unique_together(self): + def test_unique_together_schema(self): person = self.repo.schema.eschema('Personne') self.assertEqual(len(person._unique_together), 1) self.assertItemsEqual(person._unique_together[0], @@ -272,19 +274,21 @@ def test_initial_schema(self): schema = self.repo.schema # check order of attributes is respected - self.assertListEqual([r.type for r in schema.eschema('CWAttribute').ordered_relations() - if not r.type in ('eid', 'is', 'is_instance_of', 'identity', - 'creation_date', 'modification_date', 'cwuri', - 'owned_by', 'created_by', 'cw_source', - 'update_permission', 'read_permission', - 'in_basket')], - ['relation_type', - 'from_entity', 'to_entity', - 'constrained_by', - 'cardinality', 'ordernum', - 'indexed', 'fulltextindexed', 'internationalizable', - 'defaultval', 'extra_props', - 'description', 'description_format']) + notin = set(('eid', 'is', 'is_instance_of', 'identity', + 'creation_date', 'modification_date', 'cwuri', + 'owned_by', 'created_by', 'cw_source', + 'update_permission', 'read_permission', + 'in_basket')) + self.assertListEqual(['relation_type', + 'from_entity', 'to_entity', + 'constrained_by', + 'cardinality', 'ordernum', + 'indexed', 'fulltextindexed', 'internationalizable', + 'defaultval', 'extra_props', + 'description', 'description_format'], + [r.type + for r in schema.eschema('CWAttribute').ordered_relations() + if r.type not in notin]) self.assertEqual(schema.eschema('CWEType').main_attribute(), 'name') self.assertEqual(schema.eschema('State').main_attribute(), 'name') @@ -349,7 +353,10 @@ self.assertTrue(user._cw.vreg) from cubicweb.entities import authobjs self.assertIsInstance(user._cw.user, authobjs.CWUser) + # make sure the tcp connection is closed properly; yes, it's disgusting. + adapter = cnx._repo.adapter cnx.close() + adapter.release() done.append(True) finally: # connect monkey patch some method by default, remove them diff -r b922929badba -r 1d42a6ab670f server/test/unittest_schemaserial.py --- a/server/test/unittest_schemaserial.py Fri Jul 26 09:47:22 2013 +0200 +++ b/server/test/unittest_schemaserial.py Fri Jul 26 09:49:53 2013 +0200 @@ -28,6 +28,7 @@ from logilab.database import get_db_helper from yams import register_base_type, unregister_base_type +schema = config = None def setUpModule(*args): register_base_type('BabarTestType', ('jungle_speed',)) helper = get_db_helper('sqlite') @@ -44,7 +45,7 @@ def tearDownModule(*args): global schema, config - del schema, config + schema = config = None unregister_base_type('BabarTestType') helper = get_db_helper('sqlite') @@ -63,29 +64,29 @@ class Schema2RQLTC(TestCase): def test_eschema2rql1(self): - self.assertListEqual(list(eschema2rql(schema.eschema('CWAttribute'))), - [ + self.assertListEqual([ ('INSERT CWEType X: X description %(description)s,X final %(final)s,X name %(name)s', {'description': u'define a final relation: link a final relation type from a non final entity to a final entity type. used to build the instance schema', - 'name': u'CWAttribute', 'final': False}) - ]) + 'name': u'CWAttribute', 'final': False})], + list(eschema2rql(schema.eschema('CWAttribute')))) def test_eschema2rql2(self): - self.assertListEqual(list(eschema2rql(schema.eschema('String'))), [ + self.assertListEqual([ ('INSERT CWEType X: X description %(description)s,X final %(final)s,X name %(name)s', - {'description': u'', 'final': True, 'name': u'String'})]) + {'description': u'', 'final': True, 'name': u'String'})], + list(eschema2rql(schema.eschema('String')))) def test_eschema2rql_specialization(self): # x: None since eschema.eid are None - self.assertListEqual(sorted(specialize2rql(schema)), - [('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s', - {'et': None, 'x': None}), - ('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s', - {'et': None, 'x': None}), - ('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s', - {'et': None, 'x': None}), - ('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s', - {'et': None, 'x': None})]) + self.assertListEqual([('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s', + {'et': None, 'x': None}), + ('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s', + {'et': None, 'x': None}), + ('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s', + {'et': None, 'x': None}), + ('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s', + {'et': None, 'x': None})], + sorted(specialize2rql(schema))) def test_esche2rql_custom_type(self): expected = [('INSERT CWEType X: X description %(description)s,X final %(final)s,X name %(name)s', @@ -95,8 +96,7 @@ self.assertListEqual(expected, got) def test_rschema2rql1(self): - self.assertListEqual(list(rschema2rql(schema.rschema('relation_type'), cstrtypemap)), - [ + self.assertListEqual([ ('INSERT CWRType X: X description %(description)s,X final %(final)s,X fulltext_container %(fulltext_container)s,X inlined %(inlined)s,X name %(name)s,X symmetric %(symmetric)s', {'description': u'link a relation definition to its relation type', 'symmetric': False, 'name': u'relation_type', 'final' : False, 'fulltext_container': None, 'inlined': True}), @@ -113,11 +113,11 @@ 'ordernum': 1, 'cardinality': u'1*'}), ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s', {'x': None, 'ct': u'RQLConstraint_eid', 'value': u';O;O final FALSE\n'}), - ]) + ], + list(rschema2rql(schema.rschema('relation_type'), cstrtypemap))) def test_rschema2rql2(self): - self.assertListEqual(list(rschema2rql(schema.rschema('add_permission'), cstrtypemap)), - [ + self.assertListEqual([ ('INSERT CWRType X: X description %(description)s,X final %(final)s,X fulltext_container %(fulltext_container)s,X inlined %(inlined)s,X name %(name)s,X symmetric %(symmetric)s', {'description': u'', 'symmetric': False, 'name': u'add_permission', 'final': False, 'fulltext_container': None, 'inlined': False}), ('INSERT CWRelation X: X cardinality %(cardinality)s,X composite %(composite)s,X description %(description)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s', @@ -132,12 +132,11 @@ 'description': u'groups allowed to add entities/relations of this type', 'composite': None, 'ordernum': 9999, 'cardinality': u'**'}), ('INSERT CWRelation X: X cardinality %(cardinality)s,X composite %(composite)s,X description %(description)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s', {'se': None, 'rt': None, 'oe': None, - 'description': u'rql expression allowing to add entities/relations of this type', 'composite': 'subject', 'ordernum': 9999, 'cardinality': u'*?'}), - ]) + 'description': u'rql expression allowing to add entities/relations of this type', 'composite': 'subject', 'ordernum': 9999, 'cardinality': u'*?'})], + list(rschema2rql(schema.rschema('add_permission'), cstrtypemap))) def test_rschema2rql3(self): - self.assertListEqual(list(rschema2rql(schema.rschema('cardinality'), cstrtypemap)), - [ + self.assertListEqual([ ('INSERT CWRType X: X description %(description)s,X final %(final)s,X fulltext_container %(fulltext_container)s,X inlined %(inlined)s,X name %(name)s,X symmetric %(symmetric)s', {'description': u'', 'symmetric': False, 'name': u'cardinality', 'final': True, 'fulltext_container': None, 'inlined': False}), @@ -155,8 +154,8 @@ ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s', {'x': None, 'ct': u'SizeConstraint_eid', 'value': u'max=2'}), ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s', - {'x': None, 'ct': u'StaticVocabularyConstraint_eid', 'value': u"u'?*', u'1*', u'+*', u'**', u'?+', u'1+', u'++', u'*+', u'?1', u'11', u'+1', u'*1', u'??', u'1?', u'+?', u'*?'"}), - ]) + {'x': None, 'ct': u'StaticVocabularyConstraint_eid', 'value': u"u'?*', u'1*', u'+*', u'**', u'?+', u'1+', u'++', u'*+', u'?1', u'11', u'+1', u'*1', u'??', u'1?', u'+?', u'*?'"})], + list(rschema2rql(schema.rschema('cardinality'), cstrtypemap))) def test_rschema2rql_custom_type(self): expected = [('INSERT CWRType X: X description %(description)s,X final %(final)s,' @@ -195,37 +194,34 @@ self.assertListEqual(expected, got) def test_rdef2rql(self): - self.assertListEqual(list(rdef2rql(schema['description_format'].rdefs[('CWRType', 'String')], cstrtypemap)), - [ + self.assertListEqual([ ('INSERT CWAttribute X: X cardinality %(cardinality)s,X defaultval %(defaultval)s,X description %(description)s,X fulltextindexed %(fulltextindexed)s,X indexed %(indexed)s,X internationalizable %(internationalizable)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s', {'se': None, 'rt': None, 'oe': None, 'description': u'', 'internationalizable': True, 'fulltextindexed': False, 'ordernum': 3, 'defaultval': u'text/plain', 'indexed': False, 'cardinality': u'?1'}), ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s', {'x': None, 'value': u'None', 'ct': 'FormatConstraint_eid'}), ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s', - {'x': None, 'value': u'max=50', 'ct': 'SizeConstraint_eid'})]) + {'x': None, 'value': u'max=50', 'ct': 'SizeConstraint_eid'})], + list(rdef2rql(schema['description_format'].rdefs[('CWRType', 'String')], cstrtypemap))) def test_updateeschema2rql1(self): - self.assertListEqual(list(updateeschema2rql(schema.eschema('CWAttribute'), 1)), - [('SET X description %(description)s,X final %(final)s,X name %(name)s WHERE X eid %(x)s', - {'description': u'define a final relation: link a final relation type from a non final entity to a final entity type. used to build the instance schema', 'x': 1, 'final': False, 'name': u'CWAttribute'}), - ]) + self.assertListEqual([('SET X description %(description)s,X final %(final)s,X name %(name)s WHERE X eid %(x)s', + {'description': u'define a final relation: link a final relation type from a non final entity to a final entity type. used to build the instance schema', 'x': 1, 'final': False, 'name': u'CWAttribute'})], + list(updateeschema2rql(schema.eschema('CWAttribute'), 1))) def test_updateeschema2rql2(self): - self.assertListEqual(list(updateeschema2rql(schema.eschema('String'), 1)), - [('SET X description %(description)s,X final %(final)s,X name %(name)s WHERE X eid %(x)s', - {'description': u'', 'x': 1, 'final': True, 'name': u'String'}) - ]) + self.assertListEqual([('SET X description %(description)s,X final %(final)s,X name %(name)s WHERE X eid %(x)s', + {'description': u'', 'x': 1, 'final': True, 'name': u'String'})], + list(updateeschema2rql(schema.eschema('String'), 1))) def test_updaterschema2rql1(self): - self.assertListEqual(list(updaterschema2rql(schema.rschema('relation_type'), 1)), - [ + self.assertListEqual([ ('SET X description %(description)s,X final %(final)s,X fulltext_container %(fulltext_container)s,X inlined %(inlined)s,X name %(name)s,X symmetric %(symmetric)s WHERE X eid %(x)s', {'x': 1, 'symmetric': False, 'description': u'link a relation definition to its relation type', - 'final': False, 'fulltext_container': None, 'inlined': True, 'name': u'relation_type'}) - ]) + 'final': False, 'fulltext_container': None, 'inlined': True, 'name': u'relation_type'})], + list(updaterschema2rql(schema.rschema('relation_type'), 1))) def test_updaterschema2rql2(self): expected = [ @@ -235,7 +231,7 @@ 'inlined': False, 'name': u'add_permission'}) ] for i, (rql, args) in enumerate(updaterschema2rql(schema.rschema('add_permission'), 1)): - yield self.assertEqual, (rql, args), expected[i] + yield self.assertEqual, expected[i], (rql, args) class Perms2RQLTC(TestCase): GROUP_MAPPING = { @@ -246,31 +242,33 @@ } def test_eperms2rql1(self): - self.assertListEqual([(rql, kwargs) for rql, kwargs in erperms2rql(schema.eschema('CWEType'), self.GROUP_MAPPING)], - [('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}), - ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 1}), - ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 2}), - ('SET X add_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}), - ('SET X update_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}), - ('SET X delete_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}), - ]) + self.assertListEqual([('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}), + ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 1}), + ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 2}), + ('SET X add_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}), + ('SET X update_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}), + ('SET X delete_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0})], + [(rql, kwargs) + for rql, kwargs in erperms2rql(schema.eschema('CWEType'), self.GROUP_MAPPING)]) def test_rperms2rql2(self): - self.assertListEqual([(rql, kwargs) for rql, kwargs in erperms2rql(schema.rschema('read_permission').rdef('CWEType', 'CWGroup'), self.GROUP_MAPPING)], - [('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}), - ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 1}), - ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 2}), - ('SET X add_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}), - ('SET X delete_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}), - ]) + self.assertListEqual([('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}), + ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 1}), + ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 2}), + ('SET X add_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}), + ('SET X delete_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0})], + [(rql, kwargs) + for rql, kwargs in erperms2rql(schema.rschema('read_permission').rdef('CWEType', 'CWGroup'), + self.GROUP_MAPPING)]) def test_rperms2rql3(self): - self.assertListEqual([(rql, kwargs) for rql, kwargs in erperms2rql(schema.rschema('name').rdef('CWEType', 'String'), self.GROUP_MAPPING)], - [('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}), - ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 1}), - ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 2}), - ('SET X update_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}), - ]) + self.assertListEqual([('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}), + ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 1}), + ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 2}), + ('SET X update_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0})], + [(rql, kwargs) + for rql, kwargs in erperms2rql(schema.rschema('name').rdef('CWEType', 'String'), + self.GROUP_MAPPING)]) #def test_perms2rql(self): # self.assertListEqual(perms2rql(schema, self.GROUP_MAPPING), diff -r b922929badba -r 1d42a6ab670f server/utils.py --- a/server/utils.py Fri Jul 26 09:47:22 2013 +0200 +++ b/server/utils.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f sobjects/notification.py --- a/sobjects/notification.py Fri Jul 26 09:47:22 2013 +0200 +++ b/sobjects/notification.py Fri Jul 26 09:49:53 2013 +0200 @@ -119,7 +119,8 @@ for something in recipients: if isinstance(something, Entity): # hi-jack self._cw to get a session for the returned user - self._cw = Session(self._cw.repo, something) + self._cw = Session(something, self._cw.repo) + self._cw.set_cnxset() emailaddr = something.cw_adapt_to('IEmailable').get_email() else: emailaddr, lang = something @@ -143,6 +144,10 @@ msg = format_mail(self.user_data, [emailaddr], content, subject, config=self._cw.vreg.config, msgid=msgid, references=refs) yield [emailaddr], msg + if isinstance(something, Entity): + self._cw.commit() + self._cw.close() + self._cw = req # restore language req.set_language(origlang) @@ -287,9 +292,8 @@ url: %(url)s """ - def context(self, **kwargs): + def context(self, changes=(), **kwargs): context = super(EntityUpdatedNotificationView, self).context(**kwargs) - changes = self._cw.transaction_data['changes'][self.cw_rset[0][0]] _ = self._cw._ formatted_changes = [] entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0) diff -r b922929badba -r 1d42a6ab670f test/data/rewrite/schema.py --- a/test/data/rewrite/schema.py Fri Jul 26 09:47:22 2013 +0200 +++ b/test/data/rewrite/schema.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f test/unittest_dataimport.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unittest_dataimport.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f test/unittest_entity.py --- a/test/unittest_entity.py Fri Jul 26 09:47:22 2013 +0200 +++ b/test/unittest_entity.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f test/unittest_predicates.py --- a/test/unittest_predicates.py Fri Jul 26 09:47:22 2013 +0200 +++ b/test/unittest_predicates.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f test/unittest_rqlrewrite.py --- a/test/unittest_rqlrewrite.py Fri Jul 26 09:47:22 2013 +0200 +++ b/test/unittest_rqlrewrite.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f test/unittest_schema.py --- a/test/unittest_schema.py Fri Jul 26 09:47:22 2013 +0200 +++ b/test/unittest_schema.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f web/application.py --- a/web/application.py Fri Jul 26 09:47:22 2013 +0200 +++ b/web/application.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f web/data/cubicweb.facets.js --- a/web/data/cubicweb.facets.js Fri Jul 26 09:47:22 2013 +0200 +++ b/web/data/cubicweb.facets.js Fri Jul 26 09:49:53 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]); @@ -30,7 +29,7 @@ }); // FacetStringWidget (e.g. has-text) $(this).find('input:text').each(function(){ - names.push(facetName); + names.push(this.name); values.push(this.value); }); }); diff -r b922929badba -r 1d42a6ab670f web/facet.py --- a/web/facet.py Fri Jul 26 09:47:22 2013 +0200 +++ b/web/facet.py Fri Jul 26 09:49:53 2013 +0200 @@ -74,10 +74,11 @@ def rtype_facet_title(facet): - ptypes = facet.cw_rset.column_types(0) - if len(ptypes) == 1: - return display_name(facet._cw, facet.rtype, form=facet.role, - context=iter(ptypes).next()) + if facet.cw_rset: + ptypes = facet.cw_rset.column_types(0) + if len(ptypes) == 1: + return display_name(facet._cw, facet.rtype, form=facet.role, + context=iter(ptypes).next()) return display_name(facet._cw, facet.rtype, form=facet.role) def get_facet(req, facetid, select, filtered_variable): @@ -1516,7 +1517,8 @@ cssclass += ' hideFacetBody' w(u'
%s
\n' % (cssclass, xml_escape(self.facet.__regid__), title)) - w(u'\n' % (facetid, self.value or u'')) + w(u'\n' % ( + xml_escape(self.facet.__regid__), self.value or u'')) w(u'\n') diff -r b922929badba -r 1d42a6ab670f web/formfields.py --- a/web/formfields.py Fri Jul 26 09:47:22 2013 +0200 +++ b/web/formfields.py Fri Jul 26 09:49:53 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. @@ -1148,12 +1148,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 b922929badba -r 1d42a6ab670f web/formwidgets.py --- a/web/formwidgets.py Fri Jul 26 09:47:22 2013 +0200 +++ b/web/formwidgets.py Fri Jul 26 09:49:53 2013 +0200 @@ -1016,6 +1016,8 @@ time, you should not give an already translated string. """ type = 'button' + css_class = 'validateButton' + def __init__(self, label=stdmsgs.BUTTON_OK, attrs=None, setdomid=None, settabindex=None, name='', value='', onclick=None, cwaction=None): @@ -1030,7 +1032,7 @@ self.value = '' self.onclick = onclick self.cwaction = cwaction - self.attrs.setdefault('class', 'validateButton') + self.attrs.setdefault('class', self.css_class) def render(self, form, field=None, renderer=None): label = form._cw._(self.label) diff -r b922929badba -r 1d42a6ab670f web/request.py --- a/web/request.py Fri Jul 26 09:47:22 2013 +0200 +++ b/web/request.py Fri Jul 26 09:49:53 2013 +0200 @@ -922,7 +922,7 @@ if reset_xmldecl is not None: warn('[3.17] reset_xmldecl is deprecated as we only serve html', DeprecationWarning, stacklevel=2) - self.main_stream.set_doctype(doctype, reset_xmldecl) + self.main_stream.set_doctype(doctype) # page data management #################################################### diff -r b922929badba -r 1d42a6ab670f web/test/data/schema.py --- a/web/test/data/schema.py Fri Jul 26 09:47:22 2013 +0200 +++ b/web/test/data/schema.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f web/test/unittest_views_basecontrollers.py --- a/web/test/unittest_views_basecontrollers.py Fri Jul 26 09:47:22 2013 +0200 +++ b/web/test/unittest_views_basecontrollers.py Fri Jul 26 09:49:53 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,30 @@ 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_edit_multiple_linked(self): req = self.request() peid = u(self.create_user(req, 'adim').eid) @@ -263,6 +289,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 +303,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 +376,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 b922929badba -r 1d42a6ab670f web/test/unittest_views_baseviews.py --- a/web/test/unittest_views_baseviews.py Fri Jul 26 09:47:22 2013 +0200 +++ b/web/test/unittest_views_baseviews.py Fri Jul 26 09:49:53 2013 +0200 @@ -144,7 +144,7 @@ class MyView(StartupView): __regid__ = 'my-view' def call(self): - self._cw.set_doctype(html_doctype, reset_xmldecl=False) + self._cw.set_doctype(html_doctype) self._cw.main_stream.set_htmlattrs([('lang', 'cz')]) with self.temporary_appobjects(MyView): diff -r b922929badba -r 1d42a6ab670f web/uihelper.py --- a/web/uihelper.py Fri Jul 26 09:47:22 2013 +0200 +++ b/web/uihelper.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f web/views/authentication.py --- a/web/views/authentication.py Fri Jul 26 09:47:22 2013 +0200 +++ b/web/views/authentication.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f web/views/autoform.py --- a/web/views/autoform.py Fri Jul 26 09:47:22 2013 +0200 +++ b/web/views/autoform.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f web/views/basetemplates.py --- a/web/views/basetemplates.py Fri Jul 26 09:47:22 2013 +0200 +++ b/web/views/basetemplates.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f web/views/editcontroller.py --- a/web/views/editcontroller.py Fri Jul 26 09:47:22 2013 +0200 +++ b/web/views/editcontroller.py Fri Jul 26 09:49:53 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,46 @@ 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 + graph.setdefault(target_eid, set()).add(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 +181,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 +232,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 +245,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 +260,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 +279,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 +298,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 +311,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 b922929badba -r 1d42a6ab670f web/views/navigation.py --- a/web/views/navigation.py Fri Jul 26 09:47:22 2013 +0200 +++ b/web/views/navigation.py Fri Jul 26 09:49:53 2013 +0200 @@ -398,7 +398,7 @@ title = self._cw._('i18nprevnext_previous') icon = self.prev_icon cssclass = u'previousEntity left' - content = icon + content + content = icon + '  ' + content else: title = self._cw._('i18nprevnext_next') icon = self.next_icon diff -r b922929badba -r 1d42a6ab670f web/views/staticcontrollers.py --- a/web/views/staticcontrollers.py Fri Jul 26 09:47:22 2013 +0200 +++ b/web/views/staticcontrollers.py Fri Jul 26 09:49:53 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 b922929badba -r 1d42a6ab670f web/views/tableview.py --- a/web/views/tableview.py Fri Jul 26 09:47:22 2013 +0200 +++ b/web/views/tableview.py Fri Jul 26 09:49:53 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''