From e154eb58c810acda244fba83a3643cf70762ccc7 Mon Sep 17 00:00:00 2001 From: winkidney Date: Tue, 19 Feb 2019 18:09:13 +0800 Subject: [PATCH 01/34] Feature: Add DRF and its tools as dependencies --- Pipfile | 3 + Pipfile.lock | 246 +++++++++++++++++++++++++++++---------------------- 2 files changed, 141 insertions(+), 108 deletions(-) diff --git a/Pipfile b/Pipfile index 77bb400..2c884f6 100644 --- a/Pipfile +++ b/Pipfile @@ -19,3 +19,6 @@ mock = "*" factory-boy = "<2.0,>=1.3" gunicorn = "*" "psycopg2" = "*" +djangorestframework = "*" +markdown = "*" +django-filter = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 4a60cef..7ece73a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "68c12441be13a252f7fdd1b5532c7befdfa56fc8307ab75a2b197a1946a54bf2" + "sha256": "3ee7f6ec06170f6e1179faa9a279fb78800ad45e39b80cefa9df9ea4d4362925" }, "pipfile-spec": 6, "requires": {}, @@ -16,10 +16,10 @@ "default": { "certifi": { "hashes": [ - "sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638", - "sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a" + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" ], - "version": "==2018.8.24" + "version": "==2018.11.29" }, "chardet": { "hashes": [ @@ -30,11 +30,11 @@ }, "django": { "hashes": [ - "sha256:8176ac7985fe6737ce3d6b2531b4a2453cb7c3377c9db00bacb2b3320f4a1311", - "sha256:b18235d82426f09733d2de9910cee975cf52ff05e5f836681eb957d105a05a40" + "sha256:0a73696e0ac71ee6177103df984f9c1e07cd297f080f8ec4dc7c6f3fb74395b5", + "sha256:43a99da08fee329480d27860d68279945b7d8bf7b537388ee2c8938c709b2041" ], "index": "pypi", - "version": "==1.11.15" + "version": "==1.11.20" }, "django-appconf": { "hashes": [ @@ -59,6 +59,14 @@ "index": "pypi", "version": "==2.2" }, + "django-filter": { + "hashes": [ + "sha256:3dafb7d2810790498895c22a1f31b2375795910680ac9c1432821cbedb1e176d", + "sha256:a3014de317bef0cd43075a0f08dfa1d319a7ccc5733c3901fb860da70b0dda68" + ], + "index": "pypi", + "version": "==2.1.0" + }, "django-taggit": { "hashes": [ "sha256:a21cbe7e0879f1364eef1c88a2eda89d593bf000ebf51c3f00423c6927075dce", @@ -69,10 +77,18 @@ }, "django-tastypie": { "hashes": [ - "sha256:1fbf61ec7467eec70bd1abcb14e3b1dc67e47cc3642ad16ed8a3709f4140678b" + "sha256:a3a2413510009649e0eac885ead96891c783ced788fd94231dc2f72b7a1b4c04" ], "index": "pypi", - "version": "==0.14.1" + "version": "==0.14.2" + }, + "djangorestframework": { + "hashes": [ + "sha256:79c6efbb2514bc50cf25906d7c0a5cfead714c7af667ff4bd110312cd380ae66", + "sha256:a4138613b67e3a223be6c97f53b13d759c5b90d2b433bad670b8ebf95402075f" + ], + "index": "pypi", + "version": "==3.9.1" }, "factory-boy": { "hashes": [ @@ -91,10 +107,18 @@ }, "idna": { "hashes": [ - "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", - "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" ], - "version": "==2.7" + "version": "==2.8" + }, + "markdown": { + "hashes": [ + "sha256:c00429bd503a47ec88d5e30a751e147dcb4c6889663cd3e2ba0afe858e009baa", + "sha256:d02e0f9b04c500cde6637c11ad7c72671f359b87b9fe924b2383649d8841db7c" + ], + "index": "pypi", + "version": "==3.0.1" }, "mock": { "hashes": [ @@ -106,89 +130,89 @@ }, "pbr": { "hashes": [ - "sha256:1b8be50d938c9bb75d0eaf7eda111eec1bf6dc88a62a6412e33bf077457e0f45", - "sha256:b486975c0cafb6beeb50ca0e17ba047647f229087bd74e37f4a7e2cac17d2caa" + "sha256:a7953f66e1f82e4b061f43096a4bcc058f7d3d41de9b94ac871770e8bdd831a2", + "sha256:d717573351cfe09f49df61906cd272abaa759b3e91744396b804965ff7bff38b" ], - "version": "==4.2.0" + "version": "==5.1.2" }, "pillow": { "hashes": [ - "sha256:00def5b638994f888d1058e4d17c86dec8e1113c3741a0a8a659039aec59a83a", - "sha256:026449b64e559226cdb8e6d8c931b5965d8fc90ec18ebbb0baa04c5b36503c72", - "sha256:03dbb224ee196ef30ed2156d41b579143e1efeb422974719a5392fc035e4f574", - "sha256:03eb0e04f929c102ae24bc436bf1c0c60a4e63b07ebd388e84d8b219df3e6acd", - "sha256:1be66b9a89e367e7d20d6cae419794997921fe105090fafd86ef39e20a3baab2", - "sha256:1e977a3ed998a599bda5021fb2c2889060617627d3ae228297a529a082a3cd5c", - "sha256:22cf3406d135cfcc13ec6228ade774c8461e125c940e80455f500638429be273", - "sha256:24adccf1e834f82718c7fc8e3ec1093738da95144b8b1e44c99d5fc7d3e9c554", - "sha256:2a3e362c97a5e6a259ee9cd66553292a1f8928a5bdfa3622fdb1501570834612", - "sha256:3832e26ecbc9d8a500821e3a1d3765bda99d04ae29ffbb2efba49f5f788dc934", - "sha256:4fd1f0c2dc02aaec729d91c92cd85a2df0289d88e9f68d1e8faba750bb9c4786", - "sha256:4fda62030f2c515b6e2e673c57caa55cb04026a81968f3128aae10fc28e5cc27", - "sha256:5044d75a68b49ce36a813c82d8201384207112d5d81643937fc758c05302f05b", - "sha256:522184556921512ec484cb93bd84e0bab915d0ac5a372d49571c241a7f73db62", - "sha256:5914cff11f3e920626da48e564be6818831713a3087586302444b9c70e8552d9", - "sha256:6661a7908d68c4a133e03dac8178287aa20a99f841ea90beeb98a233ae3fd710", - "sha256:79258a8df3e309a54c7ef2ef4a59bb8e28f7e4a8992a3ad17c24b1889ced44f3", - "sha256:7d74c20b8f1c3e99d3f781d3b8ff5abfefdd7363d61e23bdeba9992ff32cc4b4", - "sha256:81918afeafc16ba5d9d0d4e9445905f21aac969a4ebb6f2bff4b9886da100f4b", - "sha256:8194d913ca1f459377c8a4ed8f9b7ad750068b8e0e3f3f9c6963fcc87a84515f", - "sha256:84d5d31200b11b3c76fab853b89ac898bf2d05c8b3da07c1fcc23feb06359d6e", - "sha256:989981db57abffb52026b114c9a1f114c7142860a6d30a352d28f8cbf186500b", - "sha256:a3d7511d3fad1618a82299aab71a5fceee5c015653a77ffea75ced9ef917e71a", - "sha256:b3ef168d4d6fd4fa6685aef7c91400f59f7ab1c0da734541f7031699741fb23f", - "sha256:c1c5792b6e74bbf2af0f8e892272c2a6c48efa895903211f11b8342e03129fea", - "sha256:c5dcb5a56aebb8a8f2585042b2f5c496d7624f0bcfe248f0cc33ceb2fd8d39e7", - "sha256:e2bed4a04e2ca1050bb5f00865cf2f83c0b92fd62454d9244f690fcd842e27a4", - "sha256:e87a527c06319428007e8c30511e1f0ce035cb7f14bb4793b003ed532c3b9333", - "sha256:f63e420180cbe22ff6e32558b612e75f50616fc111c5e095a4631946c782e109", - "sha256:f8b3d413c5a8f84b12cd4c5df1d8e211777c9852c6be3ee9c094b626644d3eab" + "sha256:051de330a06c99d6f84bcf582960487835bcae3fc99365185dc2d4f65a390c0e", + "sha256:0ae5289948c5e0a16574750021bd8be921c27d4e3527800dc9c2c1d2abc81bf7", + "sha256:0b1efce03619cdbf8bcc61cfae81fcda59249a469f31c6735ea59badd4a6f58a", + "sha256:163136e09bd1d6c6c6026b0a662976e86c58b932b964f255ff384ecc8c3cefa3", + "sha256:18e912a6ccddf28defa196bd2021fe33600cbe5da1aa2f2e2c6df15f720b73d1", + "sha256:24ec3dea52339a610d34401d2d53d0fb3c7fd08e34b20c95d2ad3973193591f1", + "sha256:267f8e4c0a1d7e36e97c6a604f5b03ef58e2b81c1becb4fccecddcb37e063cc7", + "sha256:3273a28734175feebbe4d0a4cde04d4ed20f620b9b506d26f44379d3c72304e1", + "sha256:4c678e23006798fc8b6f4cef2eaad267d53ff4c1779bd1af8725cc11b72a63f3", + "sha256:4d4bc2e6bb6861103ea4655d6b6f67af8e5336e7216e20fff3e18ffa95d7a055", + "sha256:505738076350a337c1740a31646e1de09a164c62c07db3b996abdc0f9d2e50cf", + "sha256:5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f", + "sha256:5d95cb9f6cced2628f3e4de7e795e98b2659dfcc7176ab4a01a8b48c2c2f488f", + "sha256:7eda4c737637af74bac4b23aa82ea6fbb19002552be85f0b89bc27e3a762d239", + "sha256:801ddaa69659b36abf4694fed5aa9f61d1ecf2daaa6c92541bbbbb775d97b9fe", + "sha256:825aa6d222ce2c2b90d34a0ea31914e141a85edefc07e17342f1d2fdf121c07c", + "sha256:9c215442ff8249d41ff58700e91ef61d74f47dfd431a50253e1a1ca9436b0697", + "sha256:a3d90022f2202bbb14da991f26ca7a30b7e4c62bf0f8bf9825603b22d7e87494", + "sha256:a631fd36a9823638fe700d9225f9698fb59d049c942d322d4c09544dc2115356", + "sha256:a6523a23a205be0fe664b6b8747a5c86d55da960d9586db039eec9f5c269c0e6", + "sha256:a756ecf9f4b9b3ed49a680a649af45a8767ad038de39e6c030919c2f443eb000", + "sha256:b117287a5bdc81f1bac891187275ec7e829e961b8032c9e5ff38b70fd036c78f", + "sha256:ba04f57d1715ca5ff74bb7f8a818bf929a204b3b3c2c2826d1e1cc3b1c13398c", + "sha256:cd878195166723f30865e05d87cbaf9421614501a4bd48792c5ed28f90fd36ca", + "sha256:cee815cc62d136e96cf76771b9d3eb58e0777ec18ea50de5cfcede8a7c429aa8", + "sha256:d1722b7aa4b40cf93ac3c80d3edd48bf93b9208241d166a14ad8e7a20ee1d4f3", + "sha256:d7c1c06246b05529f9984435fc4fa5a545ea26606e7f450bdbe00c153f5aeaad", + "sha256:e9c8066249c040efdda84793a2a669076f92a301ceabe69202446abb4c5c5ef9", + "sha256:f227d7e574d050ff3996049e086e1f18c7bd2d067ef24131e50a1d3fe5831fbc", + "sha256:fc9a12aad714af36cf3ad0275a96a733526571e52710319855628f476dcb144e" ], "index": "pypi", - "version": "==5.2.0" + "version": "==5.4.1" }, "psycopg2": { "hashes": [ - "sha256:0b9e48a1c1505699a64ac58815ca99104aacace8321e455072cee4f7fe7b2698", - "sha256:0f4c784e1b5a320efb434c66a50b8dd7e30a7dc047e8f45c0a8d2694bfe72781", - "sha256:0fdbaa32c9eb09ef09d425dc154628fca6fa69d2f7c1a33f889abb7e0efb3909", - "sha256:11fbf688d5c953c0a5ba625cc42dea9aeb2321942c7c5ed9341a68f865dc8cb1", - "sha256:19eaac4eb25ab078bd0f28304a0cb08702d120caadfe76bb1e6846ed1f68635e", - "sha256:3232ec1a3bf4dba97fbf9b03ce12e4b6c1d01ea3c85773903a67ced725728232", - "sha256:36f8f9c216fcca048006f6dd60e4d3e6f406afde26cfb99e063f137070139eaf", - "sha256:59c1a0e4f9abe970062ed35d0720935197800a7ef7a62b3a9e3a70588d9ca40b", - "sha256:6506c5ff88750948c28d41852c09c5d2a49f51f28c6d90cbf1b6808e18c64e88", - "sha256:6bc3e68ee16f571681b8c0b6d5c0a77bef3c589012352b3f0cf5520e674e9d01", - "sha256:6dbbd7aabbc861eec6b910522534894d9dbb507d5819bc982032c3ea2e974f51", - "sha256:6e737915de826650d1a5f7ff4ac6cf888a26f021a647390ca7bafdba0e85462b", - "sha256:6ed9b2cfe85abc720e8943c1808eeffd41daa73e18b7c1e1a228b0b91f768ccc", - "sha256:711ec617ba453fdfc66616db2520db3a6d9a891e3bf62ef9aba4c95bb4e61230", - "sha256:844dacdf7530c5c612718cf12bc001f59b2d9329d35b495f1ff25045161aa6af", - "sha256:86b52e146da13c896e50c5a3341a9448151f1092b1a4153e425d1e8b62fec508", - "sha256:985c06c2a0f227131733ae58d6a541a5bc8b665e7305494782bebdb74202b793", - "sha256:a86dfe45f4f9c55b1a2312ff20a59b30da8d39c0e8821d00018372a2a177098f", - "sha256:aa3cd07f7f7e3183b63d48300666f920828a9dbd7d7ec53d450df2c4953687a9", - "sha256:b1964ed645ef8317806d615d9ff006c0dadc09dfc54b99ae67f9ba7a1ec9d5d2", - "sha256:b2abbff9e4141484bb89b96eb8eae186d77bc6d5ffbec6b01783ee5c3c467351", - "sha256:cc33c3a90492e21713260095f02b12bee02b8d1f2c03a221d763ce04fa90e2e9", - "sha256:d7de3bf0986d777807611c36e809b77a13bf1888f5c8db0ebf24b47a52d10726", - "sha256:db5e3c52576cc5b93a959a03ccc3b02cb8f0af1fbbdc80645f7a215f0b864f3a", - "sha256:e168aa795ffbb11379c942cf95bf813c7db9aa55538eb61de8c6815e092416f5", - "sha256:e9ca911f8e2d3117e5241d5fa9aaa991cb22fb0792627eeada47425d706b5ec8", - "sha256:eccf962d41ca46e6326b97c8fe0a6687b58dfc1a5f6540ed071ff1474cea749e", - "sha256:efa19deae6b9e504a74347fe5e25c2cb9343766c489c2ae921b05f37338b18d1", - "sha256:f4b0460a21f784abe17b496f66e74157a6c36116fa86da8bf6aa028b9e8ad5fe", - "sha256:f93d508ca64d924d478fb11e272e09524698f0c581d9032e68958cfbdd41faef" + "sha256:02445ebbb3a11a3fe8202c413d5e6faf38bb75b4e336203ee144ca2c46529f94", + "sha256:0e9873e60f98f0c52339abf8f0339d1e22bfe5aae0bcf7aabd40c055175035ec", + "sha256:1148a5eb29073280bf9057c7fc45468592c1bb75a28f6df1591adb93c8cb63d0", + "sha256:259a8324e109d4922b0fcd046e223e289830e2568d6f4132a3702439e5fd532b", + "sha256:28dffa9ed4595429e61bacac41d3f9671bb613d1442ff43bcbec63d4f73ed5e8", + "sha256:314a74302d4737a3865d40ea50e430ce1543c921ba10f39d562e807cfe2edf2a", + "sha256:36b60201b6d215d7658a71493fdf6bd5e60ad9a0cffed39906627ff9f4f3afd3", + "sha256:3f9d532bce54c4234161176ff3b8688ff337575ca441ea27597e112dfcd0ee0c", + "sha256:5d222983847b40af989ad96c07fc3f07e47925e463baa5de716be8f805b41d9b", + "sha256:6757a6d2fc58f7d8f5d471ad180a0bd7b4dd3c7d681f051504fbea7ae29c8d6f", + "sha256:6a0e0f1e74edb0ab57d89680e59e7bfefad2bfbdf7c80eb38304d897d43674bb", + "sha256:6ca703ccdf734e886a1cf53eb702261110f6a8b0ed74bcad15f1399f74d3f189", + "sha256:8513b953d8f443c446aa79a4cc8a898bd415fc5e29349054f03a7d696d495542", + "sha256:9262a5ce2038570cb81b4d6413720484cb1bc52c064b2f36228d735b1f98b794", + "sha256:97441f851d862a0c844d981cbee7ee62566c322ebb3d68f86d66aa99d483985b", + "sha256:a07feade155eb8e69b54dd6774cf6acf2d936660c61d8123b8b6b1f9247b67d6", + "sha256:a9b9c02c91b1e3ec1f1886b2d0a90a0ea07cc529cb7e6e472b556bc20ce658f3", + "sha256:ae88216f94728d691b945983140bf40d51a1ff6c7fe57def93949bf9339ed54a", + "sha256:b360ffd17659491f1a6ad7c928350e229c7b7bd83a2b922b6ee541245c7a776f", + "sha256:b4221957ceccf14b2abdabef42d806e791350be10e21b260d7c9ce49012cc19e", + "sha256:b90758e49d5e6b152a460d10b92f8a6ccf318fcc0ee814dcf53f3a6fc5328789", + "sha256:c669ea986190ed05fb289d0c100cc88064351f2b85177cbfd3564c4f4847d18c", + "sha256:d1b61999d15c79cf7f4f7cc9021477aef35277fc52452cf50fd13b713c84424d", + "sha256:de7bb043d1adaaf46e38d47e7a5f703bb3dab01376111e522b07d25e1a79c1e1", + "sha256:e393568e288d884b94d263f2669215197840d097c7e5b0acd1a51c1ea7d1aba8", + "sha256:ed7e0849337bd37d89f2c2b0216a0de863399ee5d363d31b1e5330a99044737b", + "sha256:f153f71c3164665d269a5d03c7fa76ba675c7a8de9dc09a4e2c2cdc9936a7b41", + "sha256:f1fb5a8427af099beb7f65093cbdb52e021b8e6dbdfaf020402a623f4181baf5", + "sha256:f36b333e9f86a2fba960c72b90c34be6ca71819e300f7b1fc3d2b0f0b2c546cd", + "sha256:f4526d078aedd5187d0508aa5f9a01eae6a48a470ed678406da94b4cd6524b7e" ], "index": "pypi", - "version": "==2.7.5" + "version": "==2.7.7" }, "python-dateutil": { "hashes": [ - "sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0", - "sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8" + "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", + "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" ], - "version": "==2.7.3" + "version": "==2.8.0" }, "python-mimeparse": { "hashes": [ @@ -199,10 +223,10 @@ }, "pytz": { "hashes": [ - "sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053", - "sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277" + "sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9", + "sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c" ], - "version": "==2018.5" + "version": "==2018.9" }, "rcssmin": { "hashes": [ @@ -212,11 +236,11 @@ }, "requests": { "hashes": [ - "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1", - "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a" + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" ], "index": "pypi", - "version": "==2.19.1" + "version": "==2.21.0" }, "rjsmin": { "hashes": [ @@ -226,28 +250,34 @@ }, "six": { "hashes": [ - "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", - "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" ], - "version": "==1.11.0" + "version": "==1.12.0" }, "urllib3": { "hashes": [ - "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", - "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" ], - "markers": "python_version < '4' and python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.6' and python_version != '3.3.*'", - "version": "==1.23" + "version": "==1.24.1" } }, "develop": { + "entrypoints": { + "hashes": [ + "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", + "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" + ], + "version": "==0.3" + }, "flake8": { "hashes": [ - "sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0", - "sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37" + "sha256:6d8c66a65635d46d54de59b027a1dda40abbe2275b3164b634835ac9c13fd048", + "sha256:6eab21c6e34df2c05416faa40d0c59963008fff29b6f0ccfe8fa28152ab3e383" ], "index": "pypi", - "version": "==3.5.0" + "version": "==3.7.6" }, "mccabe": { "hashes": [ @@ -258,32 +288,32 @@ }, "pycodestyle": { "hashes": [ - "sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766", - "sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9" + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" ], - "version": "==2.3.1" + "version": "==2.5.0" }, "pyflakes": { "hashes": [ - "sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f", - "sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805" + "sha256:5e8c00e30c464c99e0b501dc160b13a14af7f27d4dffb529c556e30a159e231d", + "sha256:f277f9ca3e55de669fba45b7393a1449009cff5a37d1af10ebb76c52765269cd" ], - "version": "==1.6.0" + "version": "==2.1.0" }, "qrcode": { "hashes": [ - "sha256:037b0db4c93f44586e37f84c3da3f763874fcac85b2974a69a98e399ac78e1bf", - "sha256:de4ffc15065e6ff20a551ad32b6b41264f3c75275675406ddfa8e3530d154be3" + "sha256:3996ee560fc39532910603704c82980ff6d4d5d629f9c3f25f34174ce8606cf5", + "sha256:505253854f607f2abf4d16092c61d4e9d511a3b4392e60bff957a68592b04369" ], "index": "pypi", - "version": "==6.0" + "version": "==6.1" }, "six": { "hashes": [ - "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", - "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" ], - "version": "==1.11.0" + "version": "==1.12.0" } } } From 50ffa93d46f7fe45489b171af48e026a561508c2 Mon Sep 17 00:00:00 2001 From: winkidney Date: Tue, 19 Feb 2019 18:19:12 +0800 Subject: [PATCH 02/34] Feature: Add basic settings for DRF --- pinry/settings/base.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pinry/settings/base.py b/pinry/settings/base.py index 7191b1c..55def71 100644 --- a/pinry/settings/base.py +++ b/pinry/settings/base.py @@ -14,6 +14,7 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'rest_framework', 'taggit', 'compressor', 'django_images', @@ -139,3 +140,12 @@ IS_TEST = False # User custom settings IMAGE_AUTO_DELETE = True + +# Rest Framework +REST_FRAMEWORK = { + # Use Django's standard `django.contrib.auth` permissions, + # or allow read-only access for unauthenticated users. + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' + ] +} From 220c49a7252ace05e0fcd75ae308dc4309cbe222 Mon Sep 17 00:00:00 2001 From: winkidney Date: Tue, 19 Feb 2019 18:57:36 +0800 Subject: [PATCH 03/34] Feature: add basic drf-api for user/pin --- core/drf_api.py | 117 +++++++++++++++++++++++++++++++++++++++++ core/models.py | 26 +++++++++ core/permissions.py | 42 +++++++++++++++ core/urls.py | 1 + pinry/settings/base.py | 10 +++- pinry/urls.py | 8 +++ 6 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 core/drf_api.py create mode 100644 core/permissions.py diff --git a/core/drf_api.py b/core/drf_api.py new file mode 100644 index 0000000..96f74ff --- /dev/null +++ b/core/drf_api.py @@ -0,0 +1,117 @@ +from rest_framework import serializers, viewsets, routers +from taggit.models import Tag + +from core.models import Image, Pin +from core.permissions import IsOwnerOrReadOnly +from django_images.models import Thumbnail +from django.conf import settings +from users.models import User + + +class UserSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = User + fields = ( + 'username', + 'gravatar', + 'url', + ) + + +class UserViewSet(viewsets.ModelViewSet): + queryset = User.objects.all() + serializer_class = UserSerializer + + +class ThumbnailSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Thumbnail + fields = ( + "image", + "width", + "height", + ) + + +class ImageSerializer(serializers.ModelSerializer): + class Meta: + model = Image + fields = ( + "image", + "width", + "height", + "standard", + "thumbnail", + "square", + ) + + standard = ThumbnailSerializer(read_only=True) + thumbnail = ThumbnailSerializer(read_only=True) + square = ThumbnailSerializer(read_only=True) + + +class TagSerializer(serializers.ModelSerializer): + class Meta: + model = Tag + fields = ("name", ) + + +class PinSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Pin + fields = ( + settings.DRF_URL_FIELD_NAME, + "id", + "submitter", + "url", + "origin", + "description", + "referer", + "image", + "tags", + ) + + tags = serializers.SlugRelatedField( + many=True, + source="tag_list", + queryset=Tag.objects.all(), + slug_field="name", + ) + image = ImageSerializer(required=False) + + def create(self, validated_data): + image_file = validated_data.pop('image') + if validated_data['url']: + image = Image.objects.create_for_url( + validated_data['url'], + validated_data['referer'], + ) + else: + image = Image.objects.create(image=image_file['image']) + pin = Pin.objects.create(image=image, **validated_data) + tags = validated_data.pop('tag_list') + if tags: + pin.tags.set(*tags) + return pin + + def update(self, instance, validated_data): + tags = validated_data.pop('tag_list') + if tags: + instance.tags.set(*tags) + image_file = validated_data.pop('image', None) + if image_file: + image = Image.objects.create(image=image_file['image']) + instance.image = image + return super(PinSerializer, self).update(instance, validated_data) + + +class PinViewSet(viewsets.ModelViewSet): + queryset = Pin.objects.all() + serializer_class = PinSerializer + filter_fields = ('submitter__username', ) + permission_classes = [IsOwnerOrReadOnly("submitter"), ] + + +drf_router = routers.DefaultRouter() +drf_router.register(r'users', UserViewSet) +drf_router.register(r'pins', PinViewSet) diff --git a/core/models.py b/core/models.py index 766a598..59ee382 100644 --- a/core/models.py +++ b/core/models.py @@ -43,9 +43,32 @@ class ImageManager(models.Manager): class Image(BaseImage): objects = ImageManager() + class Sizes: + standard = "standard" + thumbnail = "thumbnail" + square = "square" + class Meta: proxy = True + @property + def standard(self): + return Thumbnail.objects.get( + original=self, size=self.Sizes.standard + ) + + @property + def thumbnail(self): + return Thumbnail.objects.get( + original=self, size=self.Sizes.thumbnail + ) + + @property + def square(self): + return Thumbnail.objects.get( + original=self, size=self.Sizes.square + ) + class Pin(models.Model): submitter = models.ForeignKey(User) @@ -57,6 +80,9 @@ class Pin(models.Model): published = models.DateTimeField(auto_now_add=True) tags = TaggableManager() + def tag_list(self): + return self.tags.all() + def __unicode__(self): return '%s - %s' % (self.submitter, self.published) diff --git a/core/permissions.py b/core/permissions.py new file mode 100644 index 0000000..0df1293 --- /dev/null +++ b/core/permissions.py @@ -0,0 +1,42 @@ +from rest_framework import permissions + + +class IsOwnerOrReadOnly(permissions.IsAuthenticatedOrReadOnly): + """ + Object-level permission to only allow owners of an object to edit it. + Assumes the model instance has an `owner` attribute. + """ + def __init__(self, owner_field_name="owner"): + self.__owner_field_name = owner_field_name + + def __call__(self): + return self + + def has_object_permission(self, request, view, obj): + # Read permissions are allowed to any request, + # so we'll always allow GET, HEAD or OPTIONS requests. + if request.method in permissions.SAFE_METHODS: + return True + + return getattr(obj, self.__owner_field_name) == request.user + + +class OwnerOnly(permissions.IsAuthenticatedOrReadOnly): + + def has_permission(self, request, view): + return request.user.is_authenticated() + + def has_object_permission(self, request, view, obj): + return obj.owner == request.user + + +class SuperUserOnly(permissions.BasePermission): + """ + The request is authenticated as a user, or is a read-only request. + """ + + def has_permission(self, request, view): + return request.user.is_superuser + + def has_object_permission(self, request, view, obj): + return request.user.is_superuser diff --git a/core/urls.py b/core/urls.py index 1a45826..49ef287 100644 --- a/core/urls.py +++ b/core/urls.py @@ -3,6 +3,7 @@ from django.views.generic import TemplateView from tastypie.api import Api +from core.drf_api import drf_router from .api import ImageResource, ThumbnailResource, PinResource, UserResource from .views import CreateImage diff --git a/pinry/settings/base.py b/pinry/settings/base.py index 55def71..bcea051 100644 --- a/pinry/settings/base.py +++ b/pinry/settings/base.py @@ -15,6 +15,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', + 'django_filters', 'taggit', 'compressor', 'django_images', @@ -142,10 +143,17 @@ IS_TEST = False IMAGE_AUTO_DELETE = True # Rest Framework + +DRF_URL_FIELD_NAME = "resource_link" + REST_FRAMEWORK = { # Use Django's standard `django.contrib.auth` permissions, # or allow read-only access for unauthenticated users. 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' - ] + ], + 'DEFAULT_FILTER_BACKENDS': ( + 'django_filters.rest_framework.DjangoFilterBackend', + ), + 'URL_FIELD_NAME': DRF_URL_FIELD_NAME, } diff --git a/pinry/urls.py b/pinry/urls.py index 1311a18..e296fa7 100644 --- a/pinry/urls.py +++ b/pinry/urls.py @@ -4,10 +4,18 @@ from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.contrib import admin from django.views.static import serve +from core.drf_api import drf_router + + admin.autodiscover() urlpatterns = [ + # drf api + url(r'^drf_api/', include(drf_router.urls)), + url(r'^api-auth/', include('rest_framework.urls', namespace="rest_framework")), + + # old api and views url(r'^admin/', include(admin.site.urls)), url(r'', include('core.urls', namespace='core')), url(r'', include('users.urls', namespace='users')), From 19cff1c571b5fb1c650f8ed006d3f48f92245401 Mon Sep 17 00:00:00 2001 From: winkidney Date: Wed, 20 Feb 2019 17:20:58 +0800 Subject: [PATCH 04/34] Feature: Add coreapi as dependencies --- Pipfile | 1 + Pipfile.lock | 77 +++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/Pipfile b/Pipfile index 2c884f6..06261b3 100644 --- a/Pipfile +++ b/Pipfile @@ -22,3 +22,4 @@ gunicorn = "*" djangorestframework = "*" markdown = "*" django-filter = "*" +coreapi = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 7ece73a..c7ef5be 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "3ee7f6ec06170f6e1179faa9a279fb78800ad45e39b80cefa9df9ea4d4362925" + "sha256": "ad217c5e4d0a4207fd060d19af64ff07c87712335f95c89ac0e818c2c2cc435b" }, "pipfile-spec": 6, "requires": {}, @@ -28,6 +28,21 @@ ], "version": "==3.0.4" }, + "coreapi": { + "hashes": [ + "sha256:46145fcc1f7017c076a2ef684969b641d18a2991051fddec9458ad3f78ffc1cb", + "sha256:bf39d118d6d3e171f10df9ede5666f63ad80bba9a29a8ec17726a66cf52ee6f3" + ], + "index": "pypi", + "version": "==2.3.3" + }, + "coreschema": { + "hashes": [ + "sha256:5e6ef7bf38c1525d5e55a895934ab4273548629f16aed5c0a6caa74ebf45551f", + "sha256:9503506007d482ab0867ba14724b93c18a33b22b6d19fb419ef2d239dd4a1607" + ], + "version": "==0.0.4" + }, "django": { "hashes": [ "sha256:0a73696e0ac71ee6177103df984f9c1e07cd297f080f8ec4dc7c6f3fb74395b5", @@ -69,11 +84,11 @@ }, "django-taggit": { "hashes": [ - "sha256:a21cbe7e0879f1364eef1c88a2eda89d593bf000ebf51c3f00423c6927075dce", - "sha256:db4430ec99265341e05d0274edb0279163bd74357241f7b4d9274bdcb3338b17" + "sha256:710b4d15ec1996550cc68a0abbc41903ca7d832540e52b1336e6858737e410d8", + "sha256:bb8f27684814cd1414b2af75b857b5e26a40912631904038a7ecacd2bfafc3ac" ], "index": "pypi", - "version": "==0.23.0" + "version": "==0.24.0" }, "django-tastypie": { "hashes": [ @@ -112,6 +127,19 @@ ], "version": "==2.8" }, + "itypes": { + "hashes": [ + "sha256:c6e77bb9fd68a4bfeb9d958fea421802282451a25bac4913ec94db82a899c073" + ], + "version": "==1.1.0" + }, + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "version": "==2.10" + }, "markdown": { "hashes": [ "sha256:c00429bd503a47ec88d5e30a751e147dcb4c6889663cd3e2ba0afe858e009baa", @@ -120,6 +148,39 @@ "index": "pypi", "version": "==3.0.1" }, + "markupsafe": { + "hashes": [ + "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", + "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", + "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", + "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", + "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", + "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", + "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", + "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", + "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", + "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", + "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", + "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", + "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", + "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", + "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", + "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", + "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", + "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", + "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", + "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", + "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", + "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", + "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", + "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", + "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", + "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", + "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", + "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" + ], + "version": "==1.1.0" + }, "mock": { "hashes": [ "sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1", @@ -255,6 +316,14 @@ ], "version": "==1.12.0" }, + "uritemplate": { + "hashes": [ + "sha256:01c69f4fe8ed503b2951bef85d996a9d22434d2431584b5b107b2981ff416fbd", + "sha256:1b9c467a940ce9fb9f50df819e8ddd14696f89b9a8cc87ac77952ba416e0a8fd", + "sha256:c02643cebe23fc8adb5e6becffe201185bf06c40bda5c0b4028a93f1527d011d" + ], + "version": "==3.0.0" + }, "urllib3": { "hashes": [ "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", From f9e74f2ef767677fd72a4d26dc9ebd6a982f3e2c Mon Sep 17 00:00:00 2001 From: winkidney Date: Wed, 20 Feb 2019 17:40:06 +0800 Subject: [PATCH 05/34] Feature: Add url-filed name for DRF --- pinry/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinry/settings/base.py b/pinry/settings/base.py index bcea051..fbacfe7 100644 --- a/pinry/settings/base.py +++ b/pinry/settings/base.py @@ -150,7 +150,7 @@ REST_FRAMEWORK = { # Use Django's standard `django.contrib.auth` permissions, # or allow read-only access for unauthenticated users. 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' + 'rest_framework.permissions.IsAuthenticatedOrReadOnly' ], 'DEFAULT_FILTER_BACKENDS': ( 'django_filters.rest_framework.DjangoFilterBackend', From 32dc00fa344cd91d7aba0d33ef24d7c8036367ec Mon Sep 17 00:00:00 2001 From: winkidney Date: Wed, 20 Feb 2019 17:40:19 +0800 Subject: [PATCH 06/34] Feature: Add image creation api and api-docs of DRF api --- core/drf_api.py | 27 +++++++++++++++++++++++---- pinry/urls.py | 2 ++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/core/drf_api.py b/core/drf_api.py index 96f74ff..3acc76b 100644 --- a/core/drf_api.py +++ b/core/drf_api.py @@ -1,4 +1,5 @@ -from rest_framework import serializers, viewsets, routers +from rest_framework import serializers, viewsets, routers, mixins +from rest_framework.viewsets import GenericViewSet from taggit.models import Tag from core.models import Image, Pin @@ -14,7 +15,7 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): fields = ( 'username', 'gravatar', - 'url', + settings.DRF_URL_FIELD_NAME, ) @@ -44,16 +45,32 @@ class ImageSerializer(serializers.ModelSerializer): "thumbnail", "square", ) + extra_kwargs = { + "width": {"read_only": True}, + "height": {"read_only": True}, + } + standard = ThumbnailSerializer(read_only=True) thumbnail = ThumbnailSerializer(read_only=True) square = ThumbnailSerializer(read_only=True) + def create(self, validated_data): + image = super(ImageSerializer, self).create(validated_data) + for size in settings.IMAGE_SIZES: + Thumbnail.objects.get_or_create_at_size(image.pk, size) + return image + + +class ImageViewSet(mixins.CreateModelMixin, mixins.RetrieveModelMixin, GenericViewSet): + queryset = Image.objects.all() + serializer_class = ImageSerializer + class TagSerializer(serializers.ModelSerializer): class Meta: model = Tag - fields = ("name", ) + fields = ("name",) class PinSerializer(serializers.HyperlinkedModelSerializer): @@ -71,6 +88,7 @@ class PinSerializer(serializers.HyperlinkedModelSerializer): "tags", ) + tags = serializers.SlugRelatedField( many=True, source="tag_list", @@ -108,10 +126,11 @@ class PinSerializer(serializers.HyperlinkedModelSerializer): class PinViewSet(viewsets.ModelViewSet): queryset = Pin.objects.all() serializer_class = PinSerializer - filter_fields = ('submitter__username', ) + filter_fields = ('submitter__username',) permission_classes = [IsOwnerOrReadOnly("submitter"), ] drf_router = routers.DefaultRouter() drf_router.register(r'users', UserViewSet) drf_router.register(r'pins', PinViewSet) +drf_router.register(r'images', ImageViewSet) diff --git a/pinry/urls.py b/pinry/urls.py index e296fa7..d3954ea 100644 --- a/pinry/urls.py +++ b/pinry/urls.py @@ -3,6 +3,7 @@ from django.conf.urls import include, url from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.contrib import admin from django.views.static import serve +from rest_framework.documentation import include_docs_urls from core.drf_api import drf_router @@ -14,6 +15,7 @@ urlpatterns = [ # drf api url(r'^drf_api/', include(drf_router.urls)), url(r'^api-auth/', include('rest_framework.urls', namespace="rest_framework")), + url(r'^drf_api/docs/', include_docs_urls(title='PinryAPI', schema_url='/')), # old api and views url(r'^admin/', include(admin.site.urls)), From ec6d4447598ee72ebd31c495af255d9cdffa8a16 Mon Sep 17 00:00:00 2001 From: winkidney Date: Wed, 20 Feb 2019 18:03:27 +0800 Subject: [PATCH 07/34] Feature: Move viewsets to views.py --- core/drf_api.py | 32 ++++-------------------------- core/urls.py | 4 ---- core/views.py | 52 +++++++++++++++++++++++-------------------------- pinry/urls.py | 2 +- 4 files changed, 29 insertions(+), 61 deletions(-) diff --git a/core/drf_api.py b/core/drf_api.py index 3acc76b..7fb30e8 100644 --- a/core/drf_api.py +++ b/core/drf_api.py @@ -1,11 +1,10 @@ -from rest_framework import serializers, viewsets, routers, mixins -from rest_framework.viewsets import GenericViewSet +from django.conf import settings +from rest_framework import serializers from taggit.models import Tag -from core.models import Image, Pin -from core.permissions import IsOwnerOrReadOnly +from core.models import Image +from core.models import Pin from django_images.models import Thumbnail -from django.conf import settings from users.models import User @@ -19,11 +18,6 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): ) -class UserViewSet(viewsets.ModelViewSet): - queryset = User.objects.all() - serializer_class = UserSerializer - - class ThumbnailSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Thumbnail @@ -62,11 +56,6 @@ class ImageSerializer(serializers.ModelSerializer): return image -class ImageViewSet(mixins.CreateModelMixin, mixins.RetrieveModelMixin, GenericViewSet): - queryset = Image.objects.all() - serializer_class = ImageSerializer - - class TagSerializer(serializers.ModelSerializer): class Meta: model = Tag @@ -121,16 +110,3 @@ class PinSerializer(serializers.HyperlinkedModelSerializer): image = Image.objects.create(image=image_file['image']) instance.image = image return super(PinSerializer, self).update(instance, validated_data) - - -class PinViewSet(viewsets.ModelViewSet): - queryset = Pin.objects.all() - serializer_class = PinSerializer - filter_fields = ('submitter__username',) - permission_classes = [IsOwnerOrReadOnly("submitter"), ] - - -drf_router = routers.DefaultRouter() -drf_router.register(r'users', UserViewSet) -drf_router.register(r'pins', PinViewSet) -drf_router.register(r'images', ImageViewSet) diff --git a/core/urls.py b/core/urls.py index 49ef287..d3f7ea4 100644 --- a/core/urls.py +++ b/core/urls.py @@ -3,10 +3,7 @@ from django.views.generic import TemplateView from tastypie.api import Api -from core.drf_api import drf_router from .api import ImageResource, ThumbnailResource, PinResource, UserResource -from .views import CreateImage - v1_api = Api(api_name='v1') v1_api.register(ImageResource()) @@ -19,7 +16,6 @@ urlpatterns = [ url(r'^pins/pin-form/$', TemplateView.as_view(template_name='core/pin_form.html'), name='pin-form'), - url(r'^pins/create-image/$', CreateImage.as_view(), name='create-image'), url(r'^pins/tag/(?P(\w|-)+)/$', TemplateView.as_view(template_name='core/pins.html'), name='tag-pins'), diff --git a/core/views.py b/core/views.py index 1684c6b..cd3f123 100644 --- a/core/views.py +++ b/core/views.py @@ -1,34 +1,30 @@ -from django.http import HttpResponseRedirect -from django.conf import settings -from django.core.urlresolvers import reverse -from django.views.generic import CreateView -from django_images.models import Image +from rest_framework import viewsets, mixins, routers +from rest_framework.viewsets import GenericViewSet -from braces.views import JSONResponseMixin, LoginRequiredMixin -from django_images.models import Thumbnail - -from .forms import ImageForm +from core import drf_api as api +from core.models import Image, Pin +from core.permissions import IsOwnerOrReadOnly +from users.models import User -class CreateImage(JSONResponseMixin, LoginRequiredMixin, CreateView): - template_name = None # JavaScript-only view - model = Image - form_class = ImageForm +class UserViewSet(viewsets.ModelViewSet): + queryset = User.objects.all() + serializer_class = api.UserSerializer - def get(self, request, *args, **kwargs): - if not request.is_ajax(): - return HttpResponseRedirect(reverse('core:recent-pins')) - return super(CreateImage, self).get(request, *args, **kwargs) - def form_valid(self, form): - image = form.save() - for size in settings.IMAGE_SIZES: - Thumbnail.objects.get_or_create_at_size(image.pk, size) - return self.render_json_response({ - 'success': { - 'id': image.id - } - }) +class ImageViewSet(mixins.CreateModelMixin, GenericViewSet): + queryset = Image.objects.all() + serializer_class = api.ImageSerializer - def form_invalid(self, form): - return self.render_json_response({'error': form.errors}) + +class PinViewSet(viewsets.ModelViewSet): + queryset = Pin.objects.all() + serializer_class = api.PinSerializer + filter_fields = ('submitter__username',) + permission_classes = [IsOwnerOrReadOnly("submitter"), ] + + +drf_router = routers.DefaultRouter() +drf_router.register(r'users', UserViewSet) +drf_router.register(r'pins', PinViewSet) +drf_router.register(r'images', ImageViewSet) diff --git a/pinry/urls.py b/pinry/urls.py index d3954ea..1fedab3 100644 --- a/pinry/urls.py +++ b/pinry/urls.py @@ -5,7 +5,7 @@ from django.contrib import admin from django.views.static import serve from rest_framework.documentation import include_docs_urls -from core.drf_api import drf_router +from core.views import drf_router admin.autodiscover() From 109c46252a0b55f31c4b0b7471d9712764ade9a3 Mon Sep 17 00:00:00 2001 From: winkidney Date: Wed, 20 Feb 2019 18:08:10 +0800 Subject: [PATCH 08/34] Feature: Use api/v2 instead of drf_api/ --- pinry/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pinry/urls.py b/pinry/urls.py index 1fedab3..ef2e9d8 100644 --- a/pinry/urls.py +++ b/pinry/urls.py @@ -13,9 +13,9 @@ admin.autodiscover() urlpatterns = [ # drf api - url(r'^drf_api/', include(drf_router.urls)), + url(r'^api/v2/', include(drf_router.urls)), url(r'^api-auth/', include('rest_framework.urls', namespace="rest_framework")), - url(r'^drf_api/docs/', include_docs_urls(title='PinryAPI', schema_url='/')), + url(r'^api/v2/docs/', include_docs_urls(title='PinryAPI', schema_url='/')), # old api and views url(r'^admin/', include(admin.site.urls)), From 33d9aeee48cc1c72dac688a68d9435389a736227 Mon Sep 17 00:00:00 2001 From: winkidney Date: Wed, 20 Feb 2019 19:01:40 +0800 Subject: [PATCH 09/34] Feature: Use new image-creation api instead of the old --- core/drf_api.py | 28 ++++++++++++++++------------ core/models.py | 6 +++--- core/views.py | 3 +++ pinry/static/js/pin-form.js | 6 ++++-- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/core/drf_api.py b/core/drf_api.py index 7fb30e8..e30e4ae 100644 --- a/core/drf_api.py +++ b/core/drf_api.py @@ -32,6 +32,7 @@ class ImageSerializer(serializers.ModelSerializer): class Meta: model = Image fields = ( + "id", "image", "width", "height", @@ -42,9 +43,9 @@ class ImageSerializer(serializers.ModelSerializer): extra_kwargs = { "width": {"read_only": True}, "height": {"read_only": True}, + "image": {"read_only": True}, } - standard = ThumbnailSerializer(read_only=True) thumbnail = ThumbnailSerializer(read_only=True) square = ThumbnailSerializer(read_only=True) @@ -74,9 +75,12 @@ class PinSerializer(serializers.HyperlinkedModelSerializer): "description", "referer", "image", + "image_by_id", "tags", ) - + extra_kwargs = { + "submitter": {"read_only": True}, + } tags = serializers.SlugRelatedField( many=True, @@ -84,19 +88,22 @@ class PinSerializer(serializers.HyperlinkedModelSerializer): queryset=Tag.objects.all(), slug_field="name", ) - image = ImageSerializer(required=False) + image = ImageSerializer(required=False, read_only=True) + image_by_id = serializers.PrimaryKeyRelatedField( + queryset=Image.objects.all(), + write_only=True, + ) def create(self, validated_data): - image_file = validated_data.pop('image') - if validated_data['url']: + submitter = self.context['request'].user + image = validated_data.pop("image_by_id") + if 'url' in validated_data and validated_data['url']: image = Image.objects.create_for_url( validated_data['url'], validated_data['referer'], ) - else: - image = Image.objects.create(image=image_file['image']) - pin = Pin.objects.create(image=image, **validated_data) tags = validated_data.pop('tag_list') + pin = Pin.objects.create(submitter=submitter, image=image, **validated_data) if tags: pin.tags.set(*tags) return pin @@ -105,8 +112,5 @@ class PinSerializer(serializers.HyperlinkedModelSerializer): tags = validated_data.pop('tag_list') if tags: instance.tags.set(*tags) - image_file = validated_data.pop('image', None) - if image_file: - image = Image.objects.create(image=image_file['image']) - instance.image = image + validated_data.pop('image_id') return super(PinSerializer, self).update(instance, validated_data) diff --git a/core/models.py b/core/models.py index 59ee382..2b8ee09 100644 --- a/core/models.py +++ b/core/models.py @@ -72,9 +72,9 @@ class Image(BaseImage): class Pin(models.Model): submitter = models.ForeignKey(User) - url = models.URLField(null=True) - origin = models.URLField(null=True) - referer = models.URLField(null=True) + url = models.URLField(null=True, blank=True) + origin = models.URLField(null=True, blank=True) + referer = models.URLField(null=True, blank=True) description = models.TextField(blank=True, null=True) image = models.ForeignKey(Image, related_name='pin') published = models.DateTimeField(auto_now_add=True) diff --git a/core/views.py b/core/views.py index cd3f123..4c4b132 100644 --- a/core/views.py +++ b/core/views.py @@ -16,6 +16,9 @@ class ImageViewSet(mixins.CreateModelMixin, GenericViewSet): queryset = Image.objects.all() serializer_class = api.ImageSerializer + def create(self, request, *args, **kwargs): + super(ImageViewSet, self).create(request, *args, **kwargs) + class PinViewSet(viewsets.ModelViewSet): queryset = Pin.objects.all() diff --git a/pinry/static/js/pin-form.js b/pinry/static/js/pin-form.js index 34617f2..a189a82 100644 --- a/pinry/static/js/pin-form.js +++ b/pinry/static/js/pin-form.js @@ -9,6 +9,8 @@ $(window).load(function() { + var api_base = "/api/v2/"; + var uploadedImage = false; var editedPin = null; @@ -99,8 +101,8 @@ $(window).load(function() { } // Drag and drop upload $('#pin-form-image-upload').dropzone({ - url: '/pins/create-image/', - paramName: 'qqfile', + url: api_base + "images/", + paramName: 'image', parallelUploads: 1, uploadMultiple: false, maxFiles: 1, From e0a074fd9f6b197b9825ae07c313cbf421fed20c Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Feb 2019 12:37:19 +0800 Subject: [PATCH 10/34] Feature: Allow auto-creation for tags for pin / fix admin format --- core/admin.py | 2 +- core/drf_api.py | 21 +++++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/core/admin.py b/core/admin.py index 7d840a9..e7c76fe 100644 --- a/core/admin.py +++ b/core/admin.py @@ -6,5 +6,5 @@ from .models import Pin class PinAdmin(admin.ModelAdmin): pass -admin.site.register(Pin, PinAdmin) +admin.site.register(Pin, PinAdmin) diff --git a/core/drf_api.py b/core/drf_api.py index e30e4ae..6a08108 100644 --- a/core/drf_api.py +++ b/core/drf_api.py @@ -57,11 +57,26 @@ class ImageSerializer(serializers.ModelSerializer): return image -class TagSerializer(serializers.ModelSerializer): +class TagSerializer(serializers.SlugRelatedField): class Meta: model = Tag fields = ("name",) + queryset = Tag.objects.all() + + def __init__(self, **kwargs): + super(TagSerializer, self).__init__( + slug_field="name", + **kwargs + ) + + def to_internal_value(self, data): + obj, _ = self.get_queryset().get_or_create( + **{self.slug_field: data}, + defaults={self.slug_field: data, "slug": data} + ) + return obj + class PinSerializer(serializers.HyperlinkedModelSerializer): class Meta: @@ -82,11 +97,9 @@ class PinSerializer(serializers.HyperlinkedModelSerializer): "submitter": {"read_only": True}, } - tags = serializers.SlugRelatedField( + tags = TagSerializer( many=True, source="tag_list", - queryset=Tag.objects.all(), - slug_field="name", ) image = ImageSerializer(required=False, read_only=True) image_by_id = serializers.PrimaryKeyRelatedField( From 274134444daab67c9e51d7131c05a75959becf27 Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Feb 2019 13:14:15 +0800 Subject: [PATCH 11/34] Fix: Image should be both readable and writable --- core/drf_api.py | 1 - core/views.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/core/drf_api.py b/core/drf_api.py index 6a08108..a47baa1 100644 --- a/core/drf_api.py +++ b/core/drf_api.py @@ -43,7 +43,6 @@ class ImageSerializer(serializers.ModelSerializer): extra_kwargs = { "width": {"read_only": True}, "height": {"read_only": True}, - "image": {"read_only": True}, } standard = ThumbnailSerializer(read_only=True) diff --git a/core/views.py b/core/views.py index 4c4b132..0be8806 100644 --- a/core/views.py +++ b/core/views.py @@ -17,7 +17,7 @@ class ImageViewSet(mixins.CreateModelMixin, GenericViewSet): serializer_class = api.ImageSerializer def create(self, request, *args, **kwargs): - super(ImageViewSet, self).create(request, *args, **kwargs) + return super(ImageViewSet, self).create(request, *args, **kwargs) class PinViewSet(viewsets.ModelViewSet): From 852cb0b89df02e32f06395f21f2b8afecc55400e Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Feb 2019 13:22:28 +0800 Subject: [PATCH 12/34] Feature: Replace image upload by DRF-api --- pinry/static/js/helpers.js | 35 +++++++++++++++++++++++++++++++++++ pinry/static/js/pin-form.js | 23 ++++++++++++----------- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/pinry/static/js/helpers.js b/pinry/static/js/helpers.js index 9f22a4e..74f3d41 100644 --- a/pinry/static/js/helpers.js +++ b/pinry/static/js/helpers.js @@ -7,6 +7,41 @@ */ +function _getCookie(name) { + var cookieValue = null; + if (document.cookie && document.cookie !== '') { + var cookies = document.cookie.split(';'); + for (var i = 0; i < cookies.length; i++) { + var cookie = jQuery.trim(cookies[i]); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; +} + + +function getCSRFToken() { + return _getCookie('csrftoken'); +} + + +function csrfSafeMethod(method) { + // these HTTP methods do not require CSRF protection + return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); +} +$.ajaxSetup({ + beforeSend: function(xhr, settings) { + if (!csrfSafeMethod(settings.type) && !this.crossDomain) { + xhr.setRequestHeader("X-CSRFToken", getCSRFToken()); + } + } +}); + + function renderTemplate(templateId, context) { var template = Handlebars.compile($(templateId).html()); return template(context); diff --git a/pinry/static/js/pin-form.js b/pinry/static/js/pin-form.js index a189a82..406868b 100644 --- a/pinry/static/js/pin-form.js +++ b/pinry/static/js/pin-form.js @@ -107,18 +107,19 @@ $(window).load(function() { uploadMultiple: false, maxFiles: 1, acceptedFiles: 'image/*', + headers: { + 'X-CSRFToken': getCSRFToken(), + }, success: function(file, resp) { - $('#pin-form-image-url').parent().fadeOut(300); - var promise = getImageData(resp.success.id); - uploadedImage = resp.success.id; - promise.success(function(image) { - $('#pin-form-image-url').val(image.thumbnail.image); - createPinPreviewFromForm(); - }); - promise.error(function() { - message('Problem uploading image.', 'alert alert-error'); - }); - } + var image_url = $('#pin-form-image-url'); + image_url.parent().fadeOut(300); + uploadedImage = resp.id; + image_url.val(resp.thumbnail.image); + createPinPreviewFromForm(); + }, + error: function (error) { + message('Problem uploading image.', 'alert alert-error'); + }, }); // If bookmarklet submit if (pinFromUrl) { From 153dc0daef698fc1be471adaac3e9cd49949d4f5 Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Feb 2019 13:27:12 +0800 Subject: [PATCH 13/34] Fix: Should ignore error caused by pins which reference same image --- core/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/models.py b/core/models.py index 2b8ee09..9cd56d0 100644 --- a/core/models.py +++ b/core/models.py @@ -89,4 +89,7 @@ class Pin(models.Model): @receiver(models.signals.post_delete, sender=Pin) def delete_pin_images(sender, instance, **kwargs): - instance.image.delete() + try: + instance.image.delete() + except Image.DoesNotExist: + pass From da8bac1491c81425851da079b652569597693d72 Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Feb 2019 14:57:13 +0800 Subject: [PATCH 14/34] Feature: Use v2-api for pin-detail page --- core/drf_api.py | 4 +--- pinry/static/js/helpers.js | 10 ++-------- pinry/static/js/pin-form.js | 4 +--- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/core/drf_api.py b/core/drf_api.py index a47baa1..b5b58c7 100644 --- a/core/drf_api.py +++ b/core/drf_api.py @@ -92,10 +92,8 @@ class PinSerializer(serializers.HyperlinkedModelSerializer): "image_by_id", "tags", ) - extra_kwargs = { - "submitter": {"read_only": True}, - } + submitter = UserSerializer(read_only=True) tags = TagSerializer( many=True, source="tag_list", diff --git a/pinry/static/js/helpers.js b/pinry/static/js/helpers.js index 74f3d41..88d8ac8 100644 --- a/pinry/static/js/helpers.js +++ b/pinry/static/js/helpers.js @@ -5,6 +5,7 @@ * Updated: Feb 26th, 2013 * Require: jQuery */ +var API_BASE = "/api/v2/"; function _getCookie(name) { @@ -60,15 +61,8 @@ function cleanTags(tags) { return tags; } - -function getImageData(imageId) { - var apiUrl = '/api/v1/image/'+imageId+'/?format=json'; - return $.get(apiUrl); -} - - function getPinData(pinId) { - var apiUrl = '/api/v1/pin/'+pinId+'/?format=json'; + var apiUrl = API_BASE + "pins/" + pinId + '/?format=json'; return $.get(apiUrl); } diff --git a/pinry/static/js/pin-form.js b/pinry/static/js/pin-form.js index 406868b..a414e12 100644 --- a/pinry/static/js/pin-form.js +++ b/pinry/static/js/pin-form.js @@ -9,8 +9,6 @@ $(window).load(function() { - var api_base = "/api/v2/"; - var uploadedImage = false; var editedPin = null; @@ -101,7 +99,7 @@ $(window).load(function() { } // Drag and drop upload $('#pin-form-image-upload').dropzone({ - url: api_base + "images/", + url: API_BASE + "images/", paramName: 'image', parallelUploads: 1, uploadMultiple: false, From 9741f0311ab1a38e4316f1151ebc509edf2d38df Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Feb 2019 15:12:37 +0800 Subject: [PATCH 15/34] Feature: Update Pin via drf-api --- core/drf_api.py | 4 +++- pinry/static/js/helpers.js | 2 +- pinry/static/js/pin-form.js | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/core/drf_api.py b/core/drf_api.py index b5b58c7..e316113 100644 --- a/core/drf_api.py +++ b/core/drf_api.py @@ -97,6 +97,7 @@ class PinSerializer(serializers.HyperlinkedModelSerializer): tags = TagSerializer( many=True, source="tag_list", + required=False, ) image = ImageSerializer(required=False, read_only=True) image_by_id = serializers.PrimaryKeyRelatedField( @@ -122,5 +123,6 @@ class PinSerializer(serializers.HyperlinkedModelSerializer): tags = validated_data.pop('tag_list') if tags: instance.tags.set(*tags) - validated_data.pop('image_id') + # change for image-id is not allowed + validated_data.pop('image_by_id', None) return super(PinSerializer, self).update(instance, validated_data) diff --git a/pinry/static/js/helpers.js b/pinry/static/js/helpers.js index 88d8ac8..c9c1b0c 100644 --- a/pinry/static/js/helpers.js +++ b/pinry/static/js/helpers.js @@ -68,7 +68,7 @@ function getPinData(pinId) { function deletePinData(pinId) { - var apiUrl = '/api/v1/pin/'+pinId+'/?format=json'; + var apiUrl = API_BASE + 'pins/' +pinId + '/?format=json'; return $.ajax(apiUrl, { type: 'DELETE' }); diff --git a/pinry/static/js/pin-form.js b/pinry/static/js/pin-form.js index a414e12..1f43958 100644 --- a/pinry/static/js/pin-form.js +++ b/pinry/static/js/pin-form.js @@ -137,13 +137,13 @@ $(window).load(function() { $(this).off('click'); $(this).addClass('disabled'); if (editedPin) { - var apiUrl = '/api/v1/pin/'+editedPin.id+'/?format=json'; + var apiUrl = API_BASE + 'pins/' + editedPin.id + '/?format=json'; var data = { description: $('#pin-form-description').val(), tags: cleanTags($('#pin-form-tags').val()) } var promise = $.ajax({ - type: "put", + type: "patch", url: apiUrl, contentType: 'application/json', data: JSON.stringify(data) From 6f6d858f8ccc2ead7c5102e2a2997b85c6b21aee Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Feb 2019 15:25:03 +0800 Subject: [PATCH 16/34] Fix: Should allow null-valur for some field --- core/drf_api.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/core/drf_api.py b/core/drf_api.py index e316113..6d970f6 100644 --- a/core/drf_api.py +++ b/core/drf_api.py @@ -1,5 +1,6 @@ from django.conf import settings from rest_framework import serializers +from rest_framework.exceptions import ValidationError from taggit.models import Tag from core.models import Image @@ -103,17 +104,29 @@ class PinSerializer(serializers.HyperlinkedModelSerializer): image_by_id = serializers.PrimaryKeyRelatedField( queryset=Image.objects.all(), write_only=True, + required=False, ) + def validate(self, attrs): + if 'url' not in attrs and 'image_by_id' not in attrs: + raise ValidationError( + detail={ + "url-or-image": "Either url or image_by_id is required." + }, + ) + return attrs + def create(self, validated_data): submitter = self.context['request'].user - image = validated_data.pop("image_by_id") if 'url' in validated_data and validated_data['url']: + url = validated_data['url'] image = Image.objects.create_for_url( - validated_data['url'], - validated_data['referer'], + url, + validated_data.get('referer', url), ) - tags = validated_data.pop('tag_list') + else: + image = validated_data.pop("image_by_id") + tags = validated_data.pop('tag_list', []) pin = Pin.objects.create(submitter=submitter, image=image, **validated_data) if tags: pin.tags.set(*tags) From 4029107b5dcbe4bf0ab91022b3d132ccd9b4ef68 Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Feb 2019 17:45:22 +0800 Subject: [PATCH 17/34] Feature: Use drf-api for Pin-creation --- pinry/static/js/helpers.js | 2 +- pinry/static/js/pin-form.js | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pinry/static/js/helpers.js b/pinry/static/js/helpers.js index c9c1b0c..3956e1f 100644 --- a/pinry/static/js/helpers.js +++ b/pinry/static/js/helpers.js @@ -77,7 +77,7 @@ function deletePinData(pinId) { function postPinData(data) { return $.ajax({ type: "post", - url: "/api/v1/pin/", + url: API_BASE + "pins/", contentType: 'application/json', data: JSON.stringify(data) }); diff --git a/pinry/static/js/pin-form.js b/pinry/static/js/pin-form.js index 1f43958..9aeec93 100644 --- a/pinry/static/js/pin-form.js +++ b/pinry/static/js/pin-form.js @@ -141,7 +141,7 @@ $(window).load(function() { var data = { description: $('#pin-form-description').val(), tags: cleanTags($('#pin-form-tags').val()) - } + }; var promise = $.ajax({ type: "patch", url: apiUrl, @@ -166,13 +166,15 @@ $(window).load(function() { }); } else { var data = { - submitter: '/api/v1/user/'+currentUser.id+'/', referer: $('#pin-form-referer').val(), description: $('#pin-form-description').val(), tags: cleanTags($('#pin-form-tags').val()) }; - if (uploadedImage) data.image = '/api/v1/image/'+uploadedImage+'/'; - else data.url = $('#pin-form-image-url').val(); + if (uploadedImage) { + data.image_id = uploadedImage; + } else { + data.url = $('#pin-form-image-url').val(); + } var promise = postPinData(data); promise.success(function(pin) { if (pinFromUrl) return window.close(); From fbbe4d8c2ee6b085138ae3b8b88cf12d71be6afc Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Feb 2019 18:11:08 +0800 Subject: [PATCH 18/34] Fix: Should use image_by_id as image-field name / add pagination --- core/views.py | 6 +++++- pinry/settings/base.py | 2 ++ pinry/static/js/pin-form.js | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/core/views.py b/core/views.py index 0be8806..9746368 100644 --- a/core/views.py +++ b/core/views.py @@ -1,4 +1,5 @@ from rest_framework import viewsets, mixins, routers +from rest_framework.filters import SearchFilter, OrderingFilter from rest_framework.viewsets import GenericViewSet from core import drf_api as api @@ -23,7 +24,10 @@ class ImageViewSet(mixins.CreateModelMixin, GenericViewSet): class PinViewSet(viewsets.ModelViewSet): queryset = Pin.objects.all() serializer_class = api.PinSerializer - filter_fields = ('submitter__username',) + filter_backends = (SearchFilter, OrderingFilter) + search_fields = ('=submitter__username', ) + ordering_fields = ('id', ) + ordering = ('id', ) permission_classes = [IsOwnerOrReadOnly("submitter"), ] diff --git a/pinry/settings/base.py b/pinry/settings/base.py index fbacfe7..54e895f 100644 --- a/pinry/settings/base.py +++ b/pinry/settings/base.py @@ -156,4 +156,6 @@ REST_FRAMEWORK = { 'django_filters.rest_framework.DjangoFilterBackend', ), 'URL_FIELD_NAME': DRF_URL_FIELD_NAME, + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', + 'PAGE_SIZE': 50, } diff --git a/pinry/static/js/pin-form.js b/pinry/static/js/pin-form.js index 9aeec93..8a57ccd 100644 --- a/pinry/static/js/pin-form.js +++ b/pinry/static/js/pin-form.js @@ -171,7 +171,7 @@ $(window).load(function() { tags: cleanTags($('#pin-form-tags').val()) }; if (uploadedImage) { - data.image_id = uploadedImage; + data.image_by_id = uploadedImage; } else { data.url = $('#pin-form-image-url').val(); } From fd0e1d87ba5ec93fee9493e66767d1a5b5af493a Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Feb 2019 18:29:56 +0800 Subject: [PATCH 19/34] Feature: Fetch pins from drf-api --- pinry/static/js/pinry.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/pinry/static/js/pinry.js b/pinry/static/js/pinry.js index 230a1e7..9451149 100644 --- a/pinry/static/js/pinry.js +++ b/pinry/static/js/pinry.js @@ -10,7 +10,7 @@ $(window).load(function() { /** * tileLayout will simply tile/retile the block/pin container when run. This - * was put into a function in order to adjust frequently on screen size + * was put into a function in order to adjust frequently on screen size * changes. */ window.tileLayout = function() { @@ -104,6 +104,11 @@ $(window).load(function() { * Load our pins using the pins template into our UI, be sure to define a * offset outside the function to keep a running tally of your location. */ + + function isPinEditable(pinObject) { + return pinObject.submitter.username === currentUser.username + } + function loadPins() { // Disable scroll $(window).off('scroll'); @@ -112,21 +117,22 @@ $(window).load(function() { $('.spinner').css('display', 'block'); // Fetch our pins from the api using our current offset - var apiUrl = '/api/v1/pin/?format=json&order_by=-id&offset='+String(offset); + var apiUrl = API_BASE + 'pins/?format=json&ordering=-id&limit=50&offset='+String(offset); if (tagFilter) apiUrl = apiUrl + '&tag=' + tagFilter; if (userFilter) apiUrl = apiUrl + '&submitter__username=' + userFilter; - $.get(apiUrl, function(pins) { + $.get(apiUrl, function(pins_page) { // Set which items are editable by the current user - for (var i=0; i < pins.objects.length; i++) { - pins.objects[i].editable = (pins.objects[i].submitter.username == currentUser.username); - pins.objects[i].tags.sort(function (a, b) { + var pins = pins_page.results; + for (var i=0; i < pins.length; i++) { + pins[i].editable = isPinEditable(pins[i]); + pins[i].tags.sort(function (a, b) { return a.toLowerCase().localeCompare(b.toLowerCase()); }); } // Use the fetched pins as our context for our pins template var template = Handlebars.compile($('#pins-template').html()); - var html = template({pins: pins.objects}); + var html = template({pins: pins}); // Append the newly compiled data to our container $('#pins').append(html); @@ -140,7 +146,7 @@ $(window).load(function() { }); }); - if (pins.objects.length < apiLimitPerPage) { + if (pins.length < apiLimitPerPage) { $('.spinner').css('display', 'none'); if ($('#pins').length !== 0) { var theEnd = document.createElement('div'); From 22f070ff2b953b71d1758aed65f7e06851011c56 Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Feb 2019 18:32:36 +0800 Subject: [PATCH 20/34] Feature: Add shell command in Makefile --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 3086ae2..75ddb0b 100644 --- a/Makefile +++ b/Makefile @@ -17,3 +17,5 @@ install: pipenv install test: pipenv run python manage.py test +shell: + pipenv run python manage.py shell From e120af94cf5dbb13363e19a663081c1c197e0532 Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Feb 2019 18:44:23 +0800 Subject: [PATCH 21/34] Fix: Use API_LIMIT_PER_PAGE in drs-settings --- pinry/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinry/settings/base.py b/pinry/settings/base.py index 54e895f..0b89271 100644 --- a/pinry/settings/base.py +++ b/pinry/settings/base.py @@ -157,5 +157,5 @@ REST_FRAMEWORK = { ), 'URL_FIELD_NAME': DRF_URL_FIELD_NAME, 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', - 'PAGE_SIZE': 50, + 'PAGE_SIZE': API_LIMIT_PER_PAGE, } From e1720921fbafed6a6c5bbd76ce9a937c665a7c90 Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Feb 2019 19:03:55 +0800 Subject: [PATCH 22/34] Feature: Support filter by tags and user for Pins --- core/views.py | 9 +++++---- pinry/static/js/pinry.js | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/core/views.py b/core/views.py index 9746368..112aa7a 100644 --- a/core/views.py +++ b/core/views.py @@ -1,3 +1,4 @@ +from django_filters.rest_framework import DjangoFilterBackend from rest_framework import viewsets, mixins, routers from rest_framework.filters import SearchFilter, OrderingFilter from rest_framework.viewsets import GenericViewSet @@ -24,10 +25,10 @@ class ImageViewSet(mixins.CreateModelMixin, GenericViewSet): class PinViewSet(viewsets.ModelViewSet): queryset = Pin.objects.all() serializer_class = api.PinSerializer - filter_backends = (SearchFilter, OrderingFilter) - search_fields = ('=submitter__username', ) - ordering_fields = ('id', ) - ordering = ('id', ) + filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) + filter_fields = ("submitter__username", 'tags__name', ) + ordering_fields = ('-id', ) + ordering = ('-id', ) permission_classes = [IsOwnerOrReadOnly("submitter"), ] diff --git a/pinry/static/js/pinry.js b/pinry/static/js/pinry.js index 9451149..390b263 100644 --- a/pinry/static/js/pinry.js +++ b/pinry/static/js/pinry.js @@ -118,7 +118,7 @@ $(window).load(function() { // Fetch our pins from the api using our current offset var apiUrl = API_BASE + 'pins/?format=json&ordering=-id&limit=50&offset='+String(offset); - if (tagFilter) apiUrl = apiUrl + '&tag=' + tagFilter; + if (tagFilter) apiUrl = apiUrl + '&tags__name=' + tagFilter; if (userFilter) apiUrl = apiUrl + '&submitter__username=' + userFilter; $.get(apiUrl, function(pins_page) { // Set which items are editable by the current user From 00d9a854efeffcb74c597c333b6b1147d73fe011 Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Feb 2019 19:05:39 +0800 Subject: [PATCH 23/34] Feature: Use more restful url for user-filter and tag-filter --- core/urls.py | 4 ++-- pinry/templates/includes/pins.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/urls.py b/core/urls.py index d3f7ea4..002d353 100644 --- a/core/urls.py +++ b/core/urls.py @@ -17,9 +17,9 @@ urlpatterns = [ url(r'^pins/pin-form/$', TemplateView.as_view(template_name='core/pin_form.html'), name='pin-form'), - url(r'^pins/tag/(?P(\w|-)+)/$', TemplateView.as_view(template_name='core/pins.html'), + url(r'^pins/tags/(?P(\w|-)+)/$', TemplateView.as_view(template_name='core/pins.html'), name='tag-pins'), - url(r'^pins/user/(?P(\w|-)+)/$', TemplateView.as_view(template_name='core/pins.html'), + url(r'^pins/users/(?P(\w|-)+)/$', TemplateView.as_view(template_name='core/pins.html'), name='user-pins'), url(r'^(?P[0-9]+)/$', TemplateView.as_view(template_name='core/pins.html'), name='recent-pins'), diff --git a/pinry/templates/includes/pins.html b/pinry/templates/includes/pins.html index 861e560..e9d03b0 100644 --- a/pinry/templates/includes/pins.html +++ b/pinry/templates/includes/pins.html @@ -26,11 +26,11 @@
pinned by - {{submitter.username}} + {{submitter.username}} {{#if tags}} in {{#each tags}} - {{this}} + {{this}} {{/each}} {{/if}}
From a2e63f05d7992058b09a3d8e72b91e022cb94ef1 Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Feb 2019 19:16:58 +0800 Subject: [PATCH 24/34] Fix: Correct the name for specified pin --- core/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/urls.py b/core/urls.py index 002d353..2fb8d06 100644 --- a/core/urls.py +++ b/core/urls.py @@ -22,7 +22,7 @@ urlpatterns = [ url(r'^pins/users/(?P(\w|-)+)/$', TemplateView.as_view(template_name='core/pins.html'), name='user-pins'), url(r'^(?P[0-9]+)/$', TemplateView.as_view(template_name='core/pins.html'), - name='recent-pins'), + name='pin-detail'), url(r'^$', TemplateView.as_view(template_name='core/pins.html'), name='recent-pins'), ] From 40e2f0b25423da714f8edd5bf46f6539c6a9fc07 Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Feb 2019 19:24:29 +0800 Subject: [PATCH 25/34] Feature: Remove unused django-tastypie --- core/api.py | 227 ---------------------------- core/{drf_api.py => serializers.py} | 0 core/urls.py | 14 +- core/views.py | 2 +- users/tests.py | 1 - 5 files changed, 2 insertions(+), 242 deletions(-) delete mode 100644 core/api.py rename core/{drf_api.py => serializers.py} (100%) diff --git a/core/api.py b/core/api.py deleted file mode 100644 index 108ce50..0000000 --- a/core/api.py +++ /dev/null @@ -1,227 +0,0 @@ -from django.core.exceptions import ObjectDoesNotExist -from tastypie import fields -from tastypie.authorization import DjangoAuthorization -from tastypie.constants import ALL, ALL_WITH_RELATIONS -from tastypie.exceptions import Unauthorized -from tastypie.resources import ModelResource -from django_images.models import Thumbnail - -from .models import Pin, Image -from users.models import User - - -def _is_pin_owner(obj_or_list, user): - assert obj_or_list is not None - if not isinstance(obj_or_list, (tuple, list)): - obj_or_list = (obj_or_list,) - results = tuple( - obj.submitter == user - for obj in obj_or_list - if isinstance(obj, Pin) - ) - if len(results) <= 0: - raise ValueError( - "You should never check permission on %s with this function." - % obj_or_list - ) - return all(results) - - -def _is_authenticated_and_owner(object_list, bundle): - if bundle.request.user.is_anonymous(): - return object_list.none() - return object_list.filter(submitter=bundle.request.user) - - -class PinryAuthorization(DjangoAuthorization): - """ - Pinry-specific Authorization backend with object-level permission checking. - """ - def _is_obj_owner(self, object_list, bundle): - klass = self.base_checks(bundle.request, bundle.obj.__class__) - - if klass is False: - raise Unauthorized("You are not allowed to access that resource.") - return _is_pin_owner(bundle.obj, bundle.request.user) - - def read_list(self, object_list, bundle): - # This assumes a ``QuerySet`` from ``ModelResource``. - return object_list - - def read_detail(self, object_list, bundle): - """ - User can always read detail of any Pin object. - """ - return True - - def create_detail(self, object_list, bundle): - return self._is_obj_owner(object_list, bundle) - - def update_detail(self, object_list, bundle): - return self._is_obj_owner(object_list, bundle) - - def delete_detail(self, object_list, bundle): - return self._is_obj_owner(object_list, bundle) - - def update_list(self, object_list, bundle): - return _is_authenticated_and_owner(object_list, bundle) - - def delete_list(self, object_list, bundle): - return _is_authenticated_and_owner(object_list, bundle) - - -class ImageAuthorization(DjangoAuthorization): - """ - Pinry-specific Authorization backend with object-level permission checking. - """ - def __init__(self): - DjangoAuthorization.__init__(self) - - def read_list(self, object_list, bundle): - return object_list - - def read_detail(self, object_list, bundle): - """ - User can always read detail of any Pin object. - """ - return True - - def create_detail(self, object_list, bundle): - return bundle.request.user.is_authenticated() - - def update_detail(self, object_list, bundle): - return bundle.request.user.is_authenticated() - - def delete_detail(self, object_list, bundle): - return bundle.request.user.is_authenticated() - - def update_list(self, object_list, bundle): - if not bundle.request.user.is_authenticated(): - return object_list.none() - return object_list - - def delete_list(self, object_list, bundle): - if not bundle.request.user.is_authenticated(): - return object_list.none() - return object_list - - -class UserResource(ModelResource): - gravatar = fields.CharField(readonly=True) - - def dehydrate_gravatar(self, bundle): - return bundle.obj.gravatar - - class Meta: - list_allowed_methods = ['get'] - filtering = { - 'username': ALL - } - queryset = User.objects.all() - resource_name = 'user' - fields = ['username'] - include_resource_uri = False - - -def filter_generator_for(size): - def wrapped_func(bundle, **kwargs): - if hasattr(bundle.obj, '_prefetched_objects_cache') and 'thumbnail' in bundle.obj._prefetched_objects_cache: - for thumbnail in bundle.obj._prefetched_objects_cache['thumbnail']: - if thumbnail.size == size: - return thumbnail - raise ObjectDoesNotExist() - else: - return bundle.obj.get_by_size(size) - return wrapped_func - - -class ThumbnailResource(ModelResource): - class Meta: - list_allowed_methods = ['get'] - fields = ['image', 'width', 'height'] - queryset = Thumbnail.objects.all() - resource_name = 'thumbnail' - include_resource_uri = False - - -class ImageResource(ModelResource): - standard = fields.ToOneField( - ThumbnailResource, full=True, - attribute=lambda bundle: filter_generator_for('standard')(bundle), - related_name='thumbnail', - ) - thumbnail = fields.ToOneField( - ThumbnailResource, full=True, - attribute=lambda bundle: filter_generator_for('thumbnail')(bundle), - related_name='thumbnail', - ) - square = fields.ToOneField( - ThumbnailResource, full=True, - attribute=lambda bundle: filter_generator_for('square')(bundle), - related_name='thumbnail', - ) - - class Meta: - fields = ['image', 'width', 'height'] - include_resource_uri = False - resource_name = 'image' - queryset = Image.objects.all() - authorization = ImageAuthorization() - - -class PinResource(ModelResource): - submitter = fields.ToOneField(UserResource, 'submitter', full=True) - image = fields.ToOneField(ImageResource, 'image', full=True) - tags = fields.ListField() - - def hydrate_image(self, bundle): - url = bundle.data.get('url', None) - if url: - image = Image.objects.create_for_url( - url, - referer=bundle.data.get('referer', None), - ) - bundle.data['image'] = '/api/v1/image/{}/'.format(image.pk) - return bundle - - def hydrate(self, bundle): - """Run some early/generic processing - - Make sure that user is authorized to create Pins first, before - we hydrate the Image resource, creating the Image object in process - """ - submitter = bundle.data.get('submitter', None) - if not submitter: - bundle.data['submitter'] = '/api/v1/user/{}/'.format(bundle.request.user.pk) - else: - if not '/api/v1/user/{}/'.format(bundle.request.user.pk) == submitter: - raise Unauthorized("You are not authorized to create Pins for other users") - return bundle - - def dehydrate_tags(self, bundle): - return list(map(str, bundle.obj.tags.all())) - - def build_filters(self, filters=None, ignore_bad_filters=False): - orm_filters = super(PinResource, self).build_filters(filters, ignore_bad_filters=ignore_bad_filters) - if filters and 'tag' in filters: - orm_filters['tags__name__in'] = filters['tag'].split(',') - return orm_filters - - def save_m2m(self, bundle): - tags = bundle.data.get('tags', None) - if tags: - bundle.obj.tags.set(*tags) - return super(PinResource, self).save_m2m(bundle) - - class Meta: - fields = ['id', 'url', 'origin', 'description', 'referer'] - ordering = ['id'] - filtering = { - 'submitter': ALL_WITH_RELATIONS - } - queryset = Pin.objects.all().select_related('submitter', 'image'). \ - prefetch_related('image__thumbnail_set', 'tags') - resource_name = 'pin' - include_resource_uri = False - always_return_data = True - authorization = PinryAuthorization() diff --git a/core/drf_api.py b/core/serializers.py similarity index 100% rename from core/drf_api.py rename to core/serializers.py diff --git a/core/urls.py b/core/urls.py index 2fb8d06..e9c16e6 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,22 +1,10 @@ -from django.conf.urls import include, url +from django.conf.urls import url from django.views.generic import TemplateView -from tastypie.api import Api - -from .api import ImageResource, ThumbnailResource, PinResource, UserResource - -v1_api = Api(api_name='v1') -v1_api.register(ImageResource()) -v1_api.register(ThumbnailResource()) -v1_api.register(PinResource()) -v1_api.register(UserResource()) urlpatterns = [ - url(r'^api/', include(v1_api.urls, namespace='api')), - url(r'^pins/pin-form/$', TemplateView.as_view(template_name='core/pin_form.html'), name='pin-form'), - url(r'^pins/tags/(?P(\w|-)+)/$', TemplateView.as_view(template_name='core/pins.html'), name='tag-pins'), url(r'^pins/users/(?P(\w|-)+)/$', TemplateView.as_view(template_name='core/pins.html'), diff --git a/core/views.py b/core/views.py index 112aa7a..3462c75 100644 --- a/core/views.py +++ b/core/views.py @@ -3,7 +3,7 @@ from rest_framework import viewsets, mixins, routers from rest_framework.filters import SearchFilter, OrderingFilter from rest_framework.viewsets import GenericViewSet -from core import drf_api as api +from core import serializers as api from core.models import Image, Pin from core.permissions import IsOwnerOrReadOnly from users.models import User diff --git a/users/tests.py b/users/tests.py index 8f3f7b0..9715056 100644 --- a/users/tests.py +++ b/users/tests.py @@ -5,7 +5,6 @@ from django.test.utils import override_settings import mock from .auth.backends import CombinedAuthBackend -from core.models import Image, Pin from .models import User From a5a876b1aaf4ed6a142e83b83d1dd6a0f7ef4286 Mon Sep 17 00:00:00 2001 From: winkidney Date: Thu, 21 Feb 2019 19:35:23 +0800 Subject: [PATCH 26/34] Fix: Fix context name-conflicts for user-page --- core/urls.py | 2 +- pinry/static/js/pin-form.js | 2 -- pinry/templates/base.html | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/core/urls.py b/core/urls.py index e9c16e6..29c45b3 100644 --- a/core/urls.py +++ b/core/urls.py @@ -7,7 +7,7 @@ urlpatterns = [ name='pin-form'), url(r'^pins/tags/(?P(\w|-)+)/$', TemplateView.as_view(template_name='core/pins.html'), name='tag-pins'), - url(r'^pins/users/(?P(\w|-)+)/$', TemplateView.as_view(template_name='core/pins.html'), + url(r'^pins/users/(?P(\w|-)+)/$', TemplateView.as_view(template_name='core/pins.html'), name='user-pins'), url(r'^(?P[0-9]+)/$', TemplateView.as_view(template_name='core/pins.html'), name='pin-detail'), diff --git a/pinry/static/js/pin-form.js b/pinry/static/js/pin-form.js index 8a57ccd..dbdeada 100644 --- a/pinry/static/js/pin-form.js +++ b/pinry/static/js/pin-form.js @@ -15,7 +15,6 @@ $(window).load(function() { // Start Helper Functions function getFormData() { return { - submitter: currentUser, url: $('#pin-form-image-url').val(), referer: $('#pin-form-referer').val(), description: $('#pin-form-description').val(), @@ -25,7 +24,6 @@ $(window).load(function() { function createPinPreviewFromForm() { var context = {pins: [{ - submitter: currentUser, image: {thumbnail: {image: $('#pin-form-image-url').val()}}, referer: $('#pin-form-referer').val(), description: $('#pin-form-description').val(), diff --git a/pinry/templates/base.html b/pinry/templates/base.html index 71fd284..f9822bb 100644 --- a/pinry/templates/base.html +++ b/pinry/templates/base.html @@ -36,7 +36,7 @@ }, pinFilter = "{{ request.resolver_match.kwargs.pin }}", tagFilter = "{{ request.resolver_match.kwargs.tag }}", - userFilter = "{{ request.resolver_match.kwargs.user }}"; + userFilter = "{{ request.resolver_match.kwargs.username }}"; From d9b5a78b36729bdb3ce11c8626d00b57555fb356 Mon Sep 17 00:00:00 2001 From: winkidney Date: Fri, 22 Feb 2019 12:01:46 +0800 Subject: [PATCH 27/34] Refactor: Allow only the user-data fetching --- core/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/views.py b/core/views.py index 3462c75..4cdbcff 100644 --- a/core/views.py +++ b/core/views.py @@ -9,7 +9,7 @@ from core.permissions import IsOwnerOrReadOnly from users.models import User -class UserViewSet(viewsets.ModelViewSet): +class UserViewSet(mixins.RetrieveModelMixin, GenericViewSet): queryset = User.objects.all() serializer_class = api.UserSerializer From ef05e4ad8e8a210af547698198922c2ed0a732c9 Mon Sep 17 00:00:00 2001 From: winkidney Date: Fri, 22 Feb 2019 15:26:15 +0800 Subject: [PATCH 28/34] Feature: Remove unused tests and add tests for DRF-api --- core/forms.py | 18 --- core/serializers.py | 11 +- core/tests/__init__.py | 2 - core/tests/api.py | 303 +++++++++++------------------------------ core/tests/forms.py | 11 -- core/tests/helpers.py | 97 ++++--------- core/tests/views.py | 35 ++--- 7 files changed, 128 insertions(+), 349 deletions(-) delete mode 100644 core/forms.py delete mode 100644 core/tests/forms.py diff --git a/core/forms.py b/core/forms.py deleted file mode 100644 index 7c080b7..0000000 --- a/core/forms.py +++ /dev/null @@ -1,18 +0,0 @@ -from django import forms - -from django_images.models import Image - - -FIELD_NAME_MAPPING = { - 'image': 'qqfile', -} - - -class ImageForm(forms.ModelForm): - def add_prefix(self, field_name): - field_name = FIELD_NAME_MAPPING.get(field_name, field_name) - return super(ImageForm, self).add_prefix(field_name) - - class Meta: - model = Image - fields = ('image',) \ No newline at end of file diff --git a/core/serializers.py b/core/serializers.py index 6d970f6..98573dd 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -107,16 +107,15 @@ class PinSerializer(serializers.HyperlinkedModelSerializer): required=False, ) - def validate(self, attrs): - if 'url' not in attrs and 'image_by_id' not in attrs: + def create(self, validated_data): + if 'url' not in validated_data and\ + 'image_by_id' not in validated_data: raise ValidationError( detail={ "url-or-image": "Either url or image_by_id is required." }, ) - return attrs - def create(self, validated_data): submitter = self.context['request'].user if 'url' in validated_data and validated_data['url']: url = validated_data['url'] @@ -133,9 +132,9 @@ class PinSerializer(serializers.HyperlinkedModelSerializer): return pin def update(self, instance, validated_data): - tags = validated_data.pop('tag_list') + tags = validated_data.pop('tag_list', None) if tags: instance.tags.set(*tags) - # change for image-id is not allowed + # change for image-id or image is not allowed validated_data.pop('image_by_id', None) return super(PinSerializer, self).update(instance, validated_data) diff --git a/core/tests/__init__.py b/core/tests/__init__.py index a2f2474..fc315e5 100644 --- a/core/tests/__init__.py +++ b/core/tests/__init__.py @@ -1,5 +1,3 @@ from .api import * -from .forms import * -from .helpers import PinFactoryTest from .views import * diff --git a/core/tests/api.py b/core/tests/api.py index da00394..2e3e588 100644 --- a/core/tests/api.py +++ b/core/tests/api.py @@ -1,16 +1,15 @@ +import json + +from django.urls import reverse import mock +from rest_framework import status +from rest_framework.test import APITestCase from django_images.models import Thumbnail from taggit.models import Tag -from tastypie.exceptions import Unauthorized -from tastypie.test import ResourceTestCase -from .helpers import ImageFactory, PinFactory, UserFactory +from .helpers import create_image, create_user, create_pin from core.models import Pin, Image -from users.models import User - - -__all__ = ['ImageResourceTest', 'PinResourceTest'] def filter_generator_for(size): @@ -19,265 +18,123 @@ def filter_generator_for(size): return wrapped_func -def mock_requests_get(url): +def mock_requests_get(url, **kwargs): response = mock.Mock(content=open('logo.png', 'rb').read()) return response -class ImageResourceTest(ResourceTestCase): +class ImageTests(APITestCase): def test_post_create_unsupported(self): - """Make sure that new images can't be created using API""" - response = self.api_client.post('/api/v1/image/', format='json', data={}) - self.assertHttpUnauthorized(response) - - def test_list_detail(self): - image = ImageFactory() - thumbnail = filter_generator_for('thumbnail')(image) - standard = filter_generator_for('standard')(image) - square = filter_generator_for('square')(image) - response = self.api_client.get('/api/v1/image/', format='json') - self.assertDictEqual(self.deserialize(response)['objects'][0], { - u'image': unicode(image.image.url), - u'height': image.height, - u'width': image.width, - u'standard': { - u'image': unicode(standard.image.url), - u'width': standard.width, - u'height': standard.height, - }, - u'thumbnail': { - u'image': unicode(thumbnail.image.url), - u'width': thumbnail.width, - u'height': thumbnail.height, - }, - u'square': { - u'image': unicode(square.image.url), - u'width': square.width, - u'height': square.height, - }, - }) + url = reverse("image-list") + data = {} + response = self.client.post( + url, + data=data, + format='json', + ) + self.assertEqual(response.status_code, 403, response.data) -class PinResourceTest(ResourceTestCase): +class PinTests(APITestCase): + _JSON_TYPE = "application/json" + def setUp(self): - super(PinResourceTest, self).setUp() - self.user = UserFactory(password='password') - self.api_client.client.login(username=self.user.username, password='password') + super(PinTests, self).setUp() + self.user = create_user("default") + self.client.login(username=self.user.username, password='password') + + def tearDown(self): + Pin.objects.all().delete() + Image.objects.all().delete() + Tag.objects.all().delete() @mock.patch('requests.get', mock_requests_get) - def test_post_create_url(self): - url = 'http://testserver/mocked/logo.png' - referer = 'http://testserver/' + def test_should_create_pin(self): + url = 'http://testserver.com/mocked/logo-01.png' + create_url = reverse("pin-list") + referer = 'http://testserver.com/' post_data = { - 'submitter': '/api/v1/user/{}/'.format(self.user.pk), 'url': url, 'referer': referer, 'description': 'That\'s an Apple!' } - response = self.api_client.post('/api/v1/pin/', data=post_data) - self.assertHttpCreated(response) - self.assertEqual(Pin.objects.count(), 1) - self.assertEqual(Image.objects.count(), 1) - - # submitter is optional, current user will be used by default - post_data = { - 'url': url, - 'description': 'That\'s an Apple!', - 'origin': None - } - response = self.api_client.post('/api/v1/pin/', data=post_data) - self.assertHttpCreated(response) + response = self.client.post(create_url, data=post_data, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + pin = Pin.objects.get(url=url) + self.assertIsNotNone(pin.image.image) @mock.patch('requests.get', mock_requests_get) def test_post_create_url_with_empty_tags(self): - url = 'http://testserver/mocked/logo.png' - referer = 'http://testserver/' + url = 'http://testserver.com/mocked/logo-02.png' + create_url = reverse("pin-list") + referer = 'http://testserver.com/' post_data = { - 'submitter': '/api/v1/user/{}/'.format(self.user.pk), 'url': url, 'referer': referer, 'description': 'That\'s an Apple!', 'tags': [] } - response = self.api_client.post('/api/v1/pin/', data=post_data) - self.assertHttpCreated(response) - self.assertEqual(Pin.objects.count(), 1) + response = self.client.post(create_url, data=post_data, format="json") + self.assertEqual( + response.status_code, status.HTTP_201_CREATED, response.json() + ) self.assertEqual(Image.objects.count(), 1) pin = Pin.objects.get(url=url) + self.assertIsNotNone(pin.image.image) self.assertEqual(pin.tags.count(), 0) - @mock.patch('requests.get', mock_requests_get) - def test_post_create_url_unauthorized(self): - url = 'http://testserver/mocked/logo.png' - referer = 'http://testserver/' + def test_should_post_create_pin_with_existed_image(self): + image = create_image() + create_pin(self.user, image=image, tags=[]) + create_url = reverse("pin-list") + referer = 'http://testserver.com/' post_data = { - 'submitter': '/api/v1/user/2/', - 'url': url, 'referer': referer, - 'description': 'That\'s an Apple!', - 'tags': [] - } - with self.assertRaises(Unauthorized): - response = self.api_client.post('/api/v1/pin/', data=post_data) - self.assertEqual(Pin.objects.count(), 0) - self.assertEqual(Image.objects.count(), 0) - - @mock.patch('requests.get', mock_requests_get) - def test_post_create_url_with_empty_origin(self): - url = 'http://testserver/mocked/logo.png' - referer = 'http://testserver/' - post_data = { - 'submitter': '/api/v1/user/{}/'.format(self.user.pk), - 'url': url, - 'referer': referer, - 'description': 'That\'s an Apple!', - 'origin': None - } - response = self.api_client.post('/api/v1/pin/', data=post_data) - self.assertHttpCreated(response) - self.assertEqual(Pin.objects.count(), 1) - self.assertEqual(Image.objects.count(), 1) - self.assertEqual(Pin.objects.get(url=url).origin, None) - - @mock.patch('requests.get', mock_requests_get) - def test_post_create_url_with_origin(self): - origin = 'http://testserver/mocked/' - url = origin + 'logo.png' - referer = 'http://testserver/' - post_data = { - 'submitter': '/api/v1/user/{}/'.format(self.user.pk), - 'url': url, - 'referer': referer, - 'description': 'That\'s an Apple!', - 'origin': origin - } - response = self.api_client.post('/api/v1/pin/', data=post_data) - self.assertHttpCreated(response) - self.assertEqual(Pin.objects.count(), 1) - self.assertEqual(Image.objects.count(), 1) - self.assertEqual(Pin.objects.get(url=url).origin, origin) - - def test_post_create_obj(self): - image = ImageFactory() - referer = 'http://testserver/' - post_data = { - 'submitter': '/api/v1/user/{}/'.format(self.user.pk), - 'referer': referer, - 'image': '/api/v1/image/{}/'.format(image.pk), + 'image_by_id': image.pk, 'description': 'That\'s something else (probably a CC logo)!', 'tags': ['random', 'tags'], } - response = self.api_client.post('/api/v1/pin/', data=post_data) + response = self.client.post(create_url, data=post_data, format="json") + resp_data = response.json() + self.assertEqual(response.status_code, status.HTTP_201_CREATED, resp_data) self.assertEqual( - self.deserialize(response)['description'], - 'That\'s something else (probably a CC logo)!' + resp_data['description'], + 'That\'s something else (probably a CC logo)!', + resp_data ) - self.assertHttpCreated(response) - # A number of Image objects should stay the same as we are using an existing image - self.assertEqual(Image.objects.count(), 1) - self.assertEqual(Pin.objects.count(), 1) - self.assertEquals(Tag.objects.count(), 2) + self.assertEquals(Pin.objects.count(), 2) - def test_put_detail_unauthenticated(self): - self.api_client.client.logout() - uri = '/api/v1/pin/{}/'.format(PinFactory().pk) - response = self.api_client.put(uri, format='json', data={}) - self.assertHttpUnauthorized(response) + def test_patch_detail_unauthenticated(self): + image = create_image() + pin = create_pin(self.user, image, []) + self.client.logout() + uri = reverse("pin-detail", kwargs={"pk": pin.pk}) + response = self.client.patch(uri, format='json', data={}) + self.assertEqual(response.status_code, 403) - def test_put_detail_unauthorized(self): - uri = '/api/v1/pin/{}/'.format(PinFactory(submitter=self.user).pk) - user = UserFactory(password='password') - self.api_client.client.login(username=user.username, password='password') - response = self.api_client.put(uri, format='json', data={}) - self.assertHttpUnauthorized(response) - - def test_put_detail(self): - pin = PinFactory(submitter=self.user) - uri = '/api/v1/pin/{}/'.format(pin.pk) + def test_patch_detail(self): + image = create_image() + pin = create_pin(self.user, image, []) + uri = reverse("pin-detail", kwargs={"pk": pin.pk}) new = {'description': 'Updated description'} - response = self.api_client.put(uri, format='json', data=new) - self.assertHttpAccepted(response) + response = self.client.patch( + uri, new, format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.json()) self.assertEqual(Pin.objects.count(), 1) self.assertEqual(Pin.objects.get(pk=pin.pk).description, new['description']) def test_delete_detail_unauthenticated(self): - uri = '/api/v1/pin/{}/'.format(PinFactory(submitter=self.user).pk) - self.api_client.client.logout() - self.assertHttpUnauthorized(self.api_client.delete(uri)) - - def test_delete_detail_unauthorized(self): - uri = '/api/v1/pin/{}/'.format(PinFactory(submitter=self.user).pk) - User.objects.create_user('test', 'test@example.com', 'test') - self.api_client.client.login(username='test', password='test') - self.assertHttpUnauthorized(self.api_client.delete(uri)) + image = create_image() + pin = create_pin(self.user, image, []) + uri = reverse("pin-detail", kwargs={"pk": pin.pk}) + self.client.logout() + self.assertEqual(self.client.delete(uri).status_code, 403) def test_delete_detail(self): - uri = '/api/v1/pin/{}/'.format(PinFactory(submitter=self.user).pk) - self.assertHttpAccepted(self.api_client.delete(uri)) + image = create_image() + pin = create_pin(self.user, image, []) + uri = reverse("pin-detail", kwargs={"pk": pin.pk}) + self.client.delete(uri) self.assertEqual(Pin.objects.count(), 0) - - def test_get_list_json_ordered(self): - _, pin = PinFactory(), PinFactory() - response = self.api_client.get('/api/v1/pin/', format='json', data={'order_by': '-id'}) - self.assertValidJSONResponse(response) - self.assertEqual(self.deserialize(response)['objects'][0]['id'], pin.id) - - def test_get_list_json_filtered_by_tags(self): - pin = PinFactory() - response = self.api_client.get('/api/v1/pin/', format='json', data={'tag': pin.tags.all()[0]}) - self.assertValidJSONResponse(response) - self.assertEqual(self.deserialize(response)['objects'][0]['id'], pin.pk) - - def test_get_list_json_filtered_by_submitter(self): - pin = PinFactory(submitter=self.user) - response = self.api_client.get('/api/v1/pin/', format='json', data={'submitter__username': self.user.username}) - self.assertValidJSONResponse(response) - self.assertEqual(self.deserialize(response)['objects'][0]['id'], pin.pk) - - def test_get_list_json(self): - image = ImageFactory() - pin = PinFactory(**{ - 'submitter': self.user, - 'image': image, - 'referer': 'http://testserver/mocked/', - 'url': 'http://testserver/mocked/logo.png', - 'description': u'Mocked Description', - 'origin': None - }) - standard = filter_generator_for('standard')(image) - thumbnail = filter_generator_for('thumbnail')(image) - square = filter_generator_for('square')(image) - response = self.api_client.get('/api/v1/pin/', format='json') - self.assertValidJSONResponse(response) - self.assertDictEqual(self.deserialize(response)['objects'][0], { - u'id': pin.id, - u'submitter': { - u'username': unicode(self.user.username), - u'gravatar': unicode(self.user.gravatar) - }, - u'image': { - u'image': unicode(image.image.url), - u'width': image.width, - u'height': image.height, - u'standard': { - u'image': unicode(standard.image.url), - u'width': standard.width, - u'height': standard.height, - }, - u'thumbnail': { - u'image': unicode(thumbnail.image.url), - u'width': thumbnail.width, - u'height': thumbnail.height, - }, - u'square': { - u'image': unicode(square.image.url), - u'width': square.width, - u'height': square.height, - }, - }, - u'url': pin.url, - u'origin': pin.origin, - u'description': pin.description, - u'tags': [tag.name for tag in pin.tags.all()] - }) diff --git a/core/tests/forms.py b/core/tests/forms.py deleted file mode 100644 index 1166a85..0000000 --- a/core/tests/forms.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.test import TestCase -from ..forms import ImageForm - - -__all__ = ['ImageFormTest'] - -class ImageFormTest(TestCase): - def test_image_field_prefix(self): - """Assert that the image field has a proper name""" - form = ImageForm() - self.assertInHTML("", str(form)) \ No newline at end of file diff --git a/core/tests/helpers.py b/core/tests/helpers.py index 52adeed..be0bd27 100644 --- a/core/tests/helpers.py +++ b/core/tests/helpers.py @@ -1,11 +1,7 @@ from django.conf import settings -from django.contrib.auth.models import Permission from django.core.files.images import ImageFile -from django.db.models.query import QuerySet -from django.test import TestCase from django_images.models import Thumbnail -import factory from taggit.models import Tag from core.models import Pin, Image @@ -15,78 +11,33 @@ from users.models import User TEST_IMAGE_PATH = 'logo.png' -class UserFactory(factory.Factory): - FACTORY_FOR = User - - username = factory.Sequence(lambda n: 'user_{}'.format(n)) - email = factory.Sequence(lambda n: 'user_{}@example.com'.format(n)) - - @factory.post_generation(extract_prefix='password') - def set_password(self, create, extracted, **kwargs): - self.set_password(extracted) - self.save() - - @factory.post_generation(extract_prefix='user_permissions') - def set_user_permissions(self, create, extracted, **kwargs): - self.user_permissions = Permission.objects.filter(codename__in=['add_pin', 'add_image']) +def create_user(username): + user, _ = User.objects.get_or_create( + username='user_{}'.format(username), + defaults={ + "email": 'user_{}@example.com'.format(username) + } + ) + user.set_password("password") + user.save() + return user -class TagFactory(factory.Factory): - FACTORY_FOR = Tag - - name = factory.Sequence(lambda n: 'tag_{}'.format(n)) +def create_tag(name): + return Tag.objects.get_or_create( + name='tag_{}'.format(name), + slug='tag_{}'.format(name), + ) -class ImageFactory(factory.Factory): - FACTORY_FOR = Image - - image = factory.LazyAttribute(lambda a: ImageFile(open(TEST_IMAGE_PATH, 'rb'))) - - @factory.post_generation() - def create_thumbnails(self, create, extracted, **kwargs): - for size in settings.IMAGE_SIZES.keys(): - Thumbnail.objects.get_or_create_at_size(self.pk, size) +def create_image(): + image = Image.objects.create(image=ImageFile(open(TEST_IMAGE_PATH, 'rb'))) + for size in settings.IMAGE_SIZES.keys(): + Thumbnail.objects.get_or_create_at_size(image.pk, size) + return image -class PinFactory(factory.Factory): - FACTORY_FOR = Pin - - submitter = factory.SubFactory(UserFactory) - image = factory.SubFactory(ImageFactory) - - @factory.post_generation(extract_prefix='tags') - def add_tags(self, create, extracted, **kwargs): - if isinstance(extracted, Tag): - self.tags.add(extracted) - elif isinstance(extracted, list): - self.tags.add(*extracted) - elif isinstance(extracted, QuerySet): - self.tags = extracted - else: - self.tags.add(TagFactory()) - - -class PinFactoryTest(TestCase): - def test_default_tags(self): - tags = PinFactory.create().tags.all() - self.assertTrue(all([tag.name.startswith('tag_') for tag in tags])) - self.assertEqual(tags.count(), 1) - - def test_custom_tag(self): - custom = 'custom_tag' - self.assertEqual(PinFactory(tags=Tag.objects.create(name=custom)).tags.get(pk=1).name, custom) - - def test_custom_tags_list(self): - tags = TagFactory.create_batch(2) - PinFactory(tags=tags) - self.assertEqual(Tag.objects.count(), 2) - - def test_custom_tags_queryset(self): - TagFactory.create_batch(2) - tags = Tag.objects.all() - PinFactory(tags=tags) - self.assertEqual(Tag.objects.count(), 2) - - def test_empty_tags(self): - PinFactory(tags=[]) - self.assertEqual(Tag.objects.count(), 0) +def create_pin(user, image, tags): + pin = Pin.objects.create(submitter=user, image=image) + pin.tags.set(*tags) + return pin diff --git a/core/tests/views.py b/core/tests/views.py index e5135b4..a3fadff 100644 --- a/core/tests/views.py +++ b/core/tests/views.py @@ -3,8 +3,9 @@ from django.core.urlresolvers import reverse from django.template import TemplateDoesNotExist from django.test import TestCase -from .api import UserFactory from core.models import Image +from core.tests import create_user +from users.models import User __all__ = ['CreateImageTest'] @@ -12,25 +13,27 @@ __all__ = ['CreateImageTest'] class CreateImageTest(TestCase): def setUp(self): - self.user = UserFactory(password='password') + self.user = create_user("default") self.client.login(username=self.user.username, password='password') - def test_get_browser(self): - response = self.client.get(reverse('core:create-image')) - self.assertRedirects(response, reverse('core:recent-pins')) - - def test_get_xml_http_request(self): - with self.assertRaises(TemplateDoesNotExist): - self.client.get(reverse('core:create-image'), HTTP_X_REQUESTED_WITH='XMLHttpRequest') + def tearDown(self): + User.objects.all().delete() + Image.objects.all().delete() def test_post(self): - with open(settings.SITE_ROOT + 'logo.png', mode='rb') as image: - response = self.client.post(reverse('core:create-image'), {'qqfile': image}) + with open('logo.png', mode='rb') as image: + response = self.client.post(reverse('image-list'), {'image': image}) image = Image.objects.latest('pk') - self.assertJSONEqual(response.content, {'success': {'id': image.pk}}) + self.assertEqual(response.json()['id'], image.pk) def test_post_error(self): - response = self.client.post(reverse('core:create-image'), {'qqfile': None}) - self.assertJSONEqual(response.content, { - 'error': {'image': ['This field is required.']} - }) + response = self.client.post(reverse('image-list'), {'image': None}) + self.assertJSONEqual( + response.content, + { + 'image': [ + 'The submitted data was not a file. ' + 'Check the encoding type on the form.' + ] + } + ) From 47b58e9f9faab1e59390c3070531b0652046a760 Mon Sep 17 00:00:00 2001 From: winkidney Date: Fri, 22 Feb 2019 15:36:09 +0800 Subject: [PATCH 29/34] Feature: Remove django-tastypie and factory-boy in Pipfile --- Pipfile | 2 -- Pipfile.lock | 30 +----------------------------- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/Pipfile b/Pipfile index 06261b3..025f126 100644 --- a/Pipfile +++ b/Pipfile @@ -14,9 +14,7 @@ requests = "*" django-taggit = "*" django-braces = "*" django-compressor = "*" -django-tastypie = "*" mock = "*" -factory-boy = "<2.0,>=1.3" gunicorn = "*" "psycopg2" = "*" djangorestframework = "*" diff --git a/Pipfile.lock b/Pipfile.lock index c7ef5be..48751ae 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ad217c5e4d0a4207fd060d19af64ff07c87712335f95c89ac0e818c2c2cc435b" + "sha256": "1f3e0c6c3d258ec061b439c963d469012a67158bee4abc21a448555ade0f0fed" }, "pipfile-spec": 6, "requires": {}, @@ -90,13 +90,6 @@ "index": "pypi", "version": "==0.24.0" }, - "django-tastypie": { - "hashes": [ - "sha256:a3a2413510009649e0eac885ead96891c783ced788fd94231dc2f72b7a1b4c04" - ], - "index": "pypi", - "version": "==0.14.2" - }, "djangorestframework": { "hashes": [ "sha256:79c6efbb2514bc50cf25906d7c0a5cfead714c7af667ff4bd110312cd380ae66", @@ -105,13 +98,6 @@ "index": "pypi", "version": "==3.9.1" }, - "factory-boy": { - "hashes": [ - "sha256:bd5d87634946c8831c0d1389b5995da5dd64ccd97088eebc311eb0c9ef75ae3b" - ], - "index": "pypi", - "version": "==1.3.0" - }, "gunicorn": { "hashes": [ "sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471", @@ -268,20 +254,6 @@ "index": "pypi", "version": "==2.7.7" }, - "python-dateutil": { - "hashes": [ - "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", - "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" - ], - "version": "==2.8.0" - }, - "python-mimeparse": { - "hashes": [ - "sha256:76e4b03d700a641fd7761d3cd4fdbbdcd787eade1ebfac43f877016328334f78", - "sha256:a295f03ff20341491bfe4717a39cd0a8cc9afad619ba44b77e86b0ab8a2b8282" - ], - "version": "==1.6.0" - }, "pytz": { "hashes": [ "sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9", From 814782f5ef291728ecad6ce6bf430ce1fd82e5c4 Mon Sep 17 00:00:00 2001 From: winkidney Date: Fri, 22 Feb 2019 15:44:25 +0800 Subject: [PATCH 30/34] Fix: Use rsponse.json() to fix test-failure on json-3.5 --- core/tests/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/tests/views.py b/core/tests/views.py index a3fadff..7cb10b4 100644 --- a/core/tests/views.py +++ b/core/tests/views.py @@ -28,8 +28,8 @@ class CreateImageTest(TestCase): def test_post_error(self): response = self.client.post(reverse('image-list'), {'image': None}) - self.assertJSONEqual( - response.content, + self.assertEqual( + response.json(), { 'image': [ 'The submitted data was not a file. ' From f3f9975ca04b1e3e500c0785806e22c180325b4c Mon Sep 17 00:00:00 2001 From: winkidney Date: Fri, 22 Feb 2019 16:09:47 +0800 Subject: [PATCH 31/34] Fix: Fix syntax erro on python 3.4 --- core/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/serializers.py b/core/serializers.py index 98573dd..070b253 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -72,8 +72,8 @@ class TagSerializer(serializers.SlugRelatedField): def to_internal_value(self, data): obj, _ = self.get_queryset().get_or_create( - **{self.slug_field: data}, - defaults={self.slug_field: data, "slug": data} + defaults={self.slug_field: data, "slug": data}, + **{self.slug_field: data} ) return obj From 6b3402c9f5fb8d7a48a6f2ce1fdf1fe1bbd5dd03 Mon Sep 17 00:00:00 2001 From: winkidney Date: Fri, 22 Feb 2019 16:44:53 +0800 Subject: [PATCH 32/34] Feature: Use psycopg2-binary as dep instead of psycopg2 Due to the offical warnning from command line. --- Pipfile | 2 +- Pipfile.lock | 64 ++++++++++++++++++++++++++-------------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/Pipfile b/Pipfile index 025f126..14381e7 100644 --- a/Pipfile +++ b/Pipfile @@ -16,8 +16,8 @@ django-braces = "*" django-compressor = "*" mock = "*" gunicorn = "*" -"psycopg2" = "*" djangorestframework = "*" markdown = "*" django-filter = "*" coreapi = "*" +psycopg2-binary = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 48751ae..bf4045c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1f3e0c6c3d258ec061b439c963d469012a67158bee4abc21a448555ade0f0fed" + "sha256": "10d142378c7ba1cc68764f6ef995177af8b244f620adaac4e0347a822ecaf488" }, "pipfile-spec": 6, "requires": {}, @@ -218,38 +218,38 @@ "index": "pypi", "version": "==5.4.1" }, - "psycopg2": { + "psycopg2-binary": { "hashes": [ - "sha256:02445ebbb3a11a3fe8202c413d5e6faf38bb75b4e336203ee144ca2c46529f94", - "sha256:0e9873e60f98f0c52339abf8f0339d1e22bfe5aae0bcf7aabd40c055175035ec", - "sha256:1148a5eb29073280bf9057c7fc45468592c1bb75a28f6df1591adb93c8cb63d0", - "sha256:259a8324e109d4922b0fcd046e223e289830e2568d6f4132a3702439e5fd532b", - "sha256:28dffa9ed4595429e61bacac41d3f9671bb613d1442ff43bcbec63d4f73ed5e8", - "sha256:314a74302d4737a3865d40ea50e430ce1543c921ba10f39d562e807cfe2edf2a", - "sha256:36b60201b6d215d7658a71493fdf6bd5e60ad9a0cffed39906627ff9f4f3afd3", - "sha256:3f9d532bce54c4234161176ff3b8688ff337575ca441ea27597e112dfcd0ee0c", - "sha256:5d222983847b40af989ad96c07fc3f07e47925e463baa5de716be8f805b41d9b", - "sha256:6757a6d2fc58f7d8f5d471ad180a0bd7b4dd3c7d681f051504fbea7ae29c8d6f", - "sha256:6a0e0f1e74edb0ab57d89680e59e7bfefad2bfbdf7c80eb38304d897d43674bb", - "sha256:6ca703ccdf734e886a1cf53eb702261110f6a8b0ed74bcad15f1399f74d3f189", - "sha256:8513b953d8f443c446aa79a4cc8a898bd415fc5e29349054f03a7d696d495542", - "sha256:9262a5ce2038570cb81b4d6413720484cb1bc52c064b2f36228d735b1f98b794", - "sha256:97441f851d862a0c844d981cbee7ee62566c322ebb3d68f86d66aa99d483985b", - "sha256:a07feade155eb8e69b54dd6774cf6acf2d936660c61d8123b8b6b1f9247b67d6", - "sha256:a9b9c02c91b1e3ec1f1886b2d0a90a0ea07cc529cb7e6e472b556bc20ce658f3", - "sha256:ae88216f94728d691b945983140bf40d51a1ff6c7fe57def93949bf9339ed54a", - "sha256:b360ffd17659491f1a6ad7c928350e229c7b7bd83a2b922b6ee541245c7a776f", - "sha256:b4221957ceccf14b2abdabef42d806e791350be10e21b260d7c9ce49012cc19e", - "sha256:b90758e49d5e6b152a460d10b92f8a6ccf318fcc0ee814dcf53f3a6fc5328789", - "sha256:c669ea986190ed05fb289d0c100cc88064351f2b85177cbfd3564c4f4847d18c", - "sha256:d1b61999d15c79cf7f4f7cc9021477aef35277fc52452cf50fd13b713c84424d", - "sha256:de7bb043d1adaaf46e38d47e7a5f703bb3dab01376111e522b07d25e1a79c1e1", - "sha256:e393568e288d884b94d263f2669215197840d097c7e5b0acd1a51c1ea7d1aba8", - "sha256:ed7e0849337bd37d89f2c2b0216a0de863399ee5d363d31b1e5330a99044737b", - "sha256:f153f71c3164665d269a5d03c7fa76ba675c7a8de9dc09a4e2c2cdc9936a7b41", - "sha256:f1fb5a8427af099beb7f65093cbdb52e021b8e6dbdfaf020402a623f4181baf5", - "sha256:f36b333e9f86a2fba960c72b90c34be6ca71819e300f7b1fc3d2b0f0b2c546cd", - "sha256:f4526d078aedd5187d0508aa5f9a01eae6a48a470ed678406da94b4cd6524b7e" + "sha256:19a2d1f3567b30f6c2bb3baea23f74f69d51f0c06c2e2082d0d9c28b0733a4c2", + "sha256:2b69cf4b0fa2716fd977aa4e1fd39af6110eb47b2bb30b4e5a469d8fbecfc102", + "sha256:2e952fa17ba48cbc2dc063ddeec37d7dc4ea0ef7db0ac1eda8906365a8543f31", + "sha256:348b49dd737ff74cfb5e663e18cb069b44c64f77ec0523b5794efafbfa7df0b8", + "sha256:3d72a5fdc5f00ca85160915eb9a973cf9a0ab8148f6eda40708bf672c55ac1d1", + "sha256:4957452f7868f43f32c090dadb4188e9c74a4687323c87a882e943c2bd4780c3", + "sha256:5138cec2ee1e53a671e11cc519505eb08aaaaf390c508f25b09605763d48de4b", + "sha256:587098ca4fc46c95736459d171102336af12f0d415b3b865972a79c03f06259f", + "sha256:5b79368bcdb1da4a05f931b62760bea0955ee2c81531d8e84625df2defd3f709", + "sha256:5cf43807392247d9bc99737160da32d3fa619e0bfd85ba24d1c78db205f472a4", + "sha256:676d1a80b1eebc0cacae8dd09b2fde24213173bf65650d22b038c5ed4039f392", + "sha256:6b0211ecda389101a7d1d3df2eba0cf7ffbdd2480ca6f1d2257c7bd739e84110", + "sha256:79cde4660de6f0bb523c229763bd8ad9a93ac6760b72c369cf1213955c430934", + "sha256:7aba9786ac32c2a6d5fb446002ed936b47d5e1f10c466ef7e48f66eb9f9ebe3b", + "sha256:7c8159352244e11bdd422226aa17651110b600d175220c451a9acf795e7414e0", + "sha256:945f2eedf4fc6b2432697eb90bb98cc467de5147869e57405bfc31fa0b824741", + "sha256:96b4e902cde37a7fc6ab306b3ac089a3949e6ce3d824eeca5b19dc0bedb9f6e2", + "sha256:9a7bccb1212e63f309eb9fab47b6eaef796f59850f169a25695b248ca1bf681b", + "sha256:a3bfcac727538ec11af304b5eccadbac952d4cca1a551a29b8fe554e3ad535dc", + "sha256:b19e9f1b85c5d6136f5a0549abdc55dcbd63aba18b4f10d0d063eb65ef2c68b4", + "sha256:b664011bb14ca1f2287c17185e222f2098f7b4c857961dbcf9badb28786dbbf4", + "sha256:bde7959ef012b628868d69c474ec4920252656d0800835ed999ba5e4f57e3e2e", + "sha256:cb095a0657d792c8de9f7c9a0452385a309dfb1bbbb3357d6b1e216353ade6ca", + "sha256:d16d42a1b9772152c1fe606f679b2316551f7e1a1ce273e7f808e82a136cdb3d", + "sha256:d444b1545430ffc1e7a24ce5a9be122ccd3b135a7b7e695c5862c5aff0b11159", + "sha256:d93ccc7bf409ec0a23f2ac70977507e0b8a8d8c54e5ee46109af2f0ec9e411f3", + "sha256:df6444f952ca849016902662e1a47abf4fa0678d75f92fd9dd27f20525f809cd", + "sha256:e63850d8c52ba2b502662bf3c02603175c2397a9acc756090e444ce49508d41e", + "sha256:ec43358c105794bc2b6fd34c68d27f92bea7102393c01889e93f4b6a70975728", + "sha256:f4c6926d9c03dadce7a3b378b40d2fea912c1344ef9b29869f984fb3d2a2420b" ], "index": "pypi", "version": "==2.7.7" From de652846b6243626aac04603101d1a63e367217e Mon Sep 17 00:00:00 2001 From: winkidney Date: Fri, 22 Feb 2019 16:46:55 +0800 Subject: [PATCH 33/34] Doc: Add warnning to remove origin field --- core/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/models.py b/core/models.py index 9cd56d0..4a22e78 100644 --- a/core/models.py +++ b/core/models.py @@ -73,6 +73,8 @@ class Image(BaseImage): class Pin(models.Model): submitter = models.ForeignKey(User) url = models.URLField(null=True, blank=True) + # origin is tha same as referer but not work, + # should be removed some day origin = models.URLField(null=True, blank=True) referer = models.URLField(null=True, blank=True) description = models.TextField(blank=True, null=True) From 69ff11b6c6884f1d5d894d70761d2afadc018a5c Mon Sep 17 00:00:00 2001 From: winkidney Date: Fri, 22 Feb 2019 16:57:33 +0800 Subject: [PATCH 34/34] Fix: Use new url in light-box --- pinry/templates/includes/lightbox.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pinry/templates/includes/lightbox.html b/pinry/templates/includes/lightbox.html index dafdbec..ad83737 100644 --- a/pinry/templates/includes/lightbox.html +++ b/pinry/templates/includes/lightbox.html @@ -19,7 +19,7 @@ {{#if tags}}
in {{#each tags}} - {{this}} + {{this}} {{/each}} {{/if}}