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 diff --git a/Pipfile b/Pipfile index 77bb400..14381e7 100644 --- a/Pipfile +++ b/Pipfile @@ -14,8 +14,10 @@ requests = "*" django-taggit = "*" django-braces = "*" django-compressor = "*" -django-tastypie = "*" mock = "*" -factory-boy = "<2.0,>=1.3" gunicorn = "*" -"psycopg2" = "*" +djangorestframework = "*" +markdown = "*" +django-filter = "*" +coreapi = "*" +psycopg2-binary = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 4a60cef..bf4045c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "68c12441be13a252f7fdd1b5532c7befdfa56fc8307ab75a2b197a1946a54bf2" + "sha256": "10d142378c7ba1cc68764f6ef995177af8b244f620adaac4e0347a822ecaf488" }, "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": [ @@ -28,13 +28,28 @@ ], "version": "==3.0.4" }, - "django": { + "coreapi": { "hashes": [ - "sha256:8176ac7985fe6737ce3d6b2531b4a2453cb7c3377c9db00bacb2b3320f4a1311", - "sha256:b18235d82426f09733d2de9910cee975cf52ff05e5f836681eb957d105a05a40" + "sha256:46145fcc1f7017c076a2ef684969b641d18a2991051fddec9458ad3f78ffc1cb", + "sha256:bf39d118d6d3e171f10df9ede5666f63ad80bba9a29a8ec17726a66cf52ee6f3" ], "index": "pypi", - "version": "==1.11.15" + "version": "==2.3.3" + }, + "coreschema": { + "hashes": [ + "sha256:5e6ef7bf38c1525d5e55a895934ab4273548629f16aed5c0a6caa74ebf45551f", + "sha256:9503506007d482ab0867ba14724b93c18a33b22b6d19fb419ef2d239dd4a1607" + ], + "version": "==0.0.4" + }, + "django": { + "hashes": [ + "sha256:0a73696e0ac71ee6177103df984f9c1e07cd297f080f8ec4dc7c6f3fb74395b5", + "sha256:43a99da08fee329480d27860d68279945b7d8bf7b537388ee2c8938c709b2041" + ], + "index": "pypi", + "version": "==1.11.20" }, "django-appconf": { "hashes": [ @@ -59,27 +74,29 @@ "index": "pypi", "version": "==2.2" }, + "django-filter": { + "hashes": [ + "sha256:3dafb7d2810790498895c22a1f31b2375795910680ac9c1432821cbedb1e176d", + "sha256:a3014de317bef0cd43075a0f08dfa1d319a7ccc5733c3901fb860da70b0dda68" + ], + "index": "pypi", + "version": "==2.1.0" + }, "django-taggit": { "hashes": [ - "sha256:a21cbe7e0879f1364eef1c88a2eda89d593bf000ebf51c3f00423c6927075dce", - "sha256:db4430ec99265341e05d0274edb0279163bd74357241f7b4d9274bdcb3338b17" + "sha256:710b4d15ec1996550cc68a0abbc41903ca7d832540e52b1336e6858737e410d8", + "sha256:bb8f27684814cd1414b2af75b857b5e26a40912631904038a7ecacd2bfafc3ac" ], "index": "pypi", - "version": "==0.23.0" + "version": "==0.24.0" }, - "django-tastypie": { + "djangorestframework": { "hashes": [ - "sha256:1fbf61ec7467eec70bd1abcb14e3b1dc67e47cc3642ad16ed8a3709f4140678b" + "sha256:79c6efbb2514bc50cf25906d7c0a5cfead714c7af667ff4bd110312cd380ae66", + "sha256:a4138613b67e3a223be6c97f53b13d759c5b90d2b433bad670b8ebf95402075f" ], "index": "pypi", - "version": "==0.14.1" - }, - "factory-boy": { - "hashes": [ - "sha256:bd5d87634946c8831c0d1389b5995da5dd64ccd97088eebc311eb0c9ef75ae3b" - ], - "index": "pypi", - "version": "==1.3.0" + "version": "==3.9.1" }, "gunicorn": { "hashes": [ @@ -91,10 +108,64 @@ }, "idna": { "hashes": [ - "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", - "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" ], - "version": "==2.7" + "version": "==2.8" + }, + "itypes": { + "hashes": [ + "sha256:c6e77bb9fd68a4bfeb9d958fea421802282451a25bac4913ec94db82a899c073" + ], + "version": "==1.1.0" + }, + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "version": "==2.10" + }, + "markdown": { + "hashes": [ + "sha256:c00429bd503a47ec88d5e30a751e147dcb4c6889663cd3e2ba0afe858e009baa", + "sha256:d02e0f9b04c500cde6637c11ad7c72671f359b87b9fe924b2383649d8841db7c" + ], + "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": [ @@ -106,103 +177,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": { + "psycopg2-binary": { "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: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.5" - }, - "python-dateutil": { - "hashes": [ - "sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0", - "sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8" - ], - "version": "==2.7.3" - }, - "python-mimeparse": { - "hashes": [ - "sha256:76e4b03d700a641fd7761d3cd4fdbbdcd787eade1ebfac43f877016328334f78", - "sha256:a295f03ff20341491bfe4717a39cd0a8cc9afad619ba44b77e86b0ab8a2b8282" - ], - "version": "==1.6.0" + "version": "==2.7.7" }, "pytz": { "hashes": [ - "sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053", - "sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277" + "sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9", + "sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c" ], - "version": "==2018.5" + "version": "==2018.9" }, "rcssmin": { "hashes": [ @@ -212,11 +269,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 +283,42 @@ }, "six": { "hashes": [ - "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", - "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" ], - "version": "==1.11.0" + "version": "==1.12.0" + }, + "uritemplate": { + "hashes": [ + "sha256:01c69f4fe8ed503b2951bef85d996a9d22434d2431584b5b107b2981ff416fbd", + "sha256:1b9c467a940ce9fb9f50df819e8ddd14696f89b9a8cc87ac77952ba416e0a8fd", + "sha256:c02643cebe23fc8adb5e6becffe201185bf06c40bda5c0b4028a93f1527d011d" + ], + "version": "==3.0.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 +329,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" } } } 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/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/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/models.py b/core/models.py index 766a598..4a22e78 100644 --- a/core/models.py +++ b/core/models.py @@ -43,24 +43,55 @@ 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) - url = models.URLField(null=True) - origin = models.URLField(null=True) - referer = models.URLField(null=True) + 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) image = models.ForeignKey(Image, related_name='pin') 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) @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 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/serializers.py b/core/serializers.py new file mode 100644 index 0000000..070b253 --- /dev/null +++ b/core/serializers.py @@ -0,0 +1,140 @@ +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 +from core.models import Pin +from django_images.models import Thumbnail +from users.models import User + + +class UserSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = User + fields = ( + 'username', + 'gravatar', + settings.DRF_URL_FIELD_NAME, + ) + + +class ThumbnailSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Thumbnail + fields = ( + "image", + "width", + "height", + ) + + +class ImageSerializer(serializers.ModelSerializer): + class Meta: + model = Image + fields = ( + "id", + "image", + "width", + "height", + "standard", + "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 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( + defaults={self.slug_field: data, "slug": data}, + **{self.slug_field: data} + ) + return obj + + +class PinSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Pin + fields = ( + settings.DRF_URL_FIELD_NAME, + "id", + "submitter", + "url", + "origin", + "description", + "referer", + "image", + "image_by_id", + "tags", + ) + + submitter = UserSerializer(read_only=True) + tags = TagSerializer( + many=True, + source="tag_list", + required=False, + ) + image = ImageSerializer(required=False, read_only=True) + image_by_id = serializers.PrimaryKeyRelatedField( + queryset=Image.objects.all(), + write_only=True, + required=False, + ) + + 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." + }, + ) + + submitter = self.context['request'].user + if 'url' in validated_data and validated_data['url']: + url = validated_data['url'] + image = Image.objects.create_for_url( + url, + validated_data.get('referer', url), + ) + 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) + return pin + + def update(self, instance, validated_data): + tags = validated_data.pop('tag_list', None) + if tags: + instance.tags.set(*tags) + # 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..7cb10b4 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.assertEqual( + response.json(), + { + 'image': [ + 'The submitted data was not a file. ' + 'Check the encoding type on the form.' + ] + } + ) diff --git a/core/urls.py b/core/urls.py index 1a45826..29c45b3 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,31 +1,16 @@ -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 -from .views import CreateImage - - -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/create-image/$', CreateImage.as_view(), name='create-image'), - - 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'), + name='pin-detail'), url(r'^$', TemplateView.as_view(template_name='core/pins.html'), name='recent-pins'), ] diff --git a/core/views.py b/core/views.py index 1684c6b..4cdbcff 100644 --- a/core/views.py +++ b/core/views.py @@ -1,34 +1,38 @@ -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 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 -from braces.views import JSONResponseMixin, LoginRequiredMixin -from django_images.models import Thumbnail - -from .forms import ImageForm +from core import serializers 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(mixins.RetrieveModelMixin, GenericViewSet): + 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}) + def create(self, request, *args, **kwargs): + return super(ImageViewSet, self).create(request, *args, **kwargs) + + +class PinViewSet(viewsets.ModelViewSet): + queryset = Pin.objects.all() + serializer_class = api.PinSerializer + filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter) + filter_fields = ("submitter__username", 'tags__name', ) + ordering_fields = ('-id', ) + ordering = ('-id', ) + 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/settings/base.py b/pinry/settings/base.py index 7191b1c..0b89271 100644 --- a/pinry/settings/base.py +++ b/pinry/settings/base.py @@ -14,6 +14,8 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'rest_framework', + 'django_filters', 'taggit', 'compressor', 'django_images', @@ -139,3 +141,21 @@ IS_TEST = False # User custom settings 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.IsAuthenticatedOrReadOnly' + ], + 'DEFAULT_FILTER_BACKENDS': ( + 'django_filters.rest_framework.DjangoFilterBackend', + ), + 'URL_FIELD_NAME': DRF_URL_FIELD_NAME, + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', + 'PAGE_SIZE': API_LIMIT_PER_PAGE, +} diff --git a/pinry/static/js/helpers.js b/pinry/static/js/helpers.js index 9f22a4e..3956e1f 100644 --- a/pinry/static/js/helpers.js +++ b/pinry/static/js/helpers.js @@ -5,6 +5,42 @@ * Updated: Feb 26th, 2013 * Require: jQuery */ +var API_BASE = "/api/v2/"; + + +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) { @@ -25,21 +61,14 @@ 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); } function deletePinData(pinId) { - var apiUrl = '/api/v1/pin/'+pinId+'/?format=json'; + var apiUrl = API_BASE + 'pins/' +pinId + '/?format=json'; return $.ajax(apiUrl, { type: 'DELETE' }); @@ -48,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 34617f2..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(), @@ -99,24 +97,25 @@ $(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, 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) { @@ -136,13 +135,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) @@ -165,13 +164,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_by_id = uploadedImage; + } else { + data.url = $('#pin-form-image-url').val(); + } var promise = postPinData(data); promise.success(function(pin) { if (pinFromUrl) return window.close(); diff --git a/pinry/static/js/pinry.js b/pinry/static/js/pinry.js index 230a1e7..390b263 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); - if (tagFilter) apiUrl = apiUrl + '&tag=' + tagFilter; + var apiUrl = API_BASE + 'pins/?format=json&ordering=-id&limit=50&offset='+String(offset); + if (tagFilter) apiUrl = apiUrl + '&tags__name=' + 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'); 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 }}"; 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}} 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}}
diff --git a/pinry/urls.py b/pinry/urls.py index 1311a18..ef2e9d8 100644 --- a/pinry/urls.py +++ b/pinry/urls.py @@ -3,11 +3,21 @@ 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.views import drf_router + admin.autodiscover() urlpatterns = [ + # drf api + url(r'^api/v2/', include(drf_router.urls)), + url(r'^api-auth/', include('rest_framework.urls', namespace="rest_framework")), + url(r'^api/v2/docs/', include_docs_urls(title='PinryAPI', schema_url='/')), + + # 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')), 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