# HG changeset patch # User Julien Cristau # Date 1389617267 -3600 # Node ID 2c48c091b6a23d3c312ece6e5d1d171c2dfb45f9 # Parent aff75b69db92f7321723df08e84884124edaa426# Parent 17dc43c1f1bd6dd4da12cbbc70fe79b2f7125f0f merge 3.18.0 in 3.19 branch diff -r aff75b69db92 -r 2c48c091b6a2 .hgtags --- a/.hgtags Tue Jul 02 17:09:04 2013 +0200 +++ b/.hgtags Mon Jan 13 13:47:47 2014 +0100 @@ -30,11 +30,7 @@ c9c492787a8aa1b7916e22eb6498cba1c8fa316c cubicweb-debian-version-3_2_0-1 634c251dd032894850080c4e5aeb0a4e09f888c0 cubicweb-version-3_2_1 e784f8847a124a93e5b385d7a92a2772c050fe82 cubicweb-debian-version-3_2_1-1 -6539ce84f04357ef65ccee0896a30997b16a4ece cubicweb-version-3_2_2 -92d1a15f08f7c5fa87643ffb4273d12cb3f41c63 cubicweb-debian-version-3_2_2-1 -6539ce84f04357ef65ccee0896a30997b16a4ece cubicweb-version-3_2_2 9b21e068fef73c37bcb4e53d006a7bde485f390b cubicweb-version-3_2_2 -92d1a15f08f7c5fa87643ffb4273d12cb3f41c63 cubicweb-debian-version-3_2_2-1 0e07514264aa1b0b671226f41725ea4c066c210a cubicweb-debian-version-3_2_2-1 f60bb84b86cf371f1f25197e00c778b469297721 cubicweb-version-3_2_3 4003d24974f15f17bd03b7efd6a5047cad4e4c41 cubicweb-debian-version-3_2_3-1 @@ -43,10 +39,8 @@ a356da3e725bfcb59d8b48a89d04be05ea261fd3 3.3.1 e3aeb6e6c3bb5c18e8dcf61bae9d654beda6c036 cubicweb-version-3_3_2 bef5e74e53f9de8220451dca4b5863a24a0216fb cubicweb-debian-version-3_3_2-1 -1cf9e44e2f1f4415253b8892a0adfbd3b69e84fd cubicweb-version-3_3_3 +47b5236774a0cf3b1cfe75f6d4bd2ec989644ace cubicweb-version-3_3_3 81973c897c9e78e5e52643e03628654916473196 cubicweb-debian-version-3_3_3-1 -1cf9e44e2f1f4415253b8892a0adfbd3b69e84fd cubicweb-version-3_3_3 -47b5236774a0cf3b1cfe75f6d4bd2ec989644ace cubicweb-version-3_3_3 2ba27ce8ecd9828693ec53c517e1c8810cbbe33e cubicweb-debian-version-3_3_3-2 d46363eac5d71bc1570d69337955154dfcd8fcc8 cubicweb-version-3.3.4 7dc22caa7640bf70fcae55afb6d2326829dacced cubicweb-debian-version-3.3.4-1 @@ -82,11 +76,7 @@ 37d025b2aa7735dae4a861059014c560b45b19e6 cubicweb-debian-version-3.5.4-1 1eca47d59fd932fe23f643ca239cf2408e5b1856 cubicweb-version-3.5.5 aad818d9d9b6fdb2ffea56c0a9af718c0b69899d cubicweb-debian-version-3.5.5-1 -b79f361839a7251b35eb8378fbc0773de7c8a815 cubicweb-version-3.5.6 -e6225e8e36c6506c774e0a76acc301d8ae1c1028 cubicweb-debian-version-3.5.6-1 -b79f361839a7251b35eb8378fbc0773de7c8a815 cubicweb-version-3.5.6 4e619e97b3fd70769a0f454963193c10cb87f9d4 cubicweb-version-3.5.6 -e6225e8e36c6506c774e0a76acc301d8ae1c1028 cubicweb-debian-version-3.5.6-1 5f7c939301a1b915e17eec61c05e8e9ab8bdc182 cubicweb-debian-version-3.5.6-1 0fc300eb4746e01f2755b9eefd986d58d8366ccf cubicweb-version-3.5.7 7a96c0544c138a0c5f452e5b2428ce6e2b7cb378 cubicweb-debian-version-3.5.7-1 @@ -98,8 +88,6 @@ 4920121d41f28c8075a4f00461911677396fc566 cubicweb-debian-version-3.5.11-1 98af3d02b83e7635207781289cc3445fb0829951 cubicweb-version-3.5.12 4281e1e2d76b9a37f38c0eeb1cbdcaa2fac6533c cubicweb-debian-version-3.5.12-1 -5f957e351b0a60d5c5fff60c560b04e666c3a8c6 cubicweb-version-3.6.0 -17e88f2485d1ea1fb8a3926a274637ce19e95d69 cubicweb-debian-version-3.6.0-1 450804da3ab2476b7ede0c1f956235b4c239734f cubicweb-version-3.6.0 d2ba93fcb8da95ceab08f48f8149a480215f149c cubicweb-debian-version-3.6.0-1 4ae30c9ca11b1edad67d25b76fce672171d02023 cubicweb-version-3.6.1 @@ -107,12 +95,12 @@ 0a16f07112b90fb61d2e905855fece77e5a7e39c cubicweb-debian-version-3.6.1-2 bfebe3d14d5390492925fc294dfdafad890a7104 cubicweb-version-3.6.2 f3b4bb9121a0e7ee5961310ff79e61c890948a77 cubicweb-debian-version-3.6.2-1 +9c342fa4f1b73e06917d7dc675949baff442108b cubicweb-version-3.6.3 +f9fce56d6a0c2bc6c4b497b66039a8bbbbdc8074 cubicweb-debian-version-3.6.3-1 270aba1e6fa21dac6b070e7815e6d1291f9c87cd cubicweb-version-3.7.0 0c9ff7e496ce344b7e6bf5c9dd2847daf9034e5e cubicweb-debian-version-3.7.0-1 6b0832bbd1daf27c2ce445af5b5222e1e522fb90 cubicweb-version-3.7.1 9194740f070e64da5a89f6a9a31050a8401ebf0c cubicweb-debian-version-3.7.1-1 -9c342fa4f1b73e06917d7dc675949baff442108b cubicweb-version-3.6.3 -f9fce56d6a0c2bc6c4b497b66039a8bbbbdc8074 cubicweb-debian-version-3.6.3-1 d010f749c21d55cd85c5feb442b9cf816282953c cubicweb-version-3.7.2 8fda29a6c2191ba3cc59242c17b28b34127c75fa cubicweb-debian-version-3.7.2-1 768beb8e15f15e079f8ee6cfc35125e12b19e140 cubicweb-version-3.7.3 @@ -135,10 +123,11 @@ 5d05b08adeab1ea301e49ed8537e35ede6db92f6 cubicweb-debian-version-3.8.5-1 1a24c62aefc5e57f61be3d04affd415288e81904 cubicweb-version-3.8.6 607a90073911b6bb941a49b5ec0b0d2a9cd479af cubicweb-debian-version-3.8.6-1 +a1a334d934390043a4293a4ee42bdceb1343246e cubicweb-version-3.8.7 +1cccf88d6dfe42986e1091de4c364b7b5814c54f cubicweb-debian-version-3.8.7-1 +48f468f33704e401a8e7907e258bf1ac61eb8407 cubicweb-version-3.9.x d9936c39d478b6701a4adef17bc28888ffa011c6 cubicweb-version-3.9.0 eda4940ffef8b7d36127e68de63a52388374a489 cubicweb-debian-version-3.9.0-1 -a1a334d934390043a4293a4ee42bdceb1343246e cubicweb-version-3.8.7 -1cccf88d6dfe42986e1091de4c364b7b5814c54f cubicweb-debian-version-3.8.7-1 4d75f743ed49dd7baf8bde7b0e475244933fa08e cubicweb-version-3.9.1 9bd75af3dca36d7be5d25fc5ab1b89b34c811456 cubicweb-debian-version-3.9.1-1 e51796b9caf389c224c6f66dcb8aa75bf1b82eff cubicweb-version-3.9.2 @@ -155,6 +144,11 @@ 1c01f9dffd64d507863c9f8f68e3585b7aa24374 cubicweb-debian-version-3.9.7-1 eed788018b595d46a55805bd8d2054c401812b2b cubicweb-version-3.9.8 e4dba8ae963701a36be94ae58c790bc97ba029bb cubicweb-debian-version-3.9.8-1 +df0b2de62cec10c84a2fff5233db05852cbffe93 cubicweb-version-3.9.9 +1ba51b00fc44faa0d6d57448000aaa1fd5c6ab57 cubicweb-debian-version-3.9.9-1 +b7db1f59355832a409d2032e19c84cfffdb3b265 cubicweb-debian-version-3.9.9-2 +09c98763ae9d43616d047c1b25d82b4e41a4362f cubicweb-debian-version-3.9.9-3 +a62f24e1497e953fbaed5894f6064a64f7ac0be3 cubicweb-version-3.10.x 0793fe84651be36f8de9b4faba3781436dc07be0 cubicweb-version-3.10.0 9ef1347f8d99e7daad290738ef93aa894a2c03ce cubicweb-debian-version-3.10.0-1 6c6859a676732c845af69f92e74d4aafae12f83a cubicweb-version-3.10.1 @@ -163,15 +157,7 @@ 4a87c8af6f3ffe59c6048ebbdc1b6b204d0b9c7f cubicweb-debian-version-3.10.2-1 8eb58d00a0cedcf7b275b1c7f43b08e2165f655c cubicweb-version-3.10.3 303b150ebb7a92b2904efd52b446457999cab370 cubicweb-debian-version-3.10.3-1 -3829498510a754b1b8a40582cb8dcbca9145fc9d cubicweb-version-3.10.4 -49f1226f2fab6d9ff17eb27d5a66732a4e5b5add cubicweb-debian-version-3.10.4-1 -df0b2de62cec10c84a2fff5233db05852cbffe93 cubicweb-version-3.9.9 -1ba51b00fc44faa0d6d57448000aaa1fd5c6ab57 cubicweb-debian-version-3.9.9-1 -b7db1f59355832a409d2032e19c84cfffdb3b265 cubicweb-debian-version-3.9.9-2 -09c98763ae9d43616d047c1b25d82b4e41a4362f cubicweb-debian-version-3.9.9-3 -3829498510a754b1b8a40582cb8dcbca9145fc9d cubicweb-version-3.10.4 d73733479a3af453f06b849ed88d120784ce9224 cubicweb-version-3.10.4 -49f1226f2fab6d9ff17eb27d5a66732a4e5b5add cubicweb-debian-version-3.10.4-1 7b41930e1d32fea3989a85f6ea7281983300adb1 cubicweb-debian-version-3.10.4-1 159d0dbe07d9eb1c6ace4c5e160d1ec6e6762086 cubicweb-version-3.10.5 e2e7410e994777589aec218d31eef9ff8d893f92 cubicweb-debian-version-3.10.5-1 @@ -181,21 +167,20 @@ bf5d9a1415e3c9abe6b68ba3b24a8ad741f9de3c cubicweb-debian-version-3.10.7-1 e581a86a68f089946a98c966ebca7aee58a5718f cubicweb-version-3.10.8 132b525de25bc75ed6389c45aee77e847cb3a437 cubicweb-debian-version-3.10.8-1 -48f468f33704e401a8e7907e258bf1ac61eb8407 cubicweb-version-3.9.x 37432cede4fe55b97fc2e9be0a2dd20e8837a848 cubicweb-version-3.11.0 8daabda9f571863e8754f8ab722744c417ba3abf cubicweb-debian-version-3.11.0-1 d0410eb4d8bbf657d7f32b0c681db09b1f8119a0 cubicweb-version-3.11.1 77318f1ec4aae3523d455e884daf3708c3c79af7 cubicweb-debian-version-3.11.1-1 56ae3cd5f8553678a2b1d4121b61241598d0ca68 cubicweb-version-3.11.2 954b5b51cd9278eb45d66be1967064d01ab08453 cubicweb-debian-version-3.11.2-1 +b7a124f9aed2c7c9c86c6349ddd9f0a07023f0ca cubicweb-version-3.11.3 +b3c6702761a18a41fdbb7bc1083f92aefce07765 cubicweb-debian-version-3.11.3-1 fd502219eb76f4bfd239d838a498a1d1e8204baf cubicweb-version-3.12.0 92b56939b7c77bbf443b893c495a20f19bc30702 cubicweb-debian-version-3.12.0-1 59701627adba73ee97529f6ea0e250a0f3748e32 cubicweb-version-3.12.1 07e2c9c7df2617c5ecfa84cb819b3ee8ef91d1f2 cubicweb-debian-version-3.12.1-1 5a9b6bc5653807500c30a7eb0e95b90fd714fec3 cubicweb-version-3.12.2 6d418fb3ffed273562aae411efe323d5138b592a cubicweb-debian-version-3.12.2-1 -b7a124f9aed2c7c9c86c6349ddd9f0a07023f0ca cubicweb-version-3.11.3 -b3c6702761a18a41fdbb7bc1083f92aefce07765 cubicweb-debian-version-3.11.3-1 e712bc6f1f71684f032bfcb9bb151a066c707dec cubicweb-version-3.12.3 ba8fe4f2e408c3fdf6c297cd42c2577dcac50e71 cubicweb-debian-version-3.12.3-1 5cd0dbc26882f60e3f11ec55e7f058d94505e7ed cubicweb-version-3.12.4 @@ -204,14 +189,16 @@ 6dfe78a0797ccc34962510f8c2a57f63d65ce41e cubicweb-debian-version-3.12.5-1 a18dac758150fe9c1f9e4958d898717c32a8f679 cubicweb-version-3.12.6 105767487c7075dbcce36474f1af0485985cbf2c cubicweb-debian-version-3.12.6-1 -b661ef475260ca7d9ea5c36ba2cc86e95e5b17d3 cubicweb-version-3.13.0 -a96137858f571711678954477da6f7f435870cea cubicweb-debian-version-3.13.0-1 628fe57ce746c1dac87fb1b078b2026057df894e cubicweb-version-3.12.7 a07517985136bbbfa6610c428a1b42cd04cd530b cubicweb-debian-version-3.12.7-1 50122a47ce4fb2ecbf3cf20ed2777f4276c93609 cubicweb-version-3.12.8 cf49ed55685a810d8d73585330ad1a57cc76260d cubicweb-debian-version-3.12.8-1 cb2990aaa63cbfe593bcf3afdbb9071e4c76815a cubicweb-version-3.12.9 92464e39134c70e4ddbe6cd78a6e3338a3b88b05 cubicweb-debian-version-3.12.9-1 +074c848a3712a77737d9a1bfbb618c75f5c0cbfa cubicweb-version-3.12.10 +9dfd21fa0a8b9f121a08866ad3e2ebd1dd06790d cubicweb-debian-version-3.12.10-1 +b661ef475260ca7d9ea5c36ba2cc86e95e5b17d3 cubicweb-version-3.13.0 +a96137858f571711678954477da6f7f435870cea cubicweb-debian-version-3.13.0-1 7d84317ef185a10c5eb78e6086f2297d2f4bd1e3 cubicweb-version-3.13.1 cc0578049cbe8b1d40009728e36c17e45da1fc6b cubicweb-debian-version-3.13.1-1 f9227b9d61835f03163b8133a96da35db37a0c8d cubicweb-version-3.13.2 @@ -220,11 +207,8 @@ fb48c55cb80234bc0164c9bcc0e2cfc428836e5f cubicweb-debian-version-3.13.3-1 223ecf0620b6c87d997f8011aca0d9f0ee4750af cubicweb-version-3.13.4 52f26475d764129c5559b2d80fd57e6ea1bdd6ba cubicweb-debian-version-3.13.4-1 -a62f24e1497e953fbaed5894f6064a64f7ac0be3 cubicweb-version-3.10.x 20d9c550c57eb6f9adcb0cfab1c11b6b8793afb6 cubicweb-version-3.13.5 2e9dd7d945557c210d3b79153c65f6885e755315 cubicweb-debian-version-3.13.5-1 -074c848a3712a77737d9a1bfbb618c75f5c0cbfa cubicweb-version-3.12.10 -9dfd21fa0a8b9f121a08866ad3e2ebd1dd06790d cubicweb-debian-version-3.12.10-1 17c007ad845abbac82e12146abab32a634657574 cubicweb-version-3.13.6 8a8949ca5351d48c5cf795ccdff06c1d4aab2ce0 cubicweb-debian-version-3.13.6-1 68e8c81fa96d6bcd21cc17bc9832d388ce05a9eb cubicweb-version-3.13.7 @@ -233,10 +217,10 @@ 43f83f5d0a4d57a06e9a4990bc957fcfa691eec3 cubicweb-debian-version-3.13.8-1 07afe32945aa275052747f78ef1f55858aaf6fa9 cubicweb-version-3.13.9 0a3cb5e60d57a7a9851371b4ae487094ec2bf614 cubicweb-debian-version-3.13.9-1 +2ad4e5173c73a43804c265207bcabb8940bd42f4 cubicweb-version-3.13.10 +2eab9a5a6bf8e3b0cf706bee8cdf697759c0a33a cubicweb-debian-version-3.13.10-1 5c4390eb10c3fe76a81e6fccec109d7097dc1a8d cubicweb-version-3.14.0 0bfe22fceb383b46d62b437bf5dd0141a714afb8 cubicweb-debian-version-3.14.0-1 -2ad4e5173c73a43804c265207bcabb8940bd42f4 cubicweb-version-3.13.10 -2eab9a5a6bf8e3b0cf706bee8cdf697759c0a33a cubicweb-debian-version-3.13.10-1 793d2d327b3ebf0b82b2735cf3ccb86467d1c08a cubicweb-version-3.14.1 6928210da4fc25d086b5b8d5ff2029da41aade2e cubicweb-debian-version-3.14.1-1 049a3819f03dc79d803be054cc3bfe8425313f63 cubicweb-version-3.14.2 @@ -250,8 +234,6 @@ 55fc796ed5d5f31245ae60bd148c9e42657a1af6 cubicweb-debian-version-3.14.5-1 db021578232b885dc5e55dfca045332ce01e7f35 cubicweb-version-3.14.6 75364c0994907764715bd5011f6a59d934dbeb7d cubicweb-debian-version-3.14.6-1 -0642b2d03acaa5e065cae7590e82b388a280ca22 cubicweb-version-3.15.0 -925db25a3250c5090cf640fc2b02bde5818b9798 cubicweb-debian-version-3.15.0-1 3ba3ee5b3a89a54d1dc12ed41d5c12232eda1952 cubicweb-version-3.14.7 20ee573bd2379a00f29ff27bb88a8a3344d4cdfe cubicweb-debian-version-3.14.7-1 15fe07ff687238f8cc09d8e563a72981484085b3 cubicweb-version-3.14.8 @@ -260,6 +242,8 @@ 68c762adf2d5a2c338910ef1091df554370586f0 cubicweb-debian-version-3.14.9-1 0ff798f80138ca8f50a59f42284380ce8f6232e8 cubicweb-version-3.14.10 197bcd087c87cd3de9f21f5bf40bd6203c074f1f cubicweb-debian-version-3.14.10-1 +0642b2d03acaa5e065cae7590e82b388a280ca22 cubicweb-version-3.15.0 +925db25a3250c5090cf640fc2b02bde5818b9798 cubicweb-debian-version-3.15.0-1 783a5df54dc742e63c8a720b1582ff08366733bd cubicweb-version-3.15.1 fe5e60862b64f1beed2ccdf3a9c96502dfcd811b cubicweb-debian-version-3.15.1-1 2afc157ea9b2b92eccb0f2d704094e22ce8b5a05 cubicweb-version-3.15.2 @@ -291,19 +275,51 @@ ee860c51f56bd65c4f6ea363462c02700d1dab5a cubicweb-version-3.16.3 ee860c51f56bd65c4f6ea363462c02700d1dab5a cubicweb-debian-version-3.16.3-1 ee860c51f56bd65c4f6ea363462c02700d1dab5a cubicweb-centos-version-3.16.3-1 -cc1a0aad580cf93d26959f97d8d6638e786c1082 cubicweb-version-3.17.0 -22be40c492e9034483bfec379ca11462ea97825b cubicweb-debian-version-3.17.0-1 -09a0c7ea6c3cb97bbbeed3795b3c3715ceb9566b cubicweb-debian-version-3.17.0-2 041804bc48e91e440a5b573ceb0df5bf22863b80 cubicweb-version-3.16.4 041804bc48e91e440a5b573ceb0df5bf22863b80 cubicweb-debian-version-3.16.4-1 041804bc48e91e440a5b573ceb0df5bf22863b80 cubicweb-centos-version-3.16.4-1 810a05fba1a46ab893b6cadac109097a047f8355 cubicweb-version-3.16.5 810a05fba1a46ab893b6cadac109097a047f8355 cubicweb-debiann-version-3.16.5-1 810a05fba1a46ab893b6cadac109097a047f8355 cubicweb-centos-version-3.16.5-1 -f98d1c46ed9fd5db5262cf5be1c8e159c90efc8b cubicweb-version-3.17.1 +b4ccaf13081d2798c0414d002e743cb0bf6d81f8 cubicweb-version-3.16.6 +b4ccaf13081d2798c0414d002e743cb0bf6d81f8 cubicweb-centos-version-3.16.6-1 +b4ccaf13081d2798c0414d002e743cb0bf6d81f8 cubicweb-debian-version-3.16.6-1 +cc1a0aad580cf93d26959f97d8d6638e786c1082 cubicweb-version-3.17.0 +22be40c492e9034483bfec379ca11462ea97825b cubicweb-debian-version-3.17.0-1 +09a0c7ea6c3cb97bbbeed3795b3c3715ceb9566b cubicweb-debian-version-3.17.0-2 f98d1c46ed9fd5db5262cf5be1c8e159c90efc8b cubicweb-version-3.17.1 -73f2ad404716cd211b735e67ee16875f1fff7374 cubicweb-debian-version-3.17.1-1 f98d1c46ed9fd5db5262cf5be1c8e159c90efc8b cubicweb-debian-version-3.17.1-1 f98d1c46ed9fd5db5262cf5be1c8e159c90efc8b cubicweb-centos-version-3.17.1-1 +965f894b63cb7c4456acd82257709f563bde848f cubicweb-centos-version-3.17.1-2 195e519fe97c8d1a5ab5ccb21bf7c88e5801b657 cubicweb-version-3.17.2 195e519fe97c8d1a5ab5ccb21bf7c88e5801b657 cubicweb-debian-version-3.17.2-1 +32b4d5314fd90fe050c931886190f9a372686148 cubicweb-version-3.17.3 +32b4d5314fd90fe050c931886190f9a372686148 cubicweb-debian-version-3.17.3-1 +32b4d5314fd90fe050c931886190f9a372686148 cubicweb-centos-version-3.17.3-1 +c7ba8e5d2e45e3d1289c1403df40d7dcb5e62acb cubicweb-version-3.17.4 +c7ba8e5d2e45e3d1289c1403df40d7dcb5e62acb cubicweb-debian-version-3.17.4-1 +c7ba8e5d2e45e3d1289c1403df40d7dcb5e62acb cubicweb-centos-version-3.17.4-1 +15dd5b37998b8ef5e8fab1ea0491e6bd8e9f3355 cubicweb-centos-version-3.17.5-1 +15dd5b37998b8ef5e8fab1ea0491e6bd8e9f3355 cubicweb-version-3.17.5 +15dd5b37998b8ef5e8fab1ea0491e6bd8e9f3355 cubicweb-debian-version-3.17.5-1 +5b9fedf67a2912a80fe315a477df9e3ab104c734 cubicweb-centos-version-3.17.6-1 +5b9fedf67a2912a80fe315a477df9e3ab104c734 cubicweb-version-3.17.6 +5b9fedf67a2912a80fe315a477df9e3ab104c734 cubicweb-debian-version-3.17.6-1 +483181543899a762d068cfdc3ae751b54adc3f14 cubicweb-centos-version-3.17.7-1 +483181543899a762d068cfdc3ae751b54adc3f14 cubicweb-version-3.17.7 +483181543899a762d068cfdc3ae751b54adc3f14 cubicweb-debian-version-3.17.7-1 +909eb8b584c437b3d2580beff1325c3d5b5dcfb5 cubicweb-centos-version-3.17.8-1 +909eb8b584c437b3d2580beff1325c3d5b5dcfb5 cubicweb-version-3.17.8 +909eb8b584c437b3d2580beff1325c3d5b5dcfb5 cubicweb-debian-version-3.17.8-1 +5668d210e49c910180ff27712b6ae9ce8286e06c cubicweb-version-3.17.9 +5668d210e49c910180ff27712b6ae9ce8286e06c cubicweb-debian-version-3.17.9-1 +fe0e1863a13772836f40f743cc6fe4865f288ed3 cubicweb-centos-version-3.17.10-1 +fe0e1863a13772836f40f743cc6fe4865f288ed3 cubicweb-version-3.17.10 +fe0e1863a13772836f40f743cc6fe4865f288ed3 cubicweb-debian-version-3.17.10-1 +7f67db7c848ec20152daf489d9e11f0fc8402e9b cubicweb-centos-version-3.17.11-1 +7f67db7c848ec20152daf489d9e11f0fc8402e9b cubicweb-version-3.17.11 +7f67db7c848ec20152daf489d9e11f0fc8402e9b cubicweb-debian-version-3.17.11-1 +b02e2912cad5d80395e488c55b548495e8320198 cubicweb-debian-version-3.17.11-2 +db37bf35a1474843ded0a537f9cb4838f4a78cda cubicweb-version-3.18.0 +db37bf35a1474843ded0a537f9cb4838f4a78cda cubicweb-debian-version-3.18.0-1 +db37bf35a1474843ded0a537f9cb4838f4a78cda cubicweb-centos-version-3.18.0-1 diff -r aff75b69db92 -r 2c48c091b6a2 README --- a/README Tue Jul 02 17:09:04 2013 +0200 +++ b/README Mon Jan 13 13:47:47 2014 +0100 @@ -34,4 +34,4 @@ Look in the doc/ subdirectory or read http://docs.cubicweb.org/ - +It includes the Entypo pictograms by Daniel Bruce — www.entypo.com diff -r aff75b69db92 -r 2c48c091b6a2 __init__.py --- a/__init__.py Tue Jul 02 17:09:04 2013 +0200 +++ b/__init__.py Mon Jan 13 13:47:47 2014 +0100 @@ -22,6 +22,8 @@ # ignore the pygments UserWarnings import warnings +import cPickle +import zlib warnings.filterwarnings('ignore', category=UserWarning, message='.*was already imported', module='.*pygments') @@ -120,6 +122,26 @@ binary.seek(0) return binary + def __eq__(self, other): + if not isinstance(other, Binary): + return False + return self.getvalue(), other.getvalue() + + + # Binary helpers to store/fetch python objects + + @classmethod + def zpickle(cls, obj): + """ return a Binary containing a gzipped pickle of obj """ + retval = cls() + retval.write(zlib.compress(cPickle.dumps(obj, protocol=2))) + return retval + + def unzpickle(self): + """ decompress and loads the stream before returning it """ + return cPickle.loads(zlib.decompress(self.getvalue())) + + def str_or_binary(value): if isinstance(value, Binary): return value @@ -127,7 +149,6 @@ BASE_CONVERTERS['Password'] = str_or_binary - # use this dictionary to rename entity types while keeping bw compat ETYPE_NAME_MAP = {} diff -r aff75b69db92 -r 2c48c091b6a2 __pkginfo__.py --- a/__pkginfo__.py Tue Jul 02 17:09:04 2013 +0200 +++ b/__pkginfo__.py Mon Jan 13 13:47:47 2014 +0100 @@ -22,7 +22,7 @@ modname = distname = "cubicweb" -numversion = (3, 17, 2) +numversion = (3, 18, 0) version = '.'.join(str(num) for num in numversion) description = "a repository of entities / relations for knowledge management" @@ -40,10 +40,10 @@ ] __depends__ = { - 'logilab-common': '>= 0.59.0', + 'logilab-common': '>= 0.60.0', 'logilab-mtconverter': '>= 0.8.0', 'rql': '>= 0.31.2', - 'yams': '>= 0.37.0', + 'yams': '>= 0.39.0', #gettext # for xgettext, msgcat, etc... # web dependancies 'simplejson': '>= 2.0.9', @@ -51,8 +51,7 @@ 'Twisted': '', # XXX graphviz # server dependencies - 'logilab-database': '>= 1.10', - 'pysqlite': '>= 2.5.5', # XXX install pysqlite2 + 'logilab-database': '>= 1.11', 'passlib': '', } diff -r aff75b69db92 -r 2c48c091b6a2 _exceptions.py --- a/_exceptions.py Tue Jul 02 17:09:04 2013 +0200 +++ b/_exceptions.py Mon Jan 13 13:47:47 2014 +0100 @@ -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. @@ -19,6 +19,10 @@ __docformat__ = "restructuredtext en" +from warnings import warn + +from logilab.common.decorators import cachedproperty + from yams import ValidationError as ValidationError # abstract exceptions ######################################################### @@ -61,7 +65,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) """ @@ -81,6 +85,26 @@ class UniqueTogetherError(RepositoryError): """raised when a unique_together constraint caused an IntegrityError""" + def __init__(self, session, **kwargs): + self.session = session + assert 'rtypes' in kwargs or 'cstrname' in kwargs + self.kwargs = kwargs + + @cachedproperty + def rtypes(self): + if 'rtypes' in self.kwargs: + return self.kwargs['rtypes'] + cstrname = unicode(self.kwargs['cstrname']) + cstr = self.session.find('CWUniqueTogetherConstraint', name=cstrname).one() + return sorted(rtype.name for rtype in cstr.relations) + + @cachedproperty + def args(self): + warn('[3.18] UniqueTogetherError.args is deprecated, just use ' + 'the .rtypes accessor.', + DeprecationWarning) + # the first argument, etype, is never used and was never garanteed anyway + return None, self.rtypes # security exceptions ######################################################### @@ -134,6 +158,15 @@ a non final entity """ +class MultipleResultsError(CubicWebRuntimeError): + """raised when ResultSet.one() is called on a resultset with multiple rows + of multiple columns. + """ + +class NoResultError(CubicWebRuntimeError): + """raised when no result is found but at least one is expected. + """ + class UndoTransactionException(QueryError): """Raised when undoing a transaction could not be performed completely. diff -r aff75b69db92 -r 2c48c091b6a2 cubicweb.spec --- a/cubicweb.spec Tue Jul 02 17:09:04 2013 +0200 +++ b/cubicweb.spec Mon Jan 13 13:47:47 2014 +0100 @@ -7,7 +7,7 @@ %endif Name: cubicweb -Version: 3.17.2 +Version: 3.18.0 Release: logilab.1%{?dist} Summary: CubicWeb is a semantic web application framework Source0: http://download.logilab.org/pub/cubicweb/cubicweb-%{version}.tar.gz @@ -20,11 +20,11 @@ BuildArch: noarch Requires: %{python} -Requires: %{python}-logilab-common >= 0.59.0 +Requires: %{python}-logilab-common >= 0.60.0 Requires: %{python}-logilab-mtconverter >= 0.8.0 Requires: %{python}-rql >= 0.31.2 -Requires: %{python}-yams >= 0.37.0 -Requires: %{python}-logilab-database >= 1.10.0 +Requires: %{python}-yams >= 0.39.0 +Requires: %{python}-logilab-database >= 1.11.0 Requires: %{python}-passlib Requires: %{python}-lxml Requires: %{python}-twisted-web @@ -46,11 +46,13 @@ %install NO_SETUPTOOLS=1 %{__python} setup.py --quiet install --no-compile --prefix=%{_prefix} --root="$RPM_BUILD_ROOT" +mkdir -p $RPM_BUILD_ROOT/var/log/cubicweb %clean rm -rf $RPM_BUILD_ROOT %files %defattr(-, root, root) +%dir /var/log/cubicweb /* diff -r aff75b69db92 -r 2c48c091b6a2 cwconfig.py --- a/cwconfig.py Tue Jul 02 17:09:04 2013 +0200 +++ b/cwconfig.py Mon Jan 13 13:47:47 2014 +0100 @@ -53,8 +53,7 @@ If you are not administrator of you machine or if you need to play with some specific version of |cubicweb| you can use `virtualenv`_ a tool to create -isolated Python environments. Since version 3.9 |cubicweb| is **`virtualenv` -friendly** and won't write any file outside the virtualenv directory. +isolated Python environments. - instances are stored in :file:`/etc/cubicweb.d` - temporary files (such as pid file) in :file:`/var/run/cubicweb` @@ -206,7 +205,7 @@ """return a list of installed configurations in a directory according to \*-ctl files """ - return [name for name in ('repository', 'twisted', 'all-in-one') + return [name for name in ('repository', 'all-in-one') if exists(join(directory, '%s.conf' % name))] def guess_configuration(directory): @@ -328,7 +327,7 @@ # the format below can be useful to debug multi thread issues: # log_format = '%(asctime)s - [%(threadName)s] (%(name)s) %(levelname)s: %(message)s' # nor remove appobjects based on unused interface [???] - cleanup_interface_sobjects = True + cleanup_unused_appobjects = True if (CWDEV and _forced_mode != 'system'): mode = 'user' @@ -499,21 +498,11 @@ try: gendeps = getattr(pkginfo, key.replace('_cubes', '')) except AttributeError: - # bw compat - if hasattr(pkginfo, oldkey): - warn('[3.8] cube %s: %s is deprecated, use %s dict' - % (cube, oldkey, key), DeprecationWarning) - deps = getattr(pkginfo, oldkey) - else: - deps = {} + deps = {} else: deps = dict( (x[len('cubicweb-'):], v) for x, v in gendeps.iteritems() if x.startswith('cubicweb-')) - if not isinstance(deps, dict): - deps = dict((key, None) for key in deps) - warn('[3.8] cube %s should define %s as a dict' % (cube, key), - DeprecationWarning) for depcube in deps: try: newname = CW_MIGRATION_MAP[depcube] @@ -940,10 +929,9 @@ ' "cubicweb-ctl list")' % appid) return home - MODES = ('common', 'repository', 'Any', 'web') + MODES = ('common', 'repository', 'Any') MCOMPAT = {'all-in-one': MODES, - 'repository': ('common', 'repository', 'Any'), - 'twisted' : ('common', 'web'),} + 'repository': ('common', 'repository', 'Any')} @classmethod def accept_mode(cls, mode): #assert mode in cls.MODES, mode @@ -1189,11 +1177,13 @@ sourcedirs.append(self.i18n_lib_dir()) return i18n.compile_i18n_catalogs(sourcedirs, i18ndir, langs) - def sendmails(self, msgs): + def sendmails(self, msgs, fromaddr=None): """msgs: list of 2-uple (message object, recipients). Return False if connection to the smtp server failed, else True. """ server, port = self['smtp-host'], self['smtp-port'] + if fromaddr is None: + fromaddr = '%s <%s>' % (self['sender-name'], self['sender-addr']) SMTP_LOCK.acquire() try: try: @@ -1202,10 +1192,9 @@ self.exception("can't connect to smtp server %s:%s (%s)", server, port, ex) return False - heloaddr = '%s <%s>' % (self['sender-name'], self['sender-addr']) for msg, recipients in msgs: try: - smtp.sendmail(heloaddr, recipients, msg.as_string()) + smtp.sendmail(fromaddr, recipients, msg.as_string()) except Exception as ex: self.exception("error sending mail to %s (%s)", recipients, ex) diff -r aff75b69db92 -r 2c48c091b6a2 cwctl.py --- a/cwctl.py Tue Jul 02 17:09:04 2013 +0200 +++ b/cwctl.py Mon Jan 13 13:47:47 2014 +0100 @@ -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. @@ -48,13 +48,9 @@ from cubicweb.toolsutils import Command, rm, create_dir, underline_title from cubicweb.__pkginfo__ import version -if support_args(CommandLine, 'check_duplicated_command'): - # don't check duplicated commands, it occurs when reloading site_cubicweb - CWCTL = CommandLine('cubicweb-ctl', 'The CubicWeb swiss-knife.', - version=version, check_duplicated_command=False) -else: - CWCTL = CommandLine('cubicweb-ctl', 'The CubicWeb swiss-knife.', - version=version) +# don't check duplicated commands, it occurs when reloading site_cubicweb +CWCTL = CommandLine('cubicweb-ctl', 'The CubicWeb swiss-knife.', + version=version, check_duplicated_command=False) def wait_process_end(pid, maxtry=10, waittime=1): """wait for a process to actually die""" @@ -266,12 +262,7 @@ if tinfo: descr = getattr(tinfo, 'description', '') if not descr: - descr = getattr(tinfo, 'short_desc', '') - if descr: - warn('[3.8] short_desc is deprecated, update %s' - ' pkginfo' % cube, DeprecationWarning) - else: - descr = tinfo.__doc__ + descr = tinfo.__doc__ if descr: print ' '+ ' \n'.join(descr.splitlines()) modes = detect_available_modes(cwcfg.cube_dir(cube)) @@ -357,7 +348,7 @@ }), ('config', {'short': 'c', 'type' : 'choice', 'metavar': '', - 'choices': ('all-in-one', 'repository', 'twisted'), + 'choices': ('all-in-one', 'repository'), 'default': 'all-in-one', 'help': 'installation type, telling which part of an instance ' 'should be installed. You can list available configurations using the' @@ -751,6 +742,7 @@ print '\n' + underline_title('Upgrading the instance %s' % appid) from logilab.common.changelog import Version config = cwcfg.config_for(appid) + instance_running = exists(config['pid-file']) config.repairing = True # notice we're not starting the server config.verbosity = self.config.verbosity set_sources_mode = getattr(config, 'set_sources_mode', None) @@ -760,6 +752,7 @@ mih = config.migration_handler() repo = mih.repo_connect() vcconf = repo.get_versions() + helper = self.config_helper(config, required=False) if self.config.force_cube_version: for cube, version in self.config.force_cube_version.iteritems(): vcconf[cube] = Version(version) @@ -782,7 +775,7 @@ if cubicwebversion > applcubicwebversion: toupgrade.append(('cubicweb', applcubicwebversion, cubicwebversion)) # only stop once we're sure we have something to do - if not (CWDEV or self.config.nostartstop): + if instance_running and not (CWDEV or self.config.nostartstop): StopInstanceCommand(self.logger).stop_instance(appid) # run cubicweb/componants migration scripts if self.config.fs_only or toupgrade: @@ -798,8 +791,10 @@ if not self.i18nupgrade(config): return print + if helper: + helper.postupgrade(repo) print '-> instance migrated.' - if not (CWDEV or self.config.nostartstop): + if instance_running and not (CWDEV or self.config.nostartstop): # restart instance through fork to get a proper environment, avoid # uicfg pb (and probably gettext catalogs, to check...) forkcmd = '%s start %s' % (sys.argv[0], appid) @@ -1038,9 +1033,56 @@ raise ConfigurationError('unknown configuration key "%s" for mode %s' % (key, appcfg.name)) appcfg.save() + +# WSGI ######### + +def wsgichoices(): + try: + from werkzeug import serving + except ImportError: + return ('stdlib',) + return ('stdlib', 'werkzeug') + +class WSGIDebugStartHandler(InstanceCommand): + """Start an interactive wsgi server """ + name = 'wsgi' + actionverb = 'started' + arguments = '' + options = ( + ('method', + {'short': 'm', + 'type': 'choice', + 'metavar': '', + 'default': 'stdlib', + 'choices': wsgichoices(), + 'help': 'wsgi utility/method'}), + ('loglevel', + {'short': 'l', + 'type' : 'choice', + 'metavar': '', + 'default': 'debug', + 'choices': ('debug', 'info', 'warning', 'error'), + 'help': 'debug if -D is set, error otherwise', + }), + ) + + def wsgi_instance(self, appid): + config = cwcfg.config_for(appid, debugmode=1) + init_cmdline_log_threshold(config, self['loglevel']) + assert config.name == 'all-in-one' + meth = self['method'] + if meth == 'stdlib': + from cubicweb.wsgi import server + else: + from cubicweb.wsgi import wz as server + return server.run(config) + + + for cmdcls in (ListCommand, CreateInstanceCommand, DeleteInstanceCommand, StartInstanceCommand, StopInstanceCommand, RestartInstanceCommand, + WSGIDebugStartHandler, ReloadConfigurationCommand, StatusCommand, UpgradeInstanceCommand, ListVersionsInstanceCommand, @@ -1051,6 +1093,8 @@ ): CWCTL.register(cmdcls) + + def run(args): """command line tool""" import os diff -r aff75b69db92 -r 2c48c091b6a2 cwvreg.py --- a/cwvreg.py Tue Jul 02 17:09:04 2013 +0200 +++ b/cwvreg.py Mon Jan 13 13:47:47 2014 +0100 @@ -211,8 +211,7 @@ from cubicweb import (CW_SOFTWARE_ROOT, ETYPE_NAME_MAP, CW_EVENT_MANAGER, onevent, Binary, UnknownProperty, UnknownEid) -from cubicweb.predicates import (implements, appobject_selectable, - _reset_is_instance_cache) +from cubicweb.predicates import appobject_selectable, _reset_is_instance_cache @onevent('before-registry-reload') @@ -230,15 +229,6 @@ sys.modules.pop('cubicweb.web.uicfg', None) sys.modules.pop('cubicweb.web.uihelper', None) -def use_interfaces(obj): - """return interfaces required by the given object by searching for - `implements` predicate - """ - impl = obj.__select__.search_selector(implements) - if impl: - return sorted(impl.expected_ifaces) - return () - def require_appobject(obj): """return appobjects required by the given object by searching for `appobject_selectable` predicate @@ -568,7 +558,6 @@ def reset(self): CW_EVENT_MANAGER.emit('before-registry-reset', self) super(CWRegistryStore, self).reset() - self._needs_iface = {} self._needs_appobject = {} # two special registries, propertydefs which care all the property # definitions, and propertyvals which contains values for those @@ -626,6 +615,15 @@ self.register_objects(path) CW_EVENT_MANAGER.emit('after-registry-reload') + def load_file(self, filepath, modname): + # override to allow some instrumentation (eg localperms) + modpath = modname.split('.') + try: + self.currently_loading_cube = modpath[modpath.index('cubes') + 1] + except ValueError: + self.currently_loading_cube = 'cubicweb' + return super(CWRegistryStore, self).load_file(filepath, modname) + def _set_schema(self, schema): """set instance'schema""" self.schema = schema @@ -641,20 +639,6 @@ for obj in objects: obj.schema = schema - @deprecated('[3.9] use .register instead') - def register_if_interface_found(self, obj, ifaces, **kwargs): - """register `obj` but remove it if no entity class implements one of - the given `ifaces` interfaces at the end of the registration process. - - Extra keyword arguments are given to the - :meth:`~cubicweb.cwvreg.CWRegistryStore.register` function. - """ - self.register(obj, **kwargs) - if not isinstance(ifaces, (tuple, list)): - self._needs_iface[obj] = (ifaces,) - else: - self._needs_iface[obj] = ifaces - def register(self, obj, *args, **kwargs): """register `obj` application object into `registryname` or `obj.__registry__` if not specified, with identifier `oid` or @@ -665,15 +649,6 @@ """ obj = related_appobject(obj) super(CWRegistryStore, self).register(obj, *args, **kwargs) - # XXX bw compat - ifaces = use_interfaces(obj) - if ifaces: - if not obj.__name__.endswith('Adapter') and \ - any(iface for iface in ifaces if not isinstance(iface, basestring)): - warn('[3.9] %s: interfaces in implements selector are ' - 'deprecated in favor of adapters / adaptable ' - 'selector' % obj.__name__, DeprecationWarning) - self._needs_iface[obj] = ifaces depends_on = require_appobject(obj) if depends_on is not None: self._needs_appobject[obj] = depends_on @@ -687,41 +662,14 @@ def initialization_completed(self): """cw specific code once vreg initialization is completed: - * remove objects requiring a missing interface, unless - config.cleanup_interface_sobjects is false + * remove objects requiring a missing appobject, unless + config.cleanup_unused_appobjects is false * init rtags """ # we may want to keep interface dependent objects (e.g.for i18n # catalog generation) - if self.config.cleanup_interface_sobjects: - # XXX deprecated with cw 3.9: remove appobjects that don't support - # any available interface - implemented_interfaces = set() - if 'Any' in self.get('etypes', ()): - for etype in self.schema.entities(): - if etype.final: - continue - cls = self['etypes'].etype_class(etype) - if cls.__implements__: - warn('[3.9] %s: using __implements__/interfaces are ' - 'deprecated in favor of adapters' % cls.__name__, - DeprecationWarning) - for iface in cls.__implements__: - implemented_interfaces.update(iface.__mro__) - implemented_interfaces.update(cls.__mro__) - for obj, ifaces in self._needs_iface.items(): - ifaces = frozenset(isinstance(iface, basestring) - and iface in self.schema - and self['etypes'].etype_class(iface) - or iface - for iface in ifaces) - if not ('Any' in ifaces or ifaces & implemented_interfaces): - reg = self[obj_registries(obj)[0]] - self.debug('unregister %s (no implemented ' - 'interface among %s)', reg.objid(obj), ifaces) - self.unregister(obj) - # since 3.9: remove appobjects which depending on other, unexistant - # appobjects + if self.config.cleanup_unused_appobjects: + # remove appobjects which depend on other, unexistant appobjects for obj, (regname, regids) in self._needs_appobject.items(): try: registry = self[regname] @@ -740,8 +688,8 @@ if 'uicfg' in self: # 'uicfg' is not loaded in a pure repository mode for rtags in self['uicfg'].itervalues(): for rtag in rtags: - # don't check rtags if we don't want to cleanup_interface_sobjects - rtag.init(self.schema, check=self.config.cleanup_interface_sobjects) + # don't check rtags if we don't want to cleanup_unused_appobjects + rtag.init(self.schema, check=self.config.cleanup_unused_appobjects) # rql parsing utilities #################################################### diff -r aff75b69db92 -r 2c48c091b6a2 dataimport.py --- a/dataimport.py Tue Jul 02 17:09:04 2013 +0200 +++ b/dataimport.py Mon Jan 13 13:47:47 2014 +0100 @@ -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` @@ -792,6 +802,9 @@ assert not rtype.startswith('reverse_') self.add_relation(self.session, eid_from, rtype, eid_to, self.rschema(rtype).inlined) + if self.rschema[rtype].symmetric: + self.add_relation(self.session, eid_to, rtype, eid_from, + self.rschema(rtype).inlined) self._nb_inserted_relations += 1 @property @@ -918,6 +931,9 @@ # XXX Could subjtype be inferred ? self.source.add_relation(self.session, subj_eid, rtype, obj_eid, self.rschema(rtype).inlined, **kwargs) + if self.rschema[rtype].symmetric: + self.source.add_relation(self.session, obj_eid, rtype, subj_eid, + self.rschema(rtype).inlined, **kwargs) def drop_indexes(self, etype): """Drop indexes for a given entity type""" diff -r aff75b69db92 -r 2c48c091b6a2 dbapi.py --- a/dbapi.py Tue Jul 02 17:09:04 2013 +0200 +++ b/dbapi.py Mon Jan 13 13:47:47 2014 +0100 @@ -179,7 +179,7 @@ puri = urlparse(database) method = puri.scheme.lower() if method == 'inmemory': - config = cwconfig.instance_configuration(puri.path) + config = cwconfig.instance_configuration(puri.netloc) else: config = cwconfig.CubicWebNoAppConfiguration() repo = get_repository(database, config=config) @@ -226,7 +226,7 @@ raises an AuthenticationError if anonymous usage is not allowed """ anoninfo = vreg.config.anonymous_user() - if anoninfo is None: # no anonymous user + if anoninfo[0] is None: # no anonymous user raise AuthenticationError('anonymous access is not authorized') anon_login, anon_password = anoninfo # use vreg's repository cache @@ -365,25 +365,6 @@ """return the definition of sources used by the repository.""" return self.cnx.source_defs() - @deprecated('[3.8] use direct access to req.session.data dictionary') - def session_data(self): - """return a dictionary containing session data""" - return self.session.data - - @deprecated('[3.8] use direct access to req.session.data dictionary') - def get_session_data(self, key, default=None, pop=False): - if pop: - return self.session.data.pop(key, default) - return self.session.data.get(key, default) - - @deprecated('[3.8] use direct access to req.session.data dictionary') - def set_session_data(self, key, value): - self.session.data[key] = value - - @deprecated('[3.8] use direct access to req.session.data dictionary') - def del_session_data(self, key): - self.session.data.pop(key, None) - # these are overridden by set_log_methods below # only defining here to prevent pylint from complaining info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None @@ -419,7 +400,7 @@ def _txid(self): return self.connection._txid(self) - def execute(self, rql, args=None, eid_key=None, build_descr=True): + def execute(self, rql, args=None, build_descr=True): """execute a rql query, return resulting rows and their description in a :class:`~cubicweb.rset.ResultSet` object @@ -450,10 +431,6 @@ execute('Any X WHERE X eid %(x)s', {'x': 123}) """ - if eid_key is not None: - warn('[3.8] eid_key is deprecated, you can safely remove this argument', - DeprecationWarning, stacklevel=2) - # XXX use named argument for build_descr in case repo is < 3.8 rset = self._repo.execute(self._sessid, rql, args, build_descr=build_descr, **self._txid()) rset.req = self.req diff -r aff75b69db92 -r 2c48c091b6a2 debian.hardy/compat --- a/debian.hardy/compat Tue Jul 02 17:09:04 2013 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -5 diff -r aff75b69db92 -r 2c48c091b6a2 debian.hardy/rules --- a/debian.hardy/rules Tue Jul 02 17:09:04 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 aff75b69db92 -r 2c48c091b6a2 debian/changelog --- a/debian/changelog Tue Jul 02 17:09:04 2013 +0200 +++ b/debian/changelog Mon Jan 13 13:47:47 2014 +0100 @@ -1,3 +1,70 @@ +cubicweb (3.18.0-1) unstable; urgency=low + + * new upstream release. + + -- Julien Cristau Fri, 10 Jan 2014 17:14:18 +0100 + +cubicweb (3.17.11-2) unstable; urgency=low + + * Override lintian false-positive about debian/rules.tmpl in the cube + skeleton. + + -- Julien Cristau Wed, 11 Dec 2013 15:56:39 +0100 + +cubicweb (3.17.11-1) unstable; urgency=low + + * new upstream release + + -- Aurelien Campeas Fri, 06 Dec 2013 15:55:48 +0100 + +cubicweb (3.17.10-1) unstable; urgency=low + + * new upstream release + + -- Julien Cristau Wed, 23 Oct 2013 17:23:45 +0200 + +cubicweb (3.17.9-1) unstable; urgency=low + + * new upstream release + + -- Julien Cristau Tue, 08 Oct 2013 17:57:04 +0200 + +cubicweb (3.17.8-1) unstable; urgency=low + + * new upstream release + + -- David Douard Thu, 03 Oct 2013 15:12:32 +0200 + +cubicweb (3.17.7-1) unstable; urgency=low + + * new upstream release + + -- David Douard Thu, 26 Sep 2013 15:13:39 +0200 + +cubicweb (3.17.6-1) unstable; urgency=low + + * new upstream release + + -- David Douard Tue, 06 Aug 2013 11:49:04 +0200 + +cubicweb (3.17.5-1) unstable; urgency=low + + * new upstream release + + -- David Douard Wed, 31 Jul 2013 16:58:19 +0200 + +cubicweb (3.17.4-1) unstable; urgency=low + + * new upstream release + + -- David Douard Fri, 26 Jul 2013 09:44:19 +0200 + +cubicweb (3.17.3-1) unstable; urgency=low + + * new upstream release + + -- David Douard Tue, 09 Jul 2013 15:10:16 +0200 + cubicweb (3.17.2-1) unstable; urgency=low * new upstream release @@ -22,6 +89,12 @@ -- Pierre-Yves David Mon, 29 Apr 2013 11:20:56 +0200 +cubicweb (3.16.6-1) unstable; urgency=low + + * new upstream release + + -- Florent Cayré Sat, 13 Jul 2013 05:10:23 +0200 + cubicweb (3.16.5-1) unstable; urgency=low * new upstream release diff -r aff75b69db92 -r 2c48c091b6a2 debian/control --- a/debian/control Tue Jul 02 17:09:04 2013 +0200 +++ b/debian/control Mon Jan 13 13:47:47 2014 +0100 @@ -10,13 +10,12 @@ Build-Depends: debhelper (>= 7), python (>= 2.6), - python-central (>= 0.5), python-sphinx, python-logilab-common, - python-unittest2, + python-unittest2 | python (>= 2.7), python-logilab-mtconverter, python-rql, - python-yams (>= 0.37), + python-yams (>= 0.39), python-lxml, Standards-Version: 3.9.1 Homepage: http://www.cubicweb.org @@ -24,7 +23,6 @@ Package: cubicweb Architecture: all -XB-Python-Version: ${python:Versions} Depends: ${misc:Depends}, ${python:Depends}, @@ -45,7 +43,6 @@ Package: cubicweb-server Architecture: all -XB-Python-Version: ${python:Versions} Conflicts: cubicweb-multisources Replaces: cubicweb-multisources Provides: cubicweb-multisources @@ -54,7 +51,7 @@ ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), - python-logilab-database (>= 1.10.0), + python-logilab-database (>= 1.11.0), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2, @@ -101,7 +98,6 @@ Package: cubicweb-twisted Architecture: all -XB-Python-Version: ${python:Versions} Provides: cubicweb-web-frontend Depends: ${misc:Depends}, @@ -123,7 +119,6 @@ Package: cubicweb-web Architecture: all -XB-Python-Version: ${python:Versions} Depends: ${misc:Depends}, ${python:Depends}, @@ -136,6 +131,8 @@ python-fyzz, python-imaging, python-rdflib +Breaks: + cubicweb-inlinedit (<< 1.1.1), Description: web interface library for the CubicWeb framework CubicWeb is a semantic web application framework. . @@ -148,15 +145,14 @@ Package: cubicweb-common Architecture: all -XB-Python-Version: ${python:Versions} Depends: ${misc:Depends}, ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), - python-logilab-common (>= 0.59.0), - python-yams (>= 0.37.0), + python-logilab-common (>= 0.60.0), + python-yams (>= 0.39.0), python-rql (>= 0.31.2), python-lxml Recommends: @@ -164,6 +160,10 @@ python-crypto Conflicts: cubicweb-core Replaces: cubicweb-core +Breaks: + cubicweb-comment (<< 1.9.1), + cubicweb-person (<< 1.8.0), + cubicweb-geocoding (<< 0.2.0), Description: common library for the CubicWeb framework CubicWeb is a semantic web application framework. . @@ -173,7 +173,6 @@ Package: cubicweb-ctl Architecture: all -XB-Python-Version: ${python:Versions} Depends: ${misc:Depends}, ${python:Depends}, @@ -188,7 +187,6 @@ Package: cubicweb-dev Architecture: all -XB-Python-Version: ${python:Versions} Depends: ${misc:Depends}, ${python:Depends}, diff -r aff75b69db92 -r 2c48c091b6a2 debian/copyright --- a/debian/copyright Tue Jul 02 17:09:04 2013 +0200 +++ b/debian/copyright Mon Jan 13 13:47:47 2014 +0100 @@ -8,7 +8,7 @@ Copyright: - Copyright (c) 2003-2011 LOGILAB S.A. (Paris, FRANCE). + Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). http://www.logilab.fr/ -- mailto:contact@logilab.fr License: @@ -29,3 +29,18 @@ On Debian systems, the complete text of the GNU Lesser General Public License may be found in '/usr/share/common-licenses/LGPL-2.1'. + +Entypo pictograms: + +Author: + + Daniel Bruce (www.entypo.com) + +Licence: + + Entypo pictograms are licensed under CC BY-SA 3.0 and the font + under SIL Open Font License. + + The rights to each pictogram in the social extension are either + trademarked or copyrighted by the respective company. + diff -r aff75b69db92 -r 2c48c091b6a2 debian/cubicweb-common.install.in --- a/debian/cubicweb-common.install.in Tue Jul 02 17:09:04 2013 +0200 +++ b/debian/cubicweb-common.install.in Mon Jan 13 13:47:47 2014 +0100 @@ -1,4 +1,4 @@ -debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/entities/ usr/lib/PY_VERSION/site-packages/cubicweb -debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/ext/ usr/lib/PY_VERSION/site-packages/cubicweb -debian/tmp/usr/share/cubicweb/cubes/ usr/share/cubicweb/ -debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/*.py usr/share/pyshared/cubicweb +usr/lib/PY_VERSION/*-packages/cubicweb/entities/ +usr/lib/PY_VERSION/*-packages/cubicweb/ext/ +usr/share/cubicweb/cubes/ +usr/lib/PY_VERSION/*-packages/cubicweb/*.py diff -r aff75b69db92 -r 2c48c091b6a2 debian/cubicweb-ctl.dirs --- a/debian/cubicweb-ctl.dirs Tue Jul 02 17:09:04 2013 +0200 +++ b/debian/cubicweb-ctl.dirs Mon Jan 13 13:47:47 2014 +0100 @@ -1,4 +1,3 @@ -usr/lib/python2.4/site-packages/cubicweb/ etc/init.d etc/cubicweb.d etc/bash_completion.d diff -r aff75b69db92 -r 2c48c091b6a2 debian/cubicweb-ctl.install.in --- a/debian/cubicweb-ctl.install.in Tue Jul 02 17:09:04 2013 +0200 +++ b/debian/cubicweb-ctl.install.in Mon Jan 13 13:47:47 2014 +0100 @@ -1,3 +1,3 @@ -debian/tmp/usr/bin/cubicweb-ctl usr/bin/ -debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/cwctl.py usr/lib/PY_VERSION/site-packages/cubicweb -debian/cubicweb-ctl.bash_completion etc/bash_completion.d/cubicweb-ctl +usr/bin/cubicweb-ctl usr/bin/ +usr/lib/PY_VERSION/*-packages/cubicweb/cwctl.py +../cubicweb-ctl.bash_completion etc/bash_completion.d/cubicweb-ctl diff -r aff75b69db92 -r 2c48c091b6a2 debian/cubicweb-dev.install.in --- a/debian/cubicweb-dev.install.in Tue Jul 02 17:09:04 2013 +0200 +++ b/debian/cubicweb-dev.install.in Mon Jan 13 13:47:47 2014 +0100 @@ -1,10 +1,10 @@ -debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/devtools/ usr/lib/PY_VERSION/site-packages/cubicweb/ -debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/skeleton/ usr/lib/PY_VERSION/site-packages/cubicweb/ -debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/test usr/lib/PY_VERSION/site-packages/cubicweb/ -debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/entities/test usr/lib/PY_VERSION/site-packages/cubicweb/entities/ -debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/ext/test usr/lib/PY_VERSION/site-packages/cubicweb/ext/ -debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/server/test usr/lib/PY_VERSION/site-packages/cubicweb/server/ -debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/sobjects/test usr/lib/PY_VERSION/site-packages/cubicweb/sobjects/ -debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/hooks/test usr/lib/PY_VERSION/site-packages/cubicweb/sobjects/ -debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/web/test usr/lib/PY_VERSION/site-packages/cubicweb/web/ -debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/etwist/test usr/lib/PY_VERSION/site-packages/cubicweb/etwist/ +usr/lib/PY_VERSION/*-packages/cubicweb/devtools/ +usr/lib/PY_VERSION/*-packages/cubicweb/skeleton/ +usr/lib/PY_VERSION/*-packages/cubicweb/test +usr/lib/PY_VERSION/*-packages/cubicweb/entities/test +usr/lib/PY_VERSION/*-packages/cubicweb/ext/test +usr/lib/PY_VERSION/*-packages/cubicweb/server/test +usr/lib/PY_VERSION/*-packages/cubicweb/sobjects/test +usr/lib/PY_VERSION/*-packages/cubicweb/hooks/test +usr/lib/PY_VERSION/*-packages/cubicweb/web/test +usr/lib/PY_VERSION/*-packages/cubicweb/etwist/test diff -r aff75b69db92 -r 2c48c091b6a2 debian/cubicweb-dev.lintian-overrides --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/debian/cubicweb-dev.lintian-overrides Mon Jan 13 13:47:47 2014 +0100 @@ -0,0 +1,1 @@ +missing-dep-for-interpreter make => make | build-essential | dpkg-dev (usr/share/pyshared/cubicweb/skeleton/debian/rules.tmpl) diff -r aff75b69db92 -r 2c48c091b6a2 debian/cubicweb-documentation.install.in --- a/debian/cubicweb-documentation.install.in Tue Jul 02 17:09:04 2013 +0200 +++ b/debian/cubicweb-documentation.install.in Mon Jan 13 13:47:47 2014 +0100 @@ -1,3 +1,3 @@ -doc/book usr/share/doc/cubicweb-documentation -doc/html usr/share/doc/cubicweb-documentation -debian/cubicweb-doc usr/share/doc-base/cubicweb-doc +../../doc/book usr/share/doc/cubicweb-documentation +../../doc/html usr/share/doc/cubicweb-documentation +../../debian/cubicweb-doc usr/share/doc-base/cubicweb-doc diff -r aff75b69db92 -r 2c48c091b6a2 debian/cubicweb-server.install.in --- a/debian/cubicweb-server.install.in Tue Jul 02 17:09:04 2013 +0200 +++ b/debian/cubicweb-server.install.in Mon Jan 13 13:47:47 2014 +0100 @@ -1,5 +1,5 @@ -debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/server/ usr/lib/PY_VERSION/site-packages/cubicweb -debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/hooks/ usr/lib/PY_VERSION/site-packages/cubicweb -debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/sobjects/ usr/lib/PY_VERSION/site-packages/cubicweb -debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/schemas/ usr/lib/PY_VERSION/site-packages/cubicweb -debian/tmp/usr/share/cubicweb/migration/ usr/share/cubicweb/ +usr/lib/PY_VERSION/*-packages/cubicweb/server/ +usr/lib/PY_VERSION/*-packages/cubicweb/hooks/ +usr/lib/PY_VERSION/*-packages/cubicweb/sobjects/ +usr/lib/PY_VERSION/*-packages/cubicweb/schemas/ +usr/share/cubicweb/migration/ diff -r aff75b69db92 -r 2c48c091b6a2 debian/cubicweb-twisted.install.in --- a/debian/cubicweb-twisted.install.in Tue Jul 02 17:09:04 2013 +0200 +++ b/debian/cubicweb-twisted.install.in Mon Jan 13 13:47:47 2014 +0100 @@ -1,1 +1,1 @@ -debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/etwist/ usr/lib/PY_VERSION/site-packages/cubicweb/ +usr/lib/PY_VERSION/*-packages/cubicweb/etwist/ diff -r aff75b69db92 -r 2c48c091b6a2 debian/cubicweb-web.install.in --- a/debian/cubicweb-web.install.in Tue Jul 02 17:09:04 2013 +0200 +++ b/debian/cubicweb-web.install.in Mon Jan 13 13:47:47 2014 +0100 @@ -1,3 +1,3 @@ -debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/web usr/lib/PY_VERSION/site-packages/cubicweb -debian/tmp/usr/share/cubicweb/cubes/shared/data usr/share/cubicweb/cubes/shared -debian/tmp/usr/share/cubicweb/cubes/shared/wdoc usr/share/cubicweb/cubes/shared +usr/lib/PY_VERSION/*-packages/cubicweb/web +usr/share/cubicweb/cubes/shared/data +usr/share/cubicweb/cubes/shared/wdoc diff -r aff75b69db92 -r 2c48c091b6a2 debian/rules --- a/debian/rules Tue Jul 02 17:09:04 2013 +0200 +++ b/debian/rules Mon Jan 13 13:47:47 2014 +0100 @@ -18,17 +18,11 @@ # 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 clean: dh_testdir - dh_testroot rm -f build-stamp configure-stamp rm -rf build #rm -rf debian/cubicweb-*/ @@ -42,27 +36,26 @@ dh_clean dh_installdirs - #python setup.py install_lib --no-compile --install-dir=debian/cubicweb-common/usr/lib/python2.4/site-packages/ 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 + dh_install -vi --sourcedir=debian/tmp # cwctl in the cubicweb-ctl package - rm -f debian/cubicweb-common/usr/share/pyshared/cubicweb/cwctl.py + rm -f debian/cubicweb-common/usr/lib/python*/*/cubicweb/cwctl.py # wdoc in the cubicweb-web package rm -rf debian/cubicweb-common/usr/share/cubicweb/cubes/shared/wdoc rm -rf debian/cubicweb-common/usr/share/cubicweb/cubes/shared/data dh_lintian # 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 + rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/*-packages/cubicweb/server/test + rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/*-packages/cubicweb/hooks/test + rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/*-packages/cubicweb/sobjects/test + rm -rf debian/cubicweb-web/usr/lib/${PY_VERSION}/*-packages/cubicweb/web/test + rm -rf debian/cubicweb-twisted/usr/lib/${PY_VERSION}/*-packages/cubicweb/etwist/test + rm -rf debian/cubicweb-common/usr/lib/${PY_VERSION}/*-packages/cubicweb/ext/test + rm -rf debian/cubicweb-common/usr/lib/${PY_VERSION}/*-packages/cubicweb/entities/test %: %.in @@ -72,7 +65,7 @@ binary-indep: build install dh_testdir dh_testroot -i - dh_pycentral -i + dh_python2 -i dh_installinit -i -n --name cubicweb -u"defaults 99" dh_installlogrotate -i dh_installdocs -i -A README diff -r aff75b69db92 -r 2c48c091b6a2 devtools/__init__.py --- a/devtools/__init__.py Tue Jul 02 17:09:04 2013 +0200 +++ b/devtools/__init__.py Mon Jan 13 13:47:47 2014 +0100 @@ -26,6 +26,7 @@ import pickle import glob import warnings +import tempfile from hashlib import sha1 # pylint: disable=E0611 from datetime import timedelta from os.path import (abspath, join, exists, split, isabs, isdir) @@ -37,7 +38,7 @@ from cubicweb import ExecutionError, BadConnectionId from cubicweb import schema, cwconfig from cubicweb.server.serverconfig import ServerConfiguration -from cubicweb.etwist.twconfig import TwistedConfiguration +from cubicweb.etwist.twconfig import WebConfigurationBase cwconfig.CubicWebConfiguration.cls_adjust_sys_path() @@ -213,12 +214,12 @@ return BASE_URL -class BaseApptestConfiguration(TestServerConfiguration, TwistedConfiguration): +class BaseApptestConfiguration(TestServerConfiguration, WebConfigurationBase): name = 'all-in-one' # so it search for all-in-one.conf, not repository.conf options = cwconfig.merge_options(TestServerConfiguration.options - + TwistedConfiguration.options) - cubicweb_appobject_path = TestServerConfiguration.cubicweb_appobject_path | TwistedConfiguration.cubicweb_appobject_path - cube_appobject_path = TestServerConfiguration.cube_appobject_path | TwistedConfiguration.cube_appobject_path + + WebConfigurationBase.options) + cubicweb_appobject_path = TestServerConfiguration.cubicweb_appobject_path | WebConfigurationBase.cubicweb_appobject_path + cube_appobject_path = TestServerConfiguration.cube_appobject_path | WebConfigurationBase.cube_appobject_path def available_languages(self, *args): return self.cw_languages() @@ -287,8 +288,11 @@ The function create it if necessary""" backupdir = join(self.config.apphome, 'database') - if not isdir(backupdir): + try: os.makedirs(backupdir) + except: + if not isdir(backupdir): + raise return backupdir def config_path(self, db_id): @@ -324,8 +328,9 @@ # XXX we dump a dict of the config # This is an experimental to help config dependant setup (like BFSS) to # be propertly restored - with open(config_path, 'wb') as conf_file: + with tempfile.NamedTemporaryFile(dir=os.path.dirname(config_path), delete=False) as conf_file: conf_file.write(pickle.dumps(dict(self.config))) + os.rename(conf_file.name, config_path) self.db_cache[self.db_cache_key(db_id)] = (backup_data, config_path) def _backup_database(self, db_id): diff -r aff75b69db92 -r 2c48c091b6a2 devtools/devctl.py --- a/devtools/devctl.py Tue Jul 02 17:09:04 2013 +0200 +++ b/devtools/devctl.py Mon Jan 13 13:47:47 2014 +0100 @@ -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. @@ -46,7 +46,7 @@ a cube or for cubicweb (without a home) """ creating = True - cleanup_interface_sobjects = False + cleanup_unused_appobjects = False cubicweb_appobject_path = (ServerConfiguration.cubicweb_appobject_path | WebConfiguration.cubicweb_appobject_path) @@ -130,24 +130,31 @@ w('# singular and plural forms for each entity type\n') w('\n') vregdone = set() + afss = vreg['uicfg']['autoform_section'] + aiams = vreg['uicfg']['actionbox_appearsin_addmenu'] if libconfig is not None: + # processing a cube, libconfig being a config with all its dependencies + # (cubicweb incl.) from cubicweb.cwvreg import CWRegistryStore libschema = libconfig.load_schema(remove_unused_rtypes=False) - afs = vreg['uicfg'].select('autoform_section') - appearsin_addmenu = vreg['uicfg'].select('actionbox_appearsin_addmenu') cleanup_sys_modules(libconfig) libvreg = CWRegistryStore(libconfig) libvreg.set_schema(libschema) # trigger objects registration - libafs = libvreg['uicfg'].select('autoform_section') - libappearsin_addmenu = libvreg['uicfg'].select('actionbox_appearsin_addmenu') + libafss = libvreg['uicfg']['autoform_section'] + libaiams = libvreg['uicfg']['actionbox_appearsin_addmenu'] # prefill vregdone set list(_iter_vreg_objids(libvreg, vregdone)) + + def is_in_lib(rtags, eschema, rschema, role, tschema, predicate=bool): + return any(predicate(rtag.etype_get(eschema, rschema, role, tschema)) + for rtag in rtags) else: + # processing cubicweb itself libschema = {} - afs = vreg['uicfg'].select('autoform_section') - appearsin_addmenu = vreg['uicfg'].select('actionbox_appearsin_addmenu') for cstrtype in CONSTRAINTS: add_msg(w, cstrtype) + libafss = libaiams = None + is_in_lib = lambda *args: False done = set() for eschema in sorted(schema.entities()): if eschema.type in libschema: @@ -169,32 +176,34 @@ if rschema.final: continue for tschema in targetschemas: - fsections = afs.etype_get(eschema, rschema, role, tschema) - if 'main_inlined' in fsections and \ - (libconfig is None or not - 'main_inlined' in libafs.etype_get( - eschema, rschema, role, tschema)): - add_msg(w, 'add a %s' % tschema, - 'inlined:%s.%s.%s' % (etype, rschema, role)) - add_msg(w, str(tschema), - 'inlined:%s.%s.%s' % (etype, rschema, role)) - if appearsin_addmenu.etype_get(eschema, rschema, role, tschema): - if libconfig is not None and libappearsin_addmenu.etype_get( - eschema, rschema, role, tschema): - if eschema in libschema and tschema in libschema: - continue - if role == 'subject': - label = 'add %s %s %s %s' % (eschema, rschema, - tschema, role) - label2 = "creating %s (%s %%(linkto)s %s %s)" % ( - tschema, eschema, rschema, tschema) - else: - label = 'add %s %s %s %s' % (tschema, rschema, - eschema, role) - label2 = "creating %s (%s %s %s %%(linkto)s)" % ( - tschema, tschema, rschema, eschema) - add_msg(w, label) - add_msg(w, label2) + + for afs in afss: + fsections = afs.etype_get(eschema, rschema, role, tschema) + if 'main_inlined' in fsections and not \ + is_in_lib(libafss, eschema, rschema, role, tschema, + lambda x: 'main_inlined' in x): + add_msg(w, 'add a %s' % tschema, + 'inlined:%s.%s.%s' % (etype, rschema, role)) + add_msg(w, str(tschema), + 'inlined:%s.%s.%s' % (etype, rschema, role)) + break + + for aiam in aiams: + if aiam.etype_get(eschema, rschema, role, tschema) and not \ + is_in_lib(libaiams, eschema, rschema, role, tschema): + if role == 'subject': + label = 'add %s %s %s %s' % (eschema, rschema, + tschema, role) + label2 = "creating %s (%s %%(linkto)s %s %s)" % ( + tschema, eschema, rschema, tschema) + else: + label = 'add %s %s %s %s' % (tschema, rschema, + eschema, role) + label2 = "creating %s (%s %s %s %%(linkto)s)" % ( + tschema, tschema, rschema, eschema) + add_msg(w, label) + add_msg(w, label2) + break # XXX also generate "creating ...' messages for actions in the # addrelated submenu w('# subject and object forms for each relation type\n') diff -r aff75b69db92 -r 2c48c091b6a2 devtools/fill.py --- a/devtools/fill.py Tue Jul 02 17:09:04 2013 +0200 +++ b/devtools/fill.py Mon Jan 13 13:47:47 2014 +0100 @@ -139,7 +139,7 @@ def generate_integer(self, entity, attrname, index): """generates a consistent value for 'attrname' if it's an integer""" return self._constrained_generate(entity, attrname, 0, 1, index) - generate_int = generate_integer + generate_int = generate_bigint = generate_integer def generate_float(self, entity, attrname, index): """generates a consistent value for 'attrname' if it's a float""" diff -r aff75b69db92 -r 2c48c091b6a2 devtools/httptest.py --- a/devtools/httptest.py Tue Jul 02 17:09:04 2013 +0200 +++ b/devtools/httptest.py Mon Jan 13 13:47:47 2014 +0100 @@ -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 aff75b69db92 -r 2c48c091b6a2 devtools/instrument.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/devtools/instrument.py Mon Jan 13 13:47:47 2014 +0100 @@ -0,0 +1,224 @@ +# 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 . +"""Instrumentation utilities""" + +import os + +try: + import pygraphviz +except ImportError: + pygraphviz = None + +from cubicweb.cwvreg import CWRegistryStore +from cubicweb.devtools.devctl import DevConfiguration + + +ALL_COLORS = [ + "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF", "000000", + "800000", "008000", "000080", "808000", "800080", "008080", "808080", + "C00000", "00C000", "0000C0", "C0C000", "C000C0", "00C0C0", "C0C0C0", + "400000", "004000", "000040", "404000", "400040", "004040", "404040", + "200000", "002000", "000020", "202000", "200020", "002020", "202020", + "600000", "006000", "000060", "606000", "600060", "006060", "606060", + "A00000", "00A000", "0000A0", "A0A000", "A000A0", "00A0A0", "A0A0A0", + "E00000", "00E000", "0000E0", "E0E000", "E000E0", "00E0E0", "E0E0E0", + ] +_COLORS = {} +def get_color(key): + try: + return _COLORS[key] + except KeyError: + _COLORS[key] = '#'+ALL_COLORS[len(_COLORS) % len(ALL_COLORS)] + return _COLORS[key] + +def warn(msg, *args): + print 'WARNING: %s' % (msg % args) + +def info(msg): + print 'INFO: ' + msg + + +class PropagationAnalyzer(object): + """Abstract propagation analyzer, providing utility function to extract + entities involved in propagation from a schema, as well as propagation + rules from hooks (provided they use intrumentalized sets, see + :class:`CubeTracerSet`). + + Concrete classes should at least define `prop_rel` class attribute and + implements the `is_root` method. + + See `localperms` or `nosylist` cubes for example usage (`ccplugin` module). + """ + prop_rel = None # name of the propagation relation + + def init(self, cube): + """Initialize analyze for the given cube, returning the (already loaded) + vregistry and a set of entities which we're interested in. + """ + config = DevConfiguration(cube) + schema = config.load_schema() + vreg = CWRegistryStore(config) + vreg.set_schema(schema) # set_schema triggers objects registrations + eschemas = set(eschema for eschema in schema.entities() + if self.should_include(eschema)) + return vreg, eschemas + + def is_root(self, eschema): + """Return `True` if given entity schema is a root of the graph""" + raise NotImplementedError() + + def should_include(self, eschema): + """Return `True` if given entity schema should be included by the graph. + """ + + if self.prop_rel in eschema.subjrels or self.is_root(eschema): + return True + return False + + def prop_edges(self, s_rels, o_rels, eschemas): + """Return a set of edges where propagation has been detected. + + Each edge is defined by a 4-uple (from node, to node, rtype, package) + where `rtype` is the relation type bringing from to and `package` is the cube adding the rule to the propagation + control set (see see :class:`CubeTracerSet`). + """ + schema = iter(eschemas).next().schema + prop_edges = set() + for rtype in s_rels: + found = False + for subj, obj in schema.rschema(rtype).rdefs: + if subj in eschemas and obj in eschemas: + found = True + prop_edges.add( (subj, obj, rtype, s_rels.value_cube[rtype]) ) + if not found: + warn('no rdef match for %s', rtype) + for rtype in o_rels: + found = False + for subj, obj in schema.rschema(rtype).rdefs: + if subj in eschemas and obj in eschemas: + found = True + prop_edges.add( (obj, subj, rtype, o_rels.value_cube[rtype]) ) + if not found: + warn('no rdef match for %s', rtype) + return prop_edges + + def detect_problems(self, eschemas, edges): + """Given the set of analyzed entity schemas and edges between them, + return a set of entity schemas where a problem has been detected. + """ + problematic = set() + for eschema in eschemas: + if self.has_problem(eschema, edges): + problematic.add(eschema) + not_problematic = set(eschemas).difference(problematic) + if not_problematic: + info('nothing problematic in: %s' % + ', '.join(e.type for e in not_problematic)) + return problematic + + def has_problem(self, eschema, edges): + """Return `True` if the given schema is considered problematic, + considering base propagation rules. + """ + root = self.is_root(eschema) + has_prop_rel = self.prop_rel in eschema.subjrels + # root but no propagation relation + if root and not has_prop_rel: + warn('%s is root but miss %s', eschema, self.prop_rel) + return True + # propagated but without propagation relation / not propagated but + # with propagation relation + if not has_prop_rel and \ + any(edge for edge in edges if edge[1] == eschema): + warn("%s miss %s but is reached by propagation", + eschema, self.prop_rel) + return True + elif has_prop_rel and not root: + rdef = eschema.rdef(self.prop_rel, takefirst=True) + edges = [edge for edge in edges if edge[1] == eschema] + if not edges: + warn("%s has %s but isn't reached by " + "propagation", eschema, self.prop_rel) + return True + # require_permission relation / propagation rule not added by + # the same cube + elif not any(edge for edge in edges if edge[-1] == rdef.package): + warn('%s has %s relation / propagation rule' + ' not added by the same cube (%s / %s)', eschema, + self.prop_rel, rdef.package, edges[0][-1]) + return True + return False + + def init_graph(self, eschemas, edges, problematic): + """Initialize and return graph, adding given nodes (entity schemas) and + edges between them. + + Require pygraphviz installed. + """ + if pygraphviz is None: + raise RuntimeError('pygraphviz is not installed') + graph = pygraphviz.AGraph(strict=False, directed=True) + for eschema in eschemas: + if eschema in problematic: + params = {'color': '#ff0000', 'fontcolor': '#ff0000'} + else: + params = {}#'color': get_color(eschema.package)} + graph.add_node(eschema.type, **params) + for subj, obj, rtype, package in edges: + graph.add_edge(str(subj), str(obj), label=rtype, + color=get_color(package)) + return graph + + def add_colors_legend(self, graph): + """Add a legend of used colors to the graph.""" + for package, color in sorted(_COLORS.iteritems()): + graph.add_node(package, color=color, fontcolor=color, shape='record') + + +class CubeTracerSet(object): + """Dumb set implementation whose purpose is to keep track of which cube is + being loaded when something is added to the set. + + Results will be found in the `value_cube` attribute dictionary. + + See `localperms` or `nosylist` cubes for example usage (`hooks` module). + """ + def __init__(self, vreg, wrapped): + self.vreg = vreg + self.wrapped = wrapped + self.value_cube = {} + + def add(self, value): + self.wrapped.add(value) + cube = self.vreg.currently_loading_cube + if value in self.value_cube: + warn('%s is propagated by cube %s and cube %s', + value, self.value_cube[value], cube) + else: + self.value_cube[value] = cube + + def __iter__(self): + return iter(self.wrapped) + + def __ior__(self, other): + for value in other: + self.add(value) + return self + + def __ror__(self, other): + other |= self.wrapped + return other diff -r aff75b69db92 -r 2c48c091b6a2 devtools/test/data/cubes/__init__.py diff -r aff75b69db92 -r 2c48c091b6a2 devtools/test/data/cubes/i18ntestcube/__init__.py diff -r aff75b69db92 -r 2c48c091b6a2 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 Mon Jan 13 13:47:47 2014 +0100 @@ -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 aff75b69db92 -r 2c48c091b6a2 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 Mon Jan 13 13:47:47 2014 +0100 @@ -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 aff75b69db92 -r 2c48c091b6a2 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 Mon Jan 13 13:47:47 2014 +0100 @@ -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 aff75b69db92 -r 2c48c091b6a2 devtools/test/data/cubes/i18ntestcube/views.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/devtools/test/data/cubes/i18ntestcube/views.py Mon Jan 13 13:47:47 2014 +0100 @@ -0,0 +1,64 @@ +# copyright 2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr -- mailto:contact@logilab.fr +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with this program. If not, see . + +"""cubicweb-forum views/forms/actions/components for web ui""" + +from cubicweb import view +from cubicweb.predicates import is_instance +from cubicweb.web.views import primary, baseviews, uicfg +from cubicweb.web.views.uicfg import autoform_section as afs + +class MyAFS(uicfg.AutoformSectionRelationTags): + __select__ = is_instance('ForumThread') + +_myafs = MyAFS() + +# XXX useless ASA logilab.common.registry is fixed +_myafs.__module__ = "cubes.i18ntestcube.views" + +_myafs.tag_object_of(('*', 'in_forum', 'Forum'), 'main', 'inlined') + +afs.tag_object_of(('*', 'in_forum', 'Forum'), 'main', 'inlined') + + +class ForumSameETypeListView(baseviews.SameETypeListView): + __select__ = baseviews.SameETypeListView.__select__ & is_instance('Forum') + + def call(self, **kwargs): + _ = self._cw._ + _('Topic'), _('Description') + _('Number of threads'), _('Last activity') + _('''a long +tranlated line +hop.''') + + +class ForumLastActivity(view.EntityView): + __regid__ = 'forum_last_activity' + __select__ = view.EntityView.__select__ & is_instance('Forum') + + +class ForumPrimaryView(primary.PrimaryView): + __select__ = primary.PrimaryView.__select__ & is_instance('Forum') + + def render_entity_attributes(self, entity): + _ = self._cw._ + _('Subject'), _('Created'), _('Answers'), + _('Last answered') + _('This forum does not have any thread yet.') + +class ForumThreadPrimaryView(primary.PrimaryView): + __select__ = primary.PrimaryView.__select__ & is_instance('ForumThread') diff -r aff75b69db92 -r 2c48c091b6a2 devtools/test/unittest_i18n.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/devtools/test/unittest_i18n.py Mon Jan 13 13:47:47 2014 +0100 @@ -0,0 +1,79 @@ +# -*- coding: iso-8859-1 -*- +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of CubicWeb. +# +# CubicWeb is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# CubicWeb is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with CubicWeb. If not, see . +"""unit tests for i18n messages generator""" + +import os, os.path as osp +import sys + +from logilab.common.testlib import TestCase, unittest_main + +from cubicweb.cwconfig import CubicWebNoAppConfiguration + +DATADIR = osp.join(osp.abspath(osp.dirname(__file__)), 'data') + +def load_po(fname): + """load a po file and return a set of encountered (msgid, msgctx)""" + msgs = set() + msgid = msgctxt = None + for line in open(fname): + if line.strip() in ('', '#'): + continue + if line.startswith('msgstr'): + assert not (msgid, msgctxt) in msgs + msgs.add( (msgid, msgctxt) ) + msgid = msgctxt = None + elif line.startswith('msgid'): + msgid = line.split(' ', 1)[1][1:-1] + elif line.startswith('msgctx'): + msgctxt = line.split(' ', 1)[1][1: -1] + elif msgid is not None: + msgid += line[1:-1] + elif msgctxt is not None: + msgctxt += line[1:-1] + return msgs + + +class cubePotGeneratorTC(TestCase): + """test case for i18n pot file generator""" + + def setUp(self): + self._CUBES_PATH = CubicWebNoAppConfiguration.CUBES_PATH[:] + CubicWebNoAppConfiguration.CUBES_PATH.append(osp.join(DATADIR, 'cubes')) + CubicWebNoAppConfiguration.cls_adjust_sys_path() + + def tearDown(self): + CubicWebNoAppConfiguration.CUBES_PATH[:] = self._CUBES_PATH + + def test_i18ncube(self): + # MUST import here to make, since the import statement fire + # the cube paths setup (and then must occur after the setUp) + from cubicweb.devtools.devctl import update_cube_catalogs + cube = osp.join(DATADIR, 'cubes', 'i18ntestcube') + msgs = load_po(osp.join(cube, 'i18n', 'en.po.ref')) + update_cube_catalogs(cube) + newmsgs = load_po(osp.join(cube, 'i18n', 'en.po')) + self.assertEqual(msgs, newmsgs) + +if __name__ == '__main__': + # XXX dirty hack to make this test runnable using python (works + # fine with pytest, but not with python directly if this hack is + # not present) + # XXX to remove ASA logilab.common is fixed + sys.path.append('') + unittest_main() diff -r aff75b69db92 -r 2c48c091b6a2 devtools/test/unittest_testlib.py --- a/devtools/test/unittest_testlib.py Tue Jul 02 17:09:04 2013 +0200 +++ b/devtools/test/unittest_testlib.py Mon Jan 13 13:47:47 2014 +0100 @@ -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,7 +20,9 @@ from cStringIO import StringIO from unittest import TextTestRunner + from logilab.common.testlib import TestSuite, TestCase, unittest_main +from logilab.common.registry import yes from cubicweb.devtools import htmlparser from cubicweb.devtools.testlib import CubicWebTC @@ -172,8 +174,23 @@ self.assertTrue(rdef.permissions['add']) self.assertTrue(rdef.permissions['read'], ()) + def test_temporary_appobjects_registered(self): + class AnAppobject(object): + __registries__ = ('hip',) + __regid__ = 'hop' + __select__ = yes() + registered = None + @classmethod + def __registered__(cls, reg): + cls.registered = reg + + with self.temporary_appobjects(AnAppobject): + self.assertEqual(self.vreg['hip'], AnAppobject.registered) + self.assertIn(AnAppobject, self.vreg['hip']['hop']) + self.assertNotIn(AnAppobject, self.vreg['hip']['hop']) + + class RepoAccessTC(CubicWebTC): - def test_repo_connection(self): acc = self.new_access('admin') with acc.repo_cnx() as cnx: diff -r aff75b69db92 -r 2c48c091b6a2 devtools/testlib.py --- a/devtools/testlib.py Tue Jul 02 17:09:04 2013 +0200 +++ b/devtools/testlib.py Mon Jan 13 13:47:47 2014 +0100 @@ -90,7 +90,7 @@ MAILBOX = [] -class Email: +class Email(object): """you'll get instances of Email into MAILBOX during tests that trigger some notification. @@ -99,7 +99,8 @@ * `recipients` is a list of email address which are the recipients of this message """ - def __init__(self, recipients, msg): + def __init__(self, fromaddr, recipients, msg): + self.fromaddr = fromaddr self.recipients = recipients self.msg = msg @@ -126,8 +127,8 @@ pass def close(self): pass - def sendmail(self, helo_addr, recipients, msg): - MAILBOX.append(Email(recipients, msg)) + def sendmail(self, fromaddr, recipients, msg): + MAILBOX.append(Email(fromaddr, recipients, msg)) cwconfig.SMTP = MockSMTP @@ -391,13 +392,10 @@ @nocoverage @deprecated('[4.0] explicitly use RepoAccess object in test instead') - def execute(self, rql, args=None, eidkey=None, req=None): + def execute(self, rql, args=None, req=None): """executes , builds a resultset, and returns a couple (rset, req) where req is a FakeRequest """ - if eidkey is not None: - warn('[3.8] eidkey is deprecated, you can safely remove this argument', - DeprecationWarning, stacklevel=2) req = req or self.request(rql=rql) return req.execute(unicode(rql), args) @@ -432,10 +430,7 @@ # server side db api ####################################################### @deprecated('[4.0] explicitly use RepoAccess object in test instead') - def sexecute(self, rql, args=None, eid_key=None): - if eid_key is not None: - warn('[3.8] eid_key is deprecated, you can safely remove this argument', - DeprecationWarning, stacklevel=2) + def sexecute(self, rql, args=None): self.session.set_cnxset() return self.session.execute(rql, args) @@ -620,6 +615,10 @@ self.vreg._loadedmods.setdefault(self.__module__, {}) for obj in appobjects: self.vreg.register(obj) + registered = getattr(obj, '__registered__', None) + if registered: + for registry in obj.__registries__: + registered(self.vreg[registry]) try: yield finally: @@ -1128,15 +1127,6 @@ self.assertEqual(len(MAILBOX), nb_msgs) return messages - # deprecated ############################################################### - - @deprecated('[3.8] use self.execute(...).get_entity(0, 0)') - def entity(self, rql, args=None, eidkey=None, req=None): - if eidkey is not None: - warn('[3.8] eidkey is deprecated, you can safely remove this argument', - DeprecationWarning, stacklevel=2) - return self.execute(rql, args, req=req).get_entity(0, 0) - # auto-populating test classes and utilities ################################### diff -r aff75b69db92 -r 2c48c091b6a2 doc/3.15.rst --- a/doc/3.15.rst Tue Jul 02 17:09:04 2013 +0200 +++ b/doc/3.15.rst Mon Jan 13 13:47:47 2014 +0100 @@ -13,7 +13,7 @@ used for entity cache invalidation. * Improved WSGI support. While there is still some caveats, most of the code - which as twisted only is now generic and allows related functionalities to work + which was twisted only is now generic and allows related functionalities to work with a WSGI front-end. * Full undo/transaction support : undo of modification has eventually been diff -r aff75b69db92 -r 2c48c091b6a2 doc/3.18.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/3.18.rst Mon Jan 13 13:47:47 2014 +0100 @@ -0,0 +1,101 @@ +What's new in CubicWeb 3.18? +============================ + +The migration script does not handle sqlite nor mysql instances. + + +New functionalities +-------------------- + +* add a security debugging tool + (see `#2920304 `_) + +* introduce an `add` permission on attributes, to be interpreted at + entity creation time only and allow the implementation of complex + `update` rules that don't block entity creation (before that the + `update` attribute permission was interpreted at entity creation and + update time) + +* the primary view display controller (uicfg) now has a + `set_fields_order` method similar to the one available for forms + +* new method `ResultSet.one(col=0)` to retrive a single entity and enforce the + result has only one row (see `#3352314 https://www.cubicweb.org/ticket/3352314`_) + +* new method `RequestSessionBase.find` to look for entities + (see `#3361290 https://www.cubicweb.org/ticket/3361290`_) + +* the embedded jQuery copy has been updated to version 1.10.2, and jQuery UI to + version 1.10.3. + +* initial support for wsgi for the debug mode, available through the new + ``wsgi`` cubicweb-ctl command, which can use either python's builtin + wsgi server or the werkzeug module if present. + +* a ``rql-table`` directive is now available in ReST fields + +* cubicweb-ctl upgrade can now generate the static data resource directory + directly, without a manual call to gen-static-datadir. + +API changes +----------- + +* not really an API change, but the entity permission checks are now + systematically deferred to an operation, instead of a) trying in a + hook and b) if it failed, retrying later in an operation + +* The default value storage for attributes is no longer String, but + Bytes. This opens the road to storing arbitrary python objects, e.g. + numpy arrays, and fixes a bug where default values whose truth value + was False were not properly migrated. + +* `symmetric` relations are no more handled by an rql rewrite but are + now handled with hooks (from the `activeintegrity` category); this + may have some consequences for applications that do low-level database + manipulations or at times disable (some) hooks. + +* `unique together` constraints (multi-columns unicity constraints) + get a `name` attribute that maps the CubicWeb contraint entities to + corresponding backend index. + +* BreadCrumbEntityVComponent's open_breadcrumbs method now includes + the first breadcrumbs separator + +* entities can be compared for equality and hashed + +* the ``on_fire_transition`` predicate accepts a sequence of possible + transition names + +* the GROUP_CONCAT rql aggregate function no longer repeats duplicate + values, on the sqlite and postgresql backends + +Deprecation +--------------------- + +* ``pyrorql`` sources have been deprecated. Multisource will be fully dropped + in the next version. If you are still using pyrorql, switch to ``datafeed`` + **NOW**! + +* the old multi-source system + +* `find_one_entity` and `find_entities` in favor of `find` + (see `#3361290 https://www.cubicweb.org/ticket/3361290`_) + +* the `TmpFileViewMixin` and `TmpPngView` classes (see `#3400448 + https://www.cubicweb.org/ticket/3400448`_) + +Deprecated Code Drops +---------------------- + +* ``ldapuser`` have been dropped; use ``ldapfeed`` now + (see `#2936496 `_) + +* action ``GotRhythm`` was removed, make sure you do not + import it in your cubes (even to unregister it) + (see `#3093362 `_) + +* all 3.8 backward compat is gone + +* all 3.9 backward compat (including the javascript side) is gone + +* the ``twisted`` (web-only) instance type has been removed diff -r aff75b69db92 -r 2c48c091b6a2 doc/book/en/admin/config.rst --- a/doc/book/en/admin/config.rst Tue Jul 02 17:09:04 2013 +0200 +++ b/doc/book/en/admin/config.rst Mon Jan 13 13:47:47 2014 +0100 @@ -57,19 +57,28 @@ PostgreSQL ~~~~~~~~~~ -For installation, please refer to the `PostgreSQL project online documentation`_. - -.. _`PostgreSQL project online documentation`: http://www.postgresql.org/ +Many Linux distributions ship with the appropriate PostgreSQL packages. +Basically, you need to install the following packages: -You need to install the three following packages: `postgresql-8.X`, -`postgresql-client-8.X`, and `postgresql-plpython-8.X`. If you run postgres -version prior to 8.3, you'll also need the `postgresql-contrib-8.X` package for -full-text search extension. +* `postgresql` and `postgresql-client`, which will pull the respective + versioned packages (e.g. `postgresql-9.1` and `postgresql-client-9.1`) and, + optionally, +* a `postgresql-plpython-X.Y` package with a version corresponding to that of + the aforementioned packages (e.g. `postgresql-plpython-9.1`). + +If you run postgres version prior to 8.3, you'll also need the +`postgresql-contrib-8.X` package for full-text search extension. If you run postgres on another host than the |cubicweb| repository, you should install the `postgresql-client` package on the |cubicweb| host, and others on the database host. +For extra details concerning installation, please refer to the `PostgreSQL +project online documentation`_. + +.. _`PostgreSQL project online documentation`: http://www.postgresql.org/docs + + Database cluster ++++++++++++++++ diff -r aff75b69db92 -r 2c48c091b6a2 doc/book/en/admin/setup.rst --- a/doc/book/en/admin/setup.rst Tue Jul 02 17:09:04 2013 +0200 +++ b/doc/book/en/admin/setup.rst Mon Jan 13 13:47:47 2014 +0100 @@ -121,35 +121,50 @@ `Virtualenv` install -------------------- -Since version 3.9, |cubicweb| can be safely installed, used and contained inside -a `virtualenv`_. You can use either :ref:`pip ` or -:ref:`easy_install ` to install |cubicweb| inside an -activated virtual environment. +|cubicweb| can be safely installed, used and contained inside a +`virtualenv`_. You can use either :ref:`pip ` or +:ref:`easy_install ` to install |cubicweb| +inside an activated virtual environment. .. _PipInstallation: `pip` install ------------- -pip_ is a python utility that helps downloading, building, installing, and -managing python packages and their dependencies. It is fully compatible with -`virtualenv`_ and installs the packages from sources published on the -`The Python Package Index`_. +`pip `_ is a python tool that helps downloading, +building, installing, and managing Python packages and their dependencies. It +is fully compatible with `virtualenv`_ and installs the packages from sources +published on the `The Python Package Index`_. -.. _`pip`: http://pip.openplans.org/ .. _`virtualenv`: http://virtualenv.openplans.org/ A working compilation chain is needed to build the modules that include C -extensions. If you definitively wont, installing `Lxml `_, +extensions. If you really do not want to compile anything, installing `Lxml `_, `Twisted Web `_ and `libgecode `_ will help. -To install |cubicweb| and its dependencies, just run:: +For Debian, these minimal dependencies can be obtained by doing:: + + apt-get install gcc python-pip python-dev python-lxml + +or, if you prefer to get as much as possible from pip:: + + apt-get install gcc python-pip python-dev libxslt1-dev libxml2-dev + +For Windows, you can install pre-built packages (possible `source +`_). For a minimal setup, install +`pip `_, `setuptools +`_, `libxml-python +`_, `lxml +`_ and `twisted +`_ from this source making +sure to choose the correct architecture and version of Python. + +Finally, install |cubicweb| and its dependencies, by running:: pip install cubicweb -There is also a wide variety of :ref:`cubes `. You can access a -list of available cubes on +Many other :ref:`cubes ` are available. A list is available at `PyPI `_ or at the `CubicWeb.org forge`_. diff -r aff75b69db92 -r 2c48c091b6a2 doc/book/en/annexes/rql/language.rst --- a/doc/book/en/annexes/rql/language.rst Tue Jul 02 17:09:04 2013 +0200 +++ b/doc/book/en/annexes/rql/language.rst Mon Jan 13 13:47:47 2014 +0100 @@ -81,7 +81,7 @@ are expressed as explained below: * string should be between double or single quotes. If the value contains a - quote, it should be preceded by a backslash '\' + quote, it should be preceded by a backslash '\\' * floats separator is dot '.' @@ -747,7 +747,7 @@ .. sourcecode:: sql - INSERT Person X: X name 'foo', X friend Y WHERE name 'nice' + INSERT Person X: X name 'foo', X friend Y WHERE Y name 'nice' .. _RQLSetQuery: diff -r aff75b69db92 -r 2c48c091b6a2 doc/book/en/devrepo/datamodel/definition.rst --- a/doc/book/en/devrepo/datamodel/definition.rst Tue Jul 02 17:09:04 2013 +0200 +++ b/doc/book/en/devrepo/datamodel/definition.rst Mon Jan 13 13:47:47 2014 +0100 @@ -226,13 +226,13 @@ * `SizeConstraint`: allows to specify a minimum and/or maximum size on string (generic case of `maxsize`) -* `BoundConstraint`: allows to specify a minimum and/or maximum value +* `BoundaryConstraint`: allows to specify a minimum and/or maximum value on numeric types and date .. sourcecode:: python - from yams.constraints import BoundConstraint, TODAY - BoundConstraint('<=', TODAY()) + from yams.constraints import BoundaryConstraint, TODAY + BoundaryConstraint('<=', TODAY()) * `IntervalBoundConstraint`: allows to specify an interval with included values @@ -330,7 +330,8 @@ For a relation, the possible actions are `read`, `add`, and `delete`. -For an attribute, the possible actions are `read`, and `update`. +For an attribute, the possible actions are `read`, `add` and `update`, +and they are a refinement of an entity type permission. For each access type, a tuple indicates the name of the authorized groups and/or one or multiple RQL expressions to satisfy to grant access. The access is @@ -364,7 +365,8 @@ .. sourcecode:: python __permissions__ = {'read': ('managers', 'users', 'guests',), - 'update': ('managers', ERQLExpression('U has_update_permission X')),} + 'add': ('managers', ERQLExpression('U has_add_permission X'), + 'update': ('managers', ERQLExpression('U has_update_permission X')),} The standard user groups ```````````````````````` @@ -476,13 +478,8 @@ Here are the current rules: -1. permission to add/update entity and its attributes are checked: - - - on commit if the entity has been added - - - in an 'after_update_entity' hook if the entity has been updated. If it fails - at this time, it will be retried on commit (hence you get the permission if - you have it just after the modification or *at* commit time) +1. permission to add/update entity and its attributes are checked on + commit 2. permission to delete an entity is checked in 'before_delete_entity' hook diff -r aff75b69db92 -r 2c48c091b6a2 doc/book/en/devrepo/devcore/dbapi.rst --- a/doc/book/en/devrepo/devcore/dbapi.rst Tue Jul 02 17:09:04 2013 +0200 +++ b/doc/book/en/devrepo/devcore/dbapi.rst Mon Jan 13 13:47:47 2014 +0100 @@ -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 aff75b69db92 -r 2c48c091b6a2 doc/book/en/devrepo/entityclasses/adapters.rst --- a/doc/book/en/devrepo/entityclasses/adapters.rst Tue Jul 02 17:09:04 2013 +0200 +++ b/doc/book/en/devrepo/entityclasses/adapters.rst Mon Jan 13 13:47:47 2014 +0100 @@ -10,13 +10,7 @@ .. _`interfaces`: http://java.sun.com/docs/books/tutorial/java/concepts/interface.html .. _`adapter`: http://en.wikipedia.org/wiki/Adapter_pattern -In |cubicweb| adapters provide logical functionalities to entity types. They -are introduced in version `3.9`. Before that one had to implement Interfaces in -entity classes to achieve a similar goal. However, the problem with this -approach is that is clutters the entity class's namespace, exposing name -collision risks with schema attributes/relations or even methods names -(different interfaces may define the same method with not necessarily the same -behaviour expected). +In |cubicweb| adapters provide logical functionalities to entity types. Definition of an adapter is quite trivial. An excerpt from cubicweb itself (found in :mod:`cubicweb.entities.adapters`): diff -r aff75b69db92 -r 2c48c091b6a2 doc/book/en/devrepo/entityclasses/application-logic.rst --- a/doc/book/en/devrepo/entityclasses/application-logic.rst Tue Jul 02 17:09:04 2013 +0200 +++ b/doc/book/en/devrepo/entityclasses/application-logic.rst Mon Jan 13 13:47:47 2014 +0100 @@ -2,7 +2,7 @@ ---------------------------------------- The previous chapters detailed the classes and methods available to -the developper at the so-called `ORM`_ level. However they say little +the developer at the so-called `ORM`_ level. However they say little about the common patterns of usage of these objects. .. _`ORM`: http://en.wikipedia.org/wiki/Object-relational_mapping @@ -13,22 +13,22 @@ Hooks and Operations provide support for the implementation of rules such as computed attributes, coherency invariants, etc (they play the -same role as database triggers, but in a way that is independant of +same role as database triggers, but in a way that is independent of the actual data sources). So a lot of an application's business rules will be written in Hooks (or Operations). On the web side, views also typically operate using entity -objects. Obvious entity methods for use in views are the dublin code -method like dc_title, etc. For separation of concerns reasons, one +objects. Obvious entity methods for use in views are the Dublin Core +methods like ``dc_title``. For separation of concerns reasons, one should ensure no ui logic pervades the entities level, and also no business logic should creep into the views. In the duration of a transaction, entities objects can be instantiated many times, in views and hooks, even for the same database entity. For instance, in a classic CubicWeb deployment setup, the repository and -the web frontend are separated process communicating over the +the web front-end are separated process communicating over the wire. There is no way state can be shared between these processes (there is a specific API for that). Hence, it is not possible to use entity objects as messengers between these components of an @@ -38,24 +38,24 @@ object was built. Setting an attribute or relation value can be done in the context of a -Hook/Operation, using the obj.cw_set(x=42) notation or a plain -RQL SET expression. +Hook/Operation, using the ``obj.cw_set(x=42)`` notation or a plain +RQL ``SET`` expression. In views, it would be preferable to encapsulate the necessary logic in a method of an adapter for the concerned entity class(es). But of -course, this advice is also reasonnable for Hooks/Operations, though +course, this advice is also reasonable for Hooks/Operations, though the separation of concerns here is less stringent than in the case of views. This leads to the practical role of objects adapters: it's where an -important part of the application logic lie (the other part being +important part of the application logic lies (the other part being located in the Hook/Operations). Anatomy of an entity class -------------------------- We can look now at a real life example coming from the `tracker`_ -cube. Let us begin to study the entities/project.py content. +cube. Let us begin to study the ``entities/project.py`` content. .. sourcecode:: python @@ -77,10 +77,10 @@ The fact that the `Project` entity type implements an ``ITree`` interface is materialized by the ``ProjectAdapter`` class (inheriting -the pre-defined ``ITreeAdapter`` whose __regid__ is of course +the pre-defined ``ITreeAdapter`` whose ``__regid__`` is of course ``ITree``), which will be selected on `Project` entity types because of its selector. On this adapter, we redefine the ``tree_relation`` -attribute of the ITreeAdapter class. +attribute of the ``ITreeAdapter`` class. This is typically used in views concerned with the representation of tree-like structures (CubicWeb provides several such views). @@ -95,16 +95,16 @@ about the transitive closure of the child relation). This is a further argument to implement it at entity class level. -`fetch_attrs` configures which attributes should be prefetched when using ORM -methods retrieving entity of this type. In a same manner, the `cw_fetch_order` is +``fetch_attrs`` configures which attributes should be pre-fetched when using ORM +methods retrieving entity of this type. In a same manner, the ``cw_fetch_order`` is a class method allowing to control sort order. More on this in :ref:`FetchAttrs`. -We can observe the big TICKET_DEFAULT_STATE_RESTR is a pure +We can observe the big ``TICKET_DEFAULT_STATE_RESTR`` is a pure application domain piece of data. There is, of course, no limitation to the amount of class attributes of this kind. The ``dc_title`` method provides a (unicode string) value likely to be -consummed by views, but note that here we do not care about output +consumed by views, but note that here we do not care about output encodings. We care about providing data in the most universal format possible, because the data could be used by a web view (which would be responsible of ensuring XHTML compliance), or a console or file @@ -113,8 +113,8 @@ .. note:: - The dublin code `dc_xxx` methods are not moved to an adapter as they - are extremely prevalent in cubicweb and assorted cubes and should be + The Dublin Core `dc_xxx` methods are not moved to an adapter as they + are extremely prevalent in CubicWeb and assorted cubes and should be available for all entity types. Let us now dig into more substantial pieces of code, continuing the @@ -159,8 +159,8 @@ * entity code is concerned with the application domain -* it is NOT concerned with database coherency (this is the realm of - Hooks/Operations); in other words, it assumes a coherent world +* it is NOT concerned with database consistency (this is the realm of + Hooks/Operations); in other words, it assumes a consistent world * it is NOT (directly) concerned with end-user interfaces diff -r aff75b69db92 -r 2c48c091b6a2 doc/book/en/devrepo/migration.rst --- a/doc/book/en/devrepo/migration.rst Tue Jul 02 17:09:04 2013 +0200 +++ b/doc/book/en/devrepo/migration.rst Mon Jan 13 13:47:47 2014 +0100 @@ -46,7 +46,7 @@ Again in the directory `migration`, the file `depends.map` allows to indicate that for the migration to a particular model version, you always have to first migrate to a particular *CubicWeb* version. This file can contain comments (lines -starting by `#`) and a dependancy is listed as follows: :: +starting with `#`) and a dependency is listed as follows: :: : @@ -170,9 +170,9 @@ * `rql(rql, kwargs=None, cachekey=None, ask_confirm=True)`, executes an arbitrary RQL query, either to interrogate or update. A result set object is returned. -* `add_entity(etype, *args, **kwargs)`, adds a nes entity type of the given - type. The attribute and relation values are specified using the named and - positionned parameters. +* `add_entity(etype, *args, **kwargs)`, adds a new entity of the given type. + The attribute and relation values are specified as named positional + arguments. Workflow creation ----------------- diff -r aff75b69db92 -r 2c48c091b6a2 doc/book/en/devrepo/repo/hooks.rst --- a/doc/book/en/devrepo/repo/hooks.rst Tue Jul 02 17:09:04 2013 +0200 +++ b/doc/book/en/devrepo/repo/hooks.rst Mon Jan 13 13:47:47 2014 +0100 @@ -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 aff75b69db92 -r 2c48c091b6a2 doc/book/en/devrepo/repo/sessions.rst --- a/doc/book/en/devrepo/repo/sessions.rst Tue Jul 02 17:09:04 2013 +0200 +++ b/doc/book/en/devrepo/repo/sessions.rst Mon Jan 13 13:47:47 2014 +0100 @@ -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 aff75b69db92 -r 2c48c091b6a2 doc/book/en/devrepo/testing.rst --- a/doc/book/en/devrepo/testing.rst Tue Jul 02 17:09:04 2013 +0200 +++ b/doc/book/en/devrepo/testing.rst Mon Jan 13 13:47:47 2014 +0100 @@ -18,12 +18,7 @@ convenience methods to help test all of this. In the realm of views, automatic tests check that views are valid -XHTML. See :ref:`automatic_views_tests` for details. Since 3.9, bases -for web functional testing using `windmill -`_ are set. See test cases in -cubicweb/web/test/windmill and python wrapper in -cubicweb/web/test_windmill/ if you want to use this in your own cube. - +XHTML. See :ref:`automatic_views_tests` for details. Most unit tests need a live database to work against. This is achieved by CubicWeb using automatically sqlite (bundled with Python, see diff -r aff75b69db92 -r 2c48c091b6a2 doc/book/en/devrepo/vreg.rst --- a/doc/book/en/devrepo/vreg.rst Tue Jul 02 17:09:04 2013 +0200 +++ b/doc/book/en/devrepo/vreg.rst Mon Jan 13 13:47:47 2014 +0100 @@ -81,7 +81,6 @@ .. autoclass:: cubicweb.predicates.has_mimetype .. autoclass:: cubicweb.predicates.is_in_state .. autofunction:: cubicweb.predicates.on_fire_transition -.. autoclass:: cubicweb.predicates.implements Logged user predicates diff -r aff75b69db92 -r 2c48c091b6a2 doc/book/en/devweb/request.rst --- a/doc/book/en/devweb/request.rst Tue Jul 02 17:09:04 2013 +0200 +++ b/doc/book/en/devweb/request.rst Mon Jan 13 13:47:47 2014 +0100 @@ -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 aff75b69db92 -r 2c48c091b6a2 doc/book/en/devweb/views/wdoc.rst --- a/doc/book/en/devweb/views/wdoc.rst Tue Jul 02 17:09:04 2013 +0200 +++ b/doc/book/en/devweb/views/wdoc.rst Mon Jan 13 13:47:47 2014 +0100 @@ -8,10 +8,8 @@ Help views ---------- .. autoclass:: cubicweb.web.views.wdoc.InlineHelpView -.. autoclass:: cubicweb.web.views.wdoc.ChangeLogView Actions ------- .. autoclass:: cubicweb.web.views.wdoc.HelpAction -.. autoclass:: cubicweb.web.views.wdoc.ChangeLogAction .. autoclass:: cubicweb.web.views.wdoc.AboutAction diff -r aff75b69db92 -r 2c48c091b6a2 doc/book/en/tutorials/advanced/part02_security.rst --- a/doc/book/en/tutorials/advanced/part02_security.rst Tue Jul 02 17:09:04 2013 +0200 +++ b/doc/book/en/tutorials/advanced/part02_security.rst Mon Jan 13 13:47:47 2014 +0100 @@ -259,26 +259,26 @@ # relations where the "parent" entity is the object O_RELS = set(('filed_under', 'comments',)) - class AddEntitySecurityPropagationHook(hook.PropagateSubjectRelationHook): + class AddEntitySecurityPropagationHook(hook.PropagateRelationHook): """propagate permissions when new entity are added""" __regid__ = 'sytweb.addentity_security_propagation' - __select__ = (hook.PropagateSubjectRelationHook.__select__ + __select__ = (hook.PropagateRelationHook.__select__ & hook.match_rtype_sets(S_RELS, O_RELS)) main_rtype = 'may_be_read_by' subject_relations = S_RELS object_relations = O_RELS - class AddPermissionSecurityPropagationHook(hook.PropagateSubjectRelationAddHook): + class AddPermissionSecurityPropagationHook(hook.PropagateRelationAddHook): """propagate permissions when new entity are added""" __regid__ = 'sytweb.addperm_security_propagation' - __select__ = (hook.PropagateSubjectRelationAddHook.__select__ + __select__ = (hook.PropagateRelationAddHook.__select__ & hook.match_rtype('may_be_read_by',)) subject_relations = S_RELS object_relations = O_RELS - class DelPermissionSecurityPropagationHook(hook.PropagateSubjectRelationDelHook): + class DelPermissionSecurityPropagationHook(hook.PropagateRelationDelHook): __regid__ = 'sytweb.delperm_security_propagation' - __select__ = (hook.PropagateSubjectRelationDelHook.__select__ + __select__ = (hook.PropagateRelationDelHook.__select__ & hook.match_rtype('may_be_read_by',)) subject_relations = S_RELS object_relations = O_RELS diff -r aff75b69db92 -r 2c48c091b6a2 doc/book/en/tutorials/advanced/part04_ui-base.rst --- a/doc/book/en/tutorials/advanced/part04_ui-base.rst Tue Jul 02 17:09:04 2013 +0200 +++ b/doc/book/en/tutorials/advanced/part04_ui-base.rst Mon Jan 13 13:47:47 2014 +0100 @@ -194,8 +194,6 @@ .. Note:: - * Adapters have been introduced in CubicWeb 3.9 / cubicweb-folder 1.8. - * As seen earlier, we want to **replace** the folder's `ITree` adapter by our implementation, hence the custom `registration_callback` method. @@ -241,12 +239,6 @@ ascendant/descendant ordering and a strict comparison with current file's name (the "X" variable representing the current file). -.. Note:: - - * Former `implements` selector should be replaced by one of `is_instance` / - `adaptable` selector with CubicWeb >= 3.9. In our case, `is_instance` to - tell our adapter is able to adapt `File` entities. - Notice that this query supposes we wont have two files of the same name in the same folder, else things may go wrong. Fixing this is out of the scope of this blog. And as I would like to have at some point a smarter, context sensitive @@ -358,7 +350,7 @@ You'll have to answer some questions, as we've seen in `an earlier post`_. Now that everything is tested, I can transfer the new code to the production -server, `apt-get upgrade` cubicweb 3.9 and its dependencies, and eventually +server, `apt-get upgrade` cubicweb and its dependencies, and eventually upgrade the production instance. diff -r aff75b69db92 -r 2c48c091b6a2 doc/book/en/tutorials/advanced/part05_ui-advanced.rst --- a/doc/book/en/tutorials/advanced/part05_ui-advanced.rst Tue Jul 02 17:09:04 2013 +0200 +++ b/doc/book/en/tutorials/advanced/part05_ui-advanced.rst Mon Jan 13 13:47:47 2014 +0100 @@ -1,8 +1,6 @@ Building my photos web site with |cubicweb| part V: let's make it even more user friendly ========================================================================================= -We'll now see how to benefit from features introduced in 3.9 and 3.10 releases of CubicWeb - .. _uiprops: Step 1: tired of the default look? @@ -29,9 +27,9 @@ LOGO = data('logo.jpg') -The uiprops machinery has been introduced in `CubicWeb 3.9`_. It is used to define -some static file resources, such as the logo, default Javascript / CSS files, as -well as CSS properties (we'll see that later). +The uiprops machinery is used to define some static file resources, +such as the logo, default Javascript / CSS files, as well as CSS +properties (we'll see that later). .. Note:: This file is imported specifically by |cubicweb|, with a predefined name space, @@ -373,5 +371,4 @@ .. _`CubicWeb 3.10`: http://www.cubicweb.org/blogentry/1330518 -.. _`CubicWeb 3.9`: http://www.cubicweb.org/blogentry/1179899 .. _`here`: http://webdesign.about.com/od/css3/f/blfaqbgsize.htm diff -r aff75b69db92 -r 2c48c091b6a2 doc/tools/pyjsrest.py --- a/doc/tools/pyjsrest.py Tue Jul 02 17:09:04 2013 +0200 +++ b/doc/tools/pyjsrest.py Mon Jan 13 13:47:47 2014 +0100 @@ -136,7 +136,6 @@ 'cubicweb.preferences', 'cubicweb.edition', 'cubicweb.reledit', - 'cubicweb.rhythm', 'cubicweb.timeline-ext', ] diff -r aff75b69db92 -r 2c48c091b6a2 entities/__init__.py --- a/entities/__init__.py Tue Jul 02 17:09:04 2013 +0200 +++ b/entities/__init__.py Mon Jan 13 13:47:47 2014 +0100 @@ -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. @@ -33,6 +33,11 @@ __regid__ = 'Any' __implements__ = () + @classproperty + def cw_etype(cls): + """entity type as a string""" + return cls.__regid__ + @classmethod def cw_create_url(cls, req, **kwargs): """ return the url of the entity creation form for this entity type""" @@ -58,11 +63,6 @@ # meta data api ########################################################### - @classproperty - def cw_etype(self): - """entity Etype as a string""" - return self.__regid__ - def dc_title(self): """return a suitable *unicode* title for this entity""" for rschema, attrschema in self.e_schema.attribute_definitions(): diff -r aff75b69db92 -r 2c48c091b6a2 entities/adapters.py --- a/entities/adapters.py Tue Jul 02 17:09:04 2013 +0200 +++ b/entities/adapters.py Mon Jan 13 13:47:47 2014 +0100 @@ -1,4 +1,4 @@ -# copyright 2010-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2010-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -28,9 +28,7 @@ from logilab.common.decorators import cached from cubicweb import ValidationError, view -from cubicweb.predicates import (implements, is_instance, relation_possible, - match_exception) -from cubicweb.interfaces import IDownloadable, ITree +from cubicweb.predicates import is_instance, relation_possible, match_exception class IEmailableAdapter(view.EntityAdapter): @@ -67,11 +65,9 @@ class INotifiableAdapter(view.EntityAdapter): - __needs_bw_compat__ = True __regid__ = 'INotifiable' __select__ = is_instance('Any') - @view.implements_adapter_compat('INotifiableAdapter') def notification_references(self, view): """used to control References field of email send on notification for this entity. `view` is the notification view. @@ -167,27 +163,25 @@ class IDownloadableAdapter(view.EntityAdapter): """interface for downloadable entities""" - __needs_bw_compat__ = True __regid__ = 'IDownloadable' - __select__ = implements(IDownloadable, warn=False) # XXX for bw compat, else should be abstract + __abstract__ = True - @view.implements_adapter_compat('IDownloadable') def download_url(self, **kwargs): # XXX not really part of this interface """return an url to download entity's content""" raise NotImplementedError - @view.implements_adapter_compat('IDownloadable') + def download_content_type(self): """return MIME type of the downloadable content""" raise NotImplementedError - @view.implements_adapter_compat('IDownloadable') + def download_encoding(self): """return encoding of the downloadable content""" raise NotImplementedError - @view.implements_adapter_compat('IDownloadable') + def download_file_name(self): """return file name of the downloadable content""" raise NotImplementedError - @view.implements_adapter_compat('IDownloadable') + def download_data(self): """return actual data of the downloadable content""" raise NotImplementedError @@ -219,27 +213,16 @@ .. automethod: children_rql .. automethod: path """ - __needs_bw_compat__ = True __regid__ = 'ITree' - __select__ = implements(ITree, warn=False) # XXX for bw compat, else should be abstract + __abstract__ = True child_role = 'subject' parent_role = 'object' - @property - def tree_relation(self): - warn('[3.9] tree_attribute is deprecated, define tree_relation on a custom ' - 'ITree for %s instead' % (self.entity.__class__), - DeprecationWarning) - return self.entity.tree_attribute - - # XXX should be removed from the public interface - @view.implements_adapter_compat('ITree') def children_rql(self): """Returns RQL to get the children of the entity.""" return self.entity.cw_related_rql(self.tree_relation, self.parent_role) - @view.implements_adapter_compat('ITree') def different_type_children(self, entities=True): """Return children entities of different type as this entity. @@ -253,7 +236,6 @@ return [e for e in res if e.e_schema != eschema] return res.filtered_rset(lambda x: x.e_schema != eschema, self.entity.cw_col) - @view.implements_adapter_compat('ITree') def same_type_children(self, entities=True): """Return children entities of the same type as this entity. @@ -267,23 +249,19 @@ return [e for e in res if e.e_schema == eschema] return res.filtered_rset(lambda x: x.e_schema is eschema, self.entity.cw_col) - @view.implements_adapter_compat('ITree') def is_leaf(self): """Returns True if the entity does not have any children.""" return len(self.children()) == 0 - @view.implements_adapter_compat('ITree') def is_root(self): """Returns true if the entity is root of the tree (e.g. has no parent). """ return self.parent() is None - @view.implements_adapter_compat('ITree') def root(self): """Return the root entity of the tree.""" return self._cw.entity_from_eid(self.path()[0]) - @view.implements_adapter_compat('ITree') def parent(self): """Returns the parent entity if any, else None (e.g. if we are on the root). @@ -294,7 +272,6 @@ except (KeyError, IndexError): return None - @view.implements_adapter_compat('ITree') def children(self, entities=True, sametype=False): """Return children entities. @@ -307,7 +284,6 @@ return self.entity.related(self.tree_relation, self.parent_role, entities=entities) - @view.implements_adapter_compat('ITree') def iterparents(self, strict=True): """Return an iterator on the parents of the entity.""" def _uptoroot(self): @@ -322,7 +298,6 @@ return chain([self.entity], _uptoroot(self)) return _uptoroot(self) - @view.implements_adapter_compat('ITree') def iterchildren(self, _done=None): """Return an iterator over the item's children.""" if _done is None: @@ -334,7 +309,6 @@ yield child _done.add(child.eid) - @view.implements_adapter_compat('ITree') def prefixiter(self, _done=None): """Return an iterator over the item's descendants in a prefixed order.""" if _done is None: @@ -347,7 +321,6 @@ for entity in child.cw_adapt_to('ITree').prefixiter(_done): yield entity - @view.implements_adapter_compat('ITree') @cached def path(self): """Returns the list of eids from the root object to this object.""" @@ -379,6 +352,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 +360,16 @@ 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)) + _ = self._cw._ + rtypes = self.exc.rtypes + rtypes_msg = {} + for rtype in rtypes: + rtypes_msg[rtype] = _('%s is part of violated unicity constraint') % rtype + globalmsg = _('some relations violate a unicity constraint') + rtypes_msg['unicity constraint'] = globalmsg + raise ValidationError(self.entity.eid, rtypes_msg) # deprecated ################################################################### diff -r aff75b69db92 -r 2c48c091b6a2 entities/lib.py --- a/entities/lib.py Tue Jul 02 17:09:04 2013 +0200 +++ b/entities/lib.py Mon Jan 13 13:47:47 2014 +0100 @@ -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. @@ -15,14 +15,15 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""entity classes for optional library entities +"""entity classes for optional library entities""" -""" __docformat__ = "restructuredtext en" from urlparse import urlsplit, urlunsplit from datetime import datetime +from logilab.mtconverter import xml_escape + from cubicweb import UnknownProperty from cubicweb.entity import _marker from cubicweb.entities import AnyEntity, fetch_config @@ -81,7 +82,10 @@ format='text/html'): """overriden to return displayable address when necessary""" if attr == 'address': - return self.display_address() + address = self.display_address() + if format == 'text/html': + address = xml_escape(address) + return address return super(EmailAddress, self).printable_value(attr, value, attrtype, format) diff -r aff75b69db92 -r 2c48c091b6a2 entities/test/unittest_base.py --- a/entities/test/unittest_base.py Tue Jul 02 17:09:04 2013 +0200 +++ b/entities/test/unittest_base.py Mon Jan 13 13:47:47 2014 +0100 @@ -25,7 +25,6 @@ from cubicweb.devtools.testlib import CubicWebTC -from cubicweb.interfaces import IMileStone, ICalendarable from cubicweb.entities import AnyEntity @@ -82,12 +81,19 @@ self.assertEqual(email.display_address(), 'maarten.ter.huurne@philips.com') self.assertEqual(email.printable_value('address'), 'maarten.ter.huurne@philips.com') self.vreg.config.global_set_option('mangle-emails', True) - self.assertEqual(email.display_address(), 'maarten.ter.huurne at philips dot com') - self.assertEqual(email.printable_value('address'), 'maarten.ter.huurne at philips dot com') - email = self.execute('INSERT EmailAddress X: X address "syt"').get_entity(0, 0) - self.assertEqual(email.display_address(), 'syt') - self.assertEqual(email.printable_value('address'), 'syt') + try: + self.assertEqual(email.display_address(), 'maarten.ter.huurne at philips dot com') + self.assertEqual(email.printable_value('address'), 'maarten.ter.huurne at philips dot com') + email = self.execute('INSERT EmailAddress X: X address "syt"').get_entity(0, 0) + self.assertEqual(email.display_address(), 'syt') + self.assertEqual(email.printable_value('address'), 'syt') + finally: + self.vreg.config.global_set_option('mangle-emails', False) + def test_printable_value_escape(self): + email = self.execute('INSERT EmailAddress X: X address "maarten&ter@philips.com"').get_entity(0, 0) + self.assertEqual(email.printable_value('address'), 'maarten&ter@philips.com') + self.assertEqual(email.printable_value('address', format='text/plain'), 'maarten&ter@philips.com') class CWUserTC(BaseEntityTC): @@ -127,27 +133,6 @@ self.request().create_entity('CWGroup', name=u'logilab', reverse_in_group=e) -class InterfaceTC(CubicWebTC): - - def test_nonregr_subclasses_and_mixins_interfaces(self): - from cubicweb.entities.wfobjs import WorkflowableMixIn - WorkflowableMixIn.__implements__ = (ICalendarable,) - CWUser = self.vreg['etypes'].etype_class('CWUser') - class MyUser(CWUser): - __implements__ = (IMileStone,) - self.vreg._loadedmods[__name__] = {} - self.vreg.register(MyUser) - self.vreg['etypes'].initialization_completed() - MyUser_ = self.vreg['etypes'].etype_class('CWUser') - # a copy is done systematically - self.assertTrue(issubclass(MyUser_, MyUser)) - self.assertTrue(implements(MyUser_, IMileStone)) - self.assertTrue(implements(MyUser_, ICalendarable)) - # original class should not have beed modified, only the copy - self.assertTrue(implements(MyUser, IMileStone)) - self.assertFalse(implements(MyUser, ICalendarable)) - - class SpecializedEntityClassesTC(CubicWebTC): def select_eclass(self, etype): diff -r aff75b69db92 -r 2c48c091b6a2 entities/test/unittest_wfobjs.py --- a/entities/test/unittest_wfobjs.py Tue Jul 02 17:09:04 2013 +0200 +++ b/entities/test/unittest_wfobjs.py Mon Jan 13 13:47:47 2014 +0100 @@ -75,7 +75,7 @@ wf.add_transition(u'baz', (bar,), foo) with self.assertRaises(ValidationError) as cm: self.commit() - self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already have a transition of that name'}) + self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already has a transition of that name'}) # no pb if not in the same workflow wf2 = add_wf(self, 'Company') foo = wf.add_state(u'foo', initial=True) @@ -88,7 +88,7 @@ biz.cw_set(name=u'baz') with self.assertRaises(ValidationError) as cm: self.commit() - self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already have a transition of that name'}) + self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already has a transition of that name'}) class WorkflowTC(CubicWebTC): diff -r aff75b69db92 -r 2c48c091b6a2 entities/wfobjs.py --- a/entities/wfobjs.py Tue Jul 02 17:09:04 2013 +0200 +++ b/entities/wfobjs.py Mon Jan 13 13:47:47 2014 +0100 @@ -32,7 +32,6 @@ from cubicweb.entities import AnyEntity, fetch_config from cubicweb.view import EntityAdapter from cubicweb.predicates import relation_possible -from cubicweb.mixins import MI_REL_TRIGGERS class WorkflowException(Exception): pass @@ -379,65 +378,8 @@ return self.by_transition and self.by_transition[0] or None -class WorkflowableMixIn(object): - """base mixin providing workflow helper methods for workflowable entities. - This mixin will be automatically set on class supporting the 'in_state' - relation (which implies supporting 'wf_info_for' as well) - """ - @property - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').main_workflow") - def main_workflow(self): - return self.cw_adapt_to('IWorkflowable').main_workflow - @property - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').current_workflow") - def current_workflow(self): - return self.cw_adapt_to('IWorkflowable').current_workflow - @property - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').current_state") - def current_state(self): - return self.cw_adapt_to('IWorkflowable').current_state - @property - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').state") - def state(self): - return self.cw_adapt_to('IWorkflowable').state - @property - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').printable_state") - def printable_state(self): - return self.cw_adapt_to('IWorkflowable').printable_state - @property - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').workflow_history") - def workflow_history(self): - return self.cw_adapt_to('IWorkflowable').workflow_history - - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').cwetype_workflow()") - def cwetype_workflow(self): - return self.cw_adapt_to('IWorkflowable').main_workflow() - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').latest_trinfo()") - def latest_trinfo(self): - return self.cw_adapt_to('IWorkflowable').latest_trinfo() - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').possible_transitions()") - def possible_transitions(self, type='normal'): - return self.cw_adapt_to('IWorkflowable').possible_transitions(type) - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').fire_transition()") - def fire_transition(self, tr, comment=None, commentformat=None): - return self.cw_adapt_to('IWorkflowable').fire_transition(tr, comment, commentformat) - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').change_state()") - def change_state(self, statename, comment=None, commentformat=None, tr=None): - return self.cw_adapt_to('IWorkflowable').change_state(statename, comment, commentformat, tr) - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').subworkflow_input_trinfo()") - def subworkflow_input_trinfo(self): - return self.cw_adapt_to('IWorkflowable').subworkflow_input_trinfo() - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').subworkflow_input_transition()") - def subworkflow_input_transition(self): - return self.cw_adapt_to('IWorkflowable').subworkflow_input_transition() - - -MI_REL_TRIGGERS[('in_state', 'subject')] = WorkflowableMixIn - - - -class IWorkflowableAdapter(WorkflowableMixIn, EntityAdapter): +class IWorkflowableAdapter(EntityAdapter): """base adapter providing workflow helper methods for workflowable entities. """ __regid__ = 'IWorkflowable' diff -r aff75b69db92 -r 2c48c091b6a2 entity.py --- a/entity.py Tue Jul 02 17:09:04 2013 +0200 +++ b/entity.py Mon Jan 13 13:47:47 2014 +0100 @@ -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. @@ -42,7 +42,6 @@ from cubicweb.rqlrewrite import RQLRewriter from cubicweb.uilib import soup2xhtml -from cubicweb.mixins import MI_REL_TRIGGERS from cubicweb.mttransforms import ENGINE _marker = object() @@ -194,31 +193,11 @@ setattr(cls, rschema.type, Attribute(rschema.type)) mixins = [] for rschema, _, role in eschema.relation_definitions(): - if (rschema, role) in MI_REL_TRIGGERS: - mixin = MI_REL_TRIGGERS[(rschema, role)] - if not (issubclass(cls, mixin) or mixin in mixins): # already mixed ? - mixins.append(mixin) - for iface in getattr(mixin, '__implements__', ()): - if not interface.implements(cls, iface): - interface.extend(cls, iface) if role == 'subject': attr = rschema.type else: attr = 'reverse_%s' % rschema.type setattr(cls, attr, Relation(rschema, role)) - if mixins: - # see etype class instantation in cwvreg.ETypeRegistry.etype_class method: - # due to class dumping, cls is the generated top level class with actual - # user class as (only) parent. Since we want to be able to override mixins - # method from this user class, we have to take care to insert mixins after that - # class - # - # note that we don't plug mixins as user class parent since it causes pb - # with some cases of entity classes inheritance. - mixins.insert(0, cls.__bases__[0]) - mixins += cls.__bases__[1:] - cls.__bases__ = tuple(mixins) - cls.info('plugged %s mixins on %s', mixins, cls) fetch_attrs = ('modification_date',) @@ -308,7 +287,10 @@ select._varmaker = rqlvar_maker(defined=select.defined_vars, aliases=select.aliases, index=26) if settype: - select.add_type_restriction(mainvar, cls.__regid__) + rel = select.add_type_restriction(mainvar, cls.__regid__) + # should use 'is_instance_of' instead of 'is' so we retrieve + # subclasses instances as well + rel.r_type = 'is_instance_of' if fetchattrs is None: fetchattrs = cls.fetch_attrs cls._fetch_restrictions(mainvar, select, fetchattrs, user, ordermethod) @@ -558,7 +540,14 @@ raise NotImplementedError('comparison not implemented for %s' % self.__class__) def __eq__(self, other): - raise NotImplementedError('comparison not implemented for %s' % self.__class__) + if isinstance(self.eid, (int, long)): + return self.eid == other.eid + return self is other + + def __hash__(self): + if isinstance(self.eid, (int, long)): + return self.eid + return super(Entity, self).__hash__() def _cw_update_attr_cache(self, attrcache): # if context is a repository session, don't consider dont-cache-attrs as @@ -983,7 +972,7 @@ return value def related(self, rtype, role='subject', limit=None, entities=False, # XXX .cw_related - safe=False): + safe=False, targettypes=None): """returns a resultset of related entities :param rtype: @@ -997,10 +986,13 @@ :param safe: if True, an empty rset/list of entities will be returned in case of :exc:`Unauthorized`, else (the default), the exception is propagated + :param targettypes: + a tuple of target entity types to restrict the query """ rtype = str(rtype) - if limit is None: - # we cannot do much wrt cache on limited queries + # Caching restricted/limited results is best avoided. + cacheable = limit is None and targettypes is None + if cacheable: cache_key = '%s_%s' % (rtype, role) if cache_key in self._cw_related_cache: return self._cw_related_cache[cache_key][entities] @@ -1008,7 +1000,7 @@ if entities: return [] return self._cw.empty_rset() - rql = self.cw_related_rql(rtype, role, limit=limit) + rql = self.cw_related_rql(rtype, role, limit=limit, targettypes=targettypes) try: rset = self._cw.execute(rql, {'x': self.eid}) except Unauthorized: @@ -1016,9 +1008,9 @@ raise rset = self._cw.empty_rset() if entities: - if limit is None: + if cacheable: self.cw_set_relation_cache(rtype, role, rset) - return self.related(rtype, role, limit, entities) + return self.related(rtype, role, entities=entities) return list(rset.entities()) else: return rset @@ -1181,8 +1173,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 +1185,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]) @@ -1281,8 +1271,8 @@ >>> c = rql('Any X WHERE X is Company').get_entity(0, 0) >>> p = rql('Any X WHERE X is Person').get_entity(0, 0) - >>> c.set(name=u'Logilab') - >>> p.set(firstname=u'John', lastname=u'Doe', works_for=c) + >>> c.cw_set(name=u'Logilab') + >>> p.cw_set(firstname=u'John', lastname=u'Doe', works_for=c) You can also set relations where the entity has 'object' role by prefixing the relation name by 'reverse_'. Also, relation values may be @@ -1323,7 +1313,8 @@ @deprecated('[3.16] use cw_set() instead of set_attributes()') def set_attributes(self, **kwargs): # XXX cw_set_attributes - self.cw_set(**kwargs) + if kwargs: + self.cw_set(**kwargs) @deprecated('[3.16] use cw_set() instead of set_relations()') def set_relations(self, **kwargs): # XXX cw_set_relations @@ -1334,40 +1325,13 @@ (meaning that all relations of the given type from or to this object should be deleted). """ - self.cw_set(**kwargs) + if kwargs: + self.cw_set(**kwargs) @deprecated('[3.13] use entity.cw_clear_all_caches()') def clear_all_caches(self): return self.cw_clear_all_caches() - @deprecated('[3.9] use entity.cw_attr_value(attr)') - def get_value(self, name): - return self.cw_attr_value(name) - - @deprecated('[3.9] use entity.cw_delete()') - def delete(self, **kwargs): - return self.cw_delete(**kwargs) - - @deprecated('[3.9] use entity.cw_attr_metadata(attr, metadata)') - def attr_metadata(self, attr, metadata): - return self.cw_attr_metadata(attr, metadata) - - @deprecated('[3.9] use entity.cw_has_perm(action)') - def has_perm(self, action): - return self.cw_has_perm(action) - - @deprecated('[3.9] use entity.cw_set_relation_cache(rtype, role, rset)') - def set_related_cache(self, rtype, role, rset): - self.cw_set_relation_cache(rtype, role, rset) - - @deprecated('[3.9] use entity.cw_clear_relation_cache(rtype, role)') - def clear_related_cache(self, rtype=None, role=None): - self.cw_clear_relation_cache(rtype, role) - - @deprecated('[3.9] use entity.cw_related_rql(rtype, [role, [targettypes]])') - def related_rql(self, rtype, role='subject', targettypes=None): - return self.cw_related_rql(rtype, role, targettypes) - @property @deprecated('[3.10] use entity.cw_edited') def edited_attributes(self): diff -r aff75b69db92 -r 2c48c091b6a2 etwist/request.py --- a/etwist/request.py Tue Jul 02 17:09:04 2013 +0200 +++ b/etwist/request.py Mon Jan 13 13:47:47 2014 +0100 @@ -24,15 +24,21 @@ class CubicWebTwistedRequestAdapter(CubicWebRequestBase): + """ from twisted .req to cubicweb .form + req.files are put into .form[] + """ def __init__(self, req, vreg, https): self._twreq = req super(CubicWebTwistedRequestAdapter, self).__init__( vreg, https, req.args, headers=req.received_headers) - for key, (name, stream) in req.files.iteritems(): - if name is None: - self.form[key] = (name, stream) - else: - self.form[key] = (unicode(name, self.encoding), stream) + for key, name_stream_list in req.files.iteritems(): + for name, stream in name_stream_list: + if name is not None: + name = unicode(name, self.encoding) + self.form.setdefault(key, []).append((name, stream)) + # 3.16.4 backward compat + if len(self.form[key]) == 1: + self.form[key] = self.form[key][0] self.content = self._twreq.content # stream def http_method(self): diff -r aff75b69db92 -r 2c48c091b6a2 etwist/server.py --- a/etwist/server.py Tue Jul 02 17:09:04 2013 +0200 +++ b/etwist/server.py Mon Jan 13 13:47:47 2014 +0100 @@ -244,7 +244,6 @@ self._do_process_multipart = True self.process() - @monkeypatch(http.Request) def process_multipart(self): if not self._do_process_multipart: @@ -254,16 +253,17 @@ keep_blank_values=1, strict_parsing=1) for key in form: - value = form[key] - if isinstance(value, list): - self.args[key] = [v.value for v in value] - elif value.filename: - if value.done != -1: # -1 is transfer has been interrupted - self.files[key] = (value.filename, value.file) + values = form[key] + if not isinstance(values, list): + values = [values] + for value in values: + if value.filename: + if value.done != -1: # -1 is transfer has been interrupted + self.files.setdefault(key, []).append((value.filename, value.file)) + else: + self.files.setdefault(key, []).append((None, None)) else: - self.files[key] = (None, None) - else: - self.args[key] = value.value + self.args.setdefault(key, []).append(value.value) from logging import getLogger from cubicweb import set_log_methods diff -r aff75b69db92 -r 2c48c091b6a2 etwist/twconfig.py --- a/etwist/twconfig.py Tue Jul 02 17:09:04 2013 +0200 +++ b/etwist/twconfig.py Mon Jan 13 13:47:47 2014 +0100 @@ -34,9 +34,8 @@ from cubicweb.web.webconfig import WebConfiguration -class TwistedConfiguration(WebConfiguration): +class WebConfigurationBase(WebConfiguration): """web instance (in a twisted web server) client of a RQL server""" - name = 'twisted' options = merge_options(( # ctl configuration @@ -107,19 +106,17 @@ return 'http://%s:%s/' % (self['host'] or getfqdn(), self['port'] or 8080) -CONFIGURATIONS.append(TwistedConfiguration) - try: from cubicweb.server.serverconfig import ServerConfiguration - class AllInOneConfiguration(TwistedConfiguration, ServerConfiguration): + class AllInOneConfiguration(WebConfigurationBase, ServerConfiguration): """repository and web instance in the same twisted process""" name = 'all-in-one' - options = merge_options(TwistedConfiguration.options + options = merge_options(WebConfigurationBase.options + ServerConfiguration.options) - cubicweb_appobject_path = TwistedConfiguration.cubicweb_appobject_path | ServerConfiguration.cubicweb_appobject_path - cube_appobject_path = TwistedConfiguration.cube_appobject_path | ServerConfiguration.cube_appobject_path + cubicweb_appobject_path = WebConfigurationBase.cubicweb_appobject_path | ServerConfiguration.cubicweb_appobject_path + cube_appobject_path = WebConfigurationBase.cube_appobject_path | ServerConfiguration.cube_appobject_path def pyro_enabled(self): """tell if pyro is activated for the in memory repository""" return self['pyro-server'] diff -r aff75b69db92 -r 2c48c091b6a2 etwist/twctl.py --- a/etwist/twctl.py Tue Jul 02 17:09:04 2013 +0200 +++ b/etwist/twctl.py Mon Jan 13 13:47:47 2014 +0100 @@ -22,7 +22,7 @@ from logilab.common.shellutils import rm from cubicweb.toolsutils import CommandHandler -from cubicweb.web.webctl import WebCreateHandler +from cubicweb.web.webctl import WebCreateHandler, WebUpgradeHandler # trigger configuration registration import cubicweb.etwist.twconfig # pylint: disable=W0611 @@ -48,6 +48,9 @@ def poststop(self): pass +class TWUpgradeHandler(WebUpgradeHandler): + cfgname = 'twisted' + try: from cubicweb.server import serverctl @@ -73,5 +76,8 @@ cfgname = 'all-in-one' subcommand = 'cubicweb-twisted' + class AllInOneUpgradeHandler(TWUpgradeHandler): + cfgname = 'all-in-one' + except ImportError: pass diff -r aff75b69db92 -r 2c48c091b6a2 ext/rest.py --- a/ext/rest.py Tue Jul 02 17:09:04 2013 +0200 +++ b/ext/rest.py Mon Jan 13 13:47:47 2014 +0100 @@ -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. @@ -29,6 +29,8 @@ * `sourcecode` (if pygments is installed), source code colorization +* `rql-table`, create a table from a RQL query + """ __docformat__ = "restructuredtext en" @@ -40,7 +42,7 @@ from docutils import statemachine, nodes, utils, io from docutils.core import Publisher -from docutils.parsers.rst import Parser, states, directives +from docutils.parsers.rst import Parser, states, directives, Directive from docutils.parsers.rst.roles import register_canonical_role, set_classes from logilab.mtconverter import ESC_UCAR_TABLE, ESC_CAR_TABLE, xml_escape @@ -251,6 +253,76 @@ winclude_directive.options = {'literal': directives.flag, 'encoding': directives.encoding} +class RQLTableDirective(Directive): + """rql-table directive + + Example: + + .. rql-table:: + :vid: mytable + :headers: , , progress + :colvids: 2=progress + + Any X,U,X WHERE X is Project, X url U + + All fields but the RQL string are optionnal. The ``:headers:`` option can + contain empty column names. + """ + + required_arguments = 0 + optional_arguments = 0 + has_content= True + final_argument_whitespace = True + option_spec = {'vid': directives.unchanged, + 'headers': directives.unchanged, + 'colvids': directives.unchanged} + + def run(self): + errid = "rql-table directive" + self.assert_has_content() + if self.arguments: + raise self.warning('%s does not accept arguments' % errid) + rql = ' '.join([l.strip() for l in self.content]) + _cw = self.state.document.settings.context._cw + _cw.ensure_ro_rql(rql) + try: + rset = _cw.execute(rql) + except Exception as exc: + raise self.error("fail to execute RQL query in %s: %r" % + (errid, exc)) + if not rset: + raise self.warning("empty result set") + vid = self.options.get('vid', 'table') + try: + view = _cw.vreg['views'].select(vid, _cw, rset=rset) + except Exception as exc: + raise self.error("fail to select '%s' view in %s: %r" % + (vid, errid, exc)) + headers = None + if 'headers' in self.options: + headers = [h.strip() for h in self.options['headers'].split(',')] + while headers.count(''): + headers[headers.index('')] = None + if len(headers) != len(rset[0]): + raise self.error("the number of 'headers' does not match the " + "number of columns in %s" % errid) + cellvids = None + if 'colvids' in self.options: + cellvids = {} + for f in self.options['colvids'].split(','): + try: + idx, vid = f.strip().split('=') + except ValueError: + raise self.error("malformatted 'colvids' option in %s" % + errid) + cellvids[int(idx.strip())] = vid.strip() + try: + content = view.render(headers=headers, cellvids=cellvids) + except Exception as exc: + raise self.error("Error rendering %s (%s)" % (errid, exc)) + return [nodes.raw('', content, format='html')] + + try: from pygments import highlight from pygments.lexers import get_lexer_by_name @@ -385,3 +457,4 @@ directives.register_directive('winclude', winclude_directive) if pygments_directive is not None: directives.register_directive('sourcecode', pygments_directive) + directives.register_directive('rql-table', RQLTableDirective) diff -r aff75b69db92 -r 2c48c091b6a2 ext/test/data/views.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ext/test/data/views.py Mon Jan 13 13:47:47 2014 +0100 @@ -0,0 +1,24 @@ +# copyright 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 . + + +from cubicweb.web.views import tableview + +class CustomRsetTableView(tableview.RsetTableView): + __regid__ = 'mytable' + diff -r aff75b69db92 -r 2c48c091b6a2 ext/test/unittest_rest.py --- a/ext/test/unittest_rest.py Tue Jul 02 17:09:04 2013 +0200 +++ b/ext/test/unittest_rest.py Mon Jan 13 13:47:47 2014 +0100 @@ -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. @@ -82,5 +82,133 @@ out = rest_publish(context, ':bookmark:`%s`' % eid) self.assertEqual(out, u'

CWUser_plural

\n') + def test_rqltable_nocontent(self): + context = self.context() + out = rest_publish(context, """.. rql-table::""") + self.assertIn("System Message: ERROR", out) + self.assertIn("Content block expected for the "rql-table" " + "directive; none found" , out) + + def test_rqltable_norset(self): + context = self.context() + rql = "Any X WHERE X is CWUser, X firstname 'franky'" + out = rest_publish( + context, """\ +.. rql-table:: + + %(rql)s""" % {'rql': rql}) + self.assertIn("System Message: WARNING", out) + self.assertIn("empty result set", out) + + def test_rqltable_nooptions(self): + rql = """Any S,F,L WHERE X is CWUser, X surname S, + X firstname F, X login L""" + out = rest_publish( + self.context(), """\ +.. rql-table:: + + %(rql)s + """ % {'rql': rql}) + req = self.request() + view = self.vreg['views'].select('table', req, rset=req.execute(rql)) + self.assertEqual(view.render(w=None)[49:], out[49:]) + + def test_rqltable_vid(self): + rql = """Any S,F,L WHERE X is CWUser, X surname S, + X firstname F, X login L""" + vid = 'mytable' + out = rest_publish( + self.context(), """\ +.. rql-table:: + :vid: %(vid)s + + %(rql)s + """ % {'rql': rql, 'vid': vid}) + req = self.request() + view = self.vreg['views'].select(vid, req, rset=req.execute(rql)) + self.assertEqual(view.render(w=None)[49:], out[49:]) + self.assertIn(vid, out[:49]) + + def test_rqltable_badvid(self): + rql = """Any S,F,L WHERE X is CWUser, X surname S, + X firstname F, X login L""" + vid = 'mytabel' + out = rest_publish( + self.context(), """\ +.. rql-table:: + :vid: %(vid)s + + %(rql)s + """ % {'rql': rql, 'vid': vid}) + self.assertIn("fail to select '%s' view" % vid, out) + + def test_rqltable_headers(self): + rql = """Any S,F,L WHERE X is CWUser, X surname S, + X firstname F, X login L""" + headers = ["nom", "prenom", "identifiant"] + out = rest_publish( + self.context(), """\ +.. rql-table:: + :headers: %(headers)s + + %(rql)s + """ % {'rql': rql, 'headers': ', '.join(headers)}) + req = self.request() + view = self.vreg['views'].select('table', req, rset=req.execute(rql)) + view.headers = headers + self.assertEqual(view.render(w=None)[49:], out[49:]) + + def test_rqltable_headers_missing(self): + rql = """Any S,F,L WHERE X is CWUser, X surname S, + X firstname F, X login L""" + headers = ["nom", "", "identifiant"] + out = rest_publish( + self.context(), """\ +.. rql-table:: + :headers: %(headers)s + + %(rql)s + """ % {'rql': rql, 'headers': ', '.join(headers)}) + req = self.request() + view = self.vreg['views'].select('table', req, rset=req.execute(rql)) + view.headers = [headers[0], None, headers[2]] + self.assertEqual(view.render(w=None)[49:], out[49:]) + + def test_rqltable_headers_missing_edges(self): + rql = """Any S,F,L WHERE X is CWUser, X surname S, + X firstname F, X login L""" + headers = [" ", "prenom", ""] + out = rest_publish( + self.context(), """\ +.. rql-table:: + :headers: %(headers)s + + %(rql)s + """ % {'rql': rql, 'headers': ', '.join(headers)}) + req = self.request() + view = self.vreg['views'].select('table', req, rset=req.execute(rql)) + view.headers = [None, headers[1], None] + self.assertEqual(view.render(w=None)[49:], out[49:]) + + def test_rqltable_colvids(self): + rql = """Any X,S,F,L WHERE X is CWUser, X surname S, + X firstname F, X login L""" + colvids = {0: "oneline"} + out = rest_publish( + self.context(), """\ +.. rql-table:: + :colvids: %(colvids)s + + %(rql)s + """ % {'rql': rql, + 'colvids': ', '.join(["%d=%s" % (k, v) + for k, v in colvids.iteritems()]) + }) + req = self.request() + view = self.vreg['views'].select('table', req, rset=req.execute(rql)) + view.cellvids = colvids + self.assertEqual(view.render(w=None)[49:], out[49:]) + + if __name__ == '__main__': unittest_main() diff -r aff75b69db92 -r 2c48c091b6a2 hooks/__init__.py --- a/hooks/__init__.py Tue Jul 02 17:09:04 2013 +0200 +++ b/hooks/__init__.py Mon Jan 13 13:47:47 2014 +0100 @@ -59,7 +59,9 @@ def update_feeds(repo): # don't iter on repo.sources which doesn't include copy based # sources (the one we're looking for) - for source in repo.sources_by_eid.itervalues(): + # take a list to avoid iterating on a dictionary which size may + # change + for source in list(repo.sources_by_eid.values()): if (not source.copy_based_source or not repo.config.source_enabled(source) or not source.config['synchronize']): diff -r aff75b69db92 -r 2c48c091b6a2 hooks/integrity.py --- a/hooks/integrity.py Tue Jul 02 17:09:04 2013 +0200 +++ b/hooks/integrity.py Mon Jan 13 13:47:47 2014 +0100 @@ -109,6 +109,30 @@ category = 'integrity' +class EnsureSymmetricRelationsAdd(hook.Hook): + """ ensure X r Y => Y r X iff r is symmetric """ + __regid__ = 'cw.add_ensure_symmetry' + category = 'activeintegrity' + events = ('after_add_relation',) + # __select__ is set in the registration callback + + def __call__(self): + self._cw.repo.system_source.add_relation(self._cw, self.eidto, + self.rtype, self.eidfrom) + + +class EnsureSymmetricRelationsDelete(hook.Hook): + """ ensure X r Y => Y r X iff r is symmetric """ + __regid__ = 'cw.delete_ensure_symmetry' + category = 'activeintegrity' + events = ('after_delete_relation',) + # __select__ is set in the registration callback + + def __call__(self): + self._cw.repo.system_source.delete_relation(self._cw, self.eidto, + self.rtype, self.eidfrom) + + class CheckCardinalityHookBeforeDeleteRelation(IntegrityHook): """check cardinalities are satisfied""" __regid__ = 'checkcard_before_delete_relation' @@ -348,3 +372,11 @@ elif composite == 'object': _DelayedDeleteSEntityOp.get_instance(self._cw).add_data( (self.eidfrom, rtype)) + +def registration_callback(vreg): + vreg.register_all(globals().values(), __name__) + symmetric_rtypes = [rschema.type for rschema in vreg.schema.relations() + if rschema.symmetric] + EnsureSymmetricRelationsAdd.__select__ = hook.Hook.__select__ & hook.match_rtype(*symmetric_rtypes) + EnsureSymmetricRelationsDelete.__select__ = hook.Hook.__select__ & hook.match_rtype(*symmetric_rtypes) + diff -r aff75b69db92 -r 2c48c091b6a2 hooks/metadata.py --- a/hooks/metadata.py Tue Jul 02 17:09:04 2013 +0200 +++ b/hooks/metadata.py Mon Jan 13 13:47:47 2014 +0100 @@ -149,7 +149,7 @@ # entity source handling ####################################################### -class ChangeEntityUpdateCaches(hook.Operation): +class ChangeEntitySourceUpdateCaches(hook.Operation): oldsource = newsource = entity = None # make pylint happy def postcommit_event(self): @@ -221,6 +221,6 @@ 'mtime': datetime.now()} self._cw.system_sql(syssource.sqlgen.insert('entities', attrs), attrs) # register an operation to update repository/sources caches - ChangeEntityUpdateCaches(self._cw, entity=entity, - oldsource=oldsource.repo_source, - newsource=syssource) + ChangeEntitySourceUpdateCaches(self._cw, entity=entity, + oldsource=oldsource.repo_source, + newsource=syssource) diff -r aff75b69db92 -r 2c48c091b6a2 hooks/notification.py --- a/hooks/notification.py Tue Jul 02 17:09:04 2013 +0200 +++ b/hooks/notification.py Mon Jan 13 13:47:47 2014 +0100 @@ -52,7 +52,7 @@ All others Operations end up adding data to this Operation. The notification are done on ``postcommit_event`` to make sure to prevent - sending notification about rollbacked data. + sending notification about rolled back data. """ containercls = list diff -r aff75b69db92 -r 2c48c091b6a2 hooks/security.py --- a/hooks/security.py Tue Jul 02 17:09:04 2013 +0200 +++ b/hooks/security.py Mon Jan 13 13:47:47 2014 +0100 @@ -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,14 +20,18 @@ """ __docformat__ = "restructuredtext en" +from warnings import warn 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 -def check_entity_attributes(session, entity, editedattrs=None, creation=False): + +def check_entity_attributes(session, entity, action, editedattrs=None): eid = entity.eid eschema = entity.e_schema # ._cw_skip_security_attributes is there to bypass security for attributes @@ -39,11 +43,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 + perms = rdef.permissions.get(action) + # 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 rql expression. + if perms == buildobjs.DEFAULT_ATTRPERMS[action]: + # The default rule is to delegate to the entity + # rule. This is an historical artefact. Hence we take + # this object as a marker saying "no specific" + # permission rule for this attribute. Thus we just do + # nothing. continue - rdef.check_perm(session, 'update', eid=eid) + if perms == (): + # That means an immutable attribute. + raise Unauthorized(action, str(rdef)) + rdef.check_perm(session, action, eid=eid) class CheckEntityPermissionOp(hook.DataOperationMixIn, hook.LateOperation): @@ -52,8 +71,7 @@ for eid, action, edited in self.get_data(): entity = session.entity_from_eid(eid) entity.cw_check_perm(action) - check_entity_attributes(session, entity, edited, - creation=(action == 'add')) + check_entity_attributes(session, entity, action, edited) class CheckRelationPermissionOp(hook.DataOperationMixIn, hook.LateOperation): @@ -91,17 +109,11 @@ events = ('after_update_entity',) def __call__(self): - try: - # check user has permission right now, if not retry at commit time - self.entity.cw_check_perm('update') - check_entity_attributes(self._cw, self.entity) - except Unauthorized: - self.entity._cw_clear_local_perm_cache('update') - # save back editedattrs in case the entity is reedited later in the - # same transaction, which will lead to cw_edited being - # overwritten - CheckEntityPermissionOp.get_instance(self._cw).add_data( - (self.entity.eid, 'update', self.entity.cw_edited) ) + # save back editedattrs in case the entity is reedited later in the + # same transaction, which will lead to cw_edited being + # overwritten + CheckEntityPermissionOp.get_instance(self._cw).add_data( + (self.entity.eid, 'update', self.entity.cw_edited) ) class BeforeDelEntitySecurityHook(SecurityHook): diff -r aff75b69db92 -r 2c48c091b6a2 hooks/syncschema.py --- a/hooks/syncschema.py Tue Jul 02 17:09:04 2013 +0200 +++ b/hooks/syncschema.py Mon Jan 13 13:47:47 2014 +0100 @@ -28,7 +28,7 @@ from copy import copy from yams.schema import BASE_TYPES, RelationSchema, RelationDefinitionSchema -from yams import buildobjs as ybo, schema2sql as y2sql +from yams import buildobjs as ybo, schema2sql as y2sql, convert_default_value from logilab.common.decorators import clear_cache @@ -39,21 +39,6 @@ from cubicweb.server import hook, schemaserial as ss from cubicweb.server.sqlutils import SQL_PREFIX - -TYPE_CONVERTER = { # XXX - 'Boolean': bool, - 'Int': int, - 'BigInt': int, - 'Float': float, - 'Password': str, - 'String': unicode, - 'Date' : unicode, - 'Datetime' : unicode, - 'Time' : unicode, - 'TZDatetime' : unicode, - 'TZTime' : unicode, - } - # core entity and relation types which can't be removed CORE_TYPES = BASE_TYPES | SCHEMA_TYPES | META_RTYPES | set( ('CWUser', 'CWGroup','login', 'upassword', 'name', 'in_group')) @@ -116,7 +101,7 @@ if (specialization, rdefdef.object) in rschema.rdefs: continue sperdef = RelationDefinitionSchema(specialization, rschema, - object, props) + object, None, values=props) ss.execschemarql(session.execute, sperdef, ss.rdef2rql(sperdef, cstrtypemap, groupmap)) @@ -437,11 +422,11 @@ def precommit_event(self): session = self.session entity = self.entity - # entity.defaultval is a string or None, but we need a correctly typed + # entity.defaultval is a Binary or None, but we need a correctly typed # value default = entity.defaultval if default is not None: - default = TYPE_CONVERTER[entity.otype.name](default) + default = default.unzpickle() props = {'default': default, 'indexed': entity.indexed, 'fulltextindexed': entity.fulltextindexed, @@ -493,20 +478,11 @@ # attribute is still set to False, so we've to ensure it's False rschema.final = True insert_rdef_on_subclasses(session, eschema, rschema, rdefdef, props) - # set default value, using sql for performance and to avoid - # modification_date update - if default: - if rdefdef.object in ('Date', 'Datetime', 'TZDatetime'): - # XXX may may want to use creation_date - if default == 'TODAY': - default = syssource.dbhelper.sql_current_date() - elif default == 'NOW': - default = syssource.dbhelper.sql_current_timestamp() - session.system_sql('UPDATE %s SET %s=%s' - % (table, column, default)) - else: - session.system_sql('UPDATE %s SET %s=%%(default)s' % (table, column), - {'default': default}) + # update existing entities with the default value of newly added attribute + if default is not None: + default = convert_default_value(self.rdefdef, default) + session.system_sql('UPDATE %s SET %s=%%(default)s' % (table, column), + {'default': default}) def revertprecommit_event(self): # revert changes on in memory schema @@ -738,44 +714,38 @@ class CWUniqueTogetherConstraintAddOp(MemSchemaOperation): entity = None # make pylint happy + def precommit_event(self): session = self.session prefix = SQL_PREFIX - table = '%s%s' % (prefix, self.entity.constraint_of[0].name) - cols = ['%s%s' % (prefix, r.name) for r in self.entity.relations] - dbhelper= session.cnxset.source('system').dbhelper - sqls = dbhelper.sqls_create_multicol_unique_index(table, cols) + entity = self.entity + table = '%s%s' % (prefix, entity.constraint_of[0].name) + cols = ['%s%s' % (prefix, r.name) for r in entity.relations] + dbhelper = session.cnxset.source('system').dbhelper + sqls = dbhelper.sqls_create_multicol_unique_index(table, cols, entity.name) for sql in sqls: session.system_sql(sql) - # XXX revertprecommit_event - def postcommit_event(self): - eschema = self.session.vreg.schema.schema_by_eid(self.entity.constraint_of[0].eid) - attrs = [r.name for r in self.entity.relations] + entity = self.entity + eschema = self.session.vreg.schema.schema_by_eid(entity.constraint_of[0].eid) + attrs = [r.name for r in entity.relations] eschema._unique_together.append(attrs) class CWUniqueTogetherConstraintDelOp(MemSchemaOperation): - entity = oldcstr = None # for pylint - cols = [] # for pylint + entity = cstrname = None # for pylint + cols = () # for pylint + def precommit_event(self): session = self.session prefix = SQL_PREFIX table = '%s%s' % (prefix, self.entity.type) - dbhelper= session.cnxset.source('system').dbhelper + dbhelper = session.cnxset.source('system').dbhelper cols = ['%s%s' % (prefix, c) for c in self.cols] - sqls = dbhelper.sqls_drop_multicol_unique_index(table, cols) + sqls = dbhelper.sqls_drop_multicol_unique_index(table, cols, self.cstrname) for sql in sqls: - try: - session.system_sql(sql) - except Exception as exc: # should be ProgrammingError - if sql.startswith('DROP'): - self.error('execute of `%s` failed (cause: %s)', sql, exc) - continue - raise - - # XXX revertprecommit_event + session.system_sql(sql) def postcommit_event(self): eschema = self.session.vreg.schema.schema_by_eid(self.entity.eid) @@ -1195,9 +1165,9 @@ schema = self._cw.vreg.schema cstr = self._cw.entity_from_eid(self.eidfrom) entity = schema.schema_by_eid(self.eidto) - cols = [r.name for r in cstr.relations] + cols = tuple(r.name for r in cstr.relations) CWUniqueTogetherConstraintDelOp(self._cw, entity=entity, - oldcstr=cstr, cols=cols) + cstrname=cstr.name, cols=cols) # permissions synchronization hooks ############################################ diff -r aff75b69db92 -r 2c48c091b6a2 hooks/test/data/schema.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hooks/test/data/schema.py Mon Jan 13 13:47:47 2014 +0100 @@ -0,0 +1,25 @@ +# 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 . + +from yams.buildobjs import RelationDefinition + +class friend(RelationDefinition): + subject = ('CWUser', 'CWGroup') + object = ('CWUser', 'CWGroup') + symmetric = True + diff -r aff75b69db92 -r 2c48c091b6a2 hooks/test/unittest_hooks.py --- a/hooks/test/unittest_hooks.py Tue Jul 02 17:09:04 2013 +0200 +++ b/hooks/test/unittest_hooks.py Mon Jan 13 13:47:47 2014 +0100 @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# 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,37 @@ rset = self.execute('Any S WHERE X sender S, X eid %s' % eeid) self.assertEqual(len(rset), 1) + def test_symmetric(self): + req = self.request() + u1 = self.create_user(req, u'1') + u2 = self.create_user(req, u'2') + u3 = self.create_user(req, u'3') + ga = req.create_entity('CWGroup', name=u'A') + gb = req.create_entity('CWGroup', name=u'B') + u1.cw_set(friend=u2) + u2.cw_set(friend=u3) + ga.cw_set(friend=gb) + ga.cw_set(friend=u1) + self.commit() + req = self.request() + for l1, l2 in ((u'1', u'2'), + (u'2', u'3')): + self.assertTrue(req.execute('Any U1,U2 WHERE U1 friend U2, U1 login %(l1)s, U2 login %(l2)s', + {'l1': l1, 'l2': l2})) + self.assertTrue(req.execute('Any U1,U2 WHERE U2 friend U1, U1 login %(l1)s, U2 login %(l2)s', + {'l1': l1, 'l2': l2})) + self.assertTrue(req.execute('Any GA,GB WHERE GA friend GB, GA name "A", GB name "B"')) + self.assertTrue(req.execute('Any GA,GB WHERE GB friend GA, GA name "A", GB name "B"')) + self.assertTrue(req.execute('Any GA,U1 WHERE GA friend U1, GA name "A", U1 login "1"')) + self.assertTrue(req.execute('Any GA,U1 WHERE U1 friend GA, GA name "A", U1 login "1"')) + self.assertFalse(req.execute('Any GA,U WHERE GA friend U, GA name "A", U login "2"')) + for l1, l2 in ((u'1', u'3'), + (u'3', u'1')): + self.assertFalse(req.execute('Any U1,U2 WHERE U1 friend U2, U1 login %(l1)s, U2 login %(l2)s', + {'l1': l1, 'l2': l2})) + self.assertFalse(req.execute('Any U1,U2 WHERE U2 friend U1, U1 login %(l1)s, U2 login %(l2)s', + {'l1': l1, 'l2': l2})) + def test_html_tidy_hook(self): req = self.request() entity = req.create_entity('Workflow', name=u'wf1', diff -r aff75b69db92 -r 2c48c091b6a2 hooks/test/unittest_integrity.py --- a/hooks/test/unittest_integrity.py Tue Jul 02 17:09:04 2013 +0200 +++ b/hooks/test/unittest_integrity.py Mon Jan 13 13:47:47 2014 +0100 @@ -41,9 +41,6 @@ self.execute('SET X in_group Y WHERE X login "toto", Y name "guests"') self.commit() - def test_delete_required_relations_object(self): - self.skipTest('no sample in the schema ! YAGNI ? Kermaat ?') - def test_static_vocabulary_check(self): self.assertRaises(ValidationError, self.execute, diff -r aff75b69db92 -r 2c48c091b6a2 hooks/test/unittest_syncschema.py --- a/hooks/test/unittest_syncschema.py Tue Jul 02 17:09:04 2013 +0200 +++ b/hooks/test/unittest_syncschema.py Mon Jan 13 13:47:47 2014 +0100 @@ -19,7 +19,7 @@ from logilab.common.testlib import TestCase, unittest_main -from cubicweb import ValidationError +from cubicweb import ValidationError, Binary from cubicweb.schema import META_RTYPES from cubicweb.devtools.testlib import CubicWebTC from cubicweb.server.sqlutils import SQL_PREFIX @@ -73,9 +73,10 @@ self.commit() self.assertTrue(schema.has_entity('Societe2')) self.assertTrue(schema.has_relation('concerne2')) - attreid = self.execute('INSERT CWAttribute X: X cardinality "11", X defaultval "noname", ' + attreid = self.execute('INSERT CWAttribute X: X cardinality "11", X defaultval %(default)s, ' ' X indexed TRUE, X relation_type RT, X from_entity E, X to_entity F ' - 'WHERE RT name "name", E name "Societe2", F name "String"')[0][0] + 'WHERE RT name "name", E name "Societe2", F name "String"', + {'default': Binary.zpickle('noname')})[0][0] self._set_attr_perms(attreid) concerne2_rdef_eid = self.execute( 'INSERT CWRelation X: X cardinality "**", X relation_type RT, X from_entity E, X to_entity E ' @@ -289,8 +290,10 @@ def test_add_attribute_to_base_class(self): - attreid = self.execute('INSERT CWAttribute X: X cardinality "11", X defaultval "noname", X indexed TRUE, X relation_type RT, X from_entity E, X to_entity F ' - 'WHERE RT name "messageid", E name "BaseTransition", F name "String"')[0][0] + attreid = self.execute('INSERT CWAttribute X: X cardinality "11", X defaultval %(default)s, ' + 'X indexed TRUE, X relation_type RT, X from_entity E, X to_entity F ' + 'WHERE RT name "messageid", E name "BaseTransition", F name "String"', + {'default': Binary.zpickle('noname')})[0][0] assert self.execute('SET X read_permission Y WHERE X eid %(x)s, Y name "managers"', {'x': attreid}) self.commit() diff -r aff75b69db92 -r 2c48c091b6a2 i18n/de.po --- a/i18n/de.po Tue Jul 02 17:09:04 2013 +0200 +++ b/i18n/de.po Mon Jan 13 13:47:47 2014 +0100 @@ -114,8 +114,8 @@ msgstr "%s Fehlerbericht" #, python-format -msgid "%s not estimated" -msgstr "%s unbekannt(e)" +msgid "%s is part of violated unicity constraint" +msgstr "" #, python-format msgid "%s relation should not be in mapped" @@ -439,10 +439,6 @@ msgid "DEBUG" msgstr "" -#, python-format -msgid "Data connection graph for %s" -msgstr "Graf der Datenverbindungen für %s" - msgid "Date" msgstr "Datum" @@ -518,15 +514,9 @@ msgid "FormatConstraint" msgstr "Format-Einschränkung" -msgid "From:" -msgstr "Von:" - msgid "Garbage collection information" msgstr "Information zur Speicherbereinigung" -msgid "Got rhythm?" -msgstr "Hast Du Rhythmus ?" - msgid "Help" msgstr "Hilfe" @@ -695,9 +685,6 @@ msgid "RQLVocabularyConstraint" msgstr "RQL Wortschatz-Einschränkung" -msgid "Recipients:" -msgstr "Adressaten:" - msgid "RegexpConstraint" msgstr "regulärer Ausdruck Einschränkung" @@ -759,9 +746,6 @@ msgid "SubWorkflowExitPoint_plural" msgstr "subworkflow Endpunkte" -msgid "Subject:" -msgstr "Subjekt :" - msgid "Submit bug report" msgstr "Fehlerbericht senden" @@ -929,9 +913,6 @@ msgid "Web server" msgstr "Web-Server" -msgid "What's new?" -msgstr "Was ist neu?" - msgid "Workflow" msgstr "Workflow" @@ -965,9 +946,6 @@ "\"Durchsuchen\" oberhalb eine neue Datei hochladen, oder den Datei-Inhalt " "mit dem Widget unterhalb editieren." -msgid "You can use any of the following substitutions in your text" -msgstr "Sie können die folgenden Ersetzungen in Ihrem Text verwenden:" - msgid "You can't change this relation" msgstr "" @@ -1023,6 +1001,9 @@ msgid "abstract base class for transitions" msgstr "abstrakte Basisklasse für Übergänge" +msgid "action menu" +msgstr "" + msgid "action(s) on this selection" msgstr "Aktionen(en) bei dieser Auswahl" @@ -1041,6 +1022,9 @@ msgid "add Bookmark bookmarked_by CWUser object" msgstr "Lesezeichen" +msgid "add CWAttribute add_permission RQLExpression subject" +msgstr "" + msgid "add CWAttribute constrained_by CWConstraint subject" msgstr "Einschränkung" @@ -1149,6 +1133,10 @@ msgid "add_permission" msgstr "kann hinzugefügt werden durch" +msgctxt "CWAttribute" +msgid "add_permission" +msgstr "" + # subject and object forms for each relation type # (no object form for final relation types) msgctxt "CWEType" @@ -1185,6 +1173,9 @@ "Die Relation %(rtype)s von %(frometype)s #%(eidfrom)s zu %(toetype)s #" "%(eidto)s wurde hinzugefügt." +msgid "additional type specific properties" +msgstr "" + msgid "addrelated" msgstr "hinzufügen" @@ -1672,9 +1663,6 @@ "core relation indicating the types (including specialized types) of an entity" msgstr "" -msgid "cost" -msgstr "Kosten" - msgid "could not connect to the SMTP server" msgstr "Keine Verbindung mit dem SMTP-Server" @@ -1727,6 +1715,10 @@ msgstr "Erstelle E-Mail-Adresse für Nutzer %(linkto)s" msgid "" +"creating RQLExpression (CWAttribute %(linkto)s add_permission RQLExpression)" +msgstr "" + +msgid "" "creating RQLExpression (CWAttribute %(linkto)s read_permission RQLExpression)" msgstr "RQL-Ausdruck für Leseberechtigung für %(linkto)s" @@ -2099,6 +2091,9 @@ msgid "default value" msgstr "Standardwert" +msgid "default value as gziped pickled python object" +msgstr "" + msgid "default workflow for an entity type" msgstr "Standard-Workflow eines Entitätstyps" @@ -2373,18 +2368,9 @@ msgid "eid" msgstr "" -msgid "emails successfully sent" -msgstr "E-Mails erfolgreich versandt." - -msgid "embed" -msgstr "einbetten" - msgid "embedded html" msgstr "HTML-Inhalt" -msgid "embedding this url is forbidden" -msgstr "Einbettung dieses URLs ist nicht erlaubt." - msgid "end_timestamp" msgstr "" @@ -2437,9 +2423,6 @@ msgid "error" msgstr "" -msgid "error while embedding page" -msgstr "Fehler beim Einbetten der Seite" - msgid "error while publishing ReST text" msgstr "Fehler beim Übersetzen von reST" @@ -2449,9 +2432,6 @@ "Fehler beim Zugriff auf Quelle %s, möglicherweise sind die Daten " "unvollständig." -msgid "eta_date" -msgstr "Enddatum" - msgid "exit state must be a subworkflow state" msgstr "Exit-Zustand muss ein Subworkflow-Zustand sein." @@ -2465,9 +2445,6 @@ msgid "exiting from subworkflow %s" msgstr "verlasse Subworkflow %s" -msgid "expected:" -msgstr "erwartet:" - msgid "expression" msgstr "Ausdruck" @@ -2482,8 +2459,12 @@ msgid "exprtype" msgstr "Typ des Ausdrucks" -msgid "external page" -msgstr "externe Seite" +msgid "extra_props" +msgstr "" + +msgctxt "CWAttribute" +msgid "extra_props" +msgstr "" msgid "facet-loading-msg" msgstr "" @@ -2901,10 +2882,6 @@ msgid "info" msgstr "Information" -#, python-format -msgid "initial estimation %s" -msgstr "Erste Schätzung %s" - msgid "initial state for this workflow" msgstr "Anfangszustand für diesen Workflow" @@ -3180,9 +3157,6 @@ msgid "message" msgstr "" -msgid "milestone" -msgstr "Meilenstein" - #, python-format msgid "missing parameters for entity %s" msgstr "Fehlende Parameter für Entität %s" @@ -3242,6 +3216,10 @@ msgid "name" msgstr "" +msgctxt "CWUniqueTogetherConstraint" +msgid "name" +msgstr "" + msgctxt "State" msgid "name" msgstr "Name" @@ -3329,9 +3307,6 @@ msgid "no related entity" msgstr "keine verknüpfte Entität" -msgid "no related project" -msgstr "kein verknüpftes Projekt" - msgid "no repository sessions found" msgstr "keine Datenbank-Sitzung gefunden" @@ -3531,15 +3506,6 @@ msgid "profile" msgstr "Profil" -msgid "progress" -msgstr "Fortschritt" - -msgid "progress bar" -msgstr "Fortschrittsbalken" - -msgid "project" -msgstr "Projekt" - msgid "rdef-description" msgstr "Beschreibung" @@ -3808,9 +3774,6 @@ msgid "semantic description of this workflow" msgstr "Semantische Beschreibung dieses Workflows" -msgid "send email" -msgstr "E-Mail senden" - msgid "september" msgstr "September" @@ -3839,9 +3802,6 @@ msgid "show filter form" msgstr "Filter zeigen" -msgid "sioc" -msgstr "sioc" - msgid "site configuration" msgstr "Konfiguration der Website" @@ -3861,6 +3821,9 @@ "Eine oder mehrere frühere Transaktion(en) betreffen die Tntität. Machen Sie " "sie zuerst rückgängig." +msgid "some relations violate a unicity constraint" +msgstr "" + msgid "sorry, the server is unable to handle this query" msgstr "Der Server kann diese Anfrage leider nicht bearbeiten." @@ -4056,9 +4019,6 @@ msgid "tablefilter" msgstr "Tabellenfilter" -msgid "task progression" -msgstr "Fortschritt der Aufgabe" - msgid "text" msgstr "Text" @@ -4181,15 +4141,9 @@ msgid "to_state_object" msgstr "Übergang zu diesem Zustand" -msgid "todo_by" -msgstr "zu erledigen bis" - msgid "toggle check boxes" msgstr "Kontrollkästchen umkehren" -msgid "toggle filter" -msgstr "filter verbergen/zeigen" - msgid "tr_count" msgstr "" @@ -4480,6 +4434,14 @@ msgid "value %(KEY-value)s must be %(KEY-op)s %(KEY-boundary)s" msgstr "" +#, python-format +msgid "value %(KEY-value)s must be <= %(KEY-boundary)s" +msgstr "" + +#, python-format +msgid "value %(KEY-value)s must be >= %(KEY-boundary)s" +msgstr "" + msgid "value associated to this key is not editable manually" msgstr "" "Der mit diesem Schlüssele verbundene Wert kann n icht manuell geändert " @@ -4523,10 +4485,6 @@ msgid "view_index" msgstr "Index-Seite" -#, python-format -msgid "violates unique_together constraints (%s)" -msgstr "Verletzung der unique_together-Einschränkung (%s)" - msgid "visible" msgstr "sichtbar" @@ -4536,6 +4494,9 @@ msgid "we are not yet ready to handle this query" msgstr "Momentan können wir diese sparql-Anfrage noch nicht ausführen." +msgid "web sessions without CNX" +msgstr "" + msgid "wednesday" msgstr "Mittwoch" @@ -4566,11 +4527,11 @@ msgid "workflow" msgstr "Workflow" -msgid "workflow already have a state of that name" -msgstr "Der Workflow hat bereits einen Zustand desselben Namens." - -msgid "workflow already have a transition of that name" -msgstr "Der Workflow hat bereits einen Übergang desselben Namens." +msgid "workflow already has a state of that name" +msgstr "" + +msgid "workflow already has a transition of that name" +msgstr "" #, python-format msgid "workflow changed to \"%s\"" @@ -4641,6 +4602,9 @@ #~ msgid "%(value)r doesn't match the %(regexp)r regular expression" #~ msgstr "%(value)r entspricht nicht dem regulären Ausdruck %(regexp)r" +#~ msgid "%s not estimated" +#~ msgstr "%s unbekannt(e)" + #~ msgid "" #~ "Can't restore relation %(rtype)s of entity %(eid)s, this relation does " #~ "not exists anymore in the schema." @@ -4648,11 +4612,98 @@ #~ "Kann die Relation %(rtype)s der Entität %(eid)s nicht wieder herstellen, " #~ "diese Relation existiert nicht mehr in dem Schema." +#~ msgid "Data connection graph for %s" +#~ msgstr "Graf der Datenverbindungen für %s" + +#~ msgid "From:" +#~ msgstr "Von:" + +#~ msgid "Got rhythm?" +#~ msgstr "Hast Du Rhythmus ?" + +#~ msgid "Recipients:" +#~ msgstr "Adressaten:" + +#~ msgid "Subject:" +#~ msgstr "Subjekt :" + +#~ msgid "What's new?" +#~ msgstr "Was ist neu?" + +#~ msgid "You can use any of the following substitutions in your text" +#~ msgstr "Sie können die folgenden Ersetzungen in Ihrem Text verwenden:" + #~ msgid "can't change the %s attribute" #~ msgstr "Kann das Attribut %s nicht ändern." +#~ msgid "cost" +#~ msgstr "Kosten" + +#~ msgid "emails successfully sent" +#~ msgstr "E-Mails erfolgreich versandt." + +#~ msgid "embed" +#~ msgstr "einbetten" + +#~ msgid "embedding this url is forbidden" +#~ msgstr "Einbettung dieses URLs ist nicht erlaubt." + +#~ msgid "error while embedding page" +#~ msgstr "Fehler beim Einbetten der Seite" + +#~ msgid "eta_date" +#~ msgstr "Enddatum" + +#~ msgid "expected:" +#~ msgstr "erwartet:" + +#~ msgid "external page" +#~ msgstr "externe Seite" + #~ msgid "incorrect value (%(value)s) for type \"%(type)s\"" #~ msgstr "Wert %(value)s ungültig für den Typ \"%(type)s\"" +#~ msgid "initial estimation %s" +#~ msgstr "Erste Schätzung %s" + #~ msgid "invalid value %(value)s, it must be one of %(choices)s" #~ msgstr "Wert %(value)s ungültig, er muss zwischen %(choices)s" + +#~ msgid "milestone" +#~ msgstr "Meilenstein" + +#~ msgid "no related project" +#~ msgstr "kein verknüpftes Projekt" + +#~ msgid "progress" +#~ msgstr "Fortschritt" + +#~ msgid "progress bar" +#~ msgstr "Fortschrittsbalken" + +#~ msgid "project" +#~ msgstr "Projekt" + +#~ msgid "send email" +#~ msgstr "E-Mail senden" + +#~ msgid "sioc" +#~ msgstr "sioc" + +#~ msgid "task progression" +#~ msgstr "Fortschritt der Aufgabe" + +#~ msgid "todo_by" +#~ msgstr "zu erledigen bis" + +#~ msgid "toggle filter" +#~ msgstr "filter verbergen/zeigen" + +#~ msgid "violates unique_together constraints (%s)" +#~ msgstr "Verletzung der unique_together-Einschränkung (%s)" + +#~ msgid "workflow already have a state of that name" +#~ msgstr "Der Workflow hat bereits einen Zustand desselben Namens." + +#~ msgid "workflow already have a transition of that name" +#~ msgstr "Der Workflow hat bereits einen Übergang desselben Namens." diff -r aff75b69db92 -r 2c48c091b6a2 i18n/en.po --- a/i18n/en.po Tue Jul 02 17:09:04 2013 +0200 +++ b/i18n/en.po Mon Jan 13 13:47:47 2014 +0100 @@ -106,7 +106,7 @@ msgstr "" #, python-format -msgid "%s not estimated" +msgid "%s is part of violated unicity constraint" msgstr "" #, python-format @@ -417,10 +417,6 @@ msgid "DEBUG" msgstr "" -#, python-format -msgid "Data connection graph for %s" -msgstr "" - msgid "Date" msgstr "Date" @@ -496,15 +492,9 @@ msgid "FormatConstraint" msgstr "format constraint" -msgid "From:" -msgstr "" - msgid "Garbage collection information" msgstr "" -msgid "Got rhythm?" -msgstr "" - msgid "Help" msgstr "" @@ -671,9 +661,6 @@ msgid "RQLVocabularyConstraint" msgstr "RQL vocabulary constraint" -msgid "Recipients:" -msgstr "" - msgid "RegexpConstraint" msgstr "regular expression constrainte" @@ -735,9 +722,6 @@ msgid "SubWorkflowExitPoint_plural" msgstr "subworkflow exit-points" -msgid "Subject:" -msgstr "" - msgid "Submit bug report" msgstr "" @@ -905,9 +889,6 @@ msgid "Web server" msgstr "" -msgid "What's new?" -msgstr "" - msgid "Workflow" msgstr "Workflow" @@ -934,9 +915,6 @@ "content online with the widget below." msgstr "" -msgid "You can use any of the following substitutions in your text" -msgstr "" - msgid "You can't change this relation" msgstr "" @@ -985,6 +963,9 @@ msgid "abstract base class for transitions" msgstr "" +msgid "action menu" +msgstr "" + msgid "action(s) on this selection" msgstr "" @@ -1003,6 +984,9 @@ msgid "add Bookmark bookmarked_by CWUser object" msgstr "bookmark" +msgid "add CWAttribute add_permission RQLExpression subject" +msgstr "rql expression for add permission" + msgid "add CWAttribute constrained_by CWConstraint subject" msgstr "constraint" @@ -1111,6 +1095,10 @@ msgid "add_permission" msgstr "can be added by" +msgctxt "CWAttribute" +msgid "add_permission" +msgstr "add permission" + # subject and object forms for each relation type # (no object form for final relation types) msgctxt "CWEType" @@ -1145,6 +1133,9 @@ "%(eidto)s" msgstr "" +msgid "additional type specific properties" +msgstr "" + msgid "addrelated" msgstr "add" @@ -1627,9 +1618,6 @@ "core relation indicating the types (including specialized types) of an entity" msgstr "" -msgid "cost" -msgstr "" - msgid "could not connect to the SMTP server" msgstr "" @@ -1682,6 +1670,10 @@ msgstr "creating email address for user %(linkto)s" msgid "" +"creating RQLExpression (CWAttribute %(linkto)s add_permission RQLExpression)" +msgstr "RQL expression granting add permission on %(linkto)s" + +msgid "" "creating RQLExpression (CWAttribute %(linkto)s read_permission RQLExpression)" msgstr "RQL expression granting read permission on %(linkto)s" @@ -2056,6 +2048,9 @@ msgid "default value" msgstr "" +msgid "default value as gziped pickled python object" +msgstr "" + msgid "default workflow for an entity type" msgstr "" @@ -2322,18 +2317,9 @@ msgid "eid" msgstr "" -msgid "emails successfully sent" -msgstr "" - -msgid "embed" -msgstr "" - msgid "embedded html" msgstr "" -msgid "embedding this url is forbidden" -msgstr "" - msgid "end_timestamp" msgstr "end timestamp" @@ -2386,9 +2372,6 @@ msgid "error" msgstr "" -msgid "error while embedding page" -msgstr "" - msgid "error while publishing ReST text" msgstr "" @@ -2396,9 +2379,6 @@ msgid "error while querying source %s, some data may be missing" msgstr "" -msgid "eta_date" -msgstr "ETA date" - msgid "exit state must be a subworkflow state" msgstr "" @@ -2412,9 +2392,6 @@ msgid "exiting from subworkflow %s" msgstr "" -msgid "expected:" -msgstr "" - msgid "expression" msgstr "" @@ -2429,7 +2406,11 @@ msgid "exprtype" msgstr "expression type" -msgid "external page" +msgid "extra_props" +msgstr "" + +msgctxt "CWAttribute" +msgid "extra_props" msgstr "" msgid "facet-loading-msg" @@ -2828,10 +2809,6 @@ msgid "info" msgstr "" -#, python-format -msgid "initial estimation %s" -msgstr "" - msgid "initial state for this workflow" msgstr "" @@ -3098,9 +3075,6 @@ msgid "message" msgstr "" -msgid "milestone" -msgstr "" - #, python-format msgid "missing parameters for entity %s" msgstr "" @@ -3160,6 +3134,10 @@ msgid "name" msgstr "name" +msgctxt "CWUniqueTogetherConstraint" +msgid "name" +msgstr "" + msgctxt "State" msgid "name" msgstr "name" @@ -3245,9 +3223,6 @@ msgid "no related entity" msgstr "" -msgid "no related project" -msgstr "" - msgid "no repository sessions found" msgstr "" @@ -3446,15 +3421,6 @@ msgid "profile" msgstr "" -msgid "progress" -msgstr "" - -msgid "progress bar" -msgstr "" - -msgid "project" -msgstr "" - msgid "rdef-description" msgstr "description" @@ -3720,9 +3686,6 @@ msgid "semantic description of this workflow" msgstr "" -msgid "send email" -msgstr "" - msgid "september" msgstr "" @@ -3748,9 +3711,6 @@ msgid "show filter form" msgstr "" -msgid "sioc" -msgstr "" - msgid "site configuration" msgstr "" @@ -3766,6 +3726,9 @@ msgid "some later transaction(s) touch entity, undo them first" msgstr "" +msgid "some relations violate a unicity constraint" +msgstr "" + msgid "sorry, the server is unable to handle this query" msgstr "" @@ -3957,9 +3920,6 @@ msgid "tablefilter" msgstr "table filter" -msgid "task progression" -msgstr "" - msgid "text" msgstr "" @@ -4081,15 +4041,9 @@ msgid "to_state_object" msgstr "transitions to this state" -msgid "todo_by" -msgstr "to do by" - msgid "toggle check boxes" msgstr "" -msgid "toggle filter" -msgstr "" - msgid "tr_count" msgstr "transition number" @@ -4371,6 +4325,14 @@ msgid "value %(KEY-value)s must be %(KEY-op)s %(KEY-boundary)s" msgstr "" +#, python-format +msgid "value %(KEY-value)s must be <= %(KEY-boundary)s" +msgstr "" + +#, python-format +msgid "value %(KEY-value)s must be >= %(KEY-boundary)s" +msgstr "" + msgid "value associated to this key is not editable manually" msgstr "" @@ -4412,10 +4374,6 @@ msgid "view_index" msgstr "index" -#, python-format -msgid "violates unique_together constraints (%s)" -msgstr "violates unique_together constraints (%s)" - msgid "visible" msgstr "" @@ -4425,6 +4383,9 @@ msgid "we are not yet ready to handle this query" msgstr "" +msgid "web sessions without CNX" +msgstr "" + msgid "wednesday" msgstr "" @@ -4453,10 +4414,10 @@ msgid "workflow" msgstr "" -msgid "workflow already have a state of that name" -msgstr "" - -msgid "workflow already have a transition of that name" +msgid "workflow already has a state of that name" +msgstr "" + +msgid "workflow already has a transition of that name" msgstr "" #, python-format @@ -4521,3 +4482,12 @@ #, python-format msgid "you should un-inline relation %s which is supported and may be crossed " msgstr "" + +#~ msgid "eta_date" +#~ msgstr "ETA date" + +#~ msgid "todo_by" +#~ msgstr "to do by" + +#~ msgid "violates unique_together constraints (%s)" +#~ msgstr "violates unique_together constraints (%s)" diff -r aff75b69db92 -r 2c48c091b6a2 i18n/es.po --- a/i18n/es.po Tue Jul 02 17:09:04 2013 +0200 +++ b/i18n/es.po Mon Jan 13 13:47:47 2014 +0100 @@ -115,8 +115,8 @@ msgstr "%s reporte de errores" #, python-format -msgid "%s not estimated" -msgstr "%s no estimado(s)" +msgid "%s is part of violated unicity constraint" +msgstr "" #, python-format msgid "%s relation should not be in mapped" @@ -439,10 +439,6 @@ msgid "DEBUG" msgstr "" -#, python-format -msgid "Data connection graph for %s" -msgstr "Gráfica de conexión de datos para %s" - msgid "Date" msgstr "Fecha" @@ -518,15 +514,9 @@ msgid "FormatConstraint" msgstr "Restricción de Formato" -msgid "From:" -msgstr "De: " - msgid "Garbage collection information" msgstr "Recolector de basura en memoria" -msgid "Got rhythm?" -msgstr "Tenemos Ritmo?" - msgid "Help" msgstr "Ayuda" @@ -693,9 +683,6 @@ msgid "RQLVocabularyConstraint" msgstr "Restricción RQL de Vocabulario" -msgid "Recipients:" -msgstr "Destinatarios :" - msgid "RegexpConstraint" msgstr "restricción expresión regular" @@ -760,9 +747,6 @@ msgid "SubWorkflowExitPoint_plural" msgstr "Salidas de sub-workflow" -msgid "Subject:" -msgstr "Sujeto:" - msgid "Submit bug report" msgstr "Enviar un reporte de error (bug)" @@ -932,9 +916,6 @@ msgid "Web server" msgstr "Servidor web" -msgid "What's new?" -msgstr "Lo más reciente" - msgid "Workflow" msgstr "Workflow" @@ -968,11 +949,6 @@ "\"buscar\" en la parte superior, o editar el contenido del archivo en línea\n" "en el campo siguiente." -msgid "You can use any of the following substitutions in your text" -msgstr "" -"Puede realizar cualquiera de las siguientes sustituciones en el contenido de " -"su email." - msgid "You can't change this relation" msgstr "" @@ -1033,6 +1009,9 @@ msgid "abstract base class for transitions" msgstr "Clase de base abstracta para la transiciones" +msgid "action menu" +msgstr "" + msgid "action(s) on this selection" msgstr "Acción(es) en esta selección" @@ -1051,6 +1030,9 @@ msgid "add Bookmark bookmarked_by CWUser object" msgstr "Agregar a los favoritos " +msgid "add CWAttribute add_permission RQLExpression subject" +msgstr "" + msgid "add CWAttribute constrained_by CWConstraint subject" msgstr "Restricción" @@ -1159,6 +1141,10 @@ msgid "add_permission" msgstr "Autorización para agregar" +msgctxt "CWAttribute" +msgid "add_permission" +msgstr "" + # subject and object forms for each relation type # (no object form for final relation types) msgctxt "CWEType" @@ -1195,6 +1181,9 @@ "la relación %(rtype)s de %(frometype)s #%(eidfrom)s a %(toetype)s #%(eidto)s " "ha sido agregada" +msgid "additional type specific properties" +msgstr "" + msgid "addrelated" msgstr "Agregar" @@ -1692,9 +1681,6 @@ "Relación sistema indicando los tipos (incluídos los tipos padres) de una " "entidad" -msgid "cost" -msgstr "Costo" - msgid "could not connect to the SMTP server" msgstr "Imposible de conectarse al servidor SMTP" @@ -1747,6 +1733,10 @@ msgstr "Creación de una dirección electrónica para el usuario %(linkto)s" msgid "" +"creating RQLExpression (CWAttribute %(linkto)s add_permission RQLExpression)" +msgstr "" + +msgid "" "creating RQLExpression (CWAttribute %(linkto)s read_permission RQLExpression)" msgstr "creación de una expresión RQL por el derecho de lectura de %(linkto)s" @@ -2129,6 +2119,9 @@ msgid "default value" msgstr "Valor por defecto" +msgid "default value as gziped pickled python object" +msgstr "" + msgid "default workflow for an entity type" msgstr "Workflow por defecto para un tipo de entidad" @@ -2412,18 +2405,9 @@ msgid "eid" msgstr "eid" -msgid "emails successfully sent" -msgstr "Mensajes enviados con éxito" - -msgid "embed" -msgstr "Incrustado" - msgid "embedded html" msgstr "Html incrustado" -msgid "embedding this url is forbidden" -msgstr "La inclusión de este url esta prohibida" - msgid "end_timestamp" msgstr "" @@ -2478,9 +2462,6 @@ msgid "error" msgstr "error" -msgid "error while embedding page" -msgstr "Error durante la inclusión de la página" - msgid "error while publishing ReST text" msgstr "" "Se ha producido un error durante la interpretación del texto en formato ReST" @@ -2491,9 +2472,6 @@ "Un error ha ocurrido al interrogar %s, es posible que los \n" "datos visibles se encuentren incompletos" -msgid "eta_date" -msgstr "Fecha de fin" - msgid "exit state must be a subworkflow state" msgstr "El estado de salida debe de ser un estado del Sub-Workflow" @@ -2507,9 +2485,6 @@ msgid "exiting from subworkflow %s" msgstr "Salida del subworkflow %s" -msgid "expected:" -msgstr "Previsto :" - msgid "expression" msgstr "Expresión" @@ -2524,8 +2499,12 @@ msgid "exprtype" msgstr "Tipo" -msgid "external page" -msgstr "Página externa" +msgid "extra_props" +msgstr "" + +msgctxt "CWAttribute" +msgid "extra_props" +msgstr "" msgid "facet-loading-msg" msgstr "" @@ -2942,10 +2921,6 @@ msgid "info" msgstr "Información del Sistema" -#, python-format -msgid "initial estimation %s" -msgstr "Estimación inicial %s" - msgid "initial state for this workflow" msgstr "Estado inicial para este Workflow" @@ -3221,9 +3196,6 @@ msgid "message" msgstr "" -msgid "milestone" -msgstr "Milestone" - #, python-format msgid "missing parameters for entity %s" msgstr "Parámetros faltantes a la entidad %s" @@ -3283,6 +3255,10 @@ msgid "name" msgstr "" +msgctxt "CWUniqueTogetherConstraint" +msgid "name" +msgstr "" + msgctxt "State" msgid "name" msgstr "nombre" @@ -3370,9 +3346,6 @@ msgid "no related entity" msgstr "No posee entidad asociada" -msgid "no related project" -msgstr "No tiene proyecto relacionado" - msgid "no repository sessions found" msgstr "Ninguna sesión encontrada" @@ -3572,15 +3545,6 @@ msgid "profile" msgstr "perfil" -msgid "progress" -msgstr "Progreso" - -msgid "progress bar" -msgstr "Barra de Progreso" - -msgid "project" -msgstr "Proyecto" - msgid "rdef-description" msgstr "Descripción" @@ -3858,9 +3822,6 @@ msgid "semantic description of this workflow" msgstr "Descripcion semántica de este Workflow" -msgid "send email" -msgstr "Enviar email" - msgid "september" msgstr "Septiembre" @@ -3890,9 +3851,6 @@ msgid "show filter form" msgstr "Mostrar el Filtro" -msgid "sioc" -msgstr "SIOC" - msgid "site configuration" msgstr "Configuración Sistema" @@ -3909,6 +3867,9 @@ msgstr "" "Las transacciones más recientes modificaron esta entidad, anúlelas primero" +msgid "some relations violate a unicity constraint" +msgstr "" + msgid "sorry, the server is unable to handle this query" msgstr "Lo sentimos, el servidor no puede manejar esta consulta" @@ -4106,9 +4067,6 @@ msgid "tablefilter" msgstr "Tablero de Filtrado" -msgid "task progression" -msgstr "Progreso de la Acción" - msgid "text" msgstr "Texto" @@ -4231,15 +4189,9 @@ msgid "to_state_object" msgstr "Transición hacia este Estado" -msgid "todo_by" -msgstr "Asignada a" - msgid "toggle check boxes" msgstr "Cambiar valor" -msgid "toggle filter" -msgstr "esconder/mostrar el filtro" - msgid "tr_count" msgstr "n° de transición" @@ -4530,6 +4482,14 @@ msgid "value %(KEY-value)s must be %(KEY-op)s %(KEY-boundary)s" msgstr "" +#, python-format +msgid "value %(KEY-value)s must be <= %(KEY-boundary)s" +msgstr "" + +#, python-format +msgid "value %(KEY-value)s must be >= %(KEY-boundary)s" +msgstr "" + msgid "value associated to this key is not editable manually" msgstr "El valor asociado a este elemento no es editable manualmente" @@ -4571,10 +4531,6 @@ msgid "view_index" msgstr "Inicio" -#, python-format -msgid "violates unique_together constraints (%s)" -msgstr "viola el principio (o restricción) de singularidad (%s)" - msgid "visible" msgstr "Visible" @@ -4584,6 +4540,9 @@ msgid "we are not yet ready to handle this query" msgstr "Aún no podemos manejar este tipo de consulta Sparql" +msgid "web sessions without CNX" +msgstr "" + msgid "wednesday" msgstr "Miércoles" @@ -4615,11 +4574,11 @@ msgid "workflow" msgstr "Workflow" -msgid "workflow already have a state of that name" -msgstr "El Workflow ya tiene un Estado con ese nombre" - -msgid "workflow already have a transition of that name" -msgstr "El Workflow ya tiene una transición con ese nombre" +msgid "workflow already has a state of that name" +msgstr "" + +msgid "workflow already has a transition of that name" +msgstr "" #, python-format msgid "workflow changed to \"%s\"" @@ -4692,6 +4651,9 @@ #~ msgid "%(value)r doesn't match the %(regexp)r regular expression" #~ msgstr "%(value)r no corresponde a la expresión regular %(regexp)r" +#~ msgid "%s not estimated" +#~ msgstr "%s no estimado(s)" + #~ msgid "" #~ "Can't restore relation %(rtype)s of entity %(eid)s, this relation does " #~ "not exists anymore in the schema." @@ -4699,17 +4661,106 @@ #~ "No puede restaurar la relación %(rtype)s de la entidad %(eid)s, esta " #~ "relación ya no existe en el esquema." +#~ msgid "Data connection graph for %s" +#~ msgstr "Gráfica de conexión de datos para %s" + +#~ msgid "From:" +#~ msgstr "De: " + +#~ msgid "Got rhythm?" +#~ msgstr "Tenemos Ritmo?" + +#~ msgid "Recipients:" +#~ msgstr "Destinatarios :" + +#~ msgid "Subject:" +#~ msgstr "Sujeto:" + +#~ msgid "What's new?" +#~ msgstr "Lo más reciente" + +#~ msgid "You can use any of the following substitutions in your text" +#~ msgstr "" +#~ "Puede realizar cualquiera de las siguientes sustituciones en el contenido " +#~ "de su email." + #~ msgid "can't change the %s attribute" #~ msgstr "no puede modificar el atributo %s" #~ msgid "can't change this relation" #~ msgstr "no puede modificar esta relación" +#~ msgid "cost" +#~ msgstr "Costo" + +#~ msgid "emails successfully sent" +#~ msgstr "Mensajes enviados con éxito" + +#~ msgid "embed" +#~ msgstr "Incrustado" + +#~ msgid "embedding this url is forbidden" +#~ msgstr "La inclusión de este url esta prohibida" + +#~ msgid "error while embedding page" +#~ msgstr "Error durante la inclusión de la página" + +#~ msgid "eta_date" +#~ msgstr "Fecha de fin" + +#~ msgid "expected:" +#~ msgstr "Previsto :" + +#~ msgid "external page" +#~ msgstr "Página externa" + #~ msgid "incorrect value (%(value)s) for type \"%(type)s\"" #~ msgstr "valor %(value)s incorrecto para el tipo \"%(type)s\"" +#~ msgid "initial estimation %s" +#~ msgstr "Estimación inicial %s" + #~ msgid "invalid value %(value)s, it must be one of %(choices)s" #~ msgstr "Valor %(value)s incorrecto, debe estar entre %(choices)s" +#~ msgid "milestone" +#~ msgstr "Milestone" + +#~ msgid "no related project" +#~ msgstr "No tiene proyecto relacionado" + +#~ msgid "progress" +#~ msgstr "Progreso" + +#~ msgid "progress bar" +#~ msgstr "Barra de Progreso" + +#~ msgid "project" +#~ msgstr "Proyecto" + +#~ msgid "send email" +#~ msgstr "Enviar email" + +#~ msgid "sioc" +#~ msgstr "SIOC" + +#~ msgid "task progression" +#~ msgstr "Progreso de la Acción" + +#~ msgid "todo_by" +#~ msgstr "Asignada a" + +#~ msgid "toggle filter" +#~ msgstr "esconder/mostrar el filtro" + #~ msgid "unknown source type" #~ msgstr "tipo de fuente desconocida" + +#~ msgid "violates unique_together constraints (%s)" +#~ msgstr "viola el principio (o restricción) de singularidad (%s)" + +#~ msgid "workflow already have a state of that name" +#~ msgstr "El Workflow ya tiene un Estado con ese nombre" + +#~ msgid "workflow already have a transition of that name" +#~ msgstr "El Workflow ya tiene una transición con ese nombre" diff -r aff75b69db92 -r 2c48c091b6a2 i18n/fr.po --- a/i18n/fr.po Tue Jul 02 17:09:04 2013 +0200 +++ b/i18n/fr.po Mon Jan 13 13:47:47 2014 +0100 @@ -115,8 +115,8 @@ msgstr "%s rapport d'erreur" #, python-format -msgid "%s not estimated" -msgstr "%s non estimé(s)" +msgid "%s is part of violated unicity constraint" +msgstr "%s appartient à une contrainte d'unicité transgressée" #, python-format msgid "%s relation should not be in mapped" @@ -432,6 +432,8 @@ "Configuration of the system source goes to the 'sources' file, not in the " "database" msgstr "" +"La configuration de la source système va dans le fichier 'sources' et non " +"dans la base de données" #, python-format msgid "Created %(etype)s : %(entity)s" @@ -440,10 +442,6 @@ msgid "DEBUG" msgstr "DEBUG" -#, python-format -msgid "Data connection graph for %s" -msgstr "Graphique de connection des données pour %s" - msgid "Date" msgstr "Date" @@ -519,15 +517,9 @@ msgid "FormatConstraint" msgstr "contrainte de format" -msgid "From:" -msgstr "De :" - msgid "Garbage collection information" msgstr "Information sur le ramasse-miette" -msgid "Got rhythm?" -msgstr "T'as le rythme ?" - msgid "Help" msgstr "Aide" @@ -694,9 +686,6 @@ msgid "RQLVocabularyConstraint" msgstr "contrainte rql de vocabulaire" -msgid "Recipients:" -msgstr "Destinataires :" - msgid "RegexpConstraint" msgstr "contrainte expression régulière" @@ -761,9 +750,6 @@ msgid "SubWorkflowExitPoint_plural" msgstr "Sorties de sous-workflow" -msgid "Subject:" -msgstr "Sujet :" - msgid "Submit bug report" msgstr "Soumettre un rapport de bug" @@ -935,9 +921,6 @@ msgid "Web server" msgstr "Serveur web" -msgid "What's new?" -msgstr "Nouveautés" - msgid "Workflow" msgstr "Workflow" @@ -971,11 +954,6 @@ "\"parcourir\" ci-dessu, soit éditer le contenu du fichier en ligne\n" "avec le champ ci-dessous." -msgid "You can use any of the following substitutions in your text" -msgstr "" -"Vous pouvez utiliser n'importe quelle substitution parmi la liste suivante " -"dans le contenu de votre courriel." - msgid "You can't change this relation" msgstr "Vous ne pouvez pas modifier cette relation" @@ -1036,6 +1014,9 @@ msgid "abstract base class for transitions" msgstr "classe de base abstraite pour les transitions" +msgid "action menu" +msgstr "actions" + msgid "action(s) on this selection" msgstr "action(s) sur cette sélection" @@ -1054,6 +1035,9 @@ msgid "add Bookmark bookmarked_by CWUser object" msgstr "signet" +msgid "add CWAttribute add_permission RQLExpression subject" +msgstr "définir une expression RQL d'ajout" + msgid "add CWAttribute constrained_by CWConstraint subject" msgstr "contrainte" @@ -1162,6 +1146,10 @@ msgid "add_permission" msgstr "peut ajouter" +msgctxt "CWAttribute" +msgid "add_permission" +msgstr "permission d'ajout" + # subject and object forms for each relation type # (no object form for final relation types) msgctxt "CWEType" @@ -1198,6 +1186,9 @@ "la relation %(rtype)s de %(frometype)s #%(eidfrom)s vers %(toetype)s #" "%(eidto)s a été ajoutée" +msgid "additional type specific properties" +msgstr "propriétés supplémentaires spécifiques au type" + msgid "addrelated" msgstr "ajouter" @@ -1704,9 +1695,6 @@ "relation système indiquant les types (y compris les types parents) d'une " "entité" -msgid "cost" -msgstr "coût" - msgid "could not connect to the SMTP server" msgstr "impossible de se connecter au serveur SMTP" @@ -1759,6 +1747,10 @@ msgstr "création d'une adresse électronique pour l'utilisateur %(linkto)s" msgid "" +"creating RQLExpression (CWAttribute %(linkto)s add_permission RQLExpression)" +msgstr "création d'une expression rql pour le droit d'ajout de %(linkto)s" + +msgid "" "creating RQLExpression (CWAttribute %(linkto)s read_permission RQLExpression)" msgstr "création d'une expression rql pour le droit de lecture de %(linkto)s" @@ -2143,6 +2135,9 @@ msgid "default value" msgstr "valeur par défaut" +msgid "default value as gziped pickled python object" +msgstr "valeur par défaut, sous forme d'objet python picklé zippé" + msgid "default workflow for an entity type" msgstr "workflow par défaut pour un type d'entité" @@ -2423,18 +2418,9 @@ msgid "eid" msgstr "eid" -msgid "emails successfully sent" -msgstr "courriels envoyés avec succès" - -msgid "embed" -msgstr "embarqué" - msgid "embedded html" msgstr "HTML contenu" -msgid "embedding this url is forbidden" -msgstr "l'inclusion de cette url est interdite" - msgid "end_timestamp" msgstr "horodate de fin" @@ -2489,9 +2475,6 @@ msgid "error" msgstr "erreur" -msgid "error while embedding page" -msgstr "erreur pendant l'inclusion de la page" - msgid "error while publishing ReST text" msgstr "" "une erreur s'est produite lors de l'interprétation du texte au format ReST" @@ -2502,9 +2485,6 @@ "une erreur est survenue en interrogeant %s, il est possible que les\n" "données affichées soient incomplètes" -msgid "eta_date" -msgstr "date de fin" - msgid "exit state must be a subworkflow state" msgstr "l'état de sortie doit être un état du sous-workflow" @@ -2518,9 +2498,6 @@ msgid "exiting from subworkflow %s" msgstr "sortie du sous-workflow %s" -msgid "expected:" -msgstr "attendu :" - msgid "expression" msgstr "expression" @@ -2535,8 +2512,12 @@ msgid "exprtype" msgstr "type" -msgid "external page" -msgstr "page externe" +msgid "extra_props" +msgstr "" + +msgctxt "CWAttribute" +msgid "extra_props" +msgstr "propriétés additionnelles" msgid "facet-loading-msg" msgstr "en cours de traitement, merci de patienter" @@ -2951,10 +2932,6 @@ msgid "info" msgstr "information" -#, python-format -msgid "initial estimation %s" -msgstr "estimation initiale %s" - msgid "initial state for this workflow" msgstr "état initial pour ce workflow" @@ -3232,9 +3209,6 @@ msgid "message" msgstr "message" -msgid "milestone" -msgstr "jalon" - #, python-format msgid "missing parameters for entity %s" msgstr "paramètres manquants pour l'entité %s" @@ -3294,6 +3268,10 @@ msgid "name" msgstr "nom" +msgctxt "CWUniqueTogetherConstraint" +msgid "name" +msgstr "nom" + msgctxt "State" msgid "name" msgstr "nom" @@ -3381,9 +3359,6 @@ msgid "no related entity" msgstr "pas d'entité liée" -msgid "no related project" -msgstr "pas de projet rattaché" - msgid "no repository sessions found" msgstr "aucune session trouvée" @@ -3585,15 +3560,6 @@ msgid "profile" msgstr "profil" -msgid "progress" -msgstr "avancement" - -msgid "progress bar" -msgstr "barre d'avancement" - -msgid "project" -msgstr "projet" - msgid "rdef-description" msgstr "description" @@ -3872,9 +3838,6 @@ msgid "semantic description of this workflow" msgstr "description sémantique de ce workflow" -msgid "send email" -msgstr "envoyer un courriel" - msgid "september" msgstr "septembre" @@ -3903,9 +3866,6 @@ msgid "show filter form" msgstr "afficher le filtre" -msgid "sioc" -msgstr "sioc" - msgid "site configuration" msgstr "configuration du site" @@ -3922,6 +3882,9 @@ msgstr "" "des transactions plus récentes modifient cette entité, annulez les d'abord" +msgid "some relations violate a unicity constraint" +msgstr "certaines relations transgressent 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" @@ -4121,9 +4084,6 @@ msgid "tablefilter" msgstr "filtre de tableau" -msgid "task progression" -msgstr "avancement de la tâche" - msgid "text" msgstr "text" @@ -4246,15 +4206,9 @@ msgid "to_state_object" msgstr "transition vers cet état" -msgid "todo_by" -msgstr "à faire par" - msgid "toggle check boxes" msgstr "afficher/masquer les cases à cocher" -msgid "toggle filter" -msgstr "afficher/masquer le filtre" - msgid "tr_count" msgstr "n° de transition" @@ -4543,6 +4497,16 @@ msgid "value %(KEY-value)s must be %(KEY-op)s %(KEY-boundary)s" msgstr "la valeur %(KEY-value)s n'est pas %(KEY-op)s %(KEY-boundary)s" +#, python-format +msgid "value %(KEY-value)s must be <= %(KEY-boundary)s" +msgstr "" +"la valeur %(KEY-value)s n'est pas inférieure ou égale à %(KEY-boundary)s" + +#, python-format +msgid "value %(KEY-value)s must be >= %(KEY-boundary)s" +msgstr "" +"la valeur %(KEY-value)s n'est pas supérieure ou égale à %(KEY-boundary)s" + msgid "value associated to this key is not editable manually" msgstr "la valeur associée à cette clé n'est pas éditable manuellement" @@ -4588,10 +4552,6 @@ msgid "view_index" msgstr "accueil" -#, python-format -msgid "violates unique_together constraints (%s)" -msgstr "violation de contrainte unique_together (%s)" - msgid "visible" msgstr "visible" @@ -4602,6 +4562,9 @@ msgstr "" "nous ne sommes pas capable de gérer ce type de requête sparql pour le moment" +msgid "web sessions without CNX" +msgstr "sessions web sans connexion associée" + msgid "wednesday" msgstr "mercredi" @@ -4633,10 +4596,10 @@ msgid "workflow" msgstr "workflow" -msgid "workflow already have a state of that name" +msgid "workflow already has a state of that name" msgstr "le workflow a déja un état du même nom" -msgid "workflow already have a transition of that name" +msgid "workflow already has a transition of that name" msgstr "le workflow a déja une transition du même nom" #, python-format @@ -4703,27 +4666,3 @@ msgstr "" "vous devriez enlevé la mise en ligne de la relation %s qui est supportée et " "peut-être croisée" - -#~ msgid "Action" -#~ msgstr "Action" - -#~ msgid "day" -#~ msgstr "jour" - -#~ msgid "jump to selection" -#~ msgstr "afficher cette sélection" - -#~ msgid "log out first" -#~ msgstr "déconnecter vous d'abord" - -#~ msgid "month" -#~ msgstr "mois" - -#~ msgid "today" -#~ msgstr "aujourd'hui" - -#~ msgid "undo last change" -#~ msgstr "annuler dernier changement" - -#~ msgid "week" -#~ msgstr "semaine" diff -r aff75b69db92 -r 2c48c091b6a2 interfaces.py --- a/interfaces.py Tue Jul 02 17:09:04 2013 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,214 +0,0 @@ -# copyright 2003-2010 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 . -"""Standard interfaces. Deprecated in favor of adapters. - -.. note:: - - The `implements` selector used to match not only entity classes but also their - interfaces. This will disappear in a future version. You should define an - adapter for that interface and use `adaptable('MyIFace')` selector on appobjects - that require that interface. - -""" -__docformat__ = "restructuredtext en" - -from logilab.common.interface import Interface - - -# XXX deprecates in favor of IProgressAdapter -class IProgress(Interface): - """something that has a cost, a state and a progression""" - - @property - def cost(self): - """the total cost""" - - @property - def done(self): - """what is already done""" - - @property - def todo(self): - """what remains to be done""" - - def progress_info(self): - """returns a dictionary describing progress/estimated cost of the - version. - - - mandatory keys are (''estimated', 'done', 'todo') - - - optional keys are ('notestimated', 'notestimatedcorrected', - 'estimatedcorrected') - - 'noestimated' and 'notestimatedcorrected' should default to 0 - 'estimatedcorrected' should default to 'estimated' - """ - - def finished(self): - """returns True if status is finished""" - - def in_progress(self): - """returns True if status is not finished""" - - def progress(self): - """returns the % progress of the task item""" - -# XXX deprecates in favor of IMileStoneAdapter -class IMileStone(IProgress): - """represents an ITask's item""" - - parent_type = None # specify main task's type - - def get_main_task(self): - """returns the main ITask entity""" - - def initial_prevision_date(self): - """returns the initial expected end of the milestone""" - - def eta_date(self): - """returns expected date of completion based on what remains - to be done - """ - - def completion_date(self): - """returns date on which the subtask has been completed""" - - def contractors(self): - """returns the list of persons supposed to work on this task""" - -# XXX deprecates in favor of IEmbedableAdapter -class IEmbedable(Interface): - """interface for embedable entities""" - - def embeded_url(self): - """embed action interface""" - -# XXX deprecates in favor of ICalendarViewsAdapter -class ICalendarViews(Interface): - """calendar views interface""" - def matching_dates(self, begin, end): - """ - :param begin: day considered as begin of the range (`DateTime`) - :param end: day considered as end of the range (`DateTime`) - - :return: - a list of dates (`DateTime`) in the range [`begin`, `end`] on which - this entity apply - """ - -# XXX deprecates in favor of ICalendarableAdapter -class ICalendarable(Interface): - """interface for items that do have a begin date 'start' and an end date 'stop' - """ - - @property - def start(self): - """return start date""" - - @property - def stop(self): - """return stop state""" - -# XXX deprecates in favor of ICalendarableAdapter -class ITimetableViews(Interface): - """timetable views interface""" - def timetable_date(self): - """XXX explain - - :return: date (`DateTime`) - """ - -# XXX deprecates in favor of IGeocodableAdapter -class IGeocodable(Interface): - """interface required by geocoding views such as gmap-view""" - - @property - def latitude(self): - """returns the latitude of the entity""" - - @property - def longitude(self): - """returns the longitude of the entity""" - - def marker_icon(self): - """returns the icon that should be used as the marker""" - - -# XXX deprecates in favor of IEmailableAdapter -class IFeed(Interface): - """interface for entities with rss flux""" - - def rss_feed_url(self): - """""" - -# XXX deprecates in favor of IDownloadableAdapter -class IDownloadable(Interface): - """interface for downloadable entities""" - - def download_url(self): # XXX not really part of this interface - """return an url to download entity's content""" - def download_content_type(self): - """return MIME type of the downloadable content""" - def download_encoding(self): - """return encoding of the downloadable content""" - def download_file_name(self): - """return file name of the downloadable content""" - def download_data(self): - """return actual data of the downloadable content""" - -# XXX deprecates in favor of IPrevNextAdapter -class IPrevNext(Interface): - """interface for entities which can be linked to a previous and/or next - entity - """ - - def next_entity(self): - """return the 'next' entity""" - def previous_entity(self): - """return the 'previous' entity""" - -# XXX deprecates in favor of IBreadCrumbsAdapter -class IBreadCrumbs(Interface): - - def breadcrumbs(self, view, recurs=False): - pass - -# XXX deprecates in favor of ITreeAdapter -class ITree(Interface): - - def parent(self): - """returns the parent entity""" - - def children(self): - """returns the item's children""" - - def children_rql(self): - """XXX returns RQL to get children""" - - def iterchildren(self): - """iterates over the item's children""" - - def is_leaf(self): - """returns true if this node as no child""" - - def is_root(self): - """returns true if this node has no parent""" - - def root(self): - """returns the root object""" - diff -r aff75b69db92 -r 2c48c091b6a2 mail.py --- a/mail.py Tue Jul 02 17:09:04 2013 +0200 +++ b/mail.py Mon Jan 13 13:47:47 2014 +0100 @@ -90,7 +90,7 @@ email = u'' if uinfo.get('name'): name = uinfo['name'] - elif config and config['sender-addr']: + elif config and config['sender-name']: name = unicode(config['sender-name']) else: name = u'' diff -r aff75b69db92 -r 2c48c091b6a2 migration.py --- a/migration.py Tue Jul 02 17:09:04 2013 +0200 +++ b/migration.py Mon Jan 13 13:47:47 2014 +0100 @@ -257,7 +257,7 @@ home_key = 'HOME' if sys.platform == 'win32': home_key = 'USERPROFILE' - histfile = os.path.join(os.environ[home_key], ".eshellhist") + histfile = os.path.join(os.environ[home_key], ".cwshell_history") try: readline.read_history_file(histfile) except IOError: diff -r aff75b69db92 -r 2c48c091b6a2 misc/migration/3.17.11_Any.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/misc/migration/3.17.11_Any.py Mon Jan 13 13:47:47 2014 +0100 @@ -0,0 +1,7 @@ +for table, column in [ + ('transactions', 'tx_time'), + ('tx_entity_actions', 'tx_uuid'), + ('tx_relation_actions', 'tx_uuid')]: + session.cnxset.source('system').create_index(session, table, column) + +commit() diff -r aff75b69db92 -r 2c48c091b6a2 misc/migration/3.18.0_Any.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/misc/migration/3.18.0_Any.py Mon Jan 13 13:47:47 2014 +0100 @@ -0,0 +1,129 @@ +driver = config.sources()['system']['db-driver'] +if not (driver == 'postgres' or driver.startswith('sqlserver')): + import sys + print >>sys.stderr, 'This migration is not supported for backends other than sqlserver or postgres (yet).' + sys.exit(1) + +sync_schema_props_perms('defaultval') + +def convert_defaultval(cwattr, default): + from decimal import Decimal + import yams + from cubicweb import Binary + if default is None: + return + atype = cwattr.to_entity[0].name + if atype == 'Boolean': + # boolean attributes with default=False were stored as '' + assert default in ('True', 'False', ''), repr(default) + default = default == 'True' + elif atype in ('Int', 'BigInt'): + default = int(default) + elif atype == 'Float': + default = float(default) + elif atype == 'Decimal': + default = Decimal(default) + elif atype in ('Date', 'Datetime', 'TZDatetime', 'Time'): + try: + # handle NOW and TODAY, keep them stored as strings + yams.KEYWORD_MAP[atype][default.upper()] + default = default.upper() + except KeyError: + # otherwise get an actual date or datetime + default = yams.DATE_FACTORY_MAP[atype](default) + else: + assert atype == 'String', atype + default = unicode(default) + return Binary.zpickle(default) + +dbh = repo.system_source.dbhelper + + +sql('ALTER TABLE cw_cwattribute ADD new_defaultval %s' % dbh.TYPE_MAPPING['Bytes']) + +for cwattr in rql('CWAttribute X').entities(): + olddefault = cwattr.defaultval + if olddefault is not None: + req = "UPDATE cw_cwattribute SET new_defaultval = %(val)s WHERE cw_eid = %(eid)s" + args = {'val': dbh.binary_value(convert_defaultval(cwattr, olddefault).getvalue()), 'eid': cwattr.eid} + sql(req, args, ask_confirm=False) + +sql('ALTER TABLE cw_cwattribute DROP COLUMN cw_defaultval') +if driver == 'postgres': + sql('ALTER TABLE cw_cwattribute RENAME COLUMN new_defaultval TO cw_defaultval') +else: # sqlserver + sql("sp_rename 'cw_cwattribute.new_defaultval', 'cw_defaultval', 'COLUMN'") + + +# Set object type to "Bytes" for CWAttribute's "defaultval" attribute +rql('SET X to_entity B WHERE X is CWAttribute, X from_entity Y, Y name "CWAttribute", ' + 'X relation_type Z, Z name "defaultval", B name "Bytes"') + +from yams import buildobjs as ybo +schema.add_relation_def(ybo.RelationDefinition('CWAttribute', 'defaultval', 'Bytes')) +schema.del_relation_def('CWAttribute', 'defaultval', 'String') + +commit() + +for rschema in schema.relations(): + if rschema.symmetric: + subjects = set(repr(e.type) for e in rschema.subjects()) + objects = set(repr(e.type) for e in rschema.objects()) + assert subjects == objects + martians = set(str(eid) for eid, in sql('SELECT eid_to FROM %s_relation, entities WHERE eid_to = eid AND type NOT IN (%s)' % + (rschema.type, ','.join(subjects)))) + martians |= set(str(eid) for eid, in sql('SELECT eid_from FROM %s_relation, entities WHERE eid_from = eid AND type NOT IN (%s)' % + (rschema.type, ','.join(subjects)))) + if martians: + martians = ','.join(martians) + print 'deleting broken relations %s for eids %s' % (rschema.type, martians) + sql('DELETE FROM %s_relation WHERE eid_from IN (%s) OR eid_to IN (%s)' % (rschema.type, martians, martians)) + with session.deny_all_hooks_but(): + rql('SET X %(r)s Y WHERE Y %(r)s X, NOT X %(r)s Y' % {'r': rschema.type}) + commit() + + +# multi columns unique constraints regeneration +from cubicweb.server import schemaserial + +# syncschema hooks would try to remove indices but +# 1) we already do that below +# 2) the hook expects the CWUniqueTogetherConstraint.name attribute that hasn't +# yet been added +with session.allow_all_hooks_but('syncschema'): + rql('DELETE CWUniqueTogetherConstraint C') +commit() + +add_attribute('CWUniqueTogetherConstraint', 'name') + +# low-level wipe code for postgres & sqlserver, plain sql ... +if driver == 'postgres': + for indexname, in sql('select indexname from pg_indexes'): + if indexname.startswith('unique_'): + print 'dropping index', indexname + sql('DROP INDEX %s' % indexname) + commit() +elif driver.startswith('sqlserver'): + for viewname, in sql('select name from sys.views'): + if viewname.startswith('utv_'): + print 'dropping view (index should be cascade-deleted)', viewname + sql('DROP VIEW %s' % viewname) + commit() + +# recreate the constraints, hook will lead to low-level recreation +for eschema in sorted(schema.entities()): + if eschema._unique_together: + rql_args = schemaserial.uniquetogether2rqls(eschema) + for rql, args in rql_args: + args['x'] = eschema.eid + session.execute(rql, args) + commit() + + +add_relation_definition('CWAttribute', 'add_permission', 'CWGroup') +add_relation_definition('CWAttribute', 'add_permission', 'RQLExpression') + +# all attributes perms have to be refreshed ... +for rschema in schema.relations(): + if relation.final: + sync_schema_props_perms(rschema.type, syncprops=False) diff -r aff75b69db92 -r 2c48c091b6a2 misc/migration/bootstrapmigration_repository.py --- a/misc/migration/bootstrapmigration_repository.py Tue Jul 02 17:09:04 2013 +0200 +++ b/misc/migration/bootstrapmigration_repository.py Mon Jan 13 13:47:47 2014 +0100 @@ -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 aff75b69db92 -r 2c48c091b6a2 misc/scripts/ldapuser2ldapfeed.py --- a/misc/scripts/ldapuser2ldapfeed.py Tue Jul 02 17:09:04 2013 +0200 +++ b/misc/scripts/ldapuser2ldapfeed.py Mon Jan 13 13:47:47 2014 +0100 @@ -95,5 +95,5 @@ commit() else: rollback() - print 'rollbacked' + print 'rolled back' diff -r aff75b69db92 -r 2c48c091b6a2 mixins.py --- a/mixins.py Tue Jul 02 17:09:04 2013 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,308 +0,0 @@ -# copyright 2003-2012 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 . -"""mixins of entity/views organized somewhat in a graph or tree structure""" -__docformat__ = "restructuredtext en" - -from itertools import chain - -from logilab.common.decorators import cached -from logilab.common.deprecation import deprecated, class_deprecated - -from cubicweb.predicates import implements -from cubicweb.interfaces import ITree - - -class TreeMixIn(object): - """base tree-mixin implementing the tree interface - - This mixin has to be inherited explicitly and configured using the - tree_attribute, parent_target and children_target class attribute to - benefit from this default implementation - """ - __metaclass__ = class_deprecated - __deprecation_warning__ = '[3.9] TreeMixIn is deprecated, use/override ITreeAdapter instead (%(cls)s)' - - tree_attribute = None - # XXX misnamed - parent_target = 'subject' - children_target = 'object' - - def different_type_children(self, entities=True): - """return children entities of different type as this entity. - - according to the `entities` parameter, return entity objects or the - equivalent result set - """ - res = self.related(self.tree_attribute, self.children_target, - entities=entities) - if entities: - return [e for e in res if e.e_schema != self.e_schema] - return res.filtered_rset(lambda x: x.e_schema != self.e_schema, self.cw_col) - - def same_type_children(self, entities=True): - """return children entities of the same type as this entity. - - according to the `entities` parameter, return entity objects or the - equivalent result set - """ - res = self.related(self.tree_attribute, self.children_target, - entities=entities) - if entities: - return [e for e in res if e.e_schema == self.e_schema] - return res.filtered_rset(lambda x: x.e_schema is self.e_schema, self.cw_col) - - def iterchildren(self, _done=None): - if _done is None: - _done = set() - for child in self.children(): - if child.eid in _done: - self.error('loop in %s tree: %s', self.__regid__.lower(), child) - continue - yield child - _done.add(child.eid) - - def prefixiter(self, _done=None): - if _done is None: - _done = set() - if self.eid in _done: - return - _done.add(self.eid) - yield self - for child in self.same_type_children(): - for entity in child.prefixiter(_done): - yield entity - - @cached - def path(self): - """returns the list of eids from the root object to this object""" - path = [] - parent = self - while parent: - if parent.eid in path: - self.error('loop in %s tree: %s', self.__regid__.lower(), parent) - break - path.append(parent.eid) - try: - # check we are not leaving the tree - if (parent.tree_attribute != self.tree_attribute or - parent.parent_target != self.parent_target): - break - parent = parent.parent() - except AttributeError: - break - - path.reverse() - return path - - def iterparents(self, strict=True): - def _uptoroot(self): - curr = self - while True: - curr = curr.parent() - if curr is None: - break - yield curr - if not strict: - return chain([self], _uptoroot(self)) - return _uptoroot(self) - - ## ITree interface ######################################################## - def parent(self): - """return the parent entity if any, else None (e.g. if we are on the - root - """ - try: - return self.related(self.tree_attribute, self.parent_target, - entities=True)[0] - except (KeyError, IndexError): - return None - - def children(self, entities=True, sametype=False): - """return children entities - - according to the `entities` parameter, return entity objects or the - equivalent result set - """ - if sametype: - return self.same_type_children(entities) - else: - return self.related(self.tree_attribute, self.children_target, - entities=entities) - - def children_rql(self): - return self.cw_related_rql(self.tree_attribute, self.children_target) - - def is_leaf(self): - return len(self.children()) == 0 - - def is_root(self): - return self.parent() is None - - def root(self): - """return the root object""" - return self._cw.entity_from_eid(self.path()[0]) - - -class EmailableMixIn(object): - """base mixin providing the default get_email() method used by - the massmailing view - - NOTE: The default implementation is based on the - primary_email / use_email scheme - """ - @deprecated("[3.9] use entity.cw_adapt_to('IEmailable').get_email()") - def get_email(self): - if getattr(self, 'primary_email', None): - return self.primary_email[0].address - if getattr(self, 'use_email', None): - return self.use_email[0].address - return None - - -"""pluggable mixins system: plug classes registered in MI_REL_TRIGGERS on entity -classes which have the relation described by the dict's key. - -NOTE: pluggable mixins can't override any method of the 'explicit' user classes tree -(eg without plugged classes). This includes bases Entity and AnyEntity classes. -""" -MI_REL_TRIGGERS = { - ('primary_email', 'subject'): EmailableMixIn, - ('use_email', 'subject'): EmailableMixIn, - } - - -# XXX move to cubicweb.web.views.treeview once we delete usage from this file -def _done_init(done, view, row, col): - """handle an infinite recursion safety belt""" - if done is None: - done = set() - entity = view.cw_rset.get_entity(row, col) - if entity.eid in done: - msg = entity._cw._('loop in %(rel)s relation (%(eid)s)') % { - 'rel': entity.cw_adapt_to('ITree').tree_relation, - 'eid': entity.eid - } - return None, msg - done.add(entity.eid) - return done, entity - - -class TreeViewMixIn(object): - """a recursive tree view""" - __metaclass__ = class_deprecated - __deprecation_warning__ = '[3.9] TreeViewMixIn is deprecated, use/override BaseTreeView instead (%(cls)s)' - - __regid__ = 'tree' - __select__ = implements(ITree, warn=False) - item_vid = 'treeitem' - - def call(self, done=None, **kwargs): - if done is None: - done = set() - super(TreeViewMixIn, self).call(done=done, **kwargs) - - def cell_call(self, row, col=0, vid=None, done=None, maxlevel=None, **kwargs): - assert maxlevel is None or maxlevel > 0 - done, entity = _done_init(done, self, row, col) - if done is None: - # entity is actually an error message - self.w(u'
  • %s
  • ' % entity) - return - self.open_item(entity) - entity.view(vid or self.item_vid, w=self.w, **kwargs) - if maxlevel is not None: - maxlevel -= 1 - if maxlevel == 0: - self.close_item(entity) - return - relatedrset = entity.children(entities=False) - self.wview(self.__regid__, relatedrset, 'null', done=done, - maxlevel=maxlevel, **kwargs) - self.close_item(entity) - - def open_item(self, entity): - self.w(u'
  • \n' % entity.cw_etype.lower()) - def close_item(self, entity): - self.w(u'
  • \n') - - -class TreePathMixIn(object): - """a recursive path view""" - __metaclass__ = class_deprecated - __deprecation_warning__ = '[3.9] TreePathMixIn is deprecated, use/override TreePathView instead (%(cls)s)' - __regid__ = 'path' - item_vid = 'oneline' - separator = u' > ' - - def call(self, **kwargs): - self.w(u'
    ') - super(TreePathMixIn, self).call(**kwargs) - self.w(u'
    ') - - def cell_call(self, row, col=0, vid=None, done=None, **kwargs): - done, entity = _done_init(done, self, row, col) - if done is None: - # entity is actually an error message - self.w(u'%s' % entity) - return - parent = entity.parent() - if parent: - parent.view(self.__regid__, w=self.w, done=done) - self.w(self.separator) - entity.view(vid or self.item_vid, w=self.w) - - -class ProgressMixIn(object): - """provide a default implementations for IProgress interface methods""" - __metaclass__ = class_deprecated - __deprecation_warning__ = '[3.9] ProgressMixIn is deprecated, use/override IProgressAdapter instead (%(cls)s)' - - @property - def cost(self): - return self.progress_info()['estimated'] - - @property - def revised_cost(self): - return self.progress_info().get('estimatedcorrected', self.cost) - - @property - def done(self): - return self.progress_info()['done'] - - @property - def todo(self): - return self.progress_info()['todo'] - - @cached - def progress_info(self): - raise NotImplementedError() - - def finished(self): - return not self.in_progress() - - def in_progress(self): - raise NotImplementedError() - - def progress(self): - try: - return 100. * self.done / self.revised_cost - except ZeroDivisionError: - # total cost is 0 : if everything was estimated, task is completed - if self.progress_info().get('notestimated'): - return 0. - return 100 diff -r aff75b69db92 -r 2c48c091b6a2 predicates.py --- a/predicates.py Tue Jul 02 17:09:04 2013 +0200 +++ b/predicates.py Mon Jan 13 13:47:47 2014 +0100 @@ -204,27 +204,6 @@ # remember, these imports are there for bw compat only __BACKWARD_COMPAT_IMPORTS = (yes,) -def score_interface(etypesreg, eclass, iface): - """Return XXX if the give object (maybe an instance or class) implements - the interface. - """ - if getattr(iface, '__registry__', None) == 'etypes': - # adjust score if the interface is an entity class - parents, any = etypesreg.parent_classes(eclass.__regid__) - if iface is eclass: - return len(parents) + 4 - if iface is any: # Any - return 1 - for index, basecls in enumerate(reversed(parents)): - if iface is basecls: - return index + 3 - return 0 - # XXX iface in implements deprecated in 3.9 - if implements_iface(eclass, iface): - # implementing an interface takes precedence other special Any interface - return 2 - return 0 - # abstract predicates / mixin helpers ########################################### @@ -745,53 +724,6 @@ return 1 # necessarily true if we're there -class implements(EClassPredicate): - """Return non-zero score for entity that are of the given type(s) or - implements at least one of the given interface(s). If multiple arguments are - given, matching one of them is enough. - - Entity types should be given as string, the corresponding class will be - fetched from the entity types registry at selection time. - - See :class:`~cubicweb.predicates.EClassPredicate` documentation for entity - class lookup / score rules according to the input context. - - .. note:: - - when interface is an entity class, the score will reflect class - proximity so the most specific object will be selected. - - .. note:: - - deprecated in cubicweb >= 3.9, use either - :class:`~cubicweb.predicates.is_instance` or - :class:`~cubicweb.predicates.adaptable`. - """ - - def __init__(self, *expected_ifaces, **kwargs): - emit_warn = kwargs.pop('warn', True) - super(implements, self).__init__(**kwargs) - self.expected_ifaces = expected_ifaces - if emit_warn: - warn('[3.9] implements predicate is deprecated, use either ' - 'is_instance or adaptable', DeprecationWarning, stacklevel=2) - - def __str__(self): - return '%s(%s)' % (self.__class__.__name__, - ','.join(str(s) for s in self.expected_ifaces)) - - def score_class(self, eclass, req): - score = 0 - etypesreg = req.vreg['etypes'] - for iface in self.expected_ifaces: - if isinstance(iface, basestring): - # entity type - try: - iface = etypesreg.etype_class(iface) - except KeyError: - continue # entity type not in the schema - score += score_interface(etypesreg, eclass, iface) - return score def _reset_is_instance_cache(vreg): vreg._is_instance_predicate_cache = {} @@ -994,7 +926,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 @@ -1278,12 +1214,9 @@ ','.join(str(s) for s in self.expected)) -def on_fire_transition(etype, tr_name, from_state_name=None): +def on_fire_transition(etype, tr_names, from_state_name=None): """Return 1 when entity of the type `etype` is going through transition of - the name `tr_name`. - - If `from_state_name` is specified, this predicate will also check the - incoming state. + a name included in `tr_names`. You should use this predicate on 'after_add_entity' hook, since it's actually looking for addition of `TrInfo` entities. Hence in the hook, `self.entity` @@ -1293,9 +1226,13 @@ See :class:`cubicweb.entities.wfobjs.TrInfo` for more information. """ + if from_state_name is not None: + warn("on_fire_transition's from_state_name argument is unused", DeprecationWarning) + if isinstance(tr_names, basestring): + tr_names = set((tr_names,)) def match_etype_and_transition(trinfo): # take care trinfo.transition is None when calling change_state - return (trinfo.transition and trinfo.transition.name == tr_name + return (trinfo.transition and trinfo.transition.name in tr_names # is_instance() first two arguments are 'cls' (unused, so giving # None is fine) and the request/session and is_instance(etype)(None, trinfo._cw, entity=trinfo.for_entity)) diff -r aff75b69db92 -r 2c48c091b6a2 pylintext.py --- a/pylintext.py Tue Jul 02 17:09:04 2013 +0200 +++ b/pylintext.py Mon Jan 13 13:47:47 2014 +0100 @@ -1,7 +1,7 @@ """https://pastebin.logilab.fr/show/860/""" -from logilab.astng import MANAGER, nodes, scoped_nodes -from logilab.astng.builder import ASTNGBuilder +from astroid import MANAGER, InferenceError, nodes, scoped_nodes +from astroid.builder import AstroidBuilder def turn_function_to_class(node): """turn a Function node into a Class node (in-place)""" @@ -21,13 +21,16 @@ for node in assnodes: if isinstance(node, scoped_nodes.Function) and node.decorators: for decorator in node.decorators.nodes: - for infered in decorator.infer(): - if infered.name in ('objectify_predicate', 'objectify_selector'): - turn_function_to_class(node) - break - else: + try: + for infered in decorator.infer(): + if infered.name in ('objectify_predicate', 'objectify_selector'): + turn_function_to_class(node) + break + else: + continue + break + except InferenceError: continue - break # add yams base types into 'yams.buildobjs', astng doesn't grasp globals() # magic in there if module.name == 'yams.buildobjs': @@ -36,7 +39,7 @@ module.locals[etype] = [scoped_nodes.Class(etype, None)] # add data() to uiprops module if module.name.split('.')[-1] == 'uiprops': - fake = ASTNGBuilder(MANAGER).string_build(''' + fake = AstroidBuilder(MANAGER).string_build(''' def data(string): return u'' ''') @@ -44,5 +47,5 @@ def register(linter): """called when loaded by pylint --load-plugins, nothing to do here""" - MANAGER.register_transformer(cubicweb_transform) + MANAGER.register_transform(nodes.Module, cubicweb_transform) diff -r aff75b69db92 -r 2c48c091b6a2 req.py --- a/req.py Tue Jul 02 17:09:04 2013 +0200 +++ b/req.py Mon Jan 13 13:47:47 2014 +0100 @@ -29,7 +29,10 @@ from logilab.common.deprecation import deprecated from logilab.common.date import ustrftime, strptime, todate, todatetime -from cubicweb import Unauthorized, NoSelectableObject, uilib +from rql.utils import rqlvar_maker + +from cubicweb import (Unauthorized, NoSelectableObject, NoResultError, + MultipleResultsError, uilib) from cubicweb.rset import ResultSet ONESECOND = timedelta(0, 1, 0) @@ -169,16 +172,16 @@ cls = self.vreg['etypes'].etype_class(etype) return cls.cw_instantiate(self.execute, **kwargs) + @deprecated('[3.18] use find(etype, **kwargs).entities()') def find_entities(self, etype, **kwargs): """find entities of the given type and attribute values. >>> users = find_entities('CWGroup', name=u'users') >>> groups = find_entities('CWGroup') """ - parts = ['Any X WHERE X is %s' % etype] - parts.extend('X %(attr)s %%(%(attr)s)s' % {'attr': attr} for attr in kwargs) - return self.execute(', '.join(parts), kwargs).entities() + return self.find(etype, **kwargs).entities() + @deprecated('[3.18] use find(etype, **kwargs).one()') def find_one_entity(self, etype, **kwargs): """find one entity of the given type and attribute values. raise :exc:`FindEntityError` if can not return one and only one entity. @@ -187,14 +190,43 @@ >>> groups = find_one_entity('CWGroup') Exception() """ + try: + return self.find(etype, **kwargs).one() + except (NoResultError, MultipleResultsError) as e: + raise FindEntityError("%s: (%s, %s)" % (str(e), etype, kwargs)) + + def find(self, etype, **kwargs): + """find entities of the given type and attribute values. + + :returns: A :class:`ResultSet` + + >>> users = find('CWGroup', name=u"users").one() + >>> groups = find('CWGroup').entities() + """ parts = ['Any X WHERE X is %s' % etype] - parts.extend('X %(attr)s %%(%(attr)s)s' % {'attr': attr} for attr in kwargs) + varmaker = rqlvar_maker(defined='X') + eschema = self.vreg.schema[etype] + for attr, value in kwargs.items(): + if isinstance(value, list) or isinstance(value, tuple): + raise NotImplementedError("List of values are not supported") + if hasattr(value, 'eid'): + kwargs[attr] = value.eid + if attr.startswith('reverse_'): + attr = attr[8:] + assert attr in eschema.objrels, \ + '%s not in %s object relations' % (attr, eschema) + parts.append( + '%(varname)s %(attr)s X, ' + '%(varname)s eid %%(reverse_%(attr)s)s' + % {'attr': attr, 'varname': varmaker.next()}) + else: + assert attr in eschema.subjrels, \ + '%s not in %s subject relations' % (attr, eschema) + parts.append('X %(attr)s %%(%(attr)s)s' % {'attr': attr}) + rql = ', '.join(parts) - rset = self.execute(rql, kwargs) - if len(rset) != 1: - raise FindEntityError('Found %i entitie(s) when 1 was expected (rql=%s ; %s)' - % (len(rset), rql, repr(kwargs))) - return rset.get_entity(0,0) + + return self.execute(rql, kwargs) def ensure_ro_rql(self, rql): """raise an exception if the given rql is not a select query""" @@ -437,7 +469,7 @@ """return the root url of the instance """ if secure: - return self.vreg.config.get('https-url', self.vreg.config['base-url']) + return self.vreg.config.get('https-url') or self.vreg.config['base-url'] return self.vreg.config['base-url'] # abstract methods to override according to the web front-end ############# diff -r aff75b69db92 -r 2c48c091b6a2 rqlrewrite.py --- a/rqlrewrite.py Tue Jul 02 17:09:04 2013 +0200 +++ b/rqlrewrite.py Mon Jan 13 13:47:47 2014 +0100 @@ -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 @@ -85,6 +92,7 @@ for etype in possibletypes: node.append(n.Constant(etype, 'etype')) else: + etype = iter(possibletypes).next() node = n.Constant(etype, 'etype') comp = mytyperel.children[1] comp.replace(comp.children[0], node) @@ -132,10 +140,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 +231,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 +367,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 +408,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 +642,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 +686,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 aff75b69db92 -r 2c48c091b6a2 rset.py --- a/rset.py Tue Jul 02 17:09:04 2013 +0200 +++ b/rset.py Mon Jan 13 13:47:47 2014 +0100 @@ -23,7 +23,7 @@ from rql import nodes, stmts -from cubicweb import NotAnEntity +from cubicweb import NotAnEntity, NoResultError, MultipleResultsError class ResultSet(object): @@ -45,7 +45,7 @@ :param rql: the original RQL query string """ - def __init__(self, results, rql, args=None, description=(), rqlst=None): + def __init__(self, results, rql, args=None, description=None, rqlst=None): self.rows = results self.rowcount = results and len(results) or 0 # original query and arguments @@ -53,7 +53,10 @@ self.args = args # entity types for each cell (same shape as rows) # maybe discarded if specified when the query has been executed - self.description = description + if description is None: + self.description = [] + else: + self.description = description # parsed syntax tree if rqlst is not None: rqlst.schema = None # reset schema in case of pyro transfert @@ -434,6 +437,25 @@ raise NotAnEntity(etype) return self._build_entity(row, col) + def one(self, col=0): + """Retrieve exactly one entity from the query. + + If the result set is empty, raises :exc:`NoResultError`. + If the result set has more than one row, raises + :exc:`MultipleResultsError`. + + :type col: int + :param col: The column localising the entity in the unique row + + :return: the partially initialized `Entity` instance + """ + if len(self) == 1: + return self.get_entity(0, col) + elif len(self) == 0: + raise NoResultError("No row was found for one()") + else: + raise MultipleResultsError("Multiple rows were found for one()") + def _build_entity(self, row, col): """internal method to get a single entity, returns a partially initialized Entity instance. diff -r aff75b69db92 -r 2c48c091b6a2 schema.py --- a/schema.py Tue Jul 02 17:09:04 2013 +0200 +++ b/schema.py Mon Jan 13 13:47:47 2014 +0100 @@ -21,11 +21,12 @@ _ = unicode import re -from os.path import join +from os.path import join, basename from logging import getLogger from warnings import warn -from logilab.common.decorators import cached, clear_cache, monkeypatch +from logilab.common import tempattr +from logilab.common.decorators import cached, clear_cache, monkeypatch, cachedproperty from logilab.common.logging_ext import set_log_methods from logilab.common.deprecation import deprecated, class_moved, moved from logilab.common.textutils import splitstrip @@ -44,6 +45,15 @@ import cubicweb from cubicweb import ETYPE_NAME_MAP, ValidationError, Unauthorized +try: + from cubicweb import server +except ImportError: + # We need to lookup DEBUG from there, + # however a pure dbapi client may not have it. + class server(object): pass + server.DEBUG = False + + PURE_VIRTUAL_RTYPES = set(('identity', 'has_text',)) VIRTUAL_RTYPES = set(('eid', 'identity', 'has_text',)) @@ -84,7 +94,7 @@ 'WorkflowTransition', 'BaseTransition', 'SubWorkflowExitPoint')) -INTERNAL_TYPES = set(('CWProperty', 'CWCache', 'ExternalUri', +INTERNAL_TYPES = set(('CWProperty', 'CWCache', 'ExternalUri', 'CWDataImport', 'CWSource', 'CWSourceHostConfig', 'CWSourceSchemaConfig')) @@ -94,6 +104,309 @@ ybo.ETYPE_PROPERTIES += ('eid',) ybo.RTYPE_PROPERTIES += ('eid',) +# Bases for manipulating RQL in schema ######################################### + +def guess_rrqlexpr_mainvars(expression): + defined = set(split_expression(expression)) + mainvars = set() + if 'S' in defined: + mainvars.add('S') + if 'O' in defined: + mainvars.add('O') + if 'U' in defined: + mainvars.add('U') + if not mainvars: + raise Exception('unable to guess selection variables') + return mainvars + +def split_expression(rqlstring): + for expr in rqlstring.split(','): + for noparen1 in expr.split('('): + for noparen2 in noparen1.split(')'): + for word in noparen2.split(): + yield word + +def normalize_expression(rqlstring): + """normalize an rql expression to ease schema synchronization (avoid + suppressing and reinserting an expression if only a space has been + added/removed for instance) + """ + return u', '.join(' '.join(expr.split()) for expr in rqlstring.split(',')) + + +class RQLExpression(object): + """Base class for RQL expression used in schema (constraints and + permissions) + """ + # these are overridden by set_log_methods below + # only defining here to prevent pylint from complaining + info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None + # to be defined in concrete classes + rqlst = None + predefined_variables = None + + def __init__(self, expression, mainvars, eid): + """ + :type mainvars: sequence of RQL variables' names. Can be provided as a + comma separated string. + :param mainvars: names of the variables being selected. + + """ + self.eid = eid # eid of the entity representing this rql expression + assert mainvars, 'bad mainvars %s' % mainvars + if isinstance(mainvars, basestring): + mainvars = set(splitstrip(mainvars)) + elif not isinstance(mainvars, set): + mainvars = set(mainvars) + self.mainvars = mainvars + self.expression = normalize_expression(expression) + try: + self.full_rql = self.rqlst.as_string() + except RQLSyntaxError: + raise RQLSyntaxError(expression) + for mainvar in mainvars: + # if variable is predefined, an extra reference is inserted + # automatically (`VAR eid %(v)s`) + if mainvar in self.predefined_variables: + min_refs = 3 + else: + min_refs = 2 + if len(self.rqlst.defined_vars[mainvar].references()) < min_refs: + _LOGGER.warn('You did not use the %s variable in your RQL ' + 'expression %s', mainvar, self) + # syntax tree used by read security (inserted in queries when necessary) + self.snippet_rqlst = parse(self.minimal_rql, print_errors=False).children[0] + # graph of links between variables, used by rql rewriter + self.vargraph = vargraph(self.rqlst) + # useful for some instrumentation, e.g. localperms permcheck command + self.package = ybo.PACKAGE + + def __str__(self): + return self.full_rql + def __repr__(self): + return '%s(%s)' % (self.__class__.__name__, self.full_rql) + + def __lt__(self, other): + if hasattr(other, 'expression'): + return self.expression < other.expression + return True + + def __eq__(self, other): + if hasattr(other, 'expression'): + 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): + return (self.expression, self.mainvars) + def __setstate__(self, state): + self.__init__(*state) + + @cachedproperty + def rqlst(self): + select = parse(self.minimal_rql, print_errors=False).children[0] + defined = set(split_expression(self.expression)) + for varname in self.predefined_variables: + if varname in defined: + select.add_eid_restriction(select.get_variable(varname), varname.lower(), 'Substitute') + return select + + # permission rql expression specific stuff ################################# + + @cached + def transform_has_permission(self): + found = None + rqlst = self.rqlst + for var in rqlst.defined_vars.itervalues(): + for varref in var.references(): + rel = varref.relation() + if rel is None: + continue + try: + prefix, action, suffix = rel.r_type.split('_') + except ValueError: + continue + if prefix != 'has' or suffix != 'permission' or \ + not action in ('add', 'delete', 'update', 'read'): + continue + if found is None: + found = [] + rqlst.save_state() + assert rel.children[0].name == 'U' + objvar = rel.children[1].children[0].variable + rqlst.remove_node(rel) + selected = [v.name for v in rqlst.get_selected_variables()] + if objvar.name not in selected: + colindex = len(selected) + rqlst.add_selected(objvar) + else: + colindex = selected.index(objvar.name) + found.append((action, colindex)) + # remove U eid %(u)s if U is not used in any other relation + uvrefs = rqlst.defined_vars['U'].references() + if len(uvrefs) == 1: + rqlst.remove_node(uvrefs[0].relation()) + if found is not None: + rql = rqlst.as_string() + if len(rqlst.selection) == 1 and isinstance(rqlst.where, nodes.Relation): + # only "Any X WHERE X eid %(x)s" remaining, no need to execute the rql + keyarg = rqlst.selection[0].name.lower() + else: + keyarg = None + rqlst.recover() + return rql, found, keyarg + return rqlst.as_string(), None, None + + def _check(self, _cw, **kwargs): + """return True if the rql expression is matching the given relation + between fromeid and toeid + + _cw may be a request or a server side transaction + """ + creating = kwargs.get('creating') + if not creating and self.eid is not None: + key = (self.eid, tuple(sorted(kwargs.iteritems()))) + try: + return _cw.local_perm_cache[key] + except KeyError: + pass + rql, has_perm_defs, keyarg = self.transform_has_permission() + # when creating an entity, expression related to X satisfied + if creating and 'X' in self.rqlst.defined_vars: + return True + if keyarg is None: + kwargs.setdefault('u', _cw.user.eid) + try: + rset = _cw.execute(rql, kwargs, build_descr=True) + except NotImplementedError: + self.critical('cant check rql expression, unsupported rql %s', rql) + if self.eid is not None: + _cw.local_perm_cache[key] = False + return False + except TypeResolverException as ex: + # some expression may not be resolvable with current kwargs + # (type conflict) + self.warning('%s: %s', rql, str(ex)) + if self.eid is not None: + _cw.local_perm_cache[key] = False + return False + except Unauthorized as ex: + self.debug('unauthorized %s: %s', rql, str(ex)) + if self.eid is not None: + _cw.local_perm_cache[key] = False + return False + else: + rset = _cw.eid_rset(kwargs[keyarg]) + # if no special has_*_permission relation in the rql expression, just + # check the result set contains something + if has_perm_defs is None: + if rset: + if self.eid is not None: + _cw.local_perm_cache[key] = True + return True + elif rset: + # check every special has_*_permission relation is satisfied + get_eschema = _cw.vreg.schema.eschema + try: + for eaction, col in has_perm_defs: + for i in xrange(len(rset)): + eschema = get_eschema(rset.description[i][col]) + eschema.check_perm(_cw, eaction, eid=rset[i][col]) + if self.eid is not None: + _cw.local_perm_cache[key] = True + return True + except Unauthorized: + pass + if self.eid is not None: + _cw.local_perm_cache[key] = False + return False + + @property + def minimal_rql(self): + return 'Any %s WHERE %s' % (','.join(sorted(self.mainvars)), + self.expression) + + + +# rql expressions for use in permission definition ############################# + +class ERQLExpression(RQLExpression): + predefined_variables = 'XU' + + def __init__(self, expression, mainvars=None, eid=None): + RQLExpression.__init__(self, expression, mainvars or 'X', eid) + + def check(self, _cw, eid=None, creating=False, **kwargs): + if 'X' in self.rqlst.defined_vars: + if eid is None: + if creating: + return self._check(_cw, creating=True, **kwargs) + return False + assert creating == False + return self._check(_cw, x=eid, **kwargs) + return self._check(_cw, **kwargs) + + +def vargraph(rqlst): + """ builds an adjacency graph of variables from the rql syntax tree, e.g: + Any O,S WHERE T subworkflow_exit S, T subworkflow WF, O state_of WF + => {'WF': ['O', 'T'], 'S': ['T'], 'T': ['WF', 'S'], 'O': ['WF']} + """ + vargraph = {} + for relation in rqlst.get_nodes(nodes.Relation): + try: + rhsvarname = relation.children[1].children[0].variable.name + lhsvarname = relation.children[0].name + except AttributeError: + pass + else: + vargraph.setdefault(lhsvarname, []).append(rhsvarname) + vargraph.setdefault(rhsvarname, []).append(lhsvarname) + #vargraph[(lhsvarname, rhsvarname)] = relation.r_type + return vargraph + + +class GeneratedConstraint(object): + def __init__(self, rqlst, mainvars): + self.snippet_rqlst = rqlst + self.mainvars = mainvars + self.vargraph = vargraph(rqlst) + + +class RRQLExpression(RQLExpression): + predefined_variables = 'SOU' + + def __init__(self, expression, mainvars=None, eid=None): + if mainvars is None: + mainvars = guess_rrqlexpr_mainvars(expression) + RQLExpression.__init__(self, expression, mainvars, eid) + + def check(self, _cw, fromeid=None, toeid=None): + kwargs = {} + if 'S' in self.rqlst.defined_vars: + if fromeid is None: + return False + kwargs['s'] = fromeid + if 'O' in self.rqlst.defined_vars: + if toeid is None: + return False + kwargs['o'] = toeid + return self._check(_cw, **kwargs) + + +# In yams, default 'update' perm for attributes granted to managers and owners. +# Within cw, we want to default to users who may edit the entity holding the +# attribute. +# These default permissions won't be checked by the security hooks: +# since they delegate checking to the entity, we can skip actual checks. +ybo.DEFAULT_ATTRPERMS['update'] = ('managers', ERQLExpression('U has_update_permission X')) +ybo.DEFAULT_ATTRPERMS['add'] = ('managers', ERQLExpression('U has_add_permission X')) + + PUB_SYSTEM_ENTITY_PERMS = { 'read': ('managers', 'users', 'guests',), 'add': ('managers',), @@ -107,6 +420,7 @@ } PUB_SYSTEM_ATTR_PERMS = { 'read': ('managers', 'users', 'guests',), + 'add': ('managers',), 'update': ('managers',), } RO_REL_PERMS = { @@ -116,6 +430,7 @@ } RO_ATTR_PERMS = { 'read': ('managers', 'users', 'guests',), + 'add': ybo.DEFAULT_ATTRPERMS['add'], 'update': (), } @@ -268,13 +583,25 @@ return False PermissionMixIn.has_perm = has_perm + def check_perm(self, _cw, action, **kwargs): # NB: _cw may be a server transaction or a request object. # # check user is in an allowed group, if so that's enough internal # transactions should always stop there + DBG = False + if server.DEBUG & server.DBG_SEC: + if action in server._SECURITY_CAPS: + _self_str = str(self) + if server._SECURITY_ITEMS: + if any(item in _self_str for item in server._SECURITY_ITEMS): + DBG = True + else: + DBG = True groups = self.get_groups(action) if _cw.user.matching_groups(groups): + if DBG: + print 'check_perm: %r %r: user matches %s' % (action, _self_str, groups) return # if 'owners' in allowed groups, check if the user actually owns this # object, if so that's enough @@ -284,8 +611,15 @@ if 'owners' in groups and ( kwargs.get('creating') or ('eid' in kwargs and _cw.user.owns(kwargs['eid']))): + if DBG: + print ('check_perm: %r %r: user is owner or creation time' % + (action, _self_str)) return # else if there is some rql expressions, check them + if DBG: + print ('check_perm: %r %r %s' % + (action, _self_str, [(rqlexpr, kwargs, rqlexpr.check(_cw, **kwargs)) + for rqlexpr in self.get_rqlexprs(action)])) if any(rqlexpr.check(_cw, **kwargs) for rqlexpr in self.get_rqlexprs(action)): return @@ -630,308 +964,6 @@ def schema_by_eid(self, eid): return self._eid_index[eid] -# Bases for manipulating RQL in schema ######################################### - -def guess_rrqlexpr_mainvars(expression): - defined = set(split_expression(expression)) - mainvars = set() - if 'S' in defined: - mainvars.add('S') - if 'O' in defined: - mainvars.add('O') - if 'U' in defined: - mainvars.add('U') - if not mainvars: - raise Exception('unable to guess selection variables') - return mainvars - -def split_expression(rqlstring): - for expr in rqlstring.split(','): - for noparen1 in expr.split('('): - for noparen2 in noparen1.split(')'): - for word in noparen2.split(): - yield word - -def normalize_expression(rqlstring): - """normalize an rql expression to ease schema synchronization (avoid - suppressing and reinserting an expression if only a space has been - added/removed for instance) - """ - return u', '.join(' '.join(expr.split()) for expr in rqlstring.split(',')) - - -class RQLExpression(object): - """Base class for RQL expression used in schema (constraints and - permissions) - """ - # these are overridden by set_log_methods below - # only defining here to prevent pylint from complaining - info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None - # to be defined in concrete classes - full_rql = None - - def __init__(self, expression, mainvars, eid): - """ - :type mainvars: sequence of RQL variables' names. Can be provided as a - comma separated string. - :param mainvars: names of the variables being selected. - - """ - self.eid = eid # eid of the entity representing this rql expression - assert mainvars, 'bad mainvars %s' % mainvars - if isinstance(mainvars, basestring): - mainvars = set(splitstrip(mainvars)) - elif not isinstance(mainvars, set): - mainvars = set(mainvars) - self.mainvars = mainvars - self.expression = normalize_expression(expression) - try: - self.rqlst = parse(self.full_rql, print_errors=False).children[0] - except RQLSyntaxError: - raise RQLSyntaxError(expression) - for mainvar in mainvars: - if len(self.rqlst.defined_vars[mainvar].references()) <= 2: - _LOGGER.warn('You did not use the %s variable in your RQL ' - 'expression %s', mainvar, self) - # syntax tree used by read security (inserted in queries when necessary) - self.snippet_rqlst = parse(self.minimal_rql, print_errors=False).children[0] - - def __str__(self): - return self.full_rql - def __repr__(self): - return '%s(%s)' % (self.__class__.__name__, self.full_rql) - - def __lt__(self, other): - if hasattr(other, 'expression'): - return self.expression < other.expression - return True - - def __eq__(self, other): - if hasattr(other, 'expression'): - return self.expression == other.expression - return False - - def __deepcopy__(self, memo): - return self.__class__(self.expression, self.mainvars) - def __getstate__(self): - return (self.expression, self.mainvars) - def __setstate__(self, state): - self.__init__(*state) - - # permission rql expression specific stuff ################################# - - @cached - def transform_has_permission(self): - found = None - rqlst = self.rqlst - for var in rqlst.defined_vars.itervalues(): - for varref in var.references(): - rel = varref.relation() - if rel is None: - continue - try: - prefix, action, suffix = rel.r_type.split('_') - except ValueError: - continue - if prefix != 'has' or suffix != 'permission' or \ - not action in ('add', 'delete', 'update', 'read'): - continue - if found is None: - found = [] - rqlst.save_state() - assert rel.children[0].name == 'U' - objvar = rel.children[1].children[0].variable - rqlst.remove_node(rel) - selected = [v.name for v in rqlst.get_selected_variables()] - if objvar.name not in selected: - colindex = len(selected) - rqlst.add_selected(objvar) - else: - colindex = selected.index(objvar.name) - found.append((action, colindex)) - # remove U eid %(u)s if U is not used in any other relation - uvrefs = rqlst.defined_vars['U'].references() - if len(uvrefs) == 1: - rqlst.remove_node(uvrefs[0].relation()) - if found is not None: - rql = rqlst.as_string() - if len(rqlst.selection) == 1 and isinstance(rqlst.where, nodes.Relation): - # only "Any X WHERE X eid %(x)s" remaining, no need to execute the rql - keyarg = rqlst.selection[0].name.lower() - else: - keyarg = None - rqlst.recover() - return rql, found, keyarg - return rqlst.as_string(), None, None - - def _check(self, _cw, **kwargs): - """return True if the rql expression is matching the given relation - between fromeid and toeid - - _cw may be a request or a server side transaction - """ - creating = kwargs.get('creating') - if not creating and self.eid is not None: - key = (self.eid, tuple(sorted(kwargs.iteritems()))) - try: - return _cw.local_perm_cache[key] - except KeyError: - pass - rql, has_perm_defs, keyarg = self.transform_has_permission() - # when creating an entity, expression related to X satisfied - if creating and 'X' in self.rqlst.defined_vars: - return True - if keyarg is None: - kwargs.setdefault('u', _cw.user.eid) - try: - rset = _cw.execute(rql, kwargs, build_descr=True) - except NotImplementedError: - self.critical('cant check rql expression, unsupported rql %s', rql) - if self.eid is not None: - _cw.local_perm_cache[key] = False - return False - except TypeResolverException as ex: - # some expression may not be resolvable with current kwargs - # (type conflict) - self.warning('%s: %s', rql, str(ex)) - if self.eid is not None: - _cw.local_perm_cache[key] = False - return False - except Unauthorized as ex: - self.debug('unauthorized %s: %s', rql, str(ex)) - if self.eid is not None: - _cw.local_perm_cache[key] = False - return False - else: - rset = _cw.eid_rset(kwargs[keyarg]) - # if no special has_*_permission relation in the rql expression, just - # check the result set contains something - if has_perm_defs is None: - if rset: - if self.eid is not None: - _cw.local_perm_cache[key] = True - return True - elif rset: - # check every special has_*_permission relation is satisfied - get_eschema = _cw.vreg.schema.eschema - try: - for eaction, col in has_perm_defs: - for i in xrange(len(rset)): - eschema = get_eschema(rset.description[i][col]) - eschema.check_perm(_cw, eaction, eid=rset[i][col]) - if self.eid is not None: - _cw.local_perm_cache[key] = True - return True - except Unauthorized: - pass - if self.eid is not None: - _cw.local_perm_cache[key] = False - return False - - @property - def minimal_rql(self): - return 'Any %s WHERE %s' % (','.join(sorted(self.mainvars)), - self.expression) - -# rql expressions for use in permission definition ############################# - -class ERQLExpression(RQLExpression): - def __init__(self, expression, mainvars=None, eid=None): - RQLExpression.__init__(self, expression, mainvars or 'X', eid) - - @property - def full_rql(self): - rql = self.minimal_rql - rqlst = getattr(self, 'rqlst', None) # may be not set yet - if rqlst is not None: - defined = rqlst.defined_vars - else: - defined = set(split_expression(self.expression)) - if 'X' in defined: - rql += ', X eid %(x)s' - if 'U' in defined: - rql += ', U eid %(u)s' - return rql - - def check(self, _cw, eid=None, creating=False, **kwargs): - if 'X' in self.rqlst.defined_vars: - if eid is None: - if creating: - return self._check(_cw, creating=True, **kwargs) - return False - assert creating == False - return self._check(_cw, x=eid, **kwargs) - return self._check(_cw, **kwargs) - - -def vargraph(rqlst): - """ builds an adjacency graph of variables from the rql syntax tree, e.g: - Any O,S WHERE T subworkflow_exit S, T subworkflow WF, O state_of WF - => {'WF': ['O', 'T'], 'S': ['T'], 'T': ['WF', 'S'], 'O': ['WF']} - """ - vargraph = {} - for relation in rqlst.get_nodes(nodes.Relation): - try: - rhsvarname = relation.children[1].children[0].variable.name - lhsvarname = relation.children[0].name - except AttributeError: - pass - else: - vargraph.setdefault(lhsvarname, []).append(rhsvarname) - vargraph.setdefault(rhsvarname, []).append(lhsvarname) - #vargraph[(lhsvarname, rhsvarname)] = relation.r_type - return vargraph - - -class GeneratedConstraint(object): - def __init__(self, rqlst, mainvars): - self.snippet_rqlst = rqlst - self.mainvars = mainvars - self.vargraph = vargraph(rqlst) - - -class RRQLExpression(RQLExpression): - def __init__(self, expression, mainvars=None, eid=None): - if mainvars is None: - mainvars = guess_rrqlexpr_mainvars(expression) - RQLExpression.__init__(self, expression, mainvars, eid) - # graph of links between variable, used by rql rewriter - self.vargraph = vargraph(self.rqlst) - - @property - def full_rql(self): - rql = self.minimal_rql - rqlst = getattr(self, 'rqlst', None) # may be not set yet - if rqlst is not None: - defined = rqlst.defined_vars - else: - defined = set(split_expression(self.expression)) - if 'S' in defined: - rql += ', S eid %(s)s' - if 'O' in defined: - rql += ', O eid %(o)s' - if 'U' in defined: - rql += ', U eid %(u)s' - return rql - - def check(self, _cw, fromeid=None, toeid=None): - kwargs = {} - if 'S' in self.rqlst.defined_vars: - if fromeid is None: - return False - kwargs['s'] = fromeid - if 'O' in self.rqlst.defined_vars: - if toeid is None: - return False - kwargs['o'] = toeid - return self._check(_cw, **kwargs) - - -# in yams, default 'update' perm for attributes granted to managers and owners. -# Within cw, we want to default to users who may edit the entity holding the -# attribute. -ybo.DEFAULT_ATTRPERMS['update'] = ( - 'managers', ERQLExpression('U has_update_permission X')) # additional cw specific constraints ########################################### @@ -940,14 +972,11 @@ distinct_query = None def serialize(self): - # start with a comma for bw compat,see below + # start with a semicolon for bw compat, see below return ';' + ','.join(sorted(self.mainvars)) + ';' + self.expression @classmethod def deserialize(cls, value): - # XXX < 3.5.10 bw compat - if not value.startswith(';'): - return cls(value) _, mainvars, expression = value.split(';', 2) return cls(expression, mainvars) @@ -1002,9 +1031,6 @@ self.msg or '') def deserialize(cls, value): - # XXX < 3.5.10 bw compat - if not value.startswith(';'): - return cls(value) value, msg = value.split('\n', 1) _, mainvars, expression = value.split(';', 2) return cls(expression, mainvars, msg) @@ -1071,7 +1097,7 @@ """ # XXX turns mainvars into a required argument in __init__ distinct_query = True - + def match_condition(self, session, eidfrom, eidto): return len(self.exec_query(session, eidfrom, eidto)) <= 1 @@ -1148,7 +1174,8 @@ # bootstraping, ignore cubes filepath = join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'bootstrap.py') self.info('loading %s', filepath) - self.handle_file(filepath) + with tempattr(ybo, 'PACKAGE', 'cubicweb'): # though we don't care here + self.handle_file(filepath) def unhandled_file(self, filepath): """called when a file without handler associated has been found""" @@ -1188,11 +1215,12 @@ join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'workflow.py'), join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'Bookmark.py')): self.info('loading %s', filepath) - self.handle_file(filepath) + with tempattr(ybo, 'PACKAGE', 'cubicweb'): + self.handle_file(filepath) for cube in cubes: for filepath in self.get_schema_files(cube): - self.info('loading %s', filepath) - self.handle_file(filepath) + with tempattr(ybo, 'PACKAGE', basename(cube)): + self.handle_file(filepath) # these are overridden by set_log_methods below # only defining here to prevent pylint from complaining diff -r aff75b69db92 -r 2c48c091b6a2 schemas/_regproc.postgres.sql --- a/schemas/_regproc.postgres.sql Tue Jul 02 17:09:04 2013 +0200 +++ b/schemas/_regproc.postgres.sql Mon Jan 13 13:47:47 2014 +0100 @@ -10,10 +10,15 @@ SELECT array_to_string($1, ', ') $$ LANGUAGE SQL;; + +CREATE FUNCTION cw_array_append_unique (anyarray, anyelement) RETURNS anyarray AS $$ + SELECT array_append($1, (SELECT $2 WHERE $2 <> ALL($1))) +$$ LANGUAGE SQL + DROP AGGREGATE IF EXISTS group_concat (anyelement) CASCADE; CREATE AGGREGATE group_concat ( basetype = anyelement, - sfunc = array_append, + sfunc = cw_array_append_unique, stype = anyarray, finalfunc = comma_join, initcond = '{}' diff -r aff75b69db92 -r 2c48c091b6a2 schemas/bootstrap.py --- a/schemas/bootstrap.py Tue Jul 02 17:09:04 2013 +0200 +++ b/schemas/bootstrap.py Mon Jan 13 13:47:47 2014 +0100 @@ -83,7 +83,7 @@ indexed = Boolean(description=_('create an index for quick search on this attribute')) fulltextindexed = Boolean(description=_('index this attribute\'s value in the plain text index')) internationalizable = Boolean(description=_('is this attribute\'s value translatable')) - defaultval = String(maxsize=256) + defaultval = Bytes(description=_('default value as gziped pickled python object')) extra_props = Bytes(description=_('additional type specific properties')) description = RichString(internationalizable=True, @@ -158,6 +158,7 @@ class CWUniqueTogetherConstraint(EntityType): """defines a sql-level multicolumn unique index""" __permissions__ = PUB_SYSTEM_ENTITY_PERMS + name = String(required=True, unique=True, maxsize=64) constraint_of = SubjectRelation('CWEType', cardinality='1*', composite='object', inlined=True) relations = SubjectRelation('CWRType', cardinality='+*', @@ -235,7 +236,7 @@ """groups allowed to add entities/relations of this type""" __permissions__ = PUB_SYSTEM_REL_PERMS name = 'add_permission' - subject = ('CWEType', 'CWRelation') + subject = ('CWEType', 'CWRelation', 'CWAttribute') object = 'CWGroup' cardinality = '**' @@ -268,7 +269,7 @@ """rql expression allowing to add entities/relations of this type""" __permissions__ = PUB_SYSTEM_REL_PERMS name = 'add_permission' - subject = ('CWEType', 'CWRelation') + subject = ('CWEType', 'CWRelation', 'CWAttribute') object = 'RQLExpression' cardinality = '*?' composite = 'subject' diff -r aff75b69db92 -r 2c48c091b6a2 schemas/workflow.py --- a/schemas/workflow.py Tue Jul 02 17:09:04 2013 +0200 +++ b/schemas/workflow.py Mon Jan 13 13:47:47 2014 +0100 @@ -89,14 +89,14 @@ name = String(required=True, indexed=True, internationalizable=True, maxsize=256, constraints=[RQLUniqueConstraint('S name N, S transition_of WF, Y transition_of WF, Y name N', 'Y', - _('workflow already have a transition of that name'))]) + _('workflow already has a transition of that name'))]) type = String(vocabulary=(_('normal'), _('auto')), default='normal') description = RichString(description=_('semantic description of this transition')) transition_of = SubjectRelation('Workflow', cardinality='1*', composite='object', description=_('workflow to which this transition belongs'), constraints=[RQLUniqueConstraint('S name N, Y transition_of O, Y name N', 'Y', - _('workflow already have a transition of that name'))]) + _('workflow already has a transition of that name'))]) class require_group(RelationDefinition): diff -r aff75b69db92 -r 2c48c091b6a2 server/__init__.py --- a/server/__init__.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/__init__.py Mon Jan 13 13:47:47 2014 +0100 @@ -26,6 +26,7 @@ import sys from os.path import join, exists from glob import glob +from contextlib import contextmanager from logilab.common.modutils import LazyObject from logilab.common.textutils import splitstrip @@ -78,14 +79,57 @@ DBG_HOOKS = 16 #: operations DBG_OPS = 32 +#: security +DBG_SEC = 64 #: more verbosity -DBG_MORE = 64 +DBG_MORE = 128 #: all level enabled -DBG_ALL = DBG_RQL + DBG_SQL + DBG_REPO + DBG_MS + DBG_HOOKS + DBG_OPS + DBG_MORE +DBG_ALL = DBG_RQL + DBG_SQL + DBG_REPO + DBG_MS + DBG_HOOKS + DBG_OPS + DBG_SEC + DBG_MORE + +_SECURITY_ITEMS = [] +_SECURITY_CAPS = ['read', 'add', 'update', 'delete'] #: current debug mode DEBUG = 0 +@contextmanager +def tunesecurity(items=(), capabilities=()): + """Context manager to use in conjunction with DBG_SEC. + + This allows some tuning of: + * the monitored capabilities ('read', 'add', ....) + * the object being checked by the security checkers + + When no item is given, all of them will be watched. + By default all capabilities are monitored, unless specified. + + Example use:: + + from cubicweb.server import debugged, DBG_SEC, tunesecurity + with debugged(DBG_SEC): + with tunesecurity(items=('Elephant', 'trumps'), + capabilities=('update', 'delete')): + babar.cw_set(trumps=celeste) + flore.cw_delete() + + ==> + + check_perm: 'update' 'relation Elephant.trumps.Elephant' + [(ERQLExpression(Any X WHERE U has_update_permission X, X eid %(x)s, U eid %(u)s), + {'eid': 2167}, True)] + check_perm: 'delete' 'Elephant' + [(ERQLExpression(Any X WHERE U has_delete_permission X, X eid %(x)s, U eid %(u)s), + {'eid': 2168}, True)] + + """ + olditems = _SECURITY_ITEMS[:] + _SECURITY_ITEMS.extend(list(items)) + oldactions = _SECURITY_CAPS[:] + _SECURITY_CAPS[:] = capabilities + yield + _SECURITY_ITEMS[:] = olditems + _SECURITY_CAPS[:] = oldactions + def set_debug(debugmode): """change the repository debugging mode""" global DEBUG @@ -103,13 +147,13 @@ It can be used either as a context manager: - >>> with debugged(server.DBG_RQL | server.DBG_REPO): + >>> with debugged('DBG_RQL | DBG_REPO'): ... # some code in which you want to debug repository activity, ... # seing information about RQL being executed an repository events. or as a function decorator: - >>> @debugged(server.DBG_RQL | server.DBG_REPO) + >>> @debugged('DBG_RQL | DBG_REPO') ... def some_function(): ... # some code in which you want to debug repository activity, ... # seing information about RQL being executed an repository events @@ -203,7 +247,11 @@ schemasql = sqlschema(schema, driver) #skip_entities=[str(e) for e in schema.entities() # if not repo.system_source.support_entity(str(e))]) - sqlexec(schemasql, execute, pbtitle=_title, delimiter=';;') + failed = sqlexec(schemasql, execute, pbtitle=_title, delimiter=';;') + if failed: + print 'The following SQL statements failed. You should check your schema.' + print failed + raise Exception('execution of the sql schema failed, you should check your schema') sqlcursor.close() sqlcnx.commit() sqlcnx.close() diff -r aff75b69db92 -r 2c48c091b6a2 server/checkintegrity.py --- a/server/checkintegrity.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/checkintegrity.py Mon Jan 13 13:47:47 2014 +0100 @@ -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 aff75b69db92 -r 2c48c091b6a2 server/edition.py --- a/server/edition.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/edition.py Mon Jan 13 13:47:47 2014 +0100 @@ -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 aff75b69db92 -r 2c48c091b6a2 server/hook.py --- a/server/hook.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/hook.py Mon Jan 13 13:47:47 2014 +0100 @@ -41,7 +41,7 @@ defined over data events. Also, some :class:`~cubicweb.server.hook.Operation` may be registered by hooks, -which will be fired when the transaction is commited or rollbacked. +which will be fired when the transaction is commited or rolled back. The purpose of data event hooks is usually to complement the data model as defined in the schema, which is static by nature and only provide a restricted @@ -169,7 +169,10 @@ * `after_add_relation`, `after_delete_relation` -Take note that relations can be added or deleted, but not updated. +Specific selectors are shipped for these kinds of events, see in particular +:class:`~cubicweb.server.hook.match_rtype`. + +Also note that relations can be added or deleted, but not updated. Non data events ~~~~~~~~~~~~~~~ @@ -439,11 +442,13 @@ class match_rtype(ExpectedValuePredicate): - """accept if parameters specified as initializer arguments are specified - in named arguments given to the predicate + """accept if the relation type is found in expected ones. Optional + named parameters `frometypes` and `toetypes` can be used to restrict + target subject and/or object entity types of the relation. - :param \*expected: parameters (eg `basestring`) which are expected to be - found in named arguments (kwargs) + :param \*expected: possible relation types + :param frometypes: candidate entity types as subject of relation + :param toetypes: candidate entity types as object of relation """ def __init__(self, *expected, **more): self.expected = expected @@ -673,16 +678,6 @@ {'x': self.eidfrom, 'p': self.eidto}) -PropagateSubjectRelationHook = class_renamed( - 'PropagateSubjectRelationHook', PropagateRelationHook, - '[3.9] PropagateSubjectRelationHook has been renamed to PropagateRelationHook') -PropagateSubjectRelationAddHook = class_renamed( - 'PropagateSubjectRelationAddHook', PropagateRelationAddHook, - '[3.9] PropagateSubjectRelationAddHook has been renamed to PropagateRelationAddHook') -PropagateSubjectRelationDelHook = class_renamed( - 'PropagateSubjectRelationDelHook', PropagateRelationDelHook, - '[3.9] PropagateSubjectRelationDelHook has been renamed to PropagateRelationDelHook') - # abstract classes for operation ############################################### @@ -715,10 +710,10 @@ * `rollback`: - the transaction has been either rollbacked either: + the transaction has been either rolled back either: * intentionaly - * a 'precommit' event failed, in which case all operations are rollbacked + * a 'precommit' event failed, in which case all operations are rolled back once 'revertprecommit'' has been called * `postcommit`: @@ -780,7 +775,7 @@ """ def rollback_event(self): - """the observed connections set has been rollbacked + """the observed connections set has been rolled back do nothing by default """ @@ -1044,7 +1039,7 @@ type/source cache eids of entities added in that transaction. NOTE: querier's rqlst/solutions cache may have been polluted too with - queries such as Any X WHERE X eid 32 if 32 has been rollbacked however + queries such as Any X WHERE X eid 32 if 32 has been rolled back however generated queries are unpredictable and analysing all the cache probably too expensive. Notice that there is no pb when using args to specify eids instead of giving them into the rql string. @@ -1052,7 +1047,7 @@ data_key = 'neweids' def rollback_event(self): - """the observed connections set has been rollbacked, + """the observed connections set has been rolled back, remove inserted eid from repository type/source cache """ try: @@ -1066,7 +1061,7 @@ """ data_key = 'pendingeids' def postcommit_event(self): - """the observed connections set has been rollbacked, + """the observed connections set has been rolled back, remove inserted eid from repository type/source cache """ try: diff -r aff75b69db92 -r 2c48c091b6a2 server/hooksmanager.py --- a/server/hooksmanager.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/hooksmanager.py Mon Jan 13 13:47:47 2014 +0100 @@ -17,11 +17,6 @@ # with CubicWeb. If not, see . from logilab.common.deprecation import class_renamed, class_moved from cubicweb.server import hook + SystemHook = class_renamed('SystemHook', hook.Hook) -PropagateSubjectRelationHook = class_renamed('PropagateSubjectRelationHook', - hook.PropagateSubjectRelationHook) -PropagateSubjectRelationAddHook = class_renamed('PropagateSubjectRelationAddHook', - hook.PropagateSubjectRelationAddHook) -PropagateSubjectRelationDelHook = class_renamed('PropagateSubjectRelationDelHook', - hook.PropagateSubjectRelationDelHook) Hook = class_moved(hook.Hook) diff -r aff75b69db92 -r 2c48c091b6a2 server/migractions.py --- a/server/migractions.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/migractions.py Mon Jan 13 13:47:47 2014 +0100 @@ -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. @@ -44,7 +44,7 @@ from logilab.common.decorators import cached, clear_cache from yams.constraints import SizeConstraint -from yams.schema2sql import eschema2sql, rschema2sql +from yams.schema2sql import eschema2sql, rschema2sql, unique_index_name from yams.schema import RelationDefinitionSchema from cubicweb import CW_SOFTWARE_ROOT, AuthenticationError, ExecutionError @@ -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, @@ -559,26 +559,42 @@ self._synchronize_rdef_schema(subj, rschema, obj, syncprops=syncprops, syncperms=syncperms) if syncprops: # need to process __unique_together__ after rdefs were processed - repo_unique_together = set([frozenset(ut) - for ut in repoeschema._unique_together]) - unique_together = set([frozenset(ut) - for ut in eschema._unique_together]) - for ut in repo_unique_together - unique_together: - restrictions = [] - substs = {'x': repoeschema.eid} - for i, col in enumerate(ut): - restrictions.append('C relations T%(i)d, ' - 'T%(i)d name %%(T%(i)d)s' % {'i': i}) - substs['T%d'%i] = col - self.rqlexec('DELETE CWUniqueTogetherConstraint C ' - 'WHERE C constraint_of E, ' - ' E eid %%(x)s,' - ' %s' % ', '.join(restrictions), - substs) - for ut in unique_together - repo_unique_together: - rql, substs = ss.uniquetogether2rql(eschema, ut) - substs['x'] = repoeschema.eid - self.rqlexec(rql, substs) + # mappings from constraint name to columns + # filesystem (fs) and repository (repo) wise + fs = {} + repo = {} + for cols in eschema._unique_together or (): + fs[unique_index_name(repoeschema, cols)] = sorted(cols) + schemaentity = self.session.entity_from_eid(repoeschema.eid) + for entity in schemaentity.related('constraint_of', 'object', + targettypes=('CWUniqueTogetherConstraint',)).entities(): + repo[entity.name] = sorted(rel.name for rel in entity.relations) + added = set(fs) - set(repo) + removed = set(repo) - set(fs) + + for name in removed: + self.rqlexec('DELETE CWUniqueTogetherConstraint C WHERE C name %(name)s', + {'name': name}) + + def possible_unique_constraint(cols): + for name in cols: + rschema = repoeschema.subjrels.get(name) + if rschema is None: + print 'dont add %s unique constraint on %s, missing %s' % ( + ','.join(cols), eschema, name) + return False + if not (rschema.final or rschema.inlined): + print 'dont add %s unique constraint on %s, %s is neither final nor inlined' % ( + ','.join(cols), eschema, name) + return False + return True + + for name in added: + if possible_unique_constraint(fs[name]): + rql, substs = ss._uniquetogether2rql(eschema, fs[name]) + substs['x'] = repoeschema.eid + substs['name'] = name + self.rqlexec(rql, substs) def _synchronize_rdef_schema(self, subjtype, rtype, objtype, syncperms=True, syncprops=True): @@ -688,7 +704,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 +948,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 +1026,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}, @@ -1439,12 +1463,9 @@ # no result to fetch return - def rqlexec(self, rql, kwargs=None, cachekey=None, build_descr=True, + def rqlexec(self, rql, kwargs=None, build_descr=True, ask_confirm=False): """rql action""" - if cachekey is not None: - warn('[3.8] cachekey is deprecated, you can safely remove this argument', - DeprecationWarning, stacklevel=2) if not isinstance(rql, (tuple, list)): rql = ( (rql, kwargs), ) res = None diff -r aff75b69db92 -r 2c48c091b6a2 server/msplanner.py --- a/server/msplanner.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/msplanner.py Mon Jan 13 13:47:47 2014 +0100 @@ -92,6 +92,7 @@ from logilab.common.compat import any from logilab.common.decorators import cached +from logilab.common.deprecation import deprecated from rql import BadRQLQuery from rql.stmts import Union, Select @@ -100,8 +101,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 * @@ -1263,6 +1263,7 @@ inputmap.update(step.outputmap) +@deprecated('[3.18] old multi-source system will go away in the next version') class MSPlanner(SSPlanner): """MultiSourcesPlanner: build execution plan for rql queries diff -r aff75b69db92 -r 2c48c091b6a2 server/querier.py --- a/server/querier.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/querier.py Mon Jan 13 13:47:47 2014 +0100 @@ -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,16 +74,18 @@ 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. # XXX what about local read security w/ those rewritten constants... + DBG = (server.DEBUG & server.DBG_SEC) and 'read' in server._SECURITY_CAPS schema = session.repo.schema if rqlst.where is not None: for rel in rqlst.where.iget_nodes(Relation): @@ -104,8 +103,14 @@ term_etype(session, rel.children[1].children[0], solution, args)) if not session.user.matching_groups(rdef.get_groups('read')): + if DBG: + print ('check_read_access: %s %s does not match %s' % + (rdef, session.user.groups, rdef.get_groups('read'))) # XXX rqlexpr not allowed raise Unauthorized('read', rel.r_type) + if DBG: + print ('check_read_access: %s %s matches %s' % + (rdef, session.user.groups, rdef.get_groups('read'))) localchecks = {} # iterate on defined_vars and not on solutions to ignore column aliases for varname in rqlst.defined_vars: @@ -117,6 +122,9 @@ if not erqlexprs: ex = Unauthorized('read', solution[varname]) ex.var = varname + if DBG: + print ('check_read_access: %s %s %s %s' % + (varname, eschema, session.user.groups, eschema.get_groups('read'))) raise ex # don't insert security on variable only referenced by 'NOT X relation Y' or # 'NOT EXISTS(X relation Y)' @@ -130,35 +138,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 +237,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 +250,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 +310,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 +631,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) @@ -739,7 +642,7 @@ results = plan.execute() except (Unauthorized, ValidationError): # getting an Unauthorized/ValidationError exception means the - # transaction must been rollbacked + # transaction must be rolled back # # notes: # * we should not reset the connections set here, since we don't want the diff -r aff75b69db92 -r 2c48c091b6a2 server/repository.py --- a/server/repository.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/repository.py Mon Jan 13 13:47:47 2014 +0100 @@ -37,6 +37,7 @@ from datetime import datetime from time import time, localtime, strftime from contextlib import contextmanager +from warnings import warn from logilab.common.decorators import cached, clear_cache from logilab.common.deprecation import deprecated @@ -241,7 +242,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') @@ -301,6 +302,8 @@ # initialized) source.init(True, sourceent) if not source.copy_based_source: + warn('[3.18] old multi-source system will go away in the next version', + DeprecationWarning) self.sources.append(source) self.querier.set_planner() if add_to_cnxsets: @@ -354,9 +357,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): @@ -410,7 +412,7 @@ return self._cnxsets_pool.get(True, timeout=5) except Queue.Empty: raise Exception('no connections set available after 5 secs, probably either a ' - 'bug in code (too many uncommited/rollbacked ' + 'bug in code (too many uncommited/rolled back ' 'connections) or too much load on the server (in ' 'which case you can try to set a bigger ' 'connections pool size)') @@ -755,16 +757,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 @@ -839,7 +832,7 @@ """close the session with the given id""" session = self._get_session(sessionid, setcnxset=True, txid=txid, checkshuttingdown=checkshuttingdown) - # operation uncommited before close are rollbacked before hook is called + # operation uncommited before close are rolled back before hook is called session.rollback(free_cnxset=False) self.hm.call_hooks('session_close', session) # commit session at this point in case write operation has been done @@ -981,7 +974,7 @@ def _get_session(self, sessionid, setcnxset=False, txid=None, checkshuttingdown=True): - """return the user associated to the given session identifier""" + """return the session associated with the given session identifier""" if checkshuttingdown and self.shutting_down: raise ShuttingDown('Repository is shutting down') try: @@ -1408,11 +1401,9 @@ source.update_entity(session, entity) edited.saved = True except UniqueTogetherError as exc: - etype, rtypes = exc.args - problems = {} - for col in rtypes: - problems[col] = session._('violates unique_together constraints (%s)') % (','.join(rtypes)) - raise ValidationError(entity.eid, problems) + userhdlr = session.vreg['adapters'].select( + 'IUserFriendlyError', session, entity=entity, exc=exc) + userhdlr.raise_user_exception() self.system_source.update_info(session, entity, need_fti_update) if source.should_call_hooks: if not only_inline_rels: @@ -1566,10 +1557,6 @@ source.delete_relation(session, subject, rtype, object) rschema = self.schema.rschema(rtype) session.update_rel_cache_del(subject, rtype, object, rschema.symmetric) - if rschema.symmetric: - # on symmetric relation, we can't now in which sense it's - # stored so try to delete both - source.delete_relation(session, object, rtype, subject) if source.should_call_hooks: self.hm.call_hooks('after_delete_relation', session, eidfrom=subject, rtype=rtype, eidto=object) @@ -1623,7 +1610,7 @@ # client was not yet connected to the repo return if not session.closed: - session.close() + self.close(session.id) daemon.removeConnection = removeConnection return daemon @@ -1639,18 +1626,24 @@ @cached def rel_type_sources(self, rtype): + warn('[3.18] old multi-source system will go away in the next version', + DeprecationWarning) return tuple([source for source in self.sources if source.support_relation(rtype) or rtype in source.dont_cross_relations]) @cached def can_cross_relation(self, rtype): + warn('[3.18] old multi-source system will go away in the next version', + DeprecationWarning) return tuple([source for source in self.sources if source.support_relation(rtype) and rtype in source.cross_relations]) @cached def is_multi_sources_relation(self, rtype): + warn('[3.18] old multi-source system will go away in the next version', + DeprecationWarning) return any(source for source in self.sources if not source is self.system_source and source.support_relation(rtype)) diff -r aff75b69db92 -r 2c48c091b6a2 server/rqlannotation.py --- a/server/rqlannotation.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/rqlannotation.py Mon Jan 13 13:47:47 2014 +0100 @@ -130,8 +130,6 @@ # can use N.ecrit_par as principal if (stinfo['selected'] or len(stinfo['relations']) > 1): break - elif rschema.symmetric and stinfo['selected']: - break joins.add( (rel, role) ) else: # if there is at least one ambigous relation and no other to diff -r aff75b69db92 -r 2c48c091b6a2 server/schemaserial.py --- a/server/schemaserial.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/schemaserial.py Mon Jan 13 13:47:47 2014 +0100 @@ -25,9 +25,10 @@ from logilab.common.shellutils import ProgressBar -from yams import BadSchemaDefinition, schema as schemamod, buildobjs as ybo +from yams import (BadSchemaDefinition, schema as schemamod, buildobjs as ybo, + schema2sql as y2sql) -from cubicweb import CW_SOFTWARE_ROOT, Binary +from cubicweb import CW_SOFTWARE_ROOT, Binary, typed_eid from cubicweb.schema import (KNOWN_RPROPERTIES, CONSTRAINTS, ETYPE_NAME_MAP, VIRTUAL_RTYPES, PURE_VIRTUAL_RTYPES) from cubicweb.server import sqlutils @@ -77,8 +78,6 @@ def cstrtype_mapping(cursor): """cached constraint types mapping""" map = dict(cursor.execute('Any T, X WHERE X is CWConstraintType, X name T')) - if not 'BoundConstraint' in map: - map['BoundConstraint'] = map['BoundaryConstraint'] return map # schema / perms deserialization ############################################## @@ -214,6 +213,11 @@ rdefeid, seid, reid, oeid, card, ord, desc, idx, ftidx, i18n, default = values typeparams = extra_props.get(rdefeid) typeparams = json.load(typeparams) if typeparams else {} + if default is not None: + if isinstance(default, Binary): + # while migrating from 3.17 to 3.18, we still have to + # handle String defaults + default = default.unzpickle() _add_rdef(rdefeid, seid, reid, oeid, cardinality=card, description=desc, order=ord, indexed=idx, fulltextindexed=ftidx, internationalizable=i18n, @@ -234,28 +238,24 @@ if rdefs is not None: set_perms(rdefs, permsidx) unique_togethers = {} - try: - rset = session.execute( - 'Any X,E,R WHERE ' - 'X is CWUniqueTogetherConstraint, ' - 'X constraint_of E, X relations R', build_descr=False) - except Exception: - session.rollback() # first migration introducing CWUniqueTogetherConstraint cw 3.9.6 - else: - for values in rset: - uniquecstreid, eeid, releid = values - eschema = schema.schema_by_eid(eeid) - relations = unique_togethers.setdefault(uniquecstreid, (eschema, [])) - rel = ertidx[releid] - if isinstance(rel, schemamod.RelationDefinitionSchema): - # not yet migrated 3.9 database ('relations' target type changed - # to CWRType in 3.10) - rtype = rel.rtype.type - else: - rtype = str(rel) - relations[1].append(rtype) - for eschema, unique_together in unique_togethers.itervalues(): - eschema._unique_together.append(tuple(sorted(unique_together))) + rset = session.execute( + 'Any X,E,R WHERE ' + 'X is CWUniqueTogetherConstraint, ' + 'X constraint_of E, X relations R', build_descr=False) + for values in rset: + uniquecstreid, eeid, releid = values + eschema = schema.schema_by_eid(eeid) + relations = unique_togethers.setdefault(uniquecstreid, (eschema, [])) + rel = ertidx[releid] + if isinstance(rel, schemamod.RelationDefinitionSchema): + # not yet migrated 3.9 database ('relations' target type changed + # to CWRType in 3.10) + rtype = rel.rtype.type + else: + rtype = str(rel) + relations[1].append(rtype) + for eschema, unique_together in unique_togethers.itervalues(): + eschema._unique_together.append(tuple(sorted(unique_together))) schema.infer_specialization_rules() session.commit() schema.reading_from_database = False @@ -304,9 +304,6 @@ except KeyError: return for action, somethings in thispermsdict.iteritems(): - # XXX cw < 3.6.1 bw compat - if isinstance(erschema, schemamod.RelationDefinitionSchema) and erschema.final and action == 'add': - action = 'update' erschema.permissions[action] = tuple( isinstance(p, tuple) and erschema.rql_expression(*p) or p for p in somethings) @@ -344,13 +341,10 @@ cstrtypemap = {} rql = 'INSERT CWConstraintType X: X name %(ct)s' for cstrtype in CONSTRAINTS: - if cstrtype == 'BoundConstraint': - continue # XXX deprecated in yams 0.29 / cw 3.8.1 cstrtypemap[cstrtype] = execute(rql, {'ct': unicode(cstrtype)}, build_descr=False)[0][0] if pb is not None: pb.update() - cstrtypemap['BoundConstraint'] = cstrtypemap['BoundaryConstraint'] # serialize relations for rschema in schema.relations(): # skip virtual relations such as eid, has_text and identity @@ -371,8 +365,8 @@ pb.update() # serialize unique_together constraints for eschema in eschemas: - for unique_together in eschema._unique_together: - execschemarql(execute, eschema, [uniquetogether2rql(eschema, unique_together)]) + if eschema._unique_together: + execschemarql(execute, eschema, uniquetogether2rqls(eschema)) # serialize yams inheritance relationships for rql, kwargs in specialize2rql(schema): execute(rql, kwargs, build_descr=False) @@ -431,7 +425,15 @@ values = {'x': eschema.eid, 'et': specialized_type.eid} yield 'SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s', values -def uniquetogether2rql(eschema, unique_together): +def uniquetogether2rqls(eschema): + rql_args = [] + for columns in eschema._unique_together: + rql, args = _uniquetogether2rql(eschema, columns) + args['name'] = y2sql.unique_index_name(eschema, columns) + rql_args.append((rql, args)) + return rql_args + +def _uniquetogether2rql(eschema, unique_together): relations = [] restrictions = [] substs = {} @@ -443,10 +445,8 @@ restrictions.append('%(rtype)s name %%(%(rtype)s)s' % {'rtype': rtype}) relations = ', '.join(relations) restrictions = ', '.join(restrictions) - rql = ('INSERT CWUniqueTogetherConstraint C: ' - ' C constraint_of X, %s ' - 'WHERE ' - ' X eid %%(x)s, %s') + rql = ('INSERT CWUniqueTogetherConstraint C: C name %%(name)s, C constraint_of X, %s ' + 'WHERE X eid %%(x)s, %s') return rql % (relations, restrictions), substs @@ -536,10 +536,7 @@ elif isinstance(value, str): value = unicode(value) if value is not None and prop == 'default': - if value is False: - value = u'' - if not isinstance(value, unicode): - value = unicode(value) + value = Binary.zpickle(value) values[amap.get(prop, prop)] = value if extra: values['extra_props'] = Binary(json.dumps(extra)) diff -r aff75b69db92 -r 2c48c091b6a2 server/server.py --- a/server/server.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/server.py Mon Jan 13 13:47:47 2014 +0100 @@ -48,7 +48,7 @@ def is_ready(self): """return true if the event is ready to be fired""" now = self.timefunc() - if self.absolute < now: + if self.absolute <= now: return True return False @@ -109,7 +109,8 @@ self.daemon.handleRequests(req_timeout) except select.error: continue - self.trigger_events() + finally: + self.trigger_events() def quit(self): """stop the server""" diff -r aff75b69db92 -r 2c48c091b6a2 server/serverconfig.py --- a/server/serverconfig.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/serverconfig.py Mon Jan 13 13:47:47 2014 +0100 @@ -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 aff75b69db92 -r 2c48c091b6a2 server/serverctl.py --- a/server/serverctl.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/serverctl.py Mon Jan 13 13:47:47 2014 +0100 @@ -1065,6 +1065,25 @@ if val: print key, ':', val + + +def permissionshandler(relation, perms): + from yams.schema import RelationDefinitionSchema + from yams.buildobjs import DEFAULT_ATTRPERMS + from cubicweb.schema import (PUB_SYSTEM_ENTITY_PERMS, PUB_SYSTEM_REL_PERMS, + PUB_SYSTEM_ATTR_PERMS, RO_REL_PERMS, RO_ATTR_PERMS) + defaultrelperms = (DEFAULT_ATTRPERMS, PUB_SYSTEM_REL_PERMS, + PUB_SYSTEM_ATTR_PERMS, RO_REL_PERMS, RO_ATTR_PERMS) + defaulteperms = (PUB_SYSTEM_ENTITY_PERMS,) + # canonicalize vs str/unicode + for p in ('read', 'add', 'update', 'delete'): + rule = perms.get(p) + if rule: + perms[p] = tuple(str(x) if isinstance(x, basestring) else x + for x in rule) + return perms, perms in defaultrelperms or perms in defaulteperms + + class SchemaDiffCommand(Command): """Generate a diff between schema and fsschema description. @@ -1085,7 +1104,7 @@ repo, cnx = repo_cnx(config) session = repo._get_session(cnx.sessionid, setcnxset=True) fsschema = config.load_schema(expand_cubes=True) - schema_diff(repo.schema, fsschema, diff_tool) + schema_diff(fsschema, repo.schema, permissionshandler, diff_tool, ignore=('eid',)) for cmdclass in (CreateInstanceDBCommand, InitInstanceCommand, diff -r aff75b69db92 -r 2c48c091b6a2 server/session.py --- a/server/session.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/session.py Mon Jan 13 13:47:47 2014 +0100 @@ -70,7 +70,7 @@ class transaction(object): - """Ensure that the transaction is either commited or rollbacked at exit + """Ensure that the transaction is either commited or rolled back at exit Context manager to enter a transaction for a session: when exiting the `with` block on exception, call `session.rollback()`, else call @@ -234,18 +234,17 @@ class CnxSetTracker(object): """Keep track of which connection use which cnxset. - There should be one of this object per session plus one another for - internal session. + There should be one of these objects per session (including internal sessions). - Session object are responsible of creating their CnxSetTracker object. + Session objects are responsible for creating their CnxSetTracker object. - Connection should use the :meth:`record` and :meth:`forget` to inform the - tracker of cnxset they have acquired. + Connections should use the :meth:`record` and :meth:`forget` to inform the + tracker of cnxsets they have acquired. .. automethod:: cubicweb.server.session.CnxSetTracker.record .. automethod:: cubicweb.server.session.CnxSetTracker.forget - Session use the :meth:`close` and :meth:`wait` method when closing. + Sessions use the :meth:`close` and :meth:`wait` methods when closing. .. automethod:: cubicweb.server.session.CnxSetTracker.close .. automethod:: cubicweb.server.session.CnxSetTracker.wait @@ -260,18 +259,18 @@ self._record = {} def __enter__(self): - self._condition.__enter__() + return self._condition.__enter__() def __exit__(self, *args): - self._condition.__exit__(*args) + return self._condition.__exit__(*args) def record(self, cnxid, cnxset): - """Inform the tracker that a cnxid have acquired a cnxset + """Inform the tracker that a cnxid has acquired a cnxset - This methode is to be used by Connection object. + This method is to be used by Connection objects. This method fails when: - - The cnxid already have a recorded cnxset. + - The cnxid already has a recorded cnxset. - The tracker is not active anymore. Notes about the caller: @@ -279,7 +278,7 @@ (2) It must be prepared to release the cnxset if the `cnxsettracker.forget` call fails. (3) It should acquire the tracker lock until the very end of the operation. - (4) However It take care to lock the CnxSetTracker object after having + (4) However it must only lock the CnxSetTracker object after having retrieved the cnxset to prevent deadlock. A typical usage look like:: @@ -294,13 +293,13 @@ repo._free_cnxset(cnxset) # (2) raise """ - # dubious since the caller is suppose to have acquired it anyway. + # dubious since the caller is supposed to have acquired it anyway. with self._condition: if not self._active: raise SessionClosedError('Closed') old = self._record.get(cnxid) if old is not None: - raise ValueError('"%s" already have a cnx_set (%r)' + raise ValueError('connection "%s" already has a cnx_set (%r)' % (cnxid, old)) self._record[cnxid] = cnxset @@ -340,19 +339,19 @@ def close(self): """Marks the tracker as inactive. - This methode is to be used by Session object. + This method is to be used by Session objects. - Inactive tracker does not accept new record anymore. + An inactive tracker does not accept new records anymore. """ with self._condition: self._active = False def wait(self, timeout=10): - """Wait for all recorded cnxset to be released + """Wait for all recorded cnxsets to be released - This methode is to be used by Session object. + This method is to be used by Session objects. - returns a tuple of connection id that remains open. + Returns a tuple of connection ids that remain open. """ with self._condition: if self._active: @@ -388,15 +387,15 @@ Holds all connection related data - Database connections resource: + Database connection resources: :attr:`running_dbapi_query`, boolean flag telling if the executing query is coming from a dbapi connection or is a query from within the repository :attr:`cnxset`, the connections set to use to execute queries on sources. If the transaction is read only, the connection set may be freed between - actual query. This allows multiple connection with a reasonable low - connection set pool size. control mechanism is detailed below + actual queries. This allows multiple connections with a reasonably low + connection set pool size. Control mechanism is detailed below. .. automethod:: cubicweb.server.session.Connection.set_cnxset .. automethod:: cubicweb.server.session.Connection.free_cnxset @@ -409,7 +408,7 @@ Internal transaction data: - :attr:`data`,is a dictionary containing some shared data + :attr:`data` is a dictionary containing some shared data cleared at the end of the transaction. Hooks and operations may put arbitrary data in there, and this may also be used as a communication channel between the client and the repository. @@ -421,7 +420,7 @@ of None (not yet committing), 'precommit' (calling precommit event on operations), 'postcommit' (calling postcommit event on operations), 'uncommitable' (some :exc:`ValidationError` or :exc:`Unauthorized` error - has been raised during the transaction and so it must be rollbacked). + has been raised during the transaction and so it must be rolled back). Hooks controls: @@ -660,7 +659,7 @@ # Entity cache management ################################################# # - # The connection entity cache as held in cnx.transaction_data it is removed at end the + # The connection entity cache as held in cnx.transaction_data is removed at the # end of the connection (commit and rollback) # # XXX connection level caching may be a pb with multiple repository @@ -833,9 +832,7 @@ entity._cw_related_cache['%s_%s' % (rtype, role)] = ( rset, tuple(entities)) - # Tracking of entity added of removed in the transaction ################## - # - # Those are function to allows cheap call from client in other process. + # Tracking of entities added of removed in the transaction ################## @_open_only def deleted_in_transaction(self, eid): @@ -982,6 +979,7 @@ num = self.transaction_data.setdefault('tx_action_count', 0) + 1 self.transaction_data['tx_action_count'] = num return num + # db-api like interface ################################################### @_open_only @@ -995,7 +993,7 @@ metas = self.repo.type_and_source_from_eid(eid, self) if asdict: return dict(zip(('type', 'source', 'extid', 'asource'), metas)) - # XXX :-1 for cw compat, use asdict=True for full information + # XXX :-1 for cw compat, use asdict=True for full information return metas[:-1] @_with_cnx_set @@ -1211,6 +1209,7 @@ if getattr(result, '_cw', None) is not None: result._cw = session return result + meth_from_cnx.__doc__ = getattr(Connection, meth_name).__doc__ return meth_from_cnx class Timestamp(object): @@ -1229,14 +1228,13 @@ # RequestSessionBase at some point """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 @@ -1280,8 +1278,8 @@ used by another session as long as no writing is done. This means we can have multiple sessions with a reasonably low connections set pool size. - .. automethod:: cubicweb.server.session.set_cnxset - .. automethod:: cubicweb.server.session.free_cnxset + .. automethod:: cubicweb.server.session.Session.set_cnxset + .. automethod:: cubicweb.server.session.Session.free_cnxset :attr:`mode`, string telling the connections set handling mode, may be one of 'read' (connections set may be freed), 'write' (some write was done in @@ -1296,7 +1294,7 @@ of None (not yet committing), 'precommit' (calling precommit event on operations), 'postcommit' (calling postcommit event on operations), 'uncommitable' (some :exc:`ValidationError` or :exc:`Unauthorized` error - has been raised during the transaction and so it must be rollbacked). + has been raised during the transaction and so it must be rolled back). .. automethod:: cubicweb.server.session.Session.commit .. automethod:: cubicweb.server.session.Session.rollback @@ -1449,7 +1447,7 @@ call `session.commit()` on normal exit. The `free_cnxset` will be given to rollback/commit methods to indicate - wether the connections set should be freed or not. + whether the connections set should be freed or not. """ return transaction(self, free_cnxset) @@ -1505,7 +1503,7 @@ def keep_cnxset_mode(self, mode): """set `mode`, e.g. how the session will keep its connections set: - * if mode == 'write', the connections set is freed after each ready + * if mode == 'write', the connections set is freed after each read query, but kept until the transaction's end (eg commit or rollback) when a write query is detected (eg INSERT/SET/DELETE queries) @@ -1631,7 +1629,7 @@ """commit the current session's transaction""" cstate = self._cnx.commit_state if cstate == 'uncommitable': - raise QueryError('transaction must be rollbacked') + raise QueryError('transaction must be rolled back') try: return self._cnx.commit(free_cnxset, reset_pool) finally: @@ -1652,7 +1650,7 @@ self._closed = True tracker.close() self.rollback() - self.info('waiting for open connection of session: %s', self) + self.debug('waiting for open connection of session: %s', self) timeout = 10 pendings = tracker.wait(timeout) if pendings: @@ -1747,6 +1745,7 @@ self.user._cw = self # XXX remove when "vreg = user._cw.vreg" hack in entity.py is gone if not safe: self.disable_hook_categories('integrity') + self._tx.ctx_count += 1 def __enter__(self): return self diff -r aff75b69db92 -r 2c48c091b6a2 server/sources/__init__.py --- a/server/sources/__init__.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/sources/__init__.py Mon Jan 13 13:47:47 2014 +0100 @@ -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 @@ -179,7 +183,7 @@ elif value is not None: # type check try: - value = configuration.convert(value, optdict, optname) + value = configuration._validate(value, optdict, optname) except Exception as ex: msg = unicode(ex) # XXX internationalization raise ValidationError(eid, {role_name('config', 'subject'): msg}) diff -r aff75b69db92 -r 2c48c091b6a2 server/sources/datafeed.py --- a/server/sources/datafeed.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/sources/datafeed.py Mon Jan 13 13:47:47 2014 +0100 @@ -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.source.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.source.http_timeout) except urllib2.HTTPError as ex: if ex.code == 404: return True diff -r aff75b69db92 -r 2c48c091b6a2 server/sources/extlite.py --- a/server/sources/extlite.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/sources/extlite.py Mon Jan 13 13:47:47 2014 +0100 @@ -295,7 +295,7 @@ query, args, ex.args[0]) try: session.cnxset.connection(self.uri).rollback() - self.critical('transaction has been rollbacked') + self.critical('transaction has been rolled back') except Exception: pass raise diff -r aff75b69db92 -r 2c48c091b6a2 server/sources/native.py --- a/server/sources/native.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/sources/native.py Mon Jan 13 13:47:47 2014 +0100 @@ -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: @@ -751,28 +751,23 @@ try: session.cnxset.connection(self.uri).rollback() if self.repo.config.mode != 'test': - self.critical('transaction has been rollbacked') + self.critical('transaction has been rolled back') except Exception as ex: pass 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, sqlserver + mo = re.search("unique_[a-z0-9]{32}", 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:] - etype = elements[0] - rtypes = elements[1:] - raise UniqueTogetherError(etype, rtypes) + raise UniqueTogetherError(session, cstrname=mo.group(0)) + # 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 rtypes = [c.strip()[3:] for c in mo.group(1).split(',')] - etype = '???' - raise UniqueTogetherError(etype, rtypes) + raise UniqueTogetherError(session, rtypes=rtypes) raise return cursor @@ -795,7 +790,7 @@ try: session.cnxset.connection(self.uri).rollback() if self.repo.config.mode != 'test': - self.critical('transaction has been rollbacked') + self.critical('transaction has been rolled back') except Exception: pass raise @@ -1537,6 +1532,7 @@ tx_time %s NOT NULL );; CREATE INDEX transactions_tx_user_idx ON transactions(tx_user);; +CREATE INDEX transactions_tx_time_idx ON transactions(tx_time);; CREATE TABLE tx_entity_actions ( tx_uuid CHAR(32) REFERENCES transactions(tx_uuid) ON DELETE CASCADE, @@ -1551,6 +1547,7 @@ CREATE INDEX tx_entity_actions_txa_public_idx ON tx_entity_actions(txa_public);; CREATE INDEX tx_entity_actions_eid_idx ON tx_entity_actions(eid);; CREATE INDEX tx_entity_actions_etype_idx ON tx_entity_actions(etype);; +CREATE INDEX tx_entity_actions_tx_uuid_idx ON tx_entity_actions(tx_uuid);; CREATE TABLE tx_relation_actions ( tx_uuid CHAR(32) REFERENCES transactions(tx_uuid) ON DELETE CASCADE, @@ -1565,6 +1562,7 @@ CREATE INDEX tx_relation_actions_txa_public_idx ON tx_relation_actions(txa_public);; CREATE INDEX tx_relation_actions_eid_from_idx ON tx_relation_actions(eid_from);; CREATE INDEX tx_relation_actions_eid_to_idx ON tx_relation_actions(eid_to);; +CREATE INDEX tx_relation_actions_tx_uuid_idx ON tx_relation_actions(tx_uuid);; """ % (helper.sql_create_sequence('entities_id_seq').replace(';', ';;'), typemap['Datetime'], typemap['Datetime'], typemap['Datetime'], typemap['Boolean'], typemap['Bytes'], typemap['Boolean']) diff -r aff75b69db92 -r 2c48c091b6a2 server/sources/pyrorql.py --- a/server/sources/pyrorql.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/sources/pyrorql.py Mon Jan 13 13:47:47 2014 +0100 @@ -20,6 +20,11 @@ __docformat__ = "restructuredtext en" _ = unicode +# module is lazily imported +import warnings +warnings.warn('Imminent drop of pyrorql source. Switch to datafeed now!', + DeprecationWarning) + import threading from Pyro.errors import PyroError, ConnectionClosedError diff -r aff75b69db92 -r 2c48c091b6a2 server/sources/rql2sql.py --- a/server/sources/rql2sql.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/sources/rql2sql.py Mon Jan 13 13:47:47 2014 +0100 @@ -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 @@ -242,12 +242,6 @@ rhsconst = None # ColumnAlias return lhs, lhsconst, rhs, rhsconst -def switch_relation_field(sql, table=''): - switchedsql = sql.replace(table + '.eid_from', '__eid_from__') - switchedsql = switchedsql.replace(table + '.eid_to', - table + '.eid_from') - return switchedsql.replace('__eid_from__', table + '.eid_to') - def sort_term_selection(sorts, rqlst, groups): # XXX beurk if isinstance(rqlst, list): @@ -486,13 +480,10 @@ return relation._q_sqltable rid = 'rel_%s%s' % (relation.r_type, self.count) # relation's table is belonging to the root scope if it is the principal - # table of one of it's variable and if that variable belong's to parent + # table of one of its variable and that variable belong's to parent # scope for varref in relation.iget_nodes(VariableRef): var = varref.variable - if isinstance(var, ColumnAlias): - scope = 0 - break # XXX may have a principal without being invariant for this generation, # not sure this is a pb or not if var.stinfo.get('principal') is relation and var.scope is var.stmt: @@ -1135,8 +1126,6 @@ sqls += self._process_relation_term(relation, rid, lhsvar, lhsconst, 'eid_from') sqls += self._process_relation_term(relation, rid, rhsvar, rhsconst, 'eid_to') sql = ' AND '.join(sqls) - if rschema.symmetric: - sql = '(%s OR %s)' % (sql, switch_relation_field(sql)) return sql def _visit_outer_join_relation(self, relation, rschema): @@ -1248,6 +1237,8 @@ except KeyError: if lhsalias is None: lhssql = lhsconst.accept(self) + elif attr == 'eid': + lhssql = lhsvar.accept(self) else: lhssql = '%s.%s%s' % (lhsalias, SQL_PREFIX, attr) condition = '%s=%s' % (lhssql, (rhsconst or rhsvar).accept(self)) diff -r aff75b69db92 -r 2c48c091b6a2 server/sqlutils.py --- a/server/sqlutils.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/sqlutils.py Mon Jan 13 13:47:47 2014 +0100 @@ -41,11 +41,6 @@ SQL_PREFIX = 'cw_' def _run_command(cmd): - """backup/restore command are string w/ lgc < 0.47, lists with earlier versions - """ - if isinstance(cmd, basestring): - print '->', cmd - return subprocess.call(cmd, shell=True) print ' '.join(cmd) return subprocess.call(cmd) @@ -332,12 +327,13 @@ class group_concat(object): def __init__(self): - self.values = [] + self.values = set() def step(self, value): if value is not None: - self.values.append(value) + self.values.add(value) def finalize(self): - return ', '.join(self.values) + return ', '.join(unicode(v) for v in self.values) + cnx.create_aggregate("GROUP_CONCAT", 1, group_concat) def _limit_size(text, maxsize, format='text/plain'): @@ -378,7 +374,7 @@ def init_postgres_connexion(cnx): cnx.cursor().execute('SET TIME ZONE UTC') # commit is needed, else setting are lost if the connection is first - # rollbacked + # rolled back cnx.commit() postgres_hooks = SQL_CONNECT_HOOKS.setdefault('postgres', []) diff -r aff75b69db92 -r 2c48c091b6a2 server/ssplanner.py --- a/server/ssplanner.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/ssplanner.py Mon Jan 13 13:47:47 2014 +0100 @@ -101,23 +101,16 @@ Return None when no query actually needed, else the given select node that will be used as substep query. - - When select has nothing selected, search in origrqlst for restriction that - should be considered. """ if origrqlst.where is not None and not select.selection: # no selection, append one randomly by searching for a relation which is - # neither a type restriction (is) nor an eid specification (not neged - # eid with constant node) + # not neged neither a type restriction (is/is_instance_of) for rel in origrqlst.where.iget_nodes(Relation): - if rel.neged(strict=True) or not ( - rel.is_types_restriction() or - (rel.r_type == 'eid' - and isinstance(rel.get_variable_parts()[1], Constant))): + if not (rel.neged(traverse_scope=True) or rel.is_types_restriction()): select.append_selected(rel.children[0].copy(select)) break else: - return + return None if select.selection: if origrqlst.where is not None: select.set_where(origrqlst.where.copy(select)) diff -r aff75b69db92 -r 2c48c091b6a2 server/test/data/extern_mapping.py --- a/server/test/data/extern_mapping.py Tue Jul 02 17:09:04 2013 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,23 +0,0 @@ -# copyright 2003-2010 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 . -"""mapping file for source used in unittest_multisources.py""" - -support_entities = {'Card': True, 'Affaire': True, 'State': True} -support_relations = {'in_state': True, 'documented_by': True, 'multisource_inlined_rel': True} - -cross_relations = set( ('documented_by',) ) diff -r aff75b69db92 -r 2c48c091b6a2 server/test/data/migratedapp/schema.py --- a/server/test/data/migratedapp/schema.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/test/data/migratedapp/schema.py Mon Jan 13 13:47:47 2014 +0100 @@ -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. @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . """cw.server.migraction test""" +import datetime as dt from yams.buildobjs import (EntityType, RelationType, RelationDefinition, SubjectRelation, Bytes, RichString, String, Int, Boolean, Datetime, Date) @@ -38,13 +39,27 @@ concerne = SubjectRelation('Societe') opt_attr = Bytes() -class concerne(RelationType): +class Societe(WorkflowableEntityType): __permissions__ = { - 'read': ('managers', 'users', 'guests'), - 'add': ('managers', RRQLExpression('U has_update_permission S')), - 'delete': ('managers', RRQLExpression('O owned_by U')), + 'read': ('managers', 'users', 'guests'), + 'update': ('managers', 'owners'), + 'delete': ('managers', 'owners'), + 'add': ('managers', 'users',) } + nom = String(maxsize=64, fulltextindexed=True) + web = String(maxsize=128) + tel = Int() + fax = Int() + rncs = String(maxsize=128) + ad1 = String(maxsize=128) + ad2 = String(maxsize=128) + ad3 = String(maxsize=128) + cp = String(maxsize=12) + ville= String(maxsize=32) +# Division and SubDivision are gone + +# New class Para(EntityType): para = String(maxsize=512) newattr = String() @@ -62,43 +77,18 @@ 'PE require_permission P, P name "add_note", ' 'P require_group G'),)} - whatever = Int(default=2) # keep it before `date` for unittest_migraction.test_add_attribute_int + whatever = Int(default=0) # keep it before `date` for unittest_migraction.test_add_attribute_int + yesno = Boolean(default=False) date = Datetime() type = String(maxsize=1) unique_id = String(maxsize=1, required=True, unique=True) mydate = Date(default='TODAY') + oldstyledefaultdate = Date(default='2013/01/01') + newstyledefaultdate = Date(default=dt.date(2013, 1, 1)) shortpara = String(maxsize=64, default='hop') ecrit_par = SubjectRelation('Personne', constraints=[RQLConstraint('S concerne A, O concerne A')]) attachment = SubjectRelation('File') -class Text(Para): - __specializes_schema__ = True - summary = String(maxsize=512) - -class ecrit_par(RelationType): - __permissions__ = {'read': ('managers', 'users', 'guests',), - 'delete': ('managers', ), - 'add': ('managers', - RRQLExpression('O require_permission P, P name "add_note", ' - 'U in_group G, P require_group G'),) - } - inlined = True - cardinality = '?*' - - -class Folder2(EntityType): - """folders are used to classify entities. They may be defined as a tree. - When you include the Folder entity, all application specific entities - may then be classified using the "filed_under" relation. - """ - name = String(required=True, indexed=True, internationalizable=True, - constraints=[UniqueConstraint(), SizeConstraint(64)]) - description = RichString(fulltextindexed=True) - -class filed_under2(RelationDefinition): - subject ='*' - object = 'Folder2' - class Personne(EntityType): __unique_together__ = [('nom', 'prenom', 'datenaiss')] @@ -120,32 +110,71 @@ concerne2 = SubjectRelation(('Affaire', 'Note'), cardinality='1*') connait = SubjectRelation('Personne', symmetric=True) +class concerne(RelationType): + __permissions__ = { + 'read': ('managers', 'users', 'guests'), + 'add': ('managers', RRQLExpression('U has_update_permission S')), + 'delete': ('managers', RRQLExpression('O owned_by U')), + } +# `Old` entity type is gonce +# `comments` is gone +# `fiche` is gone +# `multisource_*` rdefs are gone +# `see_also_*` rdefs are gone + +class evaluee(RelationDefinition): + subject = ('Personne', 'CWUser', 'Societe') + object = ('Note') + +class ecrit_par(RelationType): + __permissions__ = {'read': ('managers', 'users', 'guests',), + 'delete': ('managers', ), + 'add': ('managers', + RRQLExpression('O require_permission P, P name "add_note", ' + 'U in_group G, P require_group G'),) + } + inlined = True + cardinality = '?*' + +# `copain` rdef is gone +# `tags` rdef is gone +# `filed_under` rdef is gone +# `require_permission` rdef is gone +# `require_state` rdef is gone +# `personne_composite` rdef is gone +# `personne_inlined` rdef is gone +# `login_user` rdef is gone +# `ambiguous_inlined` rdef is gone + +# New +class Text(Para): + __specializes_schema__ = True + summary = String(maxsize=512) + + +# New +class Folder2(EntityType): + """folders are used to classify entities. They may be defined as a tree. + When you include the Folder entity, all application specific entities + may then be classified using the "filed_under" relation. + """ + name = String(required=True, indexed=True, internationalizable=True, + constraints=[UniqueConstraint(), SizeConstraint(64)]) + description = RichString(fulltextindexed=True) + +# New +class filed_under2(RelationDefinition): + subject ='*' + object = 'Folder2' + + +# New class New(EntityType): new_name = String() -class Societe(WorkflowableEntityType): - __permissions__ = { - 'read': ('managers', 'users', 'guests'), - 'update': ('managers', 'owners'), - 'delete': ('managers', 'owners'), - 'add': ('managers', 'users',) - } - nom = String(maxsize=64, fulltextindexed=True) - web = String(maxsize=128) - tel = Int() - fax = Int() - rncs = String(maxsize=128) - ad1 = String(maxsize=128) - ad2 = String(maxsize=128) - ad3 = String(maxsize=128) - cp = String(maxsize=12) - ville= String(maxsize=32) - +# New class same_as(RelationDefinition): subject = ('Societe',) object = 'ExternalUri' -class evaluee(RelationDefinition): - subject = ('Personne', 'CWUser', 'Societe') - object = ('Note') diff -r aff75b69db92 -r 2c48c091b6a2 server/test/data/schema.py --- a/server/test/data/schema.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/test/data/schema.py Mon Jan 13 13:47:47 2014 +0100 @@ -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. @@ -94,7 +94,12 @@ 'read': ('managers', 'users', 'guests'), 'update': ('managers', ERQLExpression('X in_state S, S name "todo"')), }) - + something = String(maxsize=1, + __permissions__ = { + 'read': ('managers', 'users', 'guests'), + 'add': (ERQLExpression('NOT X para NULL'),), + 'update': ('managers', 'owners') + }) migrated_from = SubjectRelation('Note') attachment = SubjectRelation('File') inline1 = SubjectRelation('Affaire', inlined=True, cardinality='?*', @@ -119,6 +124,7 @@ tzdatenaiss = TZDatetime() test = Boolean(__permissions__={ 'read': ('managers', 'users', 'guests'), + 'add': ('managers',), 'update': ('managers',), }) description = String() diff -r aff75b69db92 -r 2c48c091b6a2 server/test/unittest_ldapsource.py --- a/server/test/unittest_ldapsource.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/test/unittest_ldapsource.py Mon Jan 13 13:47:47 2014 +0100 @@ -142,16 +142,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 @@ -328,9 +338,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], @@ -429,8 +453,5 @@ - - - if __name__ == '__main__': unittest_main() diff -r aff75b69db92 -r 2c48c091b6a2 server/test/unittest_migractions.py --- a/server/test/unittest_migractions.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/test/unittest_migractions.py Mon Jan 13 13:47:47 2014 +0100 @@ -71,6 +71,22 @@ CubicWebTC.tearDown(self) self.repo.vreg['etypes'].clear_caches() + def test_add_attribute_bool(self): + self.assertFalse('yesno' in self.schema) + self.session.create_entity('Note') + self.commit() + self.mh.cmd_add_attribute('Note', 'yesno') + self.assertTrue('yesno' in self.schema) + self.assertEqual(self.schema['yesno'].subjects(), ('Note',)) + self.assertEqual(self.schema['yesno'].objects(), ('Boolean',)) + self.assertEqual(self.schema['Note'].default('yesno'), False) + # test default value set on existing entities + note = self.session.execute('Note X').get_entity(0, 0) + self.assertEqual(note.yesno, False) + # test default value set for next entities + self.assertEqual(self.session.create_entity('Note').yesno, False) + self.mh.rollback() + def test_add_attribute_int(self): self.assertFalse('whatever' in self.schema) self.session.create_entity('Note') @@ -81,12 +97,13 @@ self.assertTrue('whatever' in self.schema) self.assertEqual(self.schema['whatever'].subjects(), ('Note',)) self.assertEqual(self.schema['whatever'].objects(), ('Int',)) - self.assertEqual(self.schema['Note'].default('whatever'), 2) + self.assertEqual(self.schema['Note'].default('whatever'), 0) # test default value set on existing entities note = self.session.execute('Note X').get_entity(0, 0) - self.assertEqual(note.whatever, 2) + self.assertIsInstance(note.whatever, int) + self.assertEqual(note.whatever, 0) # test default value set for next entities - self.assertEqual(self.session.create_entity('Note').whatever, 2) + self.assertEqual(self.session.create_entity('Note').whatever, 0) # test attribute order orderdict2 = dict(self.mh.rqlexec('Any RTN, O WHERE X name "Note", RDEF from_entity X, ' 'RDEF relation_type RT, RDEF ordernum O, RT name RTN')) @@ -126,9 +143,14 @@ def test_add_datetime_with_default_value_attribute(self): self.assertFalse('mydate' in self.schema) - self.assertFalse('shortpara' in self.schema) + self.assertFalse('oldstyledefaultdate' in self.schema) + self.assertFalse('newstyledefaultdate' in self.schema) self.mh.cmd_add_attribute('Note', 'mydate') + self.mh.cmd_add_attribute('Note', 'oldstyledefaultdate') + self.mh.cmd_add_attribute('Note', 'newstyledefaultdate') self.assertTrue('mydate' in self.schema) + self.assertTrue('oldstyledefaultdate' in self.schema) + self.assertTrue('newstyledefaultdate' in self.schema) self.assertEqual(self.schema['mydate'].subjects(), ('Note', )) self.assertEqual(self.schema['mydate'].objects(), ('Date', )) testdate = date(2005, 12, 13) @@ -136,8 +158,13 @@ eid2 = self.mh.rqlexec('INSERT Note N: N mydate %(mydate)s', {'mydate' : testdate})[0][0] d1 = self.mh.rqlexec('Any D WHERE X eid %(x)s, X mydate D', {'x': eid1})[0][0] d2 = self.mh.rqlexec('Any D WHERE X eid %(x)s, X mydate D', {'x': eid2})[0][0] + d3 = self.mh.rqlexec('Any D WHERE X eid %(x)s, X oldstyledefaultdate D', {'x': eid1})[0][0] + d4 = self.mh.rqlexec('Any D WHERE X eid %(x)s, X newstyledefaultdate D', {'x': eid1})[0][0] self.assertEqual(d1, date.today()) self.assertEqual(d2, testdate) + myfavoritedate = date(2013, 1, 1) + self.assertEqual(d3, myfavoritedate) + self.assertEqual(d4, myfavoritedate) self.mh.rollback() def test_drop_chosen_constraints_ctxmanager(self): @@ -369,14 +396,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 @@ -388,8 +415,8 @@ self.assertEqual(eexpr.reverse_read_permission, ()) self.assertEqual(eexpr.reverse_delete_permission, ()) self.assertEqual(eexpr.reverse_update_permission, ()) - # no more rqlexpr to delete and add para attribute - self.assertFalse(self._rrqlexpr_rset('add', 'para')) + self.assertTrue(self._rrqlexpr_rset('add', 'para')) + # no rqlexpr to delete para attribute self.assertFalse(self._rrqlexpr_rset('delete', 'para')) # new rql expr to add ecrit_par relation rexpr = self._rrqlexpr_entity('add', 'ecrit_par') @@ -417,28 +444,33 @@ self.assertEqual(len(self._rrqlexpr_rset('delete', 'concerne')), len(delete_concerne_rqlexpr)) self.assertEqual(len(self._rrqlexpr_rset('add', 'concerne')), len(add_concerne_rqlexpr)) # * migrschema involve: - # * 7 rqlexprs deletion (2 in (Affaire read + Societe + travaille) + 1 - # in para attribute) + # * 7 erqlexprs deletions (2 in (Affaire + Societe + Note.para) + 1 Note.something + # * 2 rrqlexprs deletions (travaille) # * 1 update (Affaire update) # * 2 new (Note add, ecrit_par add) - # * 2 implicit new for attributes update_permission (Note.para, Personne.test) + # * 2 implicit new for attributes (Note.para, Person.test) # remaining orphan rql expr which should be deleted at commit (composite relation) - self.assertEqual(cursor.execute('Any COUNT(X) WHERE X is RQLExpression, ' - 'NOT ET1 read_permission X, NOT ET2 add_permission X, ' - 'NOT ET3 delete_permission X, NOT ET4 update_permission X')[0][0], - 7+1) + # unattached expressions -> pending deletion on commit + self.assertEqual(cursor.execute('Any COUNT(X) WHERE X is RQLExpression, X exprtype "ERQLExpression",' + 'NOT ET1 read_permission X, NOT ET2 add_permission X, ' + 'NOT ET3 delete_permission X, NOT ET4 update_permission X')[0][0], + 7) + self.assertEqual(cursor.execute('Any COUNT(X) WHERE X is RQLExpression, X exprtype "RRQLExpression",' + 'NOT ET1 read_permission X, NOT ET2 add_permission X, ' + 'NOT ET3 delete_permission X, NOT ET4 update_permission X')[0][0], + 2) # finally self.assertEqual(cursor.execute('Any COUNT(X) WHERE X is RQLExpression')[0][0], - nbrqlexpr_start + 1 + 2 + 2) + nbrqlexpr_start + 1 + 2 + 2 + 2) self.mh.commit() # unique_together test self.assertEqual(len(self.schema.eschema('Personne')._unique_together), 1) - self.assertItemsEqual(self.schema.eschema('Personne')._unique_together[0], + self.assertCountEqual(self.schema.eschema('Personne')._unique_together[0], ('nom', 'prenom', 'datenaiss')) rset = cursor.execute('Any C WHERE C is CWUniqueTogetherConstraint, C constraint_of ET, ET name "Personne"') self.assertEqual(len(rset), 1) relations = [r.name for r in rset.get_entity(0, 0).relations] - self.assertItemsEqual(relations, ('nom', 'prenom', 'datenaiss')) + self.assertCountEqual(relations, ('nom', 'prenom', 'datenaiss')) def _erqlexpr_rset(self, action, ertype): rql = 'RQLExpression X WHERE ET is CWEType, ET %s_permission X, ET name %%(name)s' % action diff -r aff75b69db92 -r 2c48c091b6a2 server/test/unittest_querier.py --- a/server/test/unittest_querier.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/test/unittest_querier.py Mon Jan 13 13:47:47 2014 +0100 @@ -1,5 +1,5 @@ # -*- coding: iso-8859-1 -*- -# 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. @@ -383,9 +383,9 @@ self.execute("INSERT Personne X: X nom 'bidule'")[0] rset = self.execute('Any Y where X name TMP, Y nom in (TMP, "bidule")') #self.assertEqual(rset.description, [('Personne',), ('Personne',)]) - self.assert_(('Personne',) in rset.description) + self.assertIn(('Personne',), rset.description) rset = self.execute('DISTINCT Any Y where X name TMP, Y nom in (TMP, "bidule")') - self.assert_(('Personne',) in rset.description) + self.assertIn(('Personne',), rset.description) def test_select_not_attr(self): peid = self.execute("INSERT Personne X: X nom 'bidule'")[0][0] @@ -466,8 +466,8 @@ self.execute("SET X tags Y WHERE X eid %(t)s, Y eid %(g)s", {'g': geid, 't': teid}) rset = self.execute("Any GN,TN ORDERBY GN WHERE T? tags G, T name TN, G name GN") - self.assertTrue(['users', 'tag'] in rset.rows) - self.assertTrue(['activated', None] in rset.rows) + self.assertIn(['users', 'tag'], rset.rows) + self.assertIn(['activated', None], rset.rows) rset = self.execute("Any GN,TN ORDERBY GN WHERE T tags G?, T name TN, G name GN") self.assertEqual(rset.rows, [[None, 'tagbis'], ['users', 'tag']]) @@ -576,7 +576,7 @@ self.assertListEqual(rset.rows, [[u'description_format', 12], [u'description', 13], - [u'name', 16], + [u'name', 17], [u'created_by', 43], [u'creation_date', 43], [u'cw_source', 43], @@ -818,11 +818,11 @@ self.execute("INSERT Tag X: X name 'bidule', X creation_date NOW") self.execute("INSERT Tag Y: Y name 'toto'") rset = self.execute("Any D WHERE X name in ('bidule', 'toto') , X creation_date D") - self.assert_(isinstance(rset.rows[0][0], datetime), rset.rows) + self.assertIsInstance(rset.rows[0][0], datetime) rset = self.execute('Tag X WHERE X creation_date TODAY') self.assertEqual(len(rset.rows), 2) rset = self.execute('Any MAX(D) WHERE X is Tag, X creation_date D') - self.assertTrue(isinstance(rset[0][0], datetime), (rset[0][0], type(rset[0][0]))) + self.assertIsInstance(rset[0][0], datetime) def test_today(self): self.execute("INSERT Tag X: X name 'bidule', X creation_date TODAY") @@ -1268,12 +1268,21 @@ newname = self.execute('Any XN WHERE X eid %(x)s, X title XN', {'x': beid})[0][0] self.assertEqual(newname, 'toto-moved') + def test_update_not_exists(self): + rset = self.execute("INSERT Personne X, Societe Y: X nom 'bidule', Y nom 'toto'") + eid1, eid2 = rset[0][0], rset[0][1] + rset = self.execute("SET X travaille Y WHERE X eid %(x)s, Y eid %(y)s, " + "NOT EXISTS(Z ecrit_par X)", + {'x': unicode(eid1), 'y': unicode(eid2)}) + self.assertEqual(tuplify(rset.rows), [(eid1, eid2)]) + def test_update_query_error(self): self.execute("INSERT Personne Y: Y nom 'toto'") self.assertRaises(Exception, self.execute, "SET X nom 'toto', X is Personne") self.assertRaises(QueryError, self.execute, "SET X nom 'toto', X has_text 'tutu' WHERE X is Personne") self.assertRaises(QueryError, self.execute, "SET X login 'tutu', X eid %s" % cnx.user(self.session).eid) + # HAVING on write queries test ############################################# def test_update_having(self): diff -r aff75b69db92 -r 2c48c091b6a2 server/test/unittest_repository.py --- a/server/test/unittest_repository.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/test/unittest_repository.py Mon Jan 13 13:47:47 2014 +0100 @@ -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], @@ -135,7 +137,7 @@ self.assertTrue(self.execute('Any X WHERE X is CWGroup, X name "toto"')) with self.assertRaises(QueryError) as cm: self.commit() - self.assertEqual(str(cm.exception), 'transaction must be rollbacked') + self.assertEqual(str(cm.exception), 'transaction must be rolled back') self.rollback() self.assertFalse(self.execute('Any X WHERE X is CWGroup, X name "toto"')) @@ -152,7 +154,7 @@ self.assertTrue(self.execute('Any X WHERE X is CWGroup, X name "toto"')) with self.assertRaises(QueryError) as cm: self.commit() - self.assertEqual(str(cm.exception), 'transaction must be rollbacked') + self.assertEqual(str(cm.exception), 'transaction must be rolled back') self.rollback() self.assertFalse(self.execute('Any X WHERE X is CWGroup, X name "toto"')) @@ -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', + 'add_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') @@ -554,6 +558,30 @@ req.create_entity('Affaire', ref=u'AFF02') req.execute('SET A duration 10 WHERE A is Affaire') + + def test_user_friendly_error(self): + from cubicweb.entities.adapters import IUserFriendlyUniqueTogether + class MyIUserFriendlyUniqueTogether(IUserFriendlyUniqueTogether): + __select__ = IUserFriendlyUniqueTogether.__select__ & is_instance('Societe') + def raise_user_exception(self): + raise ValidationError(self.entity.eid, {'hip': 'hop'}) + + with self.temporary_appobjects(MyIUserFriendlyUniqueTogether): + req = self.request() + s = req.create_entity('Societe', nom=u'Logilab', type=u'ssll', cp=u'75013') + self.commit() + with self.assertRaises(ValidationError) as cm: + req.create_entity('Societe', nom=u'Logilab', type=u'ssll', cp=u'75013') + self.assertEqual(cm.exception.errors, {'hip': 'hop'}) + self.rollback() + req.create_entity('Societe', nom=u'Logilab', type=u'ssll', cp=u'31400') + with self.assertRaises(ValidationError) as cm: + s.cw_set(cp=u'31400') + self.assertEqual(cm.exception.entity, s.eid) + self.assertEqual(cm.exception.errors, {'hip': 'hop'}) + self.rollback() + + class SchemaDeserialTC(CubicWebTC): appid = 'data-schemaserial' diff -r aff75b69db92 -r 2c48c091b6a2 server/test/unittest_rql2sql.py --- a/server/test/unittest_rql2sql.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/test/unittest_rql2sql.py Mon Jan 13 13:47:47 2014 +0100 @@ -1070,68 +1070,6 @@ FROM cw_Personne AS _P'''), ] -SYMMETRIC = [ - ('Any P WHERE X eid 0, X connait P', - '''SELECT DISTINCT _P.cw_eid -FROM connait_relation AS rel_connait0, cw_Personne AS _P -WHERE (rel_connait0.eid_from=0 AND rel_connait0.eid_to=_P.cw_eid OR rel_connait0.eid_to=0 AND rel_connait0.eid_from=_P.cw_eid)''' - ), - - ('Any P WHERE X connait P', - '''SELECT DISTINCT _P.cw_eid -FROM connait_relation AS rel_connait0, cw_Personne AS _P -WHERE (rel_connait0.eid_to=_P.cw_eid OR rel_connait0.eid_from=_P.cw_eid)''' - ), - - ('Any X WHERE X connait P', - '''SELECT DISTINCT _X.cw_eid -FROM connait_relation AS rel_connait0, cw_Personne AS _X -WHERE (rel_connait0.eid_from=_X.cw_eid OR rel_connait0.eid_to=_X.cw_eid)''' - ), - - ('Any P WHERE X eid 0, NOT X connait P', - '''SELECT _P.cw_eid -FROM cw_Personne AS _P -WHERE NOT (EXISTS(SELECT 1 FROM connait_relation AS rel_connait0 WHERE (rel_connait0.eid_from=0 AND rel_connait0.eid_to=_P.cw_eid OR rel_connait0.eid_to=0 AND rel_connait0.eid_from=_P.cw_eid)))'''), - - ('Any P WHERE NOT X connait P', - '''SELECT _P.cw_eid -FROM cw_Personne AS _P -WHERE NOT (EXISTS(SELECT 1 FROM connait_relation AS rel_connait0 WHERE (rel_connait0.eid_to=_P.cw_eid OR rel_connait0.eid_from=_P.cw_eid)))'''), - - ('Any X WHERE NOT X connait P', - '''SELECT _X.cw_eid -FROM cw_Personne AS _X -WHERE NOT (EXISTS(SELECT 1 FROM connait_relation AS rel_connait0 WHERE (rel_connait0.eid_from=_X.cw_eid OR rel_connait0.eid_to=_X.cw_eid)))'''), - - ('Any P WHERE X connait P, P nom "nom"', - '''SELECT DISTINCT _P.cw_eid -FROM connait_relation AS rel_connait0, cw_Personne AS _P -WHERE (rel_connait0.eid_to=_P.cw_eid OR rel_connait0.eid_from=_P.cw_eid) AND _P.cw_nom=nom'''), - - ('Any X WHERE X connait P, P nom "nom"', - '''SELECT DISTINCT _X.cw_eid -FROM connait_relation AS rel_connait0, cw_Personne AS _P, cw_Personne AS _X -WHERE (rel_connait0.eid_from=_X.cw_eid AND rel_connait0.eid_to=_P.cw_eid OR rel_connait0.eid_to=_X.cw_eid AND rel_connait0.eid_from=_P.cw_eid) AND _P.cw_nom=nom''' - ), - - ('DISTINCT Any P WHERE P connait S OR S connait P, S nom "chouette"', - '''SELECT DISTINCT _P.cw_eid -FROM connait_relation AS rel_connait0, cw_Personne AS _P, cw_Personne AS _S -WHERE (rel_connait0.eid_from=_P.cw_eid AND rel_connait0.eid_to=_S.cw_eid OR rel_connait0.eid_to=_P.cw_eid AND rel_connait0.eid_from=_S.cw_eid) AND _S.cw_nom=chouette''' - ) - ] - -SYMMETRIC_WITH_LIMIT = [ - ('Any X ORDERBY X DESC LIMIT 9 WHERE E eid 0, E connait X', - '''SELECT DISTINCT _X.cw_eid -FROM connait_relation AS rel_connait0, cw_Personne AS _X -WHERE (rel_connait0.eid_from=0 AND rel_connait0.eid_to=_X.cw_eid OR rel_connait0.eid_to=0 AND rel_connait0.eid_from=_X.cw_eid) -ORDER BY 1 DESC -LIMIT 9''' - ), -] - INLINE = [ ('Any P WHERE N eid 1, N ecrit_par P, NOT P owned_by P2', @@ -1478,6 +1416,20 @@ def test_subquery(self): for t in self._parse(( + ('Any X,N ' + 'WHERE NOT EXISTS(X owned_by U) ' + 'WITH X,N BEING ' + '((Any X,N WHERE X name N, X is State)' + ' UNION ' + '(Any XX,NN WHERE XX name NN, XX is Transition))', + '''SELECT _T0.C0, _T0.C1 +FROM ((SELECT _X.cw_eid AS C0, _X.cw_name AS C1 +FROM cw_State AS _X) +UNION ALL +(SELECT _XX.cw_eid AS C0, _XX.cw_name AS C1 +FROM cw_Transition AS _XX)) AS _T0 +WHERE NOT (EXISTS(SELECT 1 FROM owned_by_relation AS rel_owned_by0 WHERE rel_owned_by0.eid_from=_T0.C0))'''), + ('Any N ORDERBY 1 WITH N BEING ' '((Any N WHERE X name N, X is State)' ' UNION ' @@ -1541,7 +1493,18 @@ FROM (SELECT MAX(_A.cw_ordernum) AS C0 FROM cw_CWAttribute AS _A) AS _T0 LEFT OUTER JOIN (SELECT MAX(_A.cw_ordernum) AS C0 FROM cw_CWRelation AS _A) AS _T1 ON (_T0.C0=_T1.C0)'''), - )): + + ('''Any TT1,STD,STDD WHERE TT2 identity TT1? + WITH TT1,STDD BEING (Any T,SUM(TD) GROUPBY T WHERE T is Affaire, T duration TD, TAG? tags T, TAG name "t"), + TT2,STD BEING (Any T,SUM(TD) GROUPBY T WHERE T is Affaire, T duration TD)''', + '''SELECT _T0.C0, _T1.C1, _T0.C1 +FROM (SELECT _T.cw_eid AS C0, SUM(_T.cw_duration) AS C1 +FROM cw_Affaire AS _T +GROUP BY _T.cw_eid) AS _T1 LEFT OUTER JOIN (SELECT _T.cw_eid AS C0, SUM(_T.cw_duration) AS C1 +FROM cw_Affaire AS _T LEFT OUTER JOIN tags_relation AS rel_tags0 ON (rel_tags0.eid_to=_T.cw_eid) LEFT OUTER JOIN cw_Tag AS _TAG ON (rel_tags0.eid_from=_TAG.cw_eid AND _TAG.cw_name=t) +GROUP BY _T.cw_eid) AS _T0 ON (_T1.C0=_T0.C0)'''), + + )): yield t @@ -1553,10 +1516,6 @@ rqlst = self._prepare(rql) self.assertRaises(BadRQLQuery, self.o.generate, rqlst) - def test_symmetric(self): - for t in self._parse(SYMMETRIC + SYMMETRIC_WITH_LIMIT): - yield t - def test_inline(self): for t in self._parse(INLINE): yield t @@ -1781,10 +1740,6 @@ '''SELECT DATEPART(WEEKDAY, _P.cw_creation_date) FROM cw_Personne AS _P''') - def test_symmetric(self): - for t in self._parse(SYMMETRIC): - yield t - def test_basic_parse(self): for t in self._parse(BASIC):# + BASIC_WITH_LIMIT): yield t diff -r aff75b69db92 -r 2c48c091b6a2 server/test/unittest_schemaserial.py --- a/server/test/unittest_schemaserial.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/test/unittest_schemaserial.py Mon Jan 13 13:47:47 2014 +0100 @@ -22,12 +22,14 @@ from logilab.common.testlib import TestCase, unittest_main +from cubicweb import Binary from cubicweb.schema import CubicWebSchemaLoader from cubicweb.devtools import TestServerConfiguration 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 +46,7 @@ def tearDownModule(*args): global schema, config - del schema, config + schema = config = None unregister_base_type('BabarTestType') helper = get_db_helper('sqlite') @@ -63,29 +65,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 +97,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 +114,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', @@ -133,11 +134,15 @@ ('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'*?'}), - ]) + ('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', + {'cardinality': u'**', 'composite': None, 'description': u'groups allowed to add entities/relations of this type', + 'oe': None, 'ordernum': 9999, 'rt': None, 'se': None}), + ('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', + {'cardinality': u'*?', 'composite': u'subject', 'description': u'rql expression allowing to add entities/relations of this type', 'oe': None, 'ordernum': 9999, 'rt': None, 'se': None})], + 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 +160,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,' @@ -189,43 +194,40 @@ self.assertIn('extra_props', got[1][1]) # this extr extra_props = got[1][1]['extra_props'] - from cubicweb import Binary self.assertIsInstance(extra_props, Binary) got[1][1]['extra_props'] = got[1][1]['extra_props'].getvalue() 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'}), + 'description': u'', 'internationalizable': True, 'fulltextindexed': False, + 'ordernum': 3, 'defaultval': Binary('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 +237,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 +248,34 @@ } 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 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})], + [(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 aff75b69db92 -r 2c48c091b6a2 server/test/unittest_security.py --- a/server/test/unittest_security.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/test/unittest_security.py Mon Jan 13 13:47:47 2014 +0100 @@ -413,6 +413,16 @@ self.commit() cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid}) self.commit() + cu.execute("INSERT Note X: X something 'A'") + self.assertRaises(Unauthorized, self.commit) + cu.execute("INSERT Note X: X para 'zogzog', X something 'A'") + self.commit() + note = cu.execute("INSERT Note X").get_entity(0,0) + self.commit() + note.cw_set(something=u'B') + self.commit() + note.cw_set(something=None, para=u'zogzog') + self.commit() def test_attribute_read_security(self): # anon not allowed to see users'login, but they can see users diff -r aff75b69db92 -r 2c48c091b6a2 server/test/unittest_session.py --- a/server/test/unittest_session.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/test/unittest_session.py Mon Jan 13 13:47:47 2014 +0100 @@ -25,6 +25,15 @@ self.assertFalse(session.running_dbapi_query) session.close() + def test_integrity_hooks(self): + with self.repo.internal_session() as session: + self.assertEqual(HOOKS_ALLOW_ALL, session.hooks_mode) + self.assertEqual(set(('integrity',)), session.disabled_hook_categories) + self.assertEqual(set(), session.enabled_hook_categories) + session.commit() + self.assertEqual(HOOKS_ALLOW_ALL, session.hooks_mode) + self.assertEqual(set(('integrity',)), session.disabled_hook_categories) + self.assertEqual(set(), session.enabled_hook_categories) class SessionTC(CubicWebTC): diff -r aff75b69db92 -r 2c48c091b6a2 server/test/unittest_sqlutils.py --- a/server/test/unittest_sqlutils.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/test/unittest_sqlutils.py Mon Jan 13 13:47:47 2014 +0100 @@ -1,4 +1,5 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# -*- coding: utf-8 -*- +# 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,6 +25,8 @@ from cubicweb.server.sqlutils import * +from cubicweb.devtools.testlib import CubicWebTC + BASE_CONFIG = { 'db-driver' : 'Postgres', 'db-host' : 'crater', @@ -44,5 +47,22 @@ o = SQLAdapterMixIn(config) self.assertEqual(o.dbhelper.dbencoding, 'ISO-8859-1') + +class SQLUtilsTC(CubicWebTC): + + def test_group_concat(self): + req = self.request() + g = req.create_entity('CWGroup', name=u'héhé') + u = req.create_entity('CWUser', login=u'toto', upassword=u'', + in_group=g.eid) + rset = self.execute(u'Any L,GROUP_CONCAT(G) GROUPBY L WHERE X login L,' + u'X in_group G, G name GN, NOT G name IN ("users", "héhé")') + self.assertEqual([[u'admin', u'3'], [u'anon', u'2']], + rset.rows) + rset = self.execute('Any L,GROUP_CONCAT(GN) GROUPBY L WHERE X login L,' + 'X in_group G, G name GN, NOT G name "users"') + self.assertEqual([[u'admin', u'managers'], [u'anon', u'guests'], [u'toto', u'héhé']], + rset.rows) + if __name__ == '__main__': unittest_main() diff -r aff75b69db92 -r 2c48c091b6a2 server/test/unittest_storage.py --- a/server/test/unittest_storage.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/test/unittest_storage.py Mon Jan 13 13:47:47 2014 +0100 @@ -237,7 +237,7 @@ self.assertEqual(osp.splitext(new_path)[1], '.jpg') @tag('update', 'extension', 'rollback') - def test_bfss_update_with_different_extension_rollbacked(self): + def test_bfss_update_with_different_extension_rolled_back(self): # use self.session to use server-side cache f1 = self.session.create_entity('File', data=Binary('some data'), data_format=u'text/plain', data_name=u'foo.txt') diff -r aff75b69db92 -r 2c48c091b6a2 server/utils.py --- a/server/utils.py Tue Jul 02 17:09:04 2013 +0200 +++ b/server/utils.py Mon Jan 13 13:47:47 2014 +0100 @@ -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 aff75b69db92 -r 2c48c091b6a2 setup.py --- a/setup.py Tue Jul 02 17:09:04 2013 +0200 +++ b/setup.py Mon Jan 13 13:47:47 2014 +0100 @@ -63,7 +63,7 @@ ext_modules = getattr(__pkginfo__, 'ext_modules', None) package_data = getattr(__pkginfo__, 'package_data', {}) -BASE_BLACKLIST = ('CVS', 'debian', 'dist', 'build', '__buildlog') +BASE_BLACKLIST = ('CVS', 'dist', 'build', '__buildlog') IGNORED_EXTENSIONS = ('.pyc', '.pyo', '.elc') diff -r aff75b69db92 -r 2c48c091b6a2 skeleton/debian/control.tmpl --- a/skeleton/debian/control.tmpl Tue Jul 02 17:09:04 2013 +0200 +++ b/skeleton/debian/control.tmpl Mon Jan 13 13:47:47 2014 +0100 @@ -8,7 +8,10 @@ Package: %(distname)s Architecture: all -Depends: cubicweb-common (>= %(version)s), ${python:Depends} +Depends: + cubicweb-common (>= %(version)s), + ${python:Depends}, + ${misc:Depends}, Description: %(shortdesc)s CubicWeb is a semantic web application framework. . diff -r aff75b69db92 -r 2c48c091b6a2 skeleton/debian/rules.tmpl --- a/skeleton/debian/rules.tmpl Tue Jul 02 17:09:04 2013 +0200 +++ b/skeleton/debian/rules.tmpl Mon Jan 13 13:47:47 2014 +0100 @@ -15,10 +15,9 @@ clean: dh_testdir - dh_testroot rm -f build-stamp configure-stamp rm -rf build - find . -name "*.pyc" | xargs rm -f + find . -name "*.pyc" -delete dh_clean install: build @@ -38,7 +37,7 @@ dh_install -i dh_installchangelogs -i dh_installexamples -i - dh_installdocs -i + dh_installdocs -i README dh_installman -i dh_pysupport -i /usr/share/cubicweb dh_link -i diff -r aff75b69db92 -r 2c48c091b6a2 skeleton/i18n/en.po --- a/skeleton/i18n/en.po Tue Jul 02 17:09:04 2013 +0200 +++ b/skeleton/i18n/en.po Mon Jan 13 13:47:47 2014 +0100 @@ -5,4 +5,5 @@ "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" diff -r aff75b69db92 -r 2c48c091b6a2 skeleton/i18n/es.po --- a/skeleton/i18n/es.po Tue Jul 02 17:09:04 2013 +0200 +++ b/skeleton/i18n/es.po Mon Jan 13 13:47:47 2014 +0100 @@ -5,4 +5,5 @@ "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" diff -r aff75b69db92 -r 2c48c091b6a2 skeleton/i18n/fr.po --- a/skeleton/i18n/fr.po Tue Jul 02 17:09:04 2013 +0200 +++ b/skeleton/i18n/fr.po Mon Jan 13 13:47:47 2014 +0100 @@ -5,4 +5,5 @@ "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" diff -r aff75b69db92 -r 2c48c091b6a2 sobjects/ldapparser.py --- a/sobjects/ldapparser.py Tue Jul 02 17:09:04 2013 +0200 +++ b/sobjects/ldapparser.py Mon Jan 13 13:47:47 2014 +0100 @@ -71,7 +71,7 @@ return {} def _process(self, etype, sdict): - self.warning('fetched %s %s', etype, sdict) + self.debug('fetched %s %s', etype, sdict) extid = sdict['dn'] entity = self.extid2entity(extid, etype, **sdict) if entity is not None and not self.created_during_pull(entity): @@ -105,7 +105,7 @@ for etype, eids in byetype.iteritems(): if etype != 'CWUser': continue - self.warning('deactivate %s %s entities', len(eids), etype) + self.info('deactivate %s %s entities', len(eids), etype) for eid in eids: wf = session.entity_from_eid(eid).cw_adapt_to('IWorkflowable') wf.fire_transition_if_possible('deactivate') @@ -119,7 +119,7 @@ wf = entity.cw_adapt_to('IWorkflowable') if wf.state == 'deactivated': wf.fire_transition('activate') - self.warning('user %s reactivated', entity.login) + self.info('user %s reactivated', entity.login) mdate = attrs.get('modification_date') if not mdate or mdate > entity.modification_date: attrs = dict( (k, v) for k, v in attrs.iteritems() diff -r aff75b69db92 -r 2c48c091b6a2 sobjects/notification.py --- a/sobjects/notification.py Tue Jul 02 17:09:04 2013 +0200 +++ b/sobjects/notification.py Mon Jan 13 13:47:47 2014 +0100 @@ -128,26 +128,34 @@ # since the same view (eg self) may be called multiple time and we # need a fresh stream at each iteration, reset it explicitly self.w = None - # XXX call render before subject to set .row/.col attributes on the - # view try: - content = self.render(row=0, col=0, **kwargs) - subject = self.subject() - except SkipEmail: - continue - except Exception as ex: - # shouldn't make the whole transaction fail because of rendering - # error (unauthorized or such) XXX check it doesn't actually - # occurs due to rollback on such error - self.exception(str(ex)) - continue - 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 + # XXX call render before subject to set .row/.col attributes on the + # view + try: + content = self.render(row=0, col=0, **kwargs) + subject = self.subject() + except SkipEmail: + continue + except Exception as ex: + # shouldn't make the whole transaction fail because of rendering + # error (unauthorized or such) XXX check it doesn't actually + # occurs due to rollback on such error + self.exception(str(ex)) + continue + msg = format_mail(self.user_data, [emailaddr], content, subject, + config=self._cw.vreg.config, msgid=msgid, references=refs) + yield [emailaddr], msg + except: + if isinstance(something, Entity): + self._cw.rollback() + raise + else: + if isinstance(something, Entity): + self._cw.commit() + finally: + if isinstance(something, Entity): + self._cw.close() + self._cw = req # restore language req.set_language(origlang) @@ -198,7 +206,7 @@ kwargs.update({'user': self.user_data['login'], 'eid': entity.eid, 'etype': entity.dc_type(), - 'url': entity.absolute_url(), + 'url': entity.absolute_url(__secure__=True), 'title': entity.dc_long_title(),}) return kwargs diff -r aff75b69db92 -r 2c48c091b6a2 test/data/entities.py --- a/test/data/entities.py Tue Jul 02 17:09:04 2013 +0200 +++ b/test/data/entities.py Mon Jan 13 13:47:47 2014 +0100 @@ -28,6 +28,9 @@ fetch_attrs, cw_fetch_order = fetch_config(['nom', 'prenom']) rest_attr = 'nom' +class Ami(Societe): + __regid__ = 'Ami' + rest_attr = 'nom' class Note(AnyEntity): __regid__ = 'Note' diff -r aff75b69db92 -r 2c48c091b6a2 test/data/migration/0.1.0_web.py --- a/test/data/migration/0.1.0_web.py Tue Jul 02 17:09:04 2013 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,20 +0,0 @@ -# copyright 2003-2010 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 . -"""web only - -""" diff -r aff75b69db92 -r 2c48c091b6a2 test/data/rewrite/schema.py --- a/test/data/rewrite/schema.py Tue Jul 02 17:09:04 2013 +0200 +++ b/test/data/rewrite/schema.py Mon Jan 13 13:47:47 2014 +0100 @@ -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 aff75b69db92 -r 2c48c091b6a2 test/data/schema.py --- a/test/data/schema.py Tue Jul 02 17:09:04 2013 +0200 +++ b/test/data/schema.py Mon Jan 13 13:47:47 2014 +0100 @@ -41,9 +41,13 @@ constraints=[RQLConstraint('NOT EXISTS(O contrat_exclusif S)')]) dirige = SubjectRelation('Societe', cardinality='??', constraints=[RQLConstraint('S actionnaire O')]) - associe = SubjectRelation('Personne', cardinality='1*', + associe = SubjectRelation('Personne', cardinality='?*', constraints=[RQLConstraint('S actionnaire SOC, O actionnaire SOC')]) +class Ami(EntityType): + """A Person, for which surname is not required""" + prenom = String() + nom = String() class Societe(EntityType): nom = String() diff -r aff75b69db92 -r 2c48c091b6a2 test/unittest_dataimport.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/unittest_dataimport.py Mon Jan 13 13:47:47 2014 +0100 @@ -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 aff75b69db92 -r 2c48c091b6a2 test/unittest_entity.py --- a/test/unittest_entity.py Tue Jul 02 17:09:04 2013 +0200 +++ b/test/unittest_entity.py Mon Jan 13 13:47:47 2014 +0100 @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# 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. @@ -26,6 +26,7 @@ from cubicweb import Binary, Unauthorized from cubicweb.devtools.testlib import CubicWebTC from cubicweb.mttransforms import HAS_TAL +from cubicweb.entity import can_use_rest_path from cubicweb.entities import fetch_config from cubicweb.uilib import soup2xhtml from cubicweb.schema import RQLVocabularyConstraint, RRQLExpression @@ -132,6 +133,12 @@ self.assertEqual(sorted(user._cw_related_cache), ['in_group_subject', 'primary_email_subject']) for group in groups: self.assertFalse('in_group_subject' in group._cw_related_cache, list(group._cw_related_cache)) + user.cw_clear_all_caches() + user.related('in_group', entities=True) + self.assertIn('in_group_subject', user._cw_related_cache) + user.cw_clear_all_caches() + user.related('in_group', targettypes=('CWGroup',), entities=True) + self.assertNotIn('in_group_subject', user._cw_related_cache) def test_related_limit(self): req = self.request() @@ -145,29 +152,41 @@ self.assertEqual(len(p.related('tags', 'object', entities=True, limit=2)), 2) self.assertEqual(len(p.related('tags', 'object', entities=True)), 4) + def test_related_targettypes(self): + req = self.request() + p = req.create_entity('Personne', nom=u'Loxodonta', prenom=u'Babar') + n = req.create_entity('Note', type=u'scratch', ecrit_par=p) + t = req.create_entity('Tag', name=u'a tag', tags=(p, n)) + self.commit() + req = self.request() + t = req.entity_from_eid(t.eid) + self.assertEqual(2, t.related('tags').rowcount) + self.assertEqual(1, t.related('tags', targettypes=('Personne',)).rowcount) + self.assertEqual(1, t.related('tags', targettypes=('Note',)).rowcount) + def test_cw_instantiate_relation(self): req = self.request() p1 = req.create_entity('Personne', nom=u'di') p2 = req.create_entity('Personne', nom=u'mascio') t = req.create_entity('Tag', name=u't0', tags=[]) - self.assertItemsEqual(t.tags, []) + self.assertCountEqual(t.tags, []) t = req.create_entity('Tag', name=u't1', tags=p1) - self.assertItemsEqual(t.tags, [p1]) + self.assertCountEqual(t.tags, [p1]) t = req.create_entity('Tag', name=u't2', tags=p1.eid) - self.assertItemsEqual(t.tags, [p1]) + self.assertCountEqual(t.tags, [p1]) t = req.create_entity('Tag', name=u't3', tags=[p1, p2.eid]) - self.assertItemsEqual(t.tags, [p1, p2]) + self.assertCountEqual(t.tags, [p1, p2]) def test_cw_instantiate_reverse_relation(self): req = self.request() t1 = req.create_entity('Tag', name=u't1') t2 = req.create_entity('Tag', name=u't2') p = req.create_entity('Personne', nom=u'di mascio', reverse_tags=t1) - self.assertItemsEqual(p.reverse_tags, [t1]) + self.assertCountEqual(p.reverse_tags, [t1]) p = req.create_entity('Personne', nom=u'di mascio', reverse_tags=t1.eid) - self.assertItemsEqual(p.reverse_tags, [t1]) + self.assertCountEqual(p.reverse_tags, [t1]) p = req.create_entity('Personne', nom=u'di mascio', reverse_tags=[t1, t2.eid]) - self.assertItemsEqual(p.reverse_tags, [t1, t2]) + self.assertCountEqual(p.reverse_tags, [t1, t2]) def test_fetch_rql(self): user = self.user() @@ -188,43 +207,43 @@ # testing basic fetch_attrs attribute self.assertEqual(Personne.fetch_rql(user), 'Any X,AA,AB,AC ORDERBY AA ' - 'WHERE X is Personne, X nom AA, X prenom AB, X modification_date AC') + 'WHERE X is_instance_of Personne, X nom AA, X prenom AB, X modification_date AC') # testing unknown attributes Personne.fetch_attrs = ('bloug', 'beep') - self.assertEqual(Personne.fetch_rql(user), 'Any X WHERE X is Personne') + self.assertEqual(Personne.fetch_rql(user), 'Any X WHERE X is_instance_of Personne') # testing one non final relation Personne.fetch_attrs = ('nom', 'prenom', 'travaille') self.assertEqual(Personne.fetch_rql(user), 'Any X,AA,AB,AC,AD ORDERBY AA ' - 'WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD') + 'WHERE X is_instance_of Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD') # testing two non final relations Personne.fetch_attrs = ('nom', 'prenom', 'travaille', 'evaluee') self.assertEqual(Personne.fetch_rql(user), 'Any X,AA,AB,AC,AD,AE ORDERBY AA ' - 'WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD, ' + 'WHERE X is_instance_of Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD, ' 'X evaluee AE?') # testing one non final relation with recursion Personne.fetch_attrs = ('nom', 'prenom', 'travaille') Societe.fetch_attrs = ('nom', 'evaluee') self.assertEqual(Personne.fetch_rql(user), 'Any X,AA,AB,AC,AD,AE,AF ORDERBY AA,AF DESC ' - 'WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD, ' + 'WHERE X is_instance_of Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD, ' 'AC evaluee AE?, AE modification_date AF' ) # testing symmetric relation Personne.fetch_attrs = ('nom', 'connait') self.assertEqual(Personne.fetch_rql(user), 'Any X,AA,AB ORDERBY AA ' - 'WHERE X is Personne, X nom AA, X connait AB?') + 'WHERE X is_instance_of Personne, X nom AA, X connait AB?') # testing optional relation peschema.subjrels['travaille'].rdef(peschema, seschema).cardinality = '?*' Personne.fetch_attrs = ('nom', 'prenom', 'travaille') Societe.fetch_attrs = ('nom',) self.assertEqual(Personne.fetch_rql(user), - 'Any X,AA,AB,AC,AD ORDERBY AA WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD') + 'Any X,AA,AB,AC,AD ORDERBY AA WHERE X is_instance_of Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD') # testing relation with cardinality > 1 peschema.subjrels['travaille'].rdef(peschema, seschema).cardinality = '**' self.assertEqual(Personne.fetch_rql(user), - 'Any X,AA,AB ORDERBY AA WHERE X is Personne, X nom AA, X prenom AB') + 'Any X,AA,AB ORDERBY AA WHERE X is_instance_of Personne, X nom AA, X prenom AB') # XXX test unauthorized attribute finally: # fetch_attrs restored by generic tearDown @@ -288,7 +307,7 @@ rql = user.cw_unrelated_rql('use_email', 'EmailAddress', 'subject')[0] self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC ' 'WHERE NOT A use_email O, S eid %(x)s, ' - 'O is EmailAddress, O address AA, O alias AB, O modification_date AC') + 'O is_instance_of EmailAddress, O address AA, O alias AB, O modification_date AC') def test_unrelated_rql_security_1_user(self): req = self.request() @@ -298,7 +317,7 @@ rql = user.cw_unrelated_rql('use_email', 'EmailAddress', 'subject')[0] self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC ' 'WHERE NOT A use_email O, S eid %(x)s, ' - 'O is EmailAddress, O address AA, O alias AB, O modification_date AC') + 'O is_instance_of EmailAddress, O address AA, O alias AB, O modification_date AC') user = self.execute('Any X WHERE X login "admin"').get_entity(0, 0) rql = user.cw_unrelated_rql('use_email', 'EmailAddress', 'subject')[0] self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC ' @@ -319,7 +338,7 @@ email = self.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0) rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0] self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AA ' - 'WHERE NOT S use_email O, O eid %(x)s, S is CWUser, ' + 'WHERE NOT S use_email O, O eid %(x)s, S is_instance_of CWUser, ' 'S login AA, S firstname AB, S surname AC, S modification_date AD') self.login('anon') rperms = self.schema['EmailAddress'].permissions['read'] @@ -353,7 +372,7 @@ rql = person.cw_unrelated_rql('connait', 'Personne', 'subject')[0] self.assertEqual( rql, 'Any O,AA,AB,AC ORDERBY AC DESC WHERE ' - 'O is Personne, O nom AA, O prenom AB, O modification_date AC') + 'O is_instance_of Personne, O nom AA, O prenom AB, O modification_date AC') def test_unrelated_rql_constraints_creation_object(self): person = self.vreg['etypes'].etype_class('Personne')(self.request()) @@ -373,7 +392,7 @@ person = self.vreg['etypes'].etype_class('Personne')(self.request()) rql = person.cw_unrelated_rql('connait', 'Personne', 'subject')[0] self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC WHERE ' - 'O is Personne, O nom AA, O prenom AB, ' + 'O is_instance_of Personne, O nom AA, O prenom AB, ' 'O modification_date AC') def test_unrelated_rql_constraints_edition_subject(self): @@ -416,7 +435,7 @@ rql, args = person.cw_unrelated_rql('actionnaire', 'Societe', 'subject', lt_infos=lt_infos) self.assertEqual(u'Any O ORDERBY O WHERE NOT A actionnaire O, ' - u'O is Societe, NOT EXISTS(O eid %(O)s), ' + u'O is_instance_of Societe, NOT EXISTS(O eid %(O)s), ' u'A is Personne', rql) self.assertEqual({'O': soc.eid}, args) @@ -429,7 +448,7 @@ rql, args = soc.cw_unrelated_rql('actionnaire', 'Personne', 'object', lt_infos=lt_infos) self.assertEqual(u'Any S ORDERBY S WHERE NOT S actionnaire A, ' - u'S is Personne, NOT EXISTS(S eid %(S)s), ' + u'S is_instance_of Personne, NOT EXISTS(S eid %(S)s), ' u'A is Societe', rql) self.assertEqual({'S': person.eid}, args) @@ -442,7 +461,7 @@ rql, args = soc.cw_unrelated_rql('dirige', 'Personne', 'object', lt_infos=lt_infos) self.assertEqual(u'Any S ORDERBY S WHERE NOT S dirige A, ' - u'S is Personne, EXISTS(S eid %(S)s), ' + u'S is_instance_of Personne, EXISTS(S eid %(S)s), ' u'A is Societe', rql) self.assertEqual({'S': person.eid}, args) @@ -452,7 +471,7 @@ self.vreg['etypes'].etype_class('Personne').fetch_attrs = () soc = req.create_entity('Societe', nom=u'logilab') rql, args = person.cw_unrelated_rql('associe', 'Personne', 'subject') - self.assertEqual(u'Any O ORDERBY O WHERE O is Personne', rql) + self.assertEqual(u'Any O ORDERBY O WHERE O is_instance_of Personne', rql) self.assertEqual({}, args) def test_unrelated_rql_s_linkto_s_unused_info(self): @@ -463,7 +482,7 @@ lt_infos = {('dirige', 'subject'): [other_p.eid]} rql, args = person.cw_unrelated_rql('associe', 'Personne', 'subject', lt_infos=lt_infos) - self.assertEqual(u'Any O ORDERBY O WHERE O is Personne', rql) + self.assertEqual(u'Any O ORDERBY O WHERE O is_instance_of Personne', rql) def test_unrelated_base(self): req = self.request() @@ -643,8 +662,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): @@ -690,17 +711,16 @@ person.cw_clear_all_caches() self.assertEqual(person.rest_path(), unicode(person.eid)) self.assertEqual(person2.rest_path(), unicode(person2.eid)) - # unique attr with None value (wikiid in this case) - card1 = req.create_entity('Card', title=u'hop') - self.assertEqual(card1.rest_path(), unicode(card1.eid)) + # unique attr with None value (nom in this case) + friend = req.create_entity('Ami', prenom=u'bob') + self.assertEqual(friend.rest_path(), unicode(friend.eid)) + + def test_can_use_rest_path(self): + self.assertTrue(can_use_rest_path(u'zobi')) # don't use rest if we have /, ? or & in the path (breaks mod_proxy) - card2 = req.create_entity('Card', title=u'pod', wikiid=u'zo/bi') - self.assertEqual(card2.rest_path(), unicode(card2.eid)) - card3 = req.create_entity('Card', title=u'pod', wikiid=u'zo&bi') - self.assertEqual(card3.rest_path(), unicode(card3.eid)) - card4 = req.create_entity('Card', title=u'pod', wikiid=u'zo?bi') - self.assertEqual(card4.rest_path(), unicode(card4.eid)) - + self.assertFalse(can_use_rest_path(u'zo/bi')) + self.assertFalse(can_use_rest_path(u'zo&bi')) + self.assertFalse(can_use_rest_path(u'zo?bi')) def test_cw_set_attributes(self): req = self.request() @@ -740,7 +760,7 @@ self.assertEqual(card.absolute_url(), 'http://testing.fr/cubicweb/%s' % card.eid) - def test_create_entity(self): + def test_create_and_compare_entity(self): req = self.request() p1 = req.create_entity('Personne', nom=u'fayolle', prenom=u'alexandre') p2 = req.create_entity('Personne', nom=u'campeas', prenom=u'aurelien') @@ -754,6 +774,15 @@ self.assertEqual(sorted([c.nom for c in p.evaluee]), ['campeas', 'fayolle']) self.assertEqual([c.type for c in p.reverse_ecrit_par], ['z']) + req = self.request() + auc = req.execute('Personne P WHERE P prenom "aurelien"').get_entity(0,0) + persons = set() + persons.add(p1) + persons.add(p2) + persons.add(auc) + self.assertEqual(2, len(persons)) + self.assertNotEqual(p1, p2) + self.assertEqual(p2, auc) if __name__ == '__main__': diff -r aff75b69db92 -r 2c48c091b6a2 test/unittest_mail.py --- a/test/unittest_mail.py Tue Jul 02 17:09:04 2013 +0200 +++ b/test/unittest_mail.py Mon Jan 13 13:47:47 2014 +0100 @@ -31,7 +31,7 @@ def getlogin(): - """avoid usinng os.getlogin() because of strange tty / stdin problems + """avoid using os.getlogin() because of strange tty / stdin problems (man 3 getlogin) Another solution would be to use $LOGNAME, $USER or $USERNAME """ diff -r aff75b69db92 -r 2c48c091b6a2 test/unittest_migration.py --- a/test/unittest_migration.py Tue Jul 02 17:09:04 2013 +0200 +++ b/test/unittest_migration.py Mon Jan 13 13:47:47 2014 +0100 @@ -79,10 +79,6 @@ self.assert_(not isinstance(config.migration_handler(), ServerMigrationHelper)) self.assertIsInstance(config.migration_handler(), MigrationHelper) config = self.config - config.__class__.name = 'twisted' - self.assertListEqual(filter_scripts(config, TMIGRDIR, (0,0,4), (0,1,0)), - [((0, 1 ,0), TMIGRDIR+'0.1.0_common.py'), - ((0, 1 ,0), TMIGRDIR+'0.1.0_web.py')]) config.__class__.name = 'repository' self.assertListEqual(filter_scripts(config, TMIGRDIR, (0,0,4), (0,1,0)), [((0, 1 ,0), TMIGRDIR+'0.1.0_Any.py'), @@ -92,8 +88,7 @@ self.assertListEqual(filter_scripts(config, TMIGRDIR, (0,0,4), (0,1,0)), [((0, 1 ,0), TMIGRDIR+'0.1.0_Any.py'), ((0, 1 ,0), TMIGRDIR+'0.1.0_common.py'), - ((0, 1 ,0), TMIGRDIR+'0.1.0_repository.py'), - ((0, 1 ,0), TMIGRDIR+'0.1.0_web.py')]) + ((0, 1 ,0), TMIGRDIR+'0.1.0_repository.py')]) config.__class__.name = 'repository' diff -r aff75b69db92 -r 2c48c091b6a2 test/unittest_predicates.py --- a/test/unittest_predicates.py Tue Jul 02 17:09:04 2013 +0200 +++ b/test/unittest_predicates.py Mon Jan 13 13:47:47 2014 +0100 @@ -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 aff75b69db92 -r 2c48c091b6a2 test/unittest_req.py --- a/test/unittest_req.py Tue Jul 02 17:09:04 2013 +0200 +++ b/test/unittest_req.py Mon Jan 13 13:47:47 2014 +0100 @@ -18,7 +18,7 @@ from logilab.common.testlib import TestCase, unittest_main from cubicweb import ObjectNotFound -from cubicweb.req import RequestSessionBase +from cubicweb.req import RequestSessionBase, FindEntityError from cubicweb.devtools.testlib import CubicWebTC from cubicweb import Unauthorized @@ -53,11 +53,97 @@ class RequestCWTC(CubicWebTC): + + def test_base_url(self): + base_url = self.config['base-url'] + self.assertEqual(self.session.base_url(), base_url) + assert 'https-url' not in self.config + self.assertEqual(self.session.base_url(secure=True), base_url) + secure_base_url = base_url.replace('http', 'https') + self.config.global_set_option('https-url', secure_base_url) + self.assertEqual(self.session.base_url(secure=True), secure_base_url) + def test_view_catch_ex(self): req = self.request() rset = self.execute('CWUser X WHERE X login "hop"') self.assertEqual(req.view('oneline', rset, 'null'), '') self.assertRaises(ObjectNotFound, req.view, 'onelinee', rset, 'null') + def test_find_one_entity(self): + self.request().create_entity( + 'CWUser', login=u'cdevienne', upassword=u'cdevienne', + surname=u'de Vienne', firstname=u'Christophe', + in_group=self.request().find('CWGroup', name=u'users').one()) + + self.request().create_entity( + 'CWUser', login=u'adim', upassword='adim', surname=u'di mascio', + firstname=u'adrien', + in_group=self.request().find('CWGroup', name=u'users').one()) + + u = self.request().find_one_entity('CWUser', login=u'cdevienne') + self.assertEqual(u.firstname, u"Christophe") + + with self.assertRaises(FindEntityError): + self.request().find_one_entity('CWUser', login=u'patanok') + + with self.assertRaises(FindEntityError): + self.request().find_one_entity('CWUser') + + def test_find_entities(self): + self.request().create_entity( + 'CWUser', login=u'cdevienne', upassword=u'cdevienne', + surname=u'de Vienne', firstname=u'Christophe', + in_group=self.request().find('CWGroup', name=u'users').one()) + + self.request().create_entity( + 'CWUser', login=u'adim', upassword='adim', surname=u'di mascio', + firstname=u'adrien', + in_group=self.request().find('CWGroup', name=u'users').one()) + + l = list(self.request().find_entities('CWUser', login=u'cdevienne')) + self.assertEqual(1, len(l)) + self.assertEqual(l[0].firstname, u"Christophe") + + l = list(self.request().find_entities('CWUser', login=u'patanok')) + self.assertEqual(0, len(l)) + + l = list(self.request().find_entities('CWUser')) + self.assertEqual(4, len(l)) + + def test_find(self): + self.request().create_entity( + 'CWUser', login=u'cdevienne', upassword=u'cdevienne', + surname=u'de Vienne', firstname=u'Christophe', + in_group=self.request().find('CWGroup', name=u'users').one()) + + self.request().create_entity( + 'CWUser', login=u'adim', upassword='adim', surname=u'di mascio', + firstname=u'adrien', + in_group=self.request().find('CWGroup', name=u'users').one()) + + u = self.request().find('CWUser', login=u'cdevienne').one() + self.assertEqual(u.firstname, u"Christophe") + + users = list(self.request().find('CWUser').entities()) + self.assertEqual(len(users), 4) + + groups = list( + self.request().find('CWGroup', reverse_in_group=u).entities()) + self.assertEqual(len(groups), 1) + self.assertEqual(groups[0].name, u'users') + + users = self.request().find('CWUser', in_group=groups[0]).entities() + users = list(users) + self.assertEqual(len(users), 2) + + with self.assertRaises(AssertionError): + self.request().find('CWUser', chapeau=u"melon") + + with self.assertRaises(AssertionError): + self.request().find('CWUser', reverse_buddy=users[0]) + + with self.assertRaises(NotImplementedError): + self.request().find('CWUser', in_group=[1, 2]) + if __name__ == '__main__': unittest_main() diff -r aff75b69db92 -r 2c48c091b6a2 test/unittest_rqlrewrite.py --- a/test/unittest_rqlrewrite.py Tue Jul 02 17:09:04 2013 +0200 +++ b/test/unittest_rqlrewrite.py Mon Jan 13 13:47:47 2014 +0100 @@ -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"') @@ -411,30 +438,30 @@ u"Any C WHERE C is Card, EXISTS(C owned_by A, A is CWUser)") def test_rqlexpr_not_relation_1_1(self): - constraint = RRQLExpression('X owned_by Z, Z login "hop"', 'X') + constraint = ERQLExpression('X owned_by Z, Z login "hop"', 'X') rqlst = parse('Affaire A WHERE NOT EXISTS(A documented_by C)') rewrite(rqlst, {('C', 'X'): (constraint,)}, {}, 'X') self.assertEqual(rqlst.as_string(), u'Any A WHERE NOT EXISTS(A documented_by C, EXISTS(C owned_by B, B login "hop", B is CWUser), C is Card), A is Affaire') def test_rqlexpr_not_relation_1_2(self): - constraint = RRQLExpression('X owned_by Z, Z login "hop"', 'X') + constraint = ERQLExpression('X owned_by Z, Z login "hop"', 'X') rqlst = parse('Affaire A WHERE NOT EXISTS(A documented_by C)') rewrite(rqlst, {('A', 'X'): (constraint,)}, {}, 'X') self.assertEqual(rqlst.as_string(), u'Any A WHERE NOT EXISTS(A documented_by C, C is Card), A is Affaire, EXISTS(A owned_by B, B login "hop", B is CWUser)') def test_rqlexpr_not_relation_2(self): - constraint = RRQLExpression('X owned_by Z, Z login "hop"', 'X') + constraint = ERQLExpression('X owned_by Z, Z login "hop"', 'X') rqlst = rqlhelper.parse('Affaire A WHERE NOT A documented_by C', annotate=False) rewrite(rqlst, {('C', 'X'): (constraint,)}, {}, 'X') self.assertEqual(rqlst.as_string(), u'Any A WHERE NOT EXISTS(A documented_by C, EXISTS(C owned_by B, B login "hop", B is CWUser), C is Card), A is Affaire') def test_rqlexpr_multiexpr_outerjoin(self): - c1 = RRQLExpression('X owned_by Z, Z login "hop"', 'X') - c2 = RRQLExpression('X owned_by Z, Z login "hip"', 'X') - c3 = RRQLExpression('X owned_by Z, Z login "momo"', 'X') + c1 = ERQLExpression('X owned_by Z, Z login "hop"', 'X') + c2 = ERQLExpression('X owned_by Z, Z login "hip"', 'X') + c3 = ERQLExpression('X owned_by Z, Z login "momo"', 'X') rqlst = rqlhelper.parse('Any A WHERE A documented_by C?', annotate=False) rewrite(rqlst, {('C', 'X'): (c1, c2, c3)}, {}, 'X') self.assertEqual(rqlst.as_string(), @@ -459,5 +486,74 @@ rqlst = parse('Any A, R WHERE A ref R, S is Affaire') rewrite(rqlst, {('A', 'X'): (c_ok, c_bad)}, {}) + def test_nonregr_is_instance_of(self): + user_expr = ERQLExpression('NOT X in_group AF, AF name "guests"') + rqlst = parse('Any O WHERE S use_email O, S is CWUser, O is_instance_of EmailAddress') + rewrite(rqlst, {('S', 'X'): (user_expr,)}, {}) + self.assertEqual(rqlst.as_string(), + 'Any O WHERE S use_email O, S is CWUser, O is EmailAddress, ' + 'EXISTS(NOT S in_group A, A name "guests", A is CWGroup)') + +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_ambiguous_optional_diff_exprs(self): + """See #3013554""" + self.skipTest('bad request generated (may generate duplicated results)') + edef1 = self.schema['Societe'] + edef2 = self.schema['Division'] + edef3 = self.schema['Note'] + with self.temporary_permissions((edef1, {'read': (ERQLExpression('X created_by U'),)}), + (edef2, {'read': ('users',)}), + (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(union.as_string(), 'not generated today') + + + 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 aff75b69db92 -r 2c48c091b6a2 test/unittest_rset.py --- a/test/unittest_rset.py Tue Jul 02 17:09:04 2013 +0200 +++ b/test/unittest_rset.py Mon Jan 13 13:47:47 2014 +0100 @@ -28,6 +28,8 @@ from cubicweb.devtools.testlib import CubicWebTC from cubicweb.rset import NotAnEntity, ResultSet, attr_desc_iterator +from cubicweb import NoResultError, MultipleResultsError + def pprelcachedict(d): res = {} @@ -181,6 +183,7 @@ rs2 = rs.filtered_rset(test_filter) self.assertEqual(len(rs2), 2) self.assertEqual([login for _, login in rs2], ['adim', 'syt']) + self.assertEqual(rs2.description, rs.description[1:]) def test_transform(self): rs = ResultSet([[12, 'adim'], [13, 'syt'], [14, 'nico']], @@ -367,6 +370,39 @@ attr = etype == 'Bookmark' and 'title' or 'name' self.assertEqual(entity.cw_attr_cache[attr], n) + def test_one(self): + self.request().create_entity('CWUser', login=u'cdevienne', + upassword=u'cdevienne', + surname=u'de Vienne', + firstname=u'Christophe') + e = self.execute('Any X WHERE X login "cdevienne"').one() + + self.assertEqual(e.surname, u'de Vienne') + + e = self.execute( + 'Any X, N WHERE X login "cdevienne", X surname N').one() + self.assertEqual(e.surname, u'de Vienne') + + e = self.execute( + 'Any N, X WHERE X login "cdevienne", X surname N').one(col=1) + self.assertEqual(e.surname, u'de Vienne') + + def test_one_no_rows(self): + with self.assertRaises(NoResultError): + self.execute('Any X WHERE X login "patanok"').one() + + def test_one_multiple_rows(self): + self.request().create_entity( + 'CWUser', login=u'cdevienne', upassword=u'cdevienne', + surname=u'de Vienne', firstname=u'Christophe') + + self.request().create_entity( + 'CWUser', login=u'adim', upassword='adim', surname=u'di mascio', + firstname=u'adrien') + + with self.assertRaises(MultipleResultsError): + self.execute('Any X WHERE X is CWUser').one() + def test_related_entity_optional(self): e = self.request().create_entity('Bookmark', title=u'aaaa', path=u'path') rset = self.execute('Any B,U,L WHERE B bookmarked_by U?, U login L') diff -r aff75b69db92 -r 2c48c091b6a2 test/unittest_schema.py --- a/test/unittest_schema.py Tue Jul 02 17:09:04 2013 +0200 +++ b/test/unittest_schema.py Mon Jan 13 13:47:47 2014 +0100 @@ -132,6 +132,8 @@ self.assertRaises(RQLSyntaxError, ERQLExpression, '1') expr = ERQLExpression('X travaille S, S owned_by U') self.assertEqual(str(expr), 'Any X WHERE X travaille S, S owned_by U, X eid %(x)s, U eid %(u)s') + expr = ERQLExpression('X foo S, S bar U, X baz XE, S quux SE HAVING XE > SE') + self.assertEqual(str(expr), 'Any X WHERE X foo S, S bar U, X baz XE, S quux SE, X eid %(x)s, U eid %(u)s HAVING XE > SE') def test_rrqlexpression(self): self.assertRaises(Exception, RRQLExpression, '1') @@ -157,7 +159,7 @@ self.assert_(isinstance(schema, CubicWebSchema)) self.assertEqual(schema.name, 'data') entities = sorted([str(e) for e in schema.entities()]) - expected_entities = ['BaseTransition', 'BigInt', 'Bookmark', 'Boolean', 'Bytes', 'Card', + expected_entities = ['Ami', 'BaseTransition', 'BigInt', 'Bookmark', 'Boolean', 'Bytes', 'Card', 'Date', 'Datetime', 'Decimal', 'CWCache', 'CWConstraint', 'CWConstraintType', 'CWDataImport', 'CWEType', 'CWAttribute', 'CWGroup', 'EmailAddress', 'CWRelation', @@ -216,6 +218,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 aff75b69db92 -r 2c48c091b6a2 uilib.py --- a/uilib.py Tue Jul 02 17:09:04 2013 +0200 +++ b/uilib.py Mon Jan 13 13:47:47 2014 +0100 @@ -453,7 +453,7 @@ def rest_traceback(info, exception): - """return a ReST formated traceback""" + """return a unicode ReST formated traceback""" res = [u'Traceback\n---------\n::\n'] for stackentry in traceback.extract_tb(info[2]): res.append(u'\tFile %s, line %s, function %s' % tuple(stackentry[:3])) diff -r aff75b69db92 -r 2c48c091b6a2 utils.py --- a/utils.py Tue Jul 02 17:09:04 2013 +0200 +++ b/utils.py Mon Jan 13 13:47:47 2014 +0100 @@ -49,10 +49,10 @@ """Return a unique identifier string. if specified, `key` is used to prefix the generated uid so it can be used - for instance as a DOM id or as sql table names. + for instance as a DOM id or as sql table name. See uuid.uuid4 documentation for the shape of the generated identifier, but - this is basicallly a 32 bits hexadecimal string. + this is basically a 32 bits hexadecimal string. """ if key is None: return uuid4().hex @@ -195,6 +195,8 @@ if isinstance(other, RepeatList): return other._size == self._size and other._item == self._item return self[:] == other + # py3k future warning "Overriding __eq__ blocks inheritance of __hash__ in 3.x" + # is annoying but won't go away because we don't want to hash() the repeatlist def pop(self, i): self._size -= 1 diff -r aff75b69db92 -r 2c48c091b6a2 view.py --- a/view.py Tue Jul 02 17:09:04 2013 +0200 +++ b/view.py Mon Jan 13 13:47:47 2014 +0100 @@ -558,34 +558,6 @@ __registry__ = 'adapters' -def implements_adapter_compat(iface): - def _pre39_compat(func): - def decorated(self, *args, **kwargs): - entity = self.entity - if hasattr(entity, func.__name__): - warn('[3.9] %s method is deprecated, define it on a custom ' - '%s for %s instead' % (func.__name__, iface, - entity.__class__), - DeprecationWarning) - member = getattr(entity, func.__name__) - if callable(member): - return member(*args, **kwargs) - return member - return func(self, *args, **kwargs) - decorated.decorated = func - return decorated - return _pre39_compat - - -def unwrap_adapter_compat(cls): - parent = cls.__bases__[0] - for member_name in dir(parent): - member = getattr(parent, member_name) - if isinstance(member, types.MethodType) and hasattr(member.im_func, 'decorated') and not member_name in cls.__dict__: - method = new.instancemethod(member.im_func.decorated, None, cls) - setattr(cls, member_name, method) - - class auto_unwrap_bw_compat(type): def __new__(mcs, name, bases, classdict): cls = type.__new__(mcs, name, bases, classdict) @@ -596,7 +568,6 @@ class EntityAdapter(Adapter): """base class for entity adapters (eg adapt an entity to an interface)""" - __metaclass__ = auto_unwrap_bw_compat def __init__(self, _cw, **kwargs): try: self.entity = kwargs.pop('entity') diff -r aff75b69db92 -r 2c48c091b6a2 web/__init__.py --- a/web/__init__.py Tue Jul 02 17:09:04 2013 +0200 +++ b/web/__init__.py Mon Jan 13 13:47:47 2014 +0100 @@ -31,8 +31,6 @@ from cubicweb.uilib import eid_param assert json_dumps is not None, 'no json module installed' -dumps = deprecated('[3.9] use cubicweb.utils.json_dumps instead of dumps')( - json_dumps) INTERNAL_FIELD_VALUE = '__cubicweb_internal_field__' diff -r aff75b69db92 -r 2c48c091b6a2 web/application.py --- a/web/application.py Tue Jul 02 17:09:04 2013 +0200 +++ b/web/application.py Mon Jan 13 13:47:47 2014 +0100 @@ -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. @@ -68,7 +68,7 @@ self.session_time = vreg.config['http-session-time'] or None self.authmanager = vreg['components'].select('authmanager', repo=repo) interval = (self.session_time or 0) / 2. - if vreg.config.anonymous_user() is not None: + if vreg.config.anonymous_user()[0] is not None: self.cleanup_anon_session_time = vreg.config['cleanup-anonymous-session-time'] or 5 * 60 assert self.cleanup_anon_session_time > 0 if self.session_time is not None: @@ -319,17 +319,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': @@ -393,7 +393,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 @@ -449,8 +449,8 @@ req.update_search_state() result = controller.publish(rset=rset) except StatusResponse as ex: - warn('StatusResponse is deprecated use req.status_out', - DeprecationWarning) + warn('[3.16] StatusResponse is deprecated use req.status_out', + DeprecationWarning, stacklevel=2) result = ex.content req.status_out = ex.status except Redirect as ex: diff -r aff75b69db92 -r 2c48c091b6a2 web/controller.py --- a/web/controller.py Tue Jul 02 17:09:04 2013 +0200 +++ b/web/controller.py Mon Jan 13 13:47:47 2014 +0100 @@ -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. @@ -21,6 +21,7 @@ from logilab.mtconverter import xml_escape from logilab.common.registry import yes +from logilab.common.deprecation import deprecated from cubicweb.appobject import AppObject from cubicweb.mail import format_mail @@ -103,6 +104,8 @@ if not self._edited_entity: self._edited_entity = entity + @deprecated('[3.18] call view.set_http_cache_headers then ' + '.is_client_cache_valid() method and return instead') def validate_cache(self, view): view.set_http_cache_headers() self._cw.validate_cache() diff -r aff75b69db92 -r 2c48c091b6a2 web/data/cubicweb.ajax.box.js --- a/web/data/cubicweb.ajax.box.js Tue Jul 02 17:09:04 2013 +0200 +++ b/web/data/cubicweb.ajax.box.js Mon Jan 13 13:47:47 2014 +0100 @@ -81,12 +81,12 @@ }); $input.cwautocomplete(unrelated, {multiple: Boolean(separator)}); var buttons = DIV({'class' : "sgformbuttons"}, - A({href : "javascript: noop();", + A({href : "javascript: $.noop();", onclick : cw.utils.strFuncCall('ajaxBoxValidateSelectorInput', boxid, eid, separator, addfname, msg)}, oklabel), ' / ', - A({'href' : "javascript: noop();", + A({'href' : "javascript: $.noop();", 'onclick' : '$("#' + holderid + '").empty()'}, cancellabel)); holder.append(buttons); diff -r aff75b69db92 -r 2c48c091b6a2 web/data/cubicweb.ajax.js --- a/web/data/cubicweb.ajax.js Tue Jul 02 17:09:04 2013 +0200 +++ b/web/data/cubicweb.ajax.js Mon Jan 13 13:47:47 2014 +0100 @@ -339,11 +339,6 @@ cw.log('loadxhtml called without an element'); } var callback = null; - if (form && form.callback) { - cw.log('[3.9] callback given through form.callback is deprecated, add ' + 'callback on the defered'); - callback = form.callback; - delete form.callback; - } var node = this.get(0); // only consider the first element if (cursor) { setProgressCursor(); @@ -734,13 +729,7 @@ /* DEPRECATED *****************************************************************/ -preprocessAjaxLoad = cw.utils.deprecatedFunction( - '[3.9] preprocessAjaxLoad() is deprecated, use loadAjaxHtmlHead instead', - function(node, newdomnode) { - return loadAjaxHtmlHead(newdomnode); - } -); - +// still used in cwo and keyword cubes at least reloadComponent = cw.utils.deprecatedFunction( '[3.9] reloadComponent() is deprecated, use loadxhtml instead', function(compid, rql, registry, nodeid, extraargs) { @@ -754,52 +743,6 @@ } ); -reloadBox = cw.utils.deprecatedFunction( - '[3.9] reloadBox() is deprecated, use loadxhtml instead', - function(boxid, rql) { - return reloadComponent(boxid, rql, 'ctxcomponents', boxid); - } -); - -replacePageChunk = cw.utils.deprecatedFunction( - '[3.9] replacePageChunk() is deprecated, use loadxhtml instead', - function(nodeId, rql, vid, extraparams, /* ... */ swap, callback) { - var params = null; - if (callback) { - params = { - callback: callback - }; - } - var node = jQuery('#' + nodeId)[0]; - var props = {}; - if (node) { - props['rql'] = rql; - props['fname'] = 'view'; - props['pageid'] = pageid; - if (vid) { - props['vid'] = vid; - } - if (extraparams) { - jQuery.extend(props, extraparams); - } - // FIXME we need to do asURL(props) manually instead of - // passing `props` directly to loadxml because replacePageChunk - // is sometimes called (abusively) with some extra parameters in `vid` - var mode = swap ? 'swap': 'replace'; - var url = AJAX_BASE_URL + asURL(props); - jQuery(node).loadxhtml(url, params, 'get', mode); - } else { - cw.log('Node', nodeId, 'not found'); - } - } -); - -loadxhtml = cw.utils.deprecatedFunction( - '[3.9] loadxhtml() function is deprecated, use loadxhtml method instead', - function(nodeid, url, /* ... */ replacemode) { - jQuery('#' + nodeid).loadxhtml(url, null, 'post', replacemode); - } -); function remoteExec(fname /* ... */) { setProgressCursor(); diff -r aff75b69db92 -r 2c48c091b6a2 web/data/cubicweb.calendar.css --- a/web/data/cubicweb.calendar.css Tue Jul 02 17:09:04 2013 +0200 +++ b/web/data/cubicweb.calendar.css Mon Jan 13 13:47:47 2014 +0100 @@ -231,6 +231,8 @@ font-weight:bold; padding-bottom:0.2em; background: %(incontextBoxBodyBgColor)s; + border-top-left-radius: 6px; + border-top-right-radius: 6px; } .calendar th.month a{ diff -r aff75b69db92 -r 2c48c091b6a2 web/data/cubicweb.compat.js --- a/web/data/cubicweb.compat.js Tue Jul 02 17:09:04 2013 +0200 +++ b/web/data/cubicweb.compat.js Mon Jan 13 13:47:47 2014 +0100 @@ -1,34 +1,3 @@ -cw.utils.movedToNamespace(['log', 'jqNode', 'getNode', 'evalJSON', 'urlEncode', - 'swapDOM'], cw); -cw.utils.movedToNamespace(['nodeWalkDepthFirst', 'formContents', 'isArray', - 'isString', 'isArrayLike', 'sliceList', - 'toISOTimestamp'], cw.utils); - - -if ($.noop === undefined) { - function noop() {} -} else { - noop = cw.utils.deprecatedFunction( - '[3.9] noop() is deprecated, use $.noop() instead (XXX requires jQuery 1.4)', - $.noop); -} - -// ========== ARRAY EXTENSIONS ========== /// -Array.prototype.contains = cw.utils.deprecatedFunction( - '[3.9] array.contains(elt) is deprecated, use $.inArray(elt, array)!=-1 instead', - function(element) { - return jQuery.inArray(element, this) != - 1; - } -); - -// ========== END OF ARRAY EXTENSIONS ========== /// -forEach = cw.utils.deprecatedFunction( - '[3.9] forEach() is deprecated, use $.each() instead', - function(array, func) { - return $.each(array, func); - } -); - /** * .. function:: cw.utils.deprecatedFunction(msg, function) * @@ -41,64 +10,20 @@ * [ ["a", "b", "c"], ["d", "e"] ] */ // XXX why not the same argument order as $.map and forEach ? -map = cw.utils.deprecatedFunction( - '[3.9] map() is deprecated, use $.map instead', - function(func, array) { - var result = []; - for (var i = 0, length = array.length; i < length; i++) { - result.push(func(array[i])); - } - return result; - } -); -findValue = cw.utils.deprecatedFunction( - '[3.9] findValue(array, elt) is deprecated, use $.inArray(elt, array) instead', - function(array, element) { - return jQuery.inArray(element, array); - } -); - -filter = cw.utils.deprecatedFunction( - '[3.9] filter(func, array) is deprecated, use $.grep(array, f) instead', - function(func, array) { - return $.grep(array, func); +function map(func, array) { + var result = []; + for (var i = 0, length = array.length; i < length; i++) { + result.push(func(array[i])); } -); - -addElementClass = cw.utils.deprecatedFunction( - '[3.9] addElementClass(node, cls) is deprecated, use $(node).addClass(cls) instead', - function(node, klass) { - $(node).addClass(klass); - } -); + return result; +} -removeElementClass = cw.utils.deprecatedFunction( - '[3.9] removeElementClass(node, cls) is deprecated, use $(node).removeClass(cls) instead', - function(node, klass) { - $(node).removeClass(klass); - } -); -hasElementClass = cw.utils.deprecatedFunction( - '[3.9] hasElementClass(node, cls) is deprecated, use $(node).hasClass(cls)', - function(node, klass) { - return $(node).hasClass(klass); - } -); - +// skm cube still uses this getNodeAttribute = cw.utils.deprecatedFunction( '[3.9] getNodeAttribute(node, attr) is deprecated, use $(node).attr(attr)', function(node, attribute) { return $(node).attr(attribute); } ); - -/** - * The only known usage of KEYS is in the tag cube. Once cubicweb-tag 1.7.0 is out, - * this current definition can be removed. - */ -var KEYS = { - KEY_ESC: 27, - KEY_ENTER: 13 -}; diff -r aff75b69db92 -r 2c48c091b6a2 web/data/cubicweb.css --- a/web/data/cubicweb.css Tue Jul 02 17:09:04 2013 +0200 +++ b/web/data/cubicweb.css Mon Jan 13 13:47:47 2014 +0100 @@ -17,9 +17,6 @@ } h1, h2, h3 { margin-top:0; margin-bottom:0; } -/* got rhythm ? beat of 12*1.25 = 15 px */ -.rhythm_bg { background: url("%(baseRhythmBg)s") repeat ! important; } - h1, .vtitle { font-size: %(h1FontSize)s; @@ -297,7 +294,6 @@ div#pageContent { clear: both; - /* margin-top:-1px; *//* enable when testing rhythm */ background: %(pageContentBgColor)s; border: 1px solid %(pageContentBorderColor)s; padding: 0 %(pageContentPadding)s %(pageContentPadding)s; @@ -358,17 +354,20 @@ div.shadow{ height: 14px; - background: url("shadow.gif") no-repeat top right; } div.sideBoxTitle { background: %(incontextBoxBodyBg)s; display: block; font-weight: bold; + border-top-left-radius: 6px; + border-top-right-radius: 6px; } div.sideBox { margin-bottom: 1em; + border-top-left-radius: 6px; + border-top-right-radius: 6px; } ul.sideBox, @@ -403,6 +402,8 @@ div.boxTitle { overflow: hidden; font-weight: bold; + border-top-left-radius: 6px; + border-top-right-radius: 6px; } div.boxTitle span { @@ -467,7 +468,10 @@ #navColumnLeft div.boxFooter, #navColumnRight div.boxFooter{ height: 14px; - background: url("shadow.gif") no-repeat top right; +} + +.boxBody, .boxTitle, #pageContent, #appMsg { + box-shadow: 1px 1px 3px Gray; } /* boxes lists and menus */ diff -r aff75b69db92 -r 2c48c091b6a2 web/data/cubicweb.facets.css --- a/web/data/cubicweb.facets.css Tue Jul 02 17:09:04 2013 +0200 +++ b/web/data/cubicweb.facets.css Mon Jan 13 13:47:47 2014 +0100 @@ -8,6 +8,8 @@ background: #fff; padding: %(facet_Padding)s; margin-bottom: %(facet_MarginBottom)s; + border-top-left-radius: 5px; + border-bottom-right-radius: 7px; } .facetGroup { diff -r aff75b69db92 -r 2c48c091b6a2 web/data/cubicweb.facets.js --- a/web/data/cubicweb.facets.js Tue Jul 02 17:09:04 2013 +0200 +++ b/web/data/cubicweb.facets.js Mon Jan 13 13:47:47 2014 +0100 @@ -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); }); }); @@ -110,7 +109,7 @@ $node.loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('render', { 'rql': rql }, - 'ctxcomponents', 'edit_box')); + 'ctxcomponents', 'edit_box'), 'GET', 'swap'); } $node = $('#breadcrumbs'); if ($node.length) { @@ -170,7 +169,6 @@ if ($('#'+divid).length) { var $loadingDiv = $(DIV({id:'facetLoading'}, facetLoadingMsg)); - $loadingDiv.corner(); $($('#'+divid).get(0).parentNode).append($loadingDiv); } form.find('div.facet').each(function() { @@ -328,7 +326,6 @@ if ($('div.facetBody').length) { var $loadingDiv = $(DIV({id:'facetLoading'}, facetLoadingMsg)); - $loadingDiv.corner(); $('body').append($loadingDiv); } }); diff -r aff75b69db92 -r 2c48c091b6a2 web/data/cubicweb.htmlhelpers.js --- a/web/data/cubicweb.htmlhelpers.js Tue Jul 02 17:09:04 2013 +0200 +++ b/web/data/cubicweb.htmlhelpers.js Mon Jan 13 13:47:47 2014 +0100 @@ -78,10 +78,10 @@ // generate a list of couple key=value if key is multivalued if (cw.utils.isArrayLike(value)) { for (var i = 0; i < value.length; i++) { - chunks.push(key + '=' + urlEncode(value[i])); + chunks.push(key + '=' + cw.urlEncode(value[i])); } } else { - chunks.push(key + '=' + urlEncode(value)); + chunks.push(key + '=' + cw.urlEncode(value)); } } return chunks.join('&'); @@ -195,19 +195,4 @@ } } } -//============= page loading events ==========================================// -cw.rounded = [['div.sideBoxBody', 'bottom 6px'], - ['div.boxTitle, div.sideBoxTitle, th.month', 'top 6px']]; -function roundedCorners(node) { - if (jQuery.fn.corner !== undefined) { - node = jQuery(node); - for (var r = 0; r < cw.rounded.length; r++) { - node.find(cw.rounded[r][0]).corner(cw.rounded[r][1]); - } - } -} - -jQuery(document).ready(function() { - roundedCorners(this.body); -}); diff -r aff75b69db92 -r 2c48c091b6a2 web/data/cubicweb.js --- a/web/data/cubicweb.js Tue Jul 02 17:09:04 2013 +0200 +++ b/web/data/cubicweb.js Mon Jan 13 13:47:47 2014 +0100 @@ -45,6 +45,15 @@ return null; }, + // escapes string selectors (e.g. "foo.[subject]:42" -> "foo\.\[subject\]\:42" + escape: function(selector) { + if (typeof(selector) == 'string') { + return selector.replace( /(:|\.|\[|\])/g, "\\$1" ); + } + // cw.log('non string selector', selector); + return ''; + }, + getNode: function (node) { if (typeof(node) == 'string') { return document.getElementById(node); @@ -105,15 +114,6 @@ }; }, - movedToNamespace: function (funcnames, namespace) { - for (var i = 0; i < funcnames.length; i++) { - var funcname = funcnames[i]; - var msg = ('[3.9] ' + funcname + ' is deprecated, use ' + - namespace.__name__ + '.' + funcname + ' instead'); - window[funcname] = cw.utils.deprecatedFunction(msg, namespace[funcname]); - } - }, - createDomFunction: function (tag) { function builddom(params, children) { var node = document.createElement(tag); @@ -388,14 +388,6 @@ }); -String.prototype.startsWith = cw.utils.deprecatedFunction('[3.9] str.startsWith() is deprecated, use str.startswith() instead', function (prefix) { - return this.startswith(prefix); -}); - -String.prototype.endsWith = cw.utils.deprecatedFunction('[3.9] str.endsWith() is deprecated, use str.endswith() instead', function (suffix) { - return this.endswith(prefix); -}); - /** DOM factories ************************************************************/ A = cw.utils.createDomFunction('a'); BUTTON = cw.utils.createDomFunction('button'); @@ -472,7 +464,8 @@ return node; } -// XXX avoid crashes / backward compat +// cubes: tag, keyword and apycot seem to use this, including require/provide +// backward compat CubicWeb = cw; jQuery.extend(cw, { diff -r aff75b69db92 -r 2c48c091b6a2 web/data/cubicweb.old.css --- a/web/data/cubicweb.old.css Tue Jul 02 17:09:04 2013 +0200 +++ b/web/data/cubicweb.old.css Mon Jan 13 13:47:47 2014 +0100 @@ -411,13 +411,14 @@ div.shadow{ height: 14px; - background: url("shadow.gif") no-repeat top right; } div.sideBoxTitle { background: #cfceb7; display: block; font: bold 100% Georgia; + border-top-left-radius: 6px; + border-top-right-radius: 6px; } div.sideBox { @@ -434,6 +435,8 @@ div.sideBoxBody { padding: 0.2em 5px; background: #eeedd9; + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; } div.sideBoxBody a { @@ -457,6 +460,8 @@ div.boxTitle { overflow: hidden; font-weight: bold; + border-top-left-radius: 6px; + border-top-right-radius: 6px; } div.boxTitle span { @@ -521,7 +526,14 @@ #navColumnLeft div.boxFooter, #navColumnRight div.boxFooter{ height: 14px; - background: url("shadow.gif") no-repeat top right; +} + +.navboxes { + padding: 2px; +} + +.boxBody, .boxTitle, #pageContent, #appMsg { + box-shadow: 1px 1px 3px Gray; } /* boxes lists and menus */ diff -r aff75b69db92 -r 2c48c091b6a2 web/data/cubicweb.pictograms.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/data/cubicweb.pictograms.css Mon Jan 13 13:47:47 2014 +0100 @@ -0,0 +1,343 @@ +/*The included Entypo font have been created by Daniel Bruce (www.entypo.com) as +proposed by fontello (https://github.com/fontello/entypo). +The Entypo pictograms are licensed under CC BY 3.0 and the font under +SIL Open Font License.*/ +@font-face { + font-family: 'entypo'; + src: url('entypo.eot?8644018'); + src: url('entypo.eot?8644018#iefix') format('embedded-opentype'), + url('entypo.woff?8644018') format('woff'), + url('entypo.ttf?8644018') format('truetype'), + url('entypo.svg?8644018#entypo') format('svg'); + font-weight: normal; + font-style: normal; +} +/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ +/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ +/* +@media screen and (-webkit-min-device-pixel-ratio:0) { + @font-face { + font-family: 'entypo'; + src: url('../font/entypo.svg?8644018#entypo') format('svg'); + } +} +*/ + +[class^="icon-"]:before, +[class*=" icon-"]:before { + font-family: "entypo"; + font-style: normal; + font-weight: normal; + speak: none; + + display: inline-block; + text-decoration: inherit; + width: 1em; + margin-right: .1em; + text-align: center; + /* opacity: .8; */ + + /* For safety - reset parent styles, that can break glyph codes*/ + font-variant: normal; + text-transform: none; + + /* fix buttons height, for twitter bootstrap */ + line-height: 1em; + + /* Animation center compensation - magrins should be symmetric */ + /* remove if not needed */ + margin-left: .1em; + + /* you can be more comfortable with increased icons size */ + font-size: 160%; + vertical-align: middle; + + /* Uncomment for 3D effect */ + /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ +} + +.icon-note:before { content: '\266a'; } /* '♪' */ +.icon-note-beamed:before { content: '\266b'; } /* '♫' */ +.icon-music:before { content: '\1f3b5'; } /* '\1f3b5' */ +.icon-search:before { content: '\1f50d'; } /* '\1f50d' */ +.icon-flashlight:before { content: '\1f526'; } /* '\1f526' */ +.icon-mail:before { content: '\2709'; } /* '✉' */ +.icon-heart:before { content: '\2665'; } /* '♥' */ +.icon-heart-empty:before { content: '\2661'; } /* '♡' */ +.icon-star:before { content: '\2605'; } /* '★' */ +.icon-star-empty:before { content: '\2606'; } /* '☆' */ +.icon-user:before { content: '\1f464'; } +.icon-users:before { content: '\1f465'; } +.icon-user-add:before { content: '\e700'; } /* '' */ +.icon-video:before { content: '\1f3ac'; } +.icon-picture:before { content: '\1f304'; } +.icon-camera:before { content: '\1f4f7'; } +.icon-layout:before { content: '\268f'; } /* '⚏' */ +.icon-menu:before { content: '\2630'; } /* '☰' */ +.icon-check:before { content: '\2713'; } /* '✓' */ +.icon-cancel:before { content: '\2715'; } /* '✕' */ +.icon-cancel-circled:before { content: '\2716'; } /* '✖' */ +.icon-cancel-squared:before { content: '\274e'; } /* '❎' */ +.icon-plus:before { content: '\2b'; } /* '+' */ +.icon-plus-circled:before { content: '\2795'; } /* '➕' */ +.icon-plus-squared:before { content: '\229e'; } /* '⊞' */ +.icon-minus:before { content: '\2d'; } /* '-' */ +.icon-minus-circled:before { content: '\2796'; } /* '➖' */ +.icon-minus-squared:before { content: '\229f'; } /* '⊟' */ +.icon-help:before { content: '\2753'; } /* '❓' */ +.icon-help-circled:before { content: '\e704'; } /* '' */ +.icon-info:before { content: '\2139'; } /* 'ℹ' */ +.icon-info-circled:before { content: '\e705'; } /* '' */ +.icon-back:before { content: '\1f519'; } +.icon-home:before { content: '\2302'; } /* '⌂' */ +.icon-link:before { content: '\1f517'; } +.icon-attach:before { content: '\1f4ce'; } +.icon-lock:before { content: '\1f512'; } +.icon-lock-open:before { content: '\1f513'; } +.icon-eye:before { content: '\e70a'; } /* '' */ +.icon-tag:before { content: '\e70c'; } /* '' */ +.icon-bookmark:before { content: '\1f516'; } +.icon-bookmarks:before { content: '\1f4d1'; } +.icon-flag:before { content: '\2691'; } /* '⚑' */ +.icon-thumbs-up:before { content: '\1f44d'; } +.icon-thumbs-down:before { content: '\1f44e'; } +.icon-download:before { content: '\1f4e5'; } +.icon-upload:before { content: '\1f4e4'; } +.icon-upload-cloud:before { content: '\e711'; } /* '' */ +.icon-reply:before { content: '\e712'; } /* '' */ +.icon-reply-all:before { content: '\e713'; } /* '' */ +.icon-forward:before { content: '\27a6'; } /* '➦' */ +.icon-quote:before { content: '\275e'; } /* '❞' */ +.icon-code:before { content: '\e714'; } /* '' */ +.icon-export:before { content: '\e715'; } /* '' */ +.icon-pencil:before { content: '\270e'; } /* '✎' */ +.icon-feather:before { content: '\2712'; } /* '✒' */ +.icon-print:before { content: '\e716'; } /* '' */ +.icon-retweet:before { content: '\e717'; } /* '' */ +.icon-keyboard:before { content: '\2328'; } /* '⌨' */ +.icon-comment:before { content: '\e718'; } /* '' */ +.icon-chat:before { content: '\e720'; } /* '' */ +.icon-bell:before { content: '\1f514'; } +.icon-attention:before { content: '\26a0'; } /* '⚠' */ +.icon-alert:before { content: '\1f4a5'; } +.icon-vcard:before { content: '\e722'; } /* '' */ +.icon-address:before { content: '\e723'; } /* '' */ +.icon-location:before { content: '\e724'; } /* '' */ +.icon-map:before { content: '\e727'; } /* '' */ +.icon-direction:before { content: '\27a2'; } /* '➢' */ +.icon-compass:before { content: '\e728'; } /* '' */ +.icon-cup:before { content: '\2615'; } /* '☕' */ +.icon-trash:before { content: '\e729'; } /* '' */ +.icon-doc:before { content: '\e730'; } /* '' */ +.icon-docs:before { content: '\e736'; } /* '' */ +.icon-doc-landscape:before { content: '\e737'; } /* '' */ +.icon-doc-text:before { content: '\1f4c4'; } +.icon-doc-text-inv:before { content: '\e731'; } /* '' */ +.icon-newspaper:before { content: '\1f4f0'; } +.icon-book-open:before { content: '\1f4d6'; } +.icon-book:before { content: '\1f4d5'; } +.icon-folder:before { content: '\1f4c1'; } +.icon-archive:before { content: '\e738'; } /* '' */ +.icon-box:before { content: '\1f4e6'; } +.icon-rss:before { content: '\e73a'; } /* '' */ +.icon-phone:before { content: '\1f4de'; } +.icon-cog:before { content: '\2699'; } /* '⚙' */ +.icon-tools:before { content: '\2692'; } /* '⚒' */ +.icon-share:before { content: '\e73c'; } /* '' */ +.icon-shareable:before { content: '\e73e'; } /* '' */ +.icon-basket:before { content: '\e73d'; } /* '' */ +.icon-bag:before { content: '\1f45c'; } +.icon-calendar:before { content: '\1f4c5'; } +.icon-login:before { content: '\e740'; } /* '' */ +.icon-logout:before { content: '\e741'; } /* '' */ +.icon-mic:before { content: '\1f3a4'; } +.icon-mute:before { content: '\1f507'; } +.icon-sound:before { content: '\1f50a'; } +.icon-volume:before { content: '\e742'; } /* '' */ +.icon-clock:before { content: '\1f554'; } +.icon-hourglass:before { content: '\23f3'; } /* '⏳' */ +.icon-lamp:before { content: '\1f4a1'; } +.icon-light-down:before { content: '\1f505'; } +.icon-light-up:before { content: '\1f506'; } +.icon-adjust:before { content: '\25d1'; } /* '◑' */ +.icon-block:before { content: '\1f6ab'; } +.icon-resize-full:before { content: '\e744'; } /* '' */ +.icon-resize-small:before { content: '\e746'; } /* '' */ +.icon-popup:before { content: '\e74c'; } /* '' */ +.icon-publish:before { content: '\e74d'; } /* '' */ +.icon-window:before { content: '\e74e'; } /* '' */ +.icon-arrow-combo:before { content: '\e74f'; } /* '' */ +.icon-down-circled:before { content: '\e758'; } /* '' */ +.icon-left-circled:before { content: '\e759'; } /* '' */ +.icon-right-circled:before { content: '\e75a'; } /* '' */ +.icon-up-circled:before { content: '\e75b'; } /* '' */ +.icon-down-open:before { content: '\e75c'; } /* '' */ +.icon-left-open:before { content: '\e75d'; } /* '' */ +.icon-right-open:before { content: '\e75e'; } /* '' */ +.icon-up-open:before { content: '\e75f'; } /* '' */ +.icon-down-open-mini:before { content: '\e760'; } /* '' */ +.icon-left-open-mini:before { content: '\e761'; } /* '' */ +.icon-right-open-mini:before { content: '\e762'; } /* '' */ +.icon-up-open-mini:before { content: '\e763'; } /* '' */ +.icon-down-open-big:before { content: '\e764'; } /* '' */ +.icon-left-open-big:before { content: '\e765'; } /* '' */ +.icon-right-open-big:before { content: '\e766'; } /* '' */ +.icon-up-open-big:before { content: '\e767'; } /* '' */ +.icon-down:before { content: '\2b07'; } /* '⬇' */ +.icon-left:before { content: '\2b05'; } /* '⬅' */ +.icon-right:before { content: '\27a1'; } /* '➡' */ +.icon-up:before { content: '\2b06'; } /* '⬆' */ +.icon-down-dir:before { content: '\25be'; } /* '▾' */ +.icon-left-dir:before { content: '\25c2'; } /* '◂' */ +.icon-right-dir:before { content: '\25b8'; } /* '▸' */ +.icon-up-dir:before { content: '\25b4'; } /* '▴' */ +.icon-down-bold:before { content: '\e4b0'; } /* '' */ +.icon-left-bold:before { content: '\e4ad'; } /* '' */ +.icon-right-bold:before { content: '\e4ae'; } /* '' */ +.icon-up-bold:before { content: '\e4af'; } /* '' */ +.icon-down-thin:before { content: '\2193'; } /* '↓' */ +.icon-left-thin:before { content: '\2190'; } /* '←' */ +.icon-right-thin:before { content: '\2192'; } /* '→' */ +.icon-up-thin:before { content: '\2191'; } /* '↑' */ +.icon-ccw:before { content: '\27f2'; } /* '⟲' */ +.icon-cw:before { content: '\27f3'; } /* '⟳' */ +.icon-arrows-ccw:before { content: '\1f504'; } +.icon-level-down:before { content: '\21b3'; } /* '↳' */ +.icon-level-up:before { content: '\21b0'; } /* '↰' */ +.icon-shuffle:before { content: '\1f500'; } +.icon-loop:before { content: '\1f501'; } +.icon-switch:before { content: '\21c6'; } /* '⇆' */ +.icon-play:before { content: '\25b6'; } /* '▶' */ +.icon-stop:before { content: '\25a0'; } /* '■' */ +.icon-pause:before { content: '\2389'; } /* '⎉' */ +.icon-record:before { content: '\26ab'; } /* '⚫' */ +.icon-to-end:before { content: '\23ed'; } /* '⏭' */ +.icon-to-start:before { content: '\23ee'; } /* '⏮' */ +.icon-fast-forward:before { content: '\23e9'; } /* '⏩' */ +.icon-fast-backward:before { content: '\23ea'; } /* '⏪' */ +.icon-progress-0:before { content: '\e768'; } /* '' */ +.icon-progress-1:before { content: '\e769'; } /* '' */ +.icon-progress-2:before { content: '\e76a'; } /* '' */ +.icon-progress-3:before { content: '\e76b'; } /* '' */ +.icon-target:before { content: '\1f3af'; } +.icon-palette:before { content: '\1f3a8'; } +.icon-list:before { content: '\e005'; } /* '' */ +.icon-list-add:before { content: '\e003'; } /* '' */ +.icon-signal:before { content: '\1f4f6'; } +.icon-trophy:before { content: '\1f3c6'; } +.icon-battery:before { content: '\1f50b'; } +.icon-back-in-time:before { content: '\e771'; } /* '' */ +.icon-monitor:before { content: '\1f4bb'; } +.icon-mobile:before { content: '\1f4f1'; } +.icon-network:before { content: '\e776'; } /* '' */ +.icon-cd:before { content: '\1f4bf'; } +.icon-inbox:before { content: '\e777'; } /* '' */ +.icon-install:before { content: '\e778'; } /* '' */ +.icon-globe:before { content: '\1f30e'; } +.icon-cloud:before { content: '\2601'; } /* '☁' */ +.icon-cloud-thunder:before { content: '\26c8'; } /* '⛈' */ +.icon-flash:before { content: '\26a1'; } /* '⚡' */ +.icon-moon:before { content: '\263d'; } /* '☽' */ +.icon-flight:before { content: '\2708'; } /* '✈' */ +.icon-paper-plane:before { content: '\e79b'; } /* '' */ +.icon-leaf:before { content: '\1f342'; } +.icon-lifebuoy:before { content: '\e788'; } /* '' */ +.icon-mouse:before { content: '\e789'; } /* '' */ +.icon-briefcase:before { content: '\1f4bc'; } +.icon-suitcase:before { content: '\e78e'; } /* '' */ +.icon-dot:before { content: '\e78b'; } /* '' */ +.icon-dot-2:before { content: '\e78c'; } /* '' */ +.icon-dot-3:before { content: '\e78d'; } /* '' */ +.icon-brush:before { content: '\e79a'; } /* '' */ +.icon-magnet:before { content: '\e7a1'; } /* '' */ +.icon-infinity:before { content: '\221e'; } /* '∞' */ +.icon-erase:before { content: '\232b'; } /* '⌫' */ +.icon-chart-pie:before { content: '\e751'; } /* '' */ +.icon-chart-line:before { content: '\1f4c8'; } +.icon-chart-bar:before { content: '\1f4ca'; } +.icon-chart-area:before { content: '\1f53e'; } +.icon-tape:before { content: '\2707'; } /* '✇' */ +.icon-graduation-cap:before { content: '\1f393'; } +.icon-language:before { content: '\e752'; } /* '' */ +.icon-ticket:before { content: '\1f3ab'; } +.icon-water:before { content: '\1f4a6'; } +.icon-droplet:before { content: '\1f4a7'; } +.icon-air:before { content: '\e753'; } /* '' */ +.icon-credit-card:before { content: '\1f4b3'; } +.icon-floppy:before { content: '\1f4be'; } +.icon-clipboard:before { content: '\1f4cb'; } +.icon-megaphone:before { content: '\1f4e3'; } +.icon-database:before { content: '\e754'; } /* '' */ +.icon-drive:before { content: '\e755'; } /* '' */ +.icon-bucket:before { content: '\e756'; } /* '' */ +.icon-thermometer:before { content: '\e757'; } /* '' */ +.icon-key:before { content: '\1f511'; } +.icon-flow-cascade:before { content: '\e790'; } /* '' */ +.icon-flow-branch:before { content: '\e791'; } /* '' */ +.icon-flow-tree:before { content: '\e792'; } /* '' */ +.icon-flow-line:before { content: '\e793'; } /* '' */ +.icon-flow-parallel:before { content: '\e794'; } /* '' */ +.icon-rocket:before { content: '\1f680'; } +.icon-gauge:before { content: '\e7a2'; } /* '' */ +.icon-traffic-cone:before { content: '\e7a3'; } /* '' */ +.icon-cc:before { content: '\e7a5'; } /* '' */ +.icon-cc-by:before { content: '\e7a6'; } /* '' */ +.icon-cc-nc:before { content: '\e7a7'; } /* '' */ +.icon-cc-nc-eu:before { content: '\e7a8'; } /* '' */ +.icon-cc-nc-jp:before { content: '\e7a9'; } /* '' */ +.icon-cc-sa:before { content: '\e7aa'; } /* '' */ +.icon-cc-nd:before { content: '\e7ab'; } /* '' */ +.icon-cc-pd:before { content: '\e7ac'; } /* '' */ +.icon-cc-zero:before { content: '\e7ad'; } /* '' */ +.icon-cc-share:before { content: '\e7ae'; } /* '' */ +.icon-cc-remix:before { content: '\e7af'; } /* '' */ +.icon-github:before { content: '\f300'; } /* '' */ +.icon-github-circled:before { content: '\f301'; } /* '' */ +.icon-flickr:before { content: '\f303'; } /* '' */ +.icon-flickr-circled:before { content: '\f304'; } /* '' */ +.icon-vimeo:before { content: '\f306'; } /* '' */ +.icon-vimeo-circled:before { content: '\f307'; } /* '' */ +.icon-twitter:before { content: '\f309'; } /* '' */ +.icon-twitter-circled:before { content: '\f30a'; } /* '' */ +.icon-facebook:before { content: '\f30c'; } /* '' */ +.icon-facebook-circled:before { content: '\f30d'; } /* '' */ +.icon-facebook-squared:before { content: '\f30e'; } /* '' */ +.icon-gplus:before { content: '\f30f'; } /* '' */ +.icon-gplus-circled:before { content: '\f310'; } /* '' */ +.icon-pinterest:before { content: '\f312'; } /* '' */ +.icon-pinterest-circled:before { content: '\f313'; } /* '' */ +.icon-tumblr:before { content: '\f315'; } /* '' */ +.icon-tumblr-circled:before { content: '\f316'; } /* '' */ +.icon-linkedin:before { content: '\f318'; } /* '' */ +.icon-linkedin-circled:before { content: '\f319'; } /* '' */ +.icon-dribbble:before { content: '\f31b'; } /* '' */ +.icon-dribbble-circled:before { content: '\f31c'; } /* '' */ +.icon-stumbleupon:before { content: '\f31e'; } /* '' */ +.icon-stumbleupon-circled:before { content: '\f31f'; } /* '' */ +.icon-lastfm:before { content: '\f321'; } /* '' */ +.icon-lastfm-circled:before { content: '\f322'; } /* '' */ +.icon-rdio:before { content: '\f324'; } /* '' */ +.icon-rdio-circled:before { content: '\f325'; } /* '' */ +.icon-spotify:before { content: '\f327'; } /* '' */ +.icon-spotify-circled:before { content: '\f328'; } /* '' */ +.icon-qq:before { content: '\f32a'; } /* '' */ +.icon-instagram:before { content: '\f32d'; } /* '' */ +.icon-dropbox:before { content: '\f330'; } /* '' */ +.icon-evernote:before { content: '\f333'; } /* '' */ +.icon-flattr:before { content: '\f336'; } /* '' */ +.icon-skype:before { content: '\f339'; } /* '' */ +.icon-skype-circled:before { content: '\f33a'; } /* '' */ +.icon-renren:before { content: '\f33c'; } /* '' */ +.icon-sina-weibo:before { content: '\f33f'; } /* '' */ +.icon-paypal:before { content: '\f342'; } /* '' */ +.icon-picasa:before { content: '\f345'; } /* '' */ +.icon-soundcloud:before { content: '\f348'; } /* '' */ +.icon-mixi:before { content: '\f34b'; } /* '' */ +.icon-behance:before { content: '\f34e'; } /* '' */ +.icon-google-circles:before { content: '\f351'; } /* '' */ +.icon-vkontakte:before { content: '\f354'; } /* '' */ +.icon-smashing:before { content: '\f357'; } /* '' */ +.icon-sweden:before { content: '\f601'; } /* '' */ +.icon-db-shape:before { content: '\f600'; } /* '' */ +.icon-logo-db:before { content: '\f603'; } /* '' */ + diff -r aff75b69db92 -r 2c48c091b6a2 web/data/cubicweb.rhythm.js --- a/web/data/cubicweb.rhythm.js Tue Jul 02 17:09:04 2013 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,7 +0,0 @@ -$(document).ready(function() { - $('a.rhythm').click(function (event){ - $('div#pageContent').toggleClass('rhythm_bg'); - $('div#page').toggleClass('rhythm_bg'); - event.preventDefault(); - }); -}); diff -r aff75b69db92 -r 2c48c091b6a2 web/data/cubicweb.widgets.js --- a/web/data/cubicweb.widgets.js Tue Jul 02 17:09:04 2013 +0200 +++ b/web/data/cubicweb.widgets.js Mon Jan 13 13:47:47 2014 +0100 @@ -92,7 +92,8 @@ if (($(instanceData.userInput).attr('cubicweb:initialvalue') !== undefined) && !instanceData.hiddenInput){ hiHandlers.initializeHiddenInput(instanceData); } - $.ui.autocomplete.prototype._search = methods.search; + $.ui.autocomplete.prototype._value = methods._value; + $.data(this, 'settings', settings); if (settings.multiple) { $.ui.autocomplete.filter = methods.multiple.makeFilter(this); $(this).bind({ @@ -125,6 +126,20 @@ }); }, + _value: function() { + /* We extend the widget with the ability to lookup and + handle several terms at once ('multiple' option). E.g.: + toto, titi, tu.... The autocompletion must be + performed only on the last of such a list of terms. + */ + var settings = $(this.element).data('settings'); + var value = this.valueMethod.apply( this.element, arguments ); + if (settings.multiple & arguments.length === 0) { + return extractLast(value); + } + return value + }, + multiple: { focus: function() { // prevent value inserted on focus @@ -140,7 +155,7 @@ return false; }, keydown: function(evt) { - if ($(this).data('autocomplete').menu.active && evt.keyCode == $.ui.keyCode.TAB) { + if (evt.keyCode == $.ui.keyCode.TAB) { evt.preventDefault(); } }, @@ -161,13 +176,7 @@ methods.resetValues(instanceData); } }, - search: function(value) { - this.element.addClass("ui-autocomplete-loading"); - if (this.options.multiple) { - value = extractLast(value); - } - this.source({term: value}, this.response); - }, + ensureExactMatch: function(evt) { var instanceData = $(this).data('cwautocomplete'); if (evt.keyCode == $.ui.keyCode.ENTER || evt.keyCode == $.ui.keyCode.TAB) { @@ -179,6 +188,7 @@ } } }, + resetValues: function(instanceData){ $(instanceData.userInput).val(''); $(instanceData.hiddenInput).val(''); @@ -544,105 +554,77 @@ // IE things can not handle hide/show options on select, this cloned list solition (should propably have 2 widgets) (function ($) { - var defaultSettings = { - bindDblClick: true - }; + var methods = { - __init__: function(fromSelect, toSelect, options) { - var settings = $.extend({}, defaultSettings, options); - var bindDblClick = settings['bindDblClick']; - var $fromNode = $(cw.jqNode(fromSelect)); - var clonedSelect = $fromNode.clone(); - var $toNode = $(cw.jqNode(toSelect)); - var $addButton = $(this.find('.cwinoutadd')[0]); - var $removeButton = $(this.find('.cwinoutremove')[0]); - // bind buttons - var name = this.attr('id'); - var instanceData = {'fromNode':fromSelect, - 'toNode':toSelect, - 'cloned':clonedSelect, - 'bindDblClick':bindDblClick, - 'name': name}; - $addButton.bind('click', {'instanceData':instanceData}, methods.inOutWidgetAddValues); - $removeButton.bind('click', {'instanceData':instanceData}, methods.inOutWidgetRemoveValues); - if(bindDblClick){ - $toNode.bind('dblclick', {'instanceData': instanceData}, methods.inOutWidgetRemoveValues); - } - methods.inOutWidgetRemplaceSelect($fromNode, $toNode, clonedSelect, bindDblClick, name); - }, + __init__: function(fromSelect, toSelect) { + // closed over state + var state = {'$fromNode' : $(cw.escape('#' + fromSelect)), + '$toNode' : $(cw.escape('#' + toSelect)), + 'name' : this.attr('id')}; - inOutWidgetRemplaceSelect: function($fromNode, $toNode, clonedSelect, bindDblClick, name){ - var $newSelect = clonedSelect.clone(); - $toNode.find('option').each(function() { - $newSelect.find('$(this)[value='+$(this).val()+']').remove(); - }); - var fromparent = $fromNode.parent(); - if (bindDblClick) { - //XXX jQuery live binding does not seem to work here - $newSelect.bind('dblclick', {'instanceData': {'fromNode':$fromNode.attr('id'), - 'toNode': $toNode.attr('id'), - 'cloned':clonedSelect, - 'bindDblClick':bindDblClick, - 'name': name}}, - methods.inOutWidgetAddValues); - } - $fromNode.remove(); - fromparent.append($newSelect); - }, + function sortoptions($optionlist) { + var $sorted = $optionlist.find('option').sort(function(opt1, opt2) { + return $(opt1).text() > $(opt2).text() ? 1 : -1; + }); + // this somehow translates to an inplace sort + $optionlist.append($sorted); + }; + sortoptions(state.$fromNode); + sortoptions(state.$toNode); - inOutWidgetAddValues: function(event){ - var $fromNode = $(cw.jqNode(event.data.instanceData.fromNode)); - var $toNode = $(cw.jqNode(event.data.instanceData.toNode)); - $fromNode.find('option:selected').each(function() { - var option = $(this); - var newoption = OPTION({'value':option.val()}, - value=option.text()); - $toNode.append(newoption); - var hiddenInput = INPUT({ - type: "hidden", name: event.data.instanceData.name, - value:option.val() + // will move selected options from one list to the other + // and call an option handler on each option + function moveoptions ($fromlist, $tolist, opthandler) { + $fromlist.find('option:selected').each(function(index, option) { + var $option = $(option); + // add a new option to the target list + $tolist.append(OPTION({'value' : $option.val()}, + $option.text())); + // process callback on the option + opthandler.call(null, $option); + // remove option from the source list + $option.remove(); }); - $toNode.parent().append(hiddenInput); - }); - methods.inOutWidgetRemplaceSelect($fromNode, $toNode, event.data.instanceData.cloned, - event.data.instanceData.bindDblClick, - event.data.instanceData.name); - // for ie 7 : ie does not resize correctly the select - if($.browser.msie && $.browser.version.substr(0,1) < 8){ - var p = $toNode.parent(); - var newtoNode = $toNode.clone(); - if (event.data.instanceData.bindDblClick) { - newtoNode.bind('dblclick', {'fromNode': $fromNode.attr('id'), - 'toNode': $toNode.attr('id'), - 'cloned': event.data.instanceData.cloned, - 'bindDblClick': true, - 'name': event.data.instanceData.name}, - methods.inOutWidgetRemoveValues); - } - $toNode.remove(); - p.append(newtoNode); - } - }, + // re-sort both lists + sortoptions($fromlist); + sortoptions($tolist); + }; + + function addvalues () { + moveoptions(state.$fromNode, state.$toNode, function ($option) { + // add an hidden input for the edit controller + var hiddenInput = INPUT({ + type: 'hidden', name: state.name, + value : $option.val() + }); + state.$toNode.parent().append(hiddenInput); + }); + }; - inOutWidgetRemoveValues: function(event){ - var $fromNode = $(cw.jqNode(event.data.instanceData.toNode)); - var $toNode = $(cw.jqNode(event.data.instanceData.fromNode)); - var name = event.data.instanceData.name.replace(':', '\\:'); - $fromNode.find('option:selected').each(function(){ - var option = $(this); - var newoption = OPTION({'value':option.val()}, - value=option.text()); - option.remove(); - $fromNode.parent().find('input[name]='+ name).each(function() { - $(this).val()==option.val()?$(this).remove():null; - }); - }); - methods.inOutWidgetRemplaceSelect($toNode, $fromNode, event.data.instanceData.cloned, - event.data.instanceData.bindDblClick, - event.data.instanceData.name); + function removevalues () { + moveoptions(state.$toNode, state.$fromNode, function($option) { + // remove hidden inputs for the edit controller + var selector = 'input[name=' + cw.escape(state.name) + ']' + state.$toNode.parent().find(selector).each(function(index, input) { + if ($(input).val() == $option.val()) { + $(input).remove(); + } + }); + }); + }; + + var $this = $(this); + $this.find('.cwinoutadd').bind( // 'add >>>' symbol + 'click', {'state' : state}, addvalues); + $this.find('.cwinoutremove').bind( // 'remove <<<' symbol + 'click', {'state' : state}, removevalues); + + state.$fromNode.bind('dblclick', {'state': state}, addvalues); + state.$toNode.bind('dblclick', {'state': state}, removevalues); + } }; - $.fn.cwinoutwidget = function(fromSelect, toSelect, options){ - return methods.__init__.apply(this, [fromSelect, toSelect, options]); + $.fn.cwinoutwidget = function(fromSelect, toSelect) { + return methods.__init__.apply(this, [fromSelect, toSelect]); }; -})(jQuery); \ No newline at end of file +})(jQuery); diff -r aff75b69db92 -r 2c48c091b6a2 web/data/entypo.eot Binary file web/data/entypo.eot has changed diff -r aff75b69db92 -r 2c48c091b6a2 web/data/entypo.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/data/entypo.svg Mon Jan 13 13:47:47 2014 +0100 @@ -0,0 +1,295 @@ + + + +Copyright (C) 2012 by Daniel Bruce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff -r aff75b69db92 -r 2c48c091b6a2 web/data/entypo.ttf Binary file web/data/entypo.ttf has changed diff -r aff75b69db92 -r 2c48c091b6a2 web/data/entypo.woff Binary file web/data/entypo.woff has changed diff -r aff75b69db92 -r 2c48c091b6a2 web/data/jquery-migrate.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/data/jquery-migrate.js Mon Jan 13 13:47:47 2014 +0100 @@ -0,0 +1,521 @@ +/*! + * jQuery Migrate - v1.2.1 - 2013-05-08 + * https://github.com/jquery/jquery-migrate + * Copyright 2005, 2013 jQuery Foundation, Inc. and other contributors; Licensed MIT + */ +(function( jQuery, window, undefined ) { +// See http://bugs.jquery.com/ticket/13335 +// "use strict"; + + +var warnedAbout = {}; + +// List of warnings already given; public read only +jQuery.migrateWarnings = []; + +// Set to true to prevent console output; migrateWarnings still maintained +// jQuery.migrateMute = false; + +// Show a message on the console so devs know we're active +if ( !jQuery.migrateMute && window.console && window.console.log ) { + window.console.log("JQMIGRATE: Logging is active"); +} + +// Set to false to disable traces that appear with warnings +if ( jQuery.migrateTrace === undefined ) { + jQuery.migrateTrace = true; +} + +// Forget any warnings we've already given; public +jQuery.migrateReset = function() { + warnedAbout = {}; + jQuery.migrateWarnings.length = 0; +}; + +function migrateWarn( msg) { + var console = window.console; + if ( !warnedAbout[ msg ] ) { + warnedAbout[ msg ] = true; + jQuery.migrateWarnings.push( msg ); + if ( console && console.warn && !jQuery.migrateMute ) { + console.warn( "JQMIGRATE: " + msg ); + if ( jQuery.migrateTrace && console.trace ) { + console.trace(); + } + } + } +} + +function migrateWarnProp( obj, prop, value, msg ) { + if ( Object.defineProperty ) { + // On ES5 browsers (non-oldIE), warn if the code tries to get prop; + // allow property to be overwritten in case some other plugin wants it + try { + Object.defineProperty( obj, prop, { + configurable: true, + enumerable: true, + get: function() { + migrateWarn( msg ); + return value; + }, + set: function( newValue ) { + migrateWarn( msg ); + value = newValue; + } + }); + return; + } catch( err ) { + // IE8 is a dope about Object.defineProperty, can't warn there + } + } + + // Non-ES5 (or broken) browser; just set the property + jQuery._definePropertyBroken = true; + obj[ prop ] = value; +} + +if ( document.compatMode === "BackCompat" ) { + // jQuery has never supported or tested Quirks Mode + migrateWarn( "jQuery is not compatible with Quirks Mode" ); +} + + +var attrFn = jQuery( "", { size: 1 } ).attr("size") && jQuery.attrFn, + oldAttr = jQuery.attr, + valueAttrGet = jQuery.attrHooks.value && jQuery.attrHooks.value.get || + function() { return null; }, + valueAttrSet = jQuery.attrHooks.value && jQuery.attrHooks.value.set || + function() { return undefined; }, + rnoType = /^(?:input|button)$/i, + rnoAttrNodeType = /^[238]$/, + rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, + ruseDefault = /^(?:checked|selected)$/i; + +// jQuery.attrFn +migrateWarnProp( jQuery, "attrFn", attrFn || {}, "jQuery.attrFn is deprecated" ); + +jQuery.attr = function( elem, name, value, pass ) { + var lowerName = name.toLowerCase(), + nType = elem && elem.nodeType; + + if ( pass ) { + // Since pass is used internally, we only warn for new jQuery + // versions where there isn't a pass arg in the formal params + if ( oldAttr.length < 4 ) { + migrateWarn("jQuery.fn.attr( props, pass ) is deprecated"); + } + if ( elem && !rnoAttrNodeType.test( nType ) && + (attrFn ? name in attrFn : jQuery.isFunction(jQuery.fn[name])) ) { + return jQuery( elem )[ name ]( value ); + } + } + + // Warn if user tries to set `type`, since it breaks on IE 6/7/8; by checking + // for disconnected elements we don't warn on $( "