Merge pull request #139 from pinry/feature/upgrade2drf

Upgrade to DjangoRestframework(short as DRF) which is currently under active development support.

Summary:

    * No side effects on Pinry Browser Plugin

    * No side effects on Bookmarklet.

    * All API now working on DRF instead of tastypie.
This commit is contained in:
Ji Qu
2019-02-22 17:11:15 +08:00
committed by GitHub
25 changed files with 697 additions and 802 deletions

View File

@@ -17,3 +17,5 @@ install:
pipenv install pipenv install
test: test:
pipenv run python manage.py test pipenv run python manage.py test
shell:
pipenv run python manage.py shell

View File

@@ -14,8 +14,10 @@ requests = "*"
django-taggit = "*" django-taggit = "*"
django-braces = "*" django-braces = "*"
django-compressor = "*" django-compressor = "*"
django-tastypie = "*"
mock = "*" mock = "*"
factory-boy = "<2.0,>=1.3"
gunicorn = "*" gunicorn = "*"
"psycopg2" = "*" djangorestframework = "*"
markdown = "*"
django-filter = "*"
coreapi = "*"
psycopg2-binary = "*"

335
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "68c12441be13a252f7fdd1b5532c7befdfa56fc8307ab75a2b197a1946a54bf2" "sha256": "10d142378c7ba1cc68764f6ef995177af8b244f620adaac4e0347a822ecaf488"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": {}, "requires": {},
@@ -16,10 +16,10 @@
"default": { "default": {
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638", "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
"sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a" "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
], ],
"version": "==2018.8.24" "version": "==2018.11.29"
}, },
"chardet": { "chardet": {
"hashes": [ "hashes": [
@@ -28,13 +28,28 @@
], ],
"version": "==3.0.4" "version": "==3.0.4"
}, },
"django": { "coreapi": {
"hashes": [ "hashes": [
"sha256:8176ac7985fe6737ce3d6b2531b4a2453cb7c3377c9db00bacb2b3320f4a1311", "sha256:46145fcc1f7017c076a2ef684969b641d18a2991051fddec9458ad3f78ffc1cb",
"sha256:b18235d82426f09733d2de9910cee975cf52ff05e5f836681eb957d105a05a40" "sha256:bf39d118d6d3e171f10df9ede5666f63ad80bba9a29a8ec17726a66cf52ee6f3"
], ],
"index": "pypi", "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": { "django-appconf": {
"hashes": [ "hashes": [
@@ -59,27 +74,29 @@
"index": "pypi", "index": "pypi",
"version": "==2.2" "version": "==2.2"
}, },
"django-filter": {
"hashes": [
"sha256:3dafb7d2810790498895c22a1f31b2375795910680ac9c1432821cbedb1e176d",
"sha256:a3014de317bef0cd43075a0f08dfa1d319a7ccc5733c3901fb860da70b0dda68"
],
"index": "pypi",
"version": "==2.1.0"
},
"django-taggit": { "django-taggit": {
"hashes": [ "hashes": [
"sha256:a21cbe7e0879f1364eef1c88a2eda89d593bf000ebf51c3f00423c6927075dce", "sha256:710b4d15ec1996550cc68a0abbc41903ca7d832540e52b1336e6858737e410d8",
"sha256:db4430ec99265341e05d0274edb0279163bd74357241f7b4d9274bdcb3338b17" "sha256:bb8f27684814cd1414b2af75b857b5e26a40912631904038a7ecacd2bfafc3ac"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.23.0" "version": "==0.24.0"
}, },
"django-tastypie": { "djangorestframework": {
"hashes": [ "hashes": [
"sha256:1fbf61ec7467eec70bd1abcb14e3b1dc67e47cc3642ad16ed8a3709f4140678b" "sha256:79c6efbb2514bc50cf25906d7c0a5cfead714c7af667ff4bd110312cd380ae66",
"sha256:a4138613b67e3a223be6c97f53b13d759c5b90d2b433bad670b8ebf95402075f"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.14.1" "version": "==3.9.1"
},
"factory-boy": {
"hashes": [
"sha256:bd5d87634946c8831c0d1389b5995da5dd64ccd97088eebc311eb0c9ef75ae3b"
],
"index": "pypi",
"version": "==1.3.0"
}, },
"gunicorn": { "gunicorn": {
"hashes": [ "hashes": [
@@ -91,10 +108,64 @@
}, },
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" "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": { "mock": {
"hashes": [ "hashes": [
@@ -106,103 +177,89 @@
}, },
"pbr": { "pbr": {
"hashes": [ "hashes": [
"sha256:1b8be50d938c9bb75d0eaf7eda111eec1bf6dc88a62a6412e33bf077457e0f45", "sha256:a7953f66e1f82e4b061f43096a4bcc058f7d3d41de9b94ac871770e8bdd831a2",
"sha256:b486975c0cafb6beeb50ca0e17ba047647f229087bd74e37f4a7e2cac17d2caa" "sha256:d717573351cfe09f49df61906cd272abaa759b3e91744396b804965ff7bff38b"
], ],
"version": "==4.2.0" "version": "==5.1.2"
}, },
"pillow": { "pillow": {
"hashes": [ "hashes": [
"sha256:00def5b638994f888d1058e4d17c86dec8e1113c3741a0a8a659039aec59a83a", "sha256:051de330a06c99d6f84bcf582960487835bcae3fc99365185dc2d4f65a390c0e",
"sha256:026449b64e559226cdb8e6d8c931b5965d8fc90ec18ebbb0baa04c5b36503c72", "sha256:0ae5289948c5e0a16574750021bd8be921c27d4e3527800dc9c2c1d2abc81bf7",
"sha256:03dbb224ee196ef30ed2156d41b579143e1efeb422974719a5392fc035e4f574", "sha256:0b1efce03619cdbf8bcc61cfae81fcda59249a469f31c6735ea59badd4a6f58a",
"sha256:03eb0e04f929c102ae24bc436bf1c0c60a4e63b07ebd388e84d8b219df3e6acd", "sha256:163136e09bd1d6c6c6026b0a662976e86c58b932b964f255ff384ecc8c3cefa3",
"sha256:1be66b9a89e367e7d20d6cae419794997921fe105090fafd86ef39e20a3baab2", "sha256:18e912a6ccddf28defa196bd2021fe33600cbe5da1aa2f2e2c6df15f720b73d1",
"sha256:1e977a3ed998a599bda5021fb2c2889060617627d3ae228297a529a082a3cd5c", "sha256:24ec3dea52339a610d34401d2d53d0fb3c7fd08e34b20c95d2ad3973193591f1",
"sha256:22cf3406d135cfcc13ec6228ade774c8461e125c940e80455f500638429be273", "sha256:267f8e4c0a1d7e36e97c6a604f5b03ef58e2b81c1becb4fccecddcb37e063cc7",
"sha256:24adccf1e834f82718c7fc8e3ec1093738da95144b8b1e44c99d5fc7d3e9c554", "sha256:3273a28734175feebbe4d0a4cde04d4ed20f620b9b506d26f44379d3c72304e1",
"sha256:2a3e362c97a5e6a259ee9cd66553292a1f8928a5bdfa3622fdb1501570834612", "sha256:4c678e23006798fc8b6f4cef2eaad267d53ff4c1779bd1af8725cc11b72a63f3",
"sha256:3832e26ecbc9d8a500821e3a1d3765bda99d04ae29ffbb2efba49f5f788dc934", "sha256:4d4bc2e6bb6861103ea4655d6b6f67af8e5336e7216e20fff3e18ffa95d7a055",
"sha256:4fd1f0c2dc02aaec729d91c92cd85a2df0289d88e9f68d1e8faba750bb9c4786", "sha256:505738076350a337c1740a31646e1de09a164c62c07db3b996abdc0f9d2e50cf",
"sha256:4fda62030f2c515b6e2e673c57caa55cb04026a81968f3128aae10fc28e5cc27", "sha256:5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f",
"sha256:5044d75a68b49ce36a813c82d8201384207112d5d81643937fc758c05302f05b", "sha256:5d95cb9f6cced2628f3e4de7e795e98b2659dfcc7176ab4a01a8b48c2c2f488f",
"sha256:522184556921512ec484cb93bd84e0bab915d0ac5a372d49571c241a7f73db62", "sha256:7eda4c737637af74bac4b23aa82ea6fbb19002552be85f0b89bc27e3a762d239",
"sha256:5914cff11f3e920626da48e564be6818831713a3087586302444b9c70e8552d9", "sha256:801ddaa69659b36abf4694fed5aa9f61d1ecf2daaa6c92541bbbbb775d97b9fe",
"sha256:6661a7908d68c4a133e03dac8178287aa20a99f841ea90beeb98a233ae3fd710", "sha256:825aa6d222ce2c2b90d34a0ea31914e141a85edefc07e17342f1d2fdf121c07c",
"sha256:79258a8df3e309a54c7ef2ef4a59bb8e28f7e4a8992a3ad17c24b1889ced44f3", "sha256:9c215442ff8249d41ff58700e91ef61d74f47dfd431a50253e1a1ca9436b0697",
"sha256:7d74c20b8f1c3e99d3f781d3b8ff5abfefdd7363d61e23bdeba9992ff32cc4b4", "sha256:a3d90022f2202bbb14da991f26ca7a30b7e4c62bf0f8bf9825603b22d7e87494",
"sha256:81918afeafc16ba5d9d0d4e9445905f21aac969a4ebb6f2bff4b9886da100f4b", "sha256:a631fd36a9823638fe700d9225f9698fb59d049c942d322d4c09544dc2115356",
"sha256:8194d913ca1f459377c8a4ed8f9b7ad750068b8e0e3f3f9c6963fcc87a84515f", "sha256:a6523a23a205be0fe664b6b8747a5c86d55da960d9586db039eec9f5c269c0e6",
"sha256:84d5d31200b11b3c76fab853b89ac898bf2d05c8b3da07c1fcc23feb06359d6e", "sha256:a756ecf9f4b9b3ed49a680a649af45a8767ad038de39e6c030919c2f443eb000",
"sha256:989981db57abffb52026b114c9a1f114c7142860a6d30a352d28f8cbf186500b", "sha256:b117287a5bdc81f1bac891187275ec7e829e961b8032c9e5ff38b70fd036c78f",
"sha256:a3d7511d3fad1618a82299aab71a5fceee5c015653a77ffea75ced9ef917e71a", "sha256:ba04f57d1715ca5ff74bb7f8a818bf929a204b3b3c2c2826d1e1cc3b1c13398c",
"sha256:b3ef168d4d6fd4fa6685aef7c91400f59f7ab1c0da734541f7031699741fb23f", "sha256:cd878195166723f30865e05d87cbaf9421614501a4bd48792c5ed28f90fd36ca",
"sha256:c1c5792b6e74bbf2af0f8e892272c2a6c48efa895903211f11b8342e03129fea", "sha256:cee815cc62d136e96cf76771b9d3eb58e0777ec18ea50de5cfcede8a7c429aa8",
"sha256:c5dcb5a56aebb8a8f2585042b2f5c496d7624f0bcfe248f0cc33ceb2fd8d39e7", "sha256:d1722b7aa4b40cf93ac3c80d3edd48bf93b9208241d166a14ad8e7a20ee1d4f3",
"sha256:e2bed4a04e2ca1050bb5f00865cf2f83c0b92fd62454d9244f690fcd842e27a4", "sha256:d7c1c06246b05529f9984435fc4fa5a545ea26606e7f450bdbe00c153f5aeaad",
"sha256:e87a527c06319428007e8c30511e1f0ce035cb7f14bb4793b003ed532c3b9333", "sha256:e9c8066249c040efdda84793a2a669076f92a301ceabe69202446abb4c5c5ef9",
"sha256:f63e420180cbe22ff6e32558b612e75f50616fc111c5e095a4631946c782e109", "sha256:f227d7e574d050ff3996049e086e1f18c7bd2d067ef24131e50a1d3fe5831fbc",
"sha256:f8b3d413c5a8f84b12cd4c5df1d8e211777c9852c6be3ee9c094b626644d3eab" "sha256:fc9a12aad714af36cf3ad0275a96a733526571e52710319855628f476dcb144e"
], ],
"index": "pypi", "index": "pypi",
"version": "==5.2.0" "version": "==5.4.1"
}, },
"psycopg2": { "psycopg2-binary": {
"hashes": [ "hashes": [
"sha256:0b9e48a1c1505699a64ac58815ca99104aacace8321e455072cee4f7fe7b2698", "sha256:19a2d1f3567b30f6c2bb3baea23f74f69d51f0c06c2e2082d0d9c28b0733a4c2",
"sha256:0f4c784e1b5a320efb434c66a50b8dd7e30a7dc047e8f45c0a8d2694bfe72781", "sha256:2b69cf4b0fa2716fd977aa4e1fd39af6110eb47b2bb30b4e5a469d8fbecfc102",
"sha256:0fdbaa32c9eb09ef09d425dc154628fca6fa69d2f7c1a33f889abb7e0efb3909", "sha256:2e952fa17ba48cbc2dc063ddeec37d7dc4ea0ef7db0ac1eda8906365a8543f31",
"sha256:11fbf688d5c953c0a5ba625cc42dea9aeb2321942c7c5ed9341a68f865dc8cb1", "sha256:348b49dd737ff74cfb5e663e18cb069b44c64f77ec0523b5794efafbfa7df0b8",
"sha256:19eaac4eb25ab078bd0f28304a0cb08702d120caadfe76bb1e6846ed1f68635e", "sha256:3d72a5fdc5f00ca85160915eb9a973cf9a0ab8148f6eda40708bf672c55ac1d1",
"sha256:3232ec1a3bf4dba97fbf9b03ce12e4b6c1d01ea3c85773903a67ced725728232", "sha256:4957452f7868f43f32c090dadb4188e9c74a4687323c87a882e943c2bd4780c3",
"sha256:36f8f9c216fcca048006f6dd60e4d3e6f406afde26cfb99e063f137070139eaf", "sha256:5138cec2ee1e53a671e11cc519505eb08aaaaf390c508f25b09605763d48de4b",
"sha256:59c1a0e4f9abe970062ed35d0720935197800a7ef7a62b3a9e3a70588d9ca40b", "sha256:587098ca4fc46c95736459d171102336af12f0d415b3b865972a79c03f06259f",
"sha256:6506c5ff88750948c28d41852c09c5d2a49f51f28c6d90cbf1b6808e18c64e88", "sha256:5b79368bcdb1da4a05f931b62760bea0955ee2c81531d8e84625df2defd3f709",
"sha256:6bc3e68ee16f571681b8c0b6d5c0a77bef3c589012352b3f0cf5520e674e9d01", "sha256:5cf43807392247d9bc99737160da32d3fa619e0bfd85ba24d1c78db205f472a4",
"sha256:6dbbd7aabbc861eec6b910522534894d9dbb507d5819bc982032c3ea2e974f51", "sha256:676d1a80b1eebc0cacae8dd09b2fde24213173bf65650d22b038c5ed4039f392",
"sha256:6e737915de826650d1a5f7ff4ac6cf888a26f021a647390ca7bafdba0e85462b", "sha256:6b0211ecda389101a7d1d3df2eba0cf7ffbdd2480ca6f1d2257c7bd739e84110",
"sha256:6ed9b2cfe85abc720e8943c1808eeffd41daa73e18b7c1e1a228b0b91f768ccc", "sha256:79cde4660de6f0bb523c229763bd8ad9a93ac6760b72c369cf1213955c430934",
"sha256:711ec617ba453fdfc66616db2520db3a6d9a891e3bf62ef9aba4c95bb4e61230", "sha256:7aba9786ac32c2a6d5fb446002ed936b47d5e1f10c466ef7e48f66eb9f9ebe3b",
"sha256:844dacdf7530c5c612718cf12bc001f59b2d9329d35b495f1ff25045161aa6af", "sha256:7c8159352244e11bdd422226aa17651110b600d175220c451a9acf795e7414e0",
"sha256:86b52e146da13c896e50c5a3341a9448151f1092b1a4153e425d1e8b62fec508", "sha256:945f2eedf4fc6b2432697eb90bb98cc467de5147869e57405bfc31fa0b824741",
"sha256:985c06c2a0f227131733ae58d6a541a5bc8b665e7305494782bebdb74202b793", "sha256:96b4e902cde37a7fc6ab306b3ac089a3949e6ce3d824eeca5b19dc0bedb9f6e2",
"sha256:a86dfe45f4f9c55b1a2312ff20a59b30da8d39c0e8821d00018372a2a177098f", "sha256:9a7bccb1212e63f309eb9fab47b6eaef796f59850f169a25695b248ca1bf681b",
"sha256:aa3cd07f7f7e3183b63d48300666f920828a9dbd7d7ec53d450df2c4953687a9", "sha256:a3bfcac727538ec11af304b5eccadbac952d4cca1a551a29b8fe554e3ad535dc",
"sha256:b1964ed645ef8317806d615d9ff006c0dadc09dfc54b99ae67f9ba7a1ec9d5d2", "sha256:b19e9f1b85c5d6136f5a0549abdc55dcbd63aba18b4f10d0d063eb65ef2c68b4",
"sha256:b2abbff9e4141484bb89b96eb8eae186d77bc6d5ffbec6b01783ee5c3c467351", "sha256:b664011bb14ca1f2287c17185e222f2098f7b4c857961dbcf9badb28786dbbf4",
"sha256:cc33c3a90492e21713260095f02b12bee02b8d1f2c03a221d763ce04fa90e2e9", "sha256:bde7959ef012b628868d69c474ec4920252656d0800835ed999ba5e4f57e3e2e",
"sha256:d7de3bf0986d777807611c36e809b77a13bf1888f5c8db0ebf24b47a52d10726", "sha256:cb095a0657d792c8de9f7c9a0452385a309dfb1bbbb3357d6b1e216353ade6ca",
"sha256:db5e3c52576cc5b93a959a03ccc3b02cb8f0af1fbbdc80645f7a215f0b864f3a", "sha256:d16d42a1b9772152c1fe606f679b2316551f7e1a1ce273e7f808e82a136cdb3d",
"sha256:e168aa795ffbb11379c942cf95bf813c7db9aa55538eb61de8c6815e092416f5", "sha256:d444b1545430ffc1e7a24ce5a9be122ccd3b135a7b7e695c5862c5aff0b11159",
"sha256:e9ca911f8e2d3117e5241d5fa9aaa991cb22fb0792627eeada47425d706b5ec8", "sha256:d93ccc7bf409ec0a23f2ac70977507e0b8a8d8c54e5ee46109af2f0ec9e411f3",
"sha256:eccf962d41ca46e6326b97c8fe0a6687b58dfc1a5f6540ed071ff1474cea749e", "sha256:df6444f952ca849016902662e1a47abf4fa0678d75f92fd9dd27f20525f809cd",
"sha256:efa19deae6b9e504a74347fe5e25c2cb9343766c489c2ae921b05f37338b18d1", "sha256:e63850d8c52ba2b502662bf3c02603175c2397a9acc756090e444ce49508d41e",
"sha256:f4b0460a21f784abe17b496f66e74157a6c36116fa86da8bf6aa028b9e8ad5fe", "sha256:ec43358c105794bc2b6fd34c68d27f92bea7102393c01889e93f4b6a70975728",
"sha256:f93d508ca64d924d478fb11e272e09524698f0c581d9032e68958cfbdd41faef" "sha256:f4c6926d9c03dadce7a3b378b40d2fea912c1344ef9b29869f984fb3d2a2420b"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.7.5" "version": "==2.7.7"
},
"python-dateutil": {
"hashes": [
"sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0",
"sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8"
],
"version": "==2.7.3"
},
"python-mimeparse": {
"hashes": [
"sha256:76e4b03d700a641fd7761d3cd4fdbbdcd787eade1ebfac43f877016328334f78",
"sha256:a295f03ff20341491bfe4717a39cd0a8cc9afad619ba44b77e86b0ab8a2b8282"
],
"version": "==1.6.0"
}, },
"pytz": { "pytz": {
"hashes": [ "hashes": [
"sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053", "sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9",
"sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277" "sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"
], ],
"version": "==2018.5" "version": "==2018.9"
}, },
"rcssmin": { "rcssmin": {
"hashes": [ "hashes": [
@@ -212,11 +269,11 @@
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1", "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a" "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.19.1" "version": "==2.21.0"
}, },
"rjsmin": { "rjsmin": {
"hashes": [ "hashes": [
@@ -226,28 +283,42 @@
}, },
"six": { "six": {
"hashes": [ "hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
], ],
"version": "==1.11.0" "version": "==1.12.0"
},
"uritemplate": {
"hashes": [
"sha256:01c69f4fe8ed503b2951bef85d996a9d22434d2431584b5b107b2981ff416fbd",
"sha256:1b9c467a940ce9fb9f50df819e8ddd14696f89b9a8cc87ac77952ba416e0a8fd",
"sha256:c02643cebe23fc8adb5e6becffe201185bf06c40bda5c0b4028a93f1527d011d"
],
"version": "==3.0.0"
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" "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.24.1"
"version": "==1.23"
} }
}, },
"develop": { "develop": {
"entrypoints": {
"hashes": [
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
"sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"
],
"version": "==0.3"
},
"flake8": { "flake8": {
"hashes": [ "hashes": [
"sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0", "sha256:6d8c66a65635d46d54de59b027a1dda40abbe2275b3164b634835ac9c13fd048",
"sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37" "sha256:6eab21c6e34df2c05416faa40d0c59963008fff29b6f0ccfe8fa28152ab3e383"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.5.0" "version": "==3.7.6"
}, },
"mccabe": { "mccabe": {
"hashes": [ "hashes": [
@@ -258,32 +329,32 @@
}, },
"pycodestyle": { "pycodestyle": {
"hashes": [ "hashes": [
"sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766", "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
"sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9" "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
], ],
"version": "==2.3.1" "version": "==2.5.0"
}, },
"pyflakes": { "pyflakes": {
"hashes": [ "hashes": [
"sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f", "sha256:5e8c00e30c464c99e0b501dc160b13a14af7f27d4dffb529c556e30a159e231d",
"sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805" "sha256:f277f9ca3e55de669fba45b7393a1449009cff5a37d1af10ebb76c52765269cd"
], ],
"version": "==1.6.0" "version": "==2.1.0"
}, },
"qrcode": { "qrcode": {
"hashes": [ "hashes": [
"sha256:037b0db4c93f44586e37f84c3da3f763874fcac85b2974a69a98e399ac78e1bf", "sha256:3996ee560fc39532910603704c82980ff6d4d5d629f9c3f25f34174ce8606cf5",
"sha256:de4ffc15065e6ff20a551ad32b6b41264f3c75275675406ddfa8e3530d154be3" "sha256:505253854f607f2abf4d16092c61d4e9d511a3b4392e60bff957a68592b04369"
], ],
"index": "pypi", "index": "pypi",
"version": "==6.0" "version": "==6.1"
}, },
"six": { "six": {
"hashes": [ "hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
], ],
"version": "==1.11.0" "version": "==1.12.0"
} }
} }
} }

View File

@@ -6,5 +6,5 @@ from .models import Pin
class PinAdmin(admin.ModelAdmin): class PinAdmin(admin.ModelAdmin):
pass pass
admin.site.register(Pin, PinAdmin)
admin.site.register(Pin, PinAdmin)

View File

@@ -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()

View File

@@ -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',)

View File

@@ -43,24 +43,55 @@ class ImageManager(models.Manager):
class Image(BaseImage): class Image(BaseImage):
objects = ImageManager() objects = ImageManager()
class Sizes:
standard = "standard"
thumbnail = "thumbnail"
square = "square"
class Meta: class Meta:
proxy = True 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): class Pin(models.Model):
submitter = models.ForeignKey(User) submitter = models.ForeignKey(User)
url = models.URLField(null=True) url = models.URLField(null=True, blank=True)
origin = models.URLField(null=True) # origin is tha same as referer but not work,
referer = models.URLField(null=True) # 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) description = models.TextField(blank=True, null=True)
image = models.ForeignKey(Image, related_name='pin') image = models.ForeignKey(Image, related_name='pin')
published = models.DateTimeField(auto_now_add=True) published = models.DateTimeField(auto_now_add=True)
tags = TaggableManager() tags = TaggableManager()
def tag_list(self):
return self.tags.all()
def __unicode__(self): def __unicode__(self):
return '%s - %s' % (self.submitter, self.published) return '%s - %s' % (self.submitter, self.published)
@receiver(models.signals.post_delete, sender=Pin) @receiver(models.signals.post_delete, sender=Pin)
def delete_pin_images(sender, instance, **kwargs): def delete_pin_images(sender, instance, **kwargs):
instance.image.delete() try:
instance.image.delete()
except Image.DoesNotExist:
pass

42
core/permissions.py Normal file
View File

@@ -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

140
core/serializers.py Normal file
View File

@@ -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)

View File

@@ -1,5 +1,3 @@
from .api import * from .api import *
from .forms import *
from .helpers import PinFactoryTest
from .views import * from .views import *

View File

@@ -1,16 +1,15 @@
import json
from django.urls import reverse
import mock import mock
from rest_framework import status
from rest_framework.test import APITestCase
from django_images.models import Thumbnail from django_images.models import Thumbnail
from taggit.models import Tag 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 core.models import Pin, Image
from users.models import User
__all__ = ['ImageResourceTest', 'PinResourceTest']
def filter_generator_for(size): def filter_generator_for(size):
@@ -19,265 +18,123 @@ def filter_generator_for(size):
return wrapped_func return wrapped_func
def mock_requests_get(url): def mock_requests_get(url, **kwargs):
response = mock.Mock(content=open('logo.png', 'rb').read()) response = mock.Mock(content=open('logo.png', 'rb').read())
return response return response
class ImageResourceTest(ResourceTestCase): class ImageTests(APITestCase):
def test_post_create_unsupported(self): def test_post_create_unsupported(self):
"""Make sure that new images can't be created using API""" url = reverse("image-list")
response = self.api_client.post('/api/v1/image/', format='json', data={}) data = {}
self.assertHttpUnauthorized(response) response = self.client.post(
url,
def test_list_detail(self): data=data,
image = ImageFactory() format='json',
thumbnail = filter_generator_for('thumbnail')(image) )
standard = filter_generator_for('standard')(image) self.assertEqual(response.status_code, 403, response.data)
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,
},
})
class PinResourceTest(ResourceTestCase): class PinTests(APITestCase):
_JSON_TYPE = "application/json"
def setUp(self): def setUp(self):
super(PinResourceTest, self).setUp() super(PinTests, self).setUp()
self.user = UserFactory(password='password') self.user = create_user("default")
self.api_client.client.login(username=self.user.username, password='password') 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) @mock.patch('requests.get', mock_requests_get)
def test_post_create_url(self): def test_should_create_pin(self):
url = 'http://testserver/mocked/logo.png' url = 'http://testserver.com/mocked/logo-01.png'
referer = 'http://testserver/' create_url = reverse("pin-list")
referer = 'http://testserver.com/'
post_data = { post_data = {
'submitter': '/api/v1/user/{}/'.format(self.user.pk),
'url': url, 'url': url,
'referer': referer, 'referer': referer,
'description': 'That\'s an Apple!' 'description': 'That\'s an Apple!'
} }
response = self.api_client.post('/api/v1/pin/', data=post_data) response = self.client.post(create_url, data=post_data, format="json")
self.assertHttpCreated(response) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Pin.objects.count(), 1) pin = Pin.objects.get(url=url)
self.assertEqual(Image.objects.count(), 1) self.assertIsNotNone(pin.image.image)
# 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)
@mock.patch('requests.get', mock_requests_get) @mock.patch('requests.get', mock_requests_get)
def test_post_create_url_with_empty_tags(self): def test_post_create_url_with_empty_tags(self):
url = 'http://testserver/mocked/logo.png' url = 'http://testserver.com/mocked/logo-02.png'
referer = 'http://testserver/' create_url = reverse("pin-list")
referer = 'http://testserver.com/'
post_data = { post_data = {
'submitter': '/api/v1/user/{}/'.format(self.user.pk),
'url': url, 'url': url,
'referer': referer, 'referer': referer,
'description': 'That\'s an Apple!', 'description': 'That\'s an Apple!',
'tags': [] 'tags': []
} }
response = self.api_client.post('/api/v1/pin/', data=post_data) response = self.client.post(create_url, data=post_data, format="json")
self.assertHttpCreated(response) self.assertEqual(
self.assertEqual(Pin.objects.count(), 1) response.status_code, status.HTTP_201_CREATED, response.json()
)
self.assertEqual(Image.objects.count(), 1) self.assertEqual(Image.objects.count(), 1)
pin = Pin.objects.get(url=url) pin = Pin.objects.get(url=url)
self.assertIsNotNone(pin.image.image)
self.assertEqual(pin.tags.count(), 0) self.assertEqual(pin.tags.count(), 0)
@mock.patch('requests.get', mock_requests_get) def test_should_post_create_pin_with_existed_image(self):
def test_post_create_url_unauthorized(self): image = create_image()
url = 'http://testserver/mocked/logo.png' create_pin(self.user, image=image, tags=[])
referer = 'http://testserver/' create_url = reverse("pin-list")
referer = 'http://testserver.com/'
post_data = { post_data = {
'submitter': '/api/v1/user/2/',
'url': url,
'referer': referer, 'referer': referer,
'description': 'That\'s an Apple!', 'image_by_id': image.pk,
'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),
'description': 'That\'s something else (probably a CC logo)!', 'description': 'That\'s something else (probably a CC logo)!',
'tags': ['random', 'tags'], '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.assertEqual(
self.deserialize(response)['description'], resp_data['description'],
'That\'s something else (probably a CC logo)!' 'That\'s something else (probably a CC logo)!',
resp_data
) )
self.assertHttpCreated(response) self.assertEquals(Pin.objects.count(), 2)
# 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)
def test_put_detail_unauthenticated(self): def test_patch_detail_unauthenticated(self):
self.api_client.client.logout() image = create_image()
uri = '/api/v1/pin/{}/'.format(PinFactory().pk) pin = create_pin(self.user, image, [])
response = self.api_client.put(uri, format='json', data={}) self.client.logout()
self.assertHttpUnauthorized(response) 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): def test_patch_detail(self):
uri = '/api/v1/pin/{}/'.format(PinFactory(submitter=self.user).pk) image = create_image()
user = UserFactory(password='password') pin = create_pin(self.user, image, [])
self.api_client.client.login(username=user.username, password='password') uri = reverse("pin-detail", kwargs={"pk": pin.pk})
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)
new = {'description': 'Updated description'} new = {'description': 'Updated description'}
response = self.api_client.put(uri, format='json', data=new) response = self.client.patch(
self.assertHttpAccepted(response) 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.count(), 1)
self.assertEqual(Pin.objects.get(pk=pin.pk).description, new['description']) self.assertEqual(Pin.objects.get(pk=pin.pk).description, new['description'])
def test_delete_detail_unauthenticated(self): def test_delete_detail_unauthenticated(self):
uri = '/api/v1/pin/{}/'.format(PinFactory(submitter=self.user).pk) image = create_image()
self.api_client.client.logout() pin = create_pin(self.user, image, [])
self.assertHttpUnauthorized(self.api_client.delete(uri)) uri = reverse("pin-detail", kwargs={"pk": pin.pk})
self.client.logout()
def test_delete_detail_unauthorized(self): self.assertEqual(self.client.delete(uri).status_code, 403)
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))
def test_delete_detail(self): def test_delete_detail(self):
uri = '/api/v1/pin/{}/'.format(PinFactory(submitter=self.user).pk) image = create_image()
self.assertHttpAccepted(self.api_client.delete(uri)) pin = create_pin(self.user, image, [])
uri = reverse("pin-detail", kwargs={"pk": pin.pk})
self.client.delete(uri)
self.assertEqual(Pin.objects.count(), 0) 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()]
})

View File

@@ -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("<input id='id_qqfile' name='qqfile' type='file' />", str(form))

View File

@@ -1,11 +1,7 @@
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Permission
from django.core.files.images import ImageFile 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 from django_images.models import Thumbnail
import factory
from taggit.models import Tag from taggit.models import Tag
from core.models import Pin, Image from core.models import Pin, Image
@@ -15,78 +11,33 @@ from users.models import User
TEST_IMAGE_PATH = 'logo.png' TEST_IMAGE_PATH = 'logo.png'
class UserFactory(factory.Factory): def create_user(username):
FACTORY_FOR = User user, _ = User.objects.get_or_create(
username='user_{}'.format(username),
username = factory.Sequence(lambda n: 'user_{}'.format(n)) defaults={
email = factory.Sequence(lambda n: 'user_{}@example.com'.format(n)) "email": 'user_{}@example.com'.format(username)
}
@factory.post_generation(extract_prefix='password') )
def set_password(self, create, extracted, **kwargs): user.set_password("password")
self.set_password(extracted) user.save()
self.save() return user
@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'])
class TagFactory(factory.Factory): def create_tag(name):
FACTORY_FOR = Tag return Tag.objects.get_or_create(
name='tag_{}'.format(name),
name = factory.Sequence(lambda n: 'tag_{}'.format(n)) slug='tag_{}'.format(name),
)
class ImageFactory(factory.Factory): def create_image():
FACTORY_FOR = Image image = Image.objects.create(image=ImageFile(open(TEST_IMAGE_PATH, 'rb')))
for size in settings.IMAGE_SIZES.keys():
image = factory.LazyAttribute(lambda a: ImageFile(open(TEST_IMAGE_PATH, 'rb'))) Thumbnail.objects.get_or_create_at_size(image.pk, size)
return image
@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)
class PinFactory(factory.Factory): def create_pin(user, image, tags):
FACTORY_FOR = Pin pin = Pin.objects.create(submitter=user, image=image)
pin.tags.set(*tags)
submitter = factory.SubFactory(UserFactory) return pin
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)

View File

@@ -3,8 +3,9 @@ from django.core.urlresolvers import reverse
from django.template import TemplateDoesNotExist from django.template import TemplateDoesNotExist
from django.test import TestCase from django.test import TestCase
from .api import UserFactory
from core.models import Image from core.models import Image
from core.tests import create_user
from users.models import User
__all__ = ['CreateImageTest'] __all__ = ['CreateImageTest']
@@ -12,25 +13,27 @@ __all__ = ['CreateImageTest']
class CreateImageTest(TestCase): class CreateImageTest(TestCase):
def setUp(self): def setUp(self):
self.user = UserFactory(password='password') self.user = create_user("default")
self.client.login(username=self.user.username, password='password') self.client.login(username=self.user.username, password='password')
def test_get_browser(self): def tearDown(self):
response = self.client.get(reverse('core:create-image')) User.objects.all().delete()
self.assertRedirects(response, reverse('core:recent-pins')) Image.objects.all().delete()
def test_get_xml_http_request(self):
with self.assertRaises(TemplateDoesNotExist):
self.client.get(reverse('core:create-image'), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
def test_post(self): def test_post(self):
with open(settings.SITE_ROOT + 'logo.png', mode='rb') as image: with open('logo.png', mode='rb') as image:
response = self.client.post(reverse('core:create-image'), {'qqfile': image}) response = self.client.post(reverse('image-list'), {'image': image})
image = Image.objects.latest('pk') 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): def test_post_error(self):
response = self.client.post(reverse('core:create-image'), {'qqfile': None}) response = self.client.post(reverse('image-list'), {'image': None})
self.assertJSONEqual(response.content, { self.assertEqual(
'error': {'image': ['This field is required.']} response.json(),
}) {
'image': [
'The submitted data was not a file. '
'Check the encoding type on the form.'
]
}
)

View File

@@ -1,31 +1,16 @@
from django.conf.urls import include, url from django.conf.urls import url
from django.views.generic import TemplateView 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 = [ urlpatterns = [
url(r'^api/', include(v1_api.urls, namespace='api')),
url(r'^pins/pin-form/$', TemplateView.as_view(template_name='core/pin_form.html'), url(r'^pins/pin-form/$', TemplateView.as_view(template_name='core/pin_form.html'),
name='pin-form'), name='pin-form'),
url(r'^pins/create-image/$', CreateImage.as_view(), name='create-image'), url(r'^pins/tags/(?P<tag>(\w|-)+)/$', TemplateView.as_view(template_name='core/pins.html'),
url(r'^pins/tag/(?P<tag>(\w|-)+)/$', TemplateView.as_view(template_name='core/pins.html'),
name='tag-pins'), name='tag-pins'),
url(r'^pins/user/(?P<user>(\w|-)+)/$', TemplateView.as_view(template_name='core/pins.html'), url(r'^pins/users/(?P<username>(\w|-)+)/$', TemplateView.as_view(template_name='core/pins.html'),
name='user-pins'), name='user-pins'),
url(r'^(?P<pin>[0-9]+)/$', TemplateView.as_view(template_name='core/pins.html'), url(r'^(?P<pin>[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'), url(r'^$', TemplateView.as_view(template_name='core/pins.html'),
name='recent-pins'), name='recent-pins'),
] ]

View File

@@ -1,34 +1,38 @@
from django.http import HttpResponseRedirect from django_filters.rest_framework import DjangoFilterBackend
from django.conf import settings from rest_framework import viewsets, mixins, routers
from django.core.urlresolvers import reverse from rest_framework.filters import SearchFilter, OrderingFilter
from django.views.generic import CreateView from rest_framework.viewsets import GenericViewSet
from django_images.models import Image
from braces.views import JSONResponseMixin, LoginRequiredMixin from core import serializers as api
from django_images.models import Thumbnail from core.models import Image, Pin
from core.permissions import IsOwnerOrReadOnly
from .forms import ImageForm from users.models import User
class CreateImage(JSONResponseMixin, LoginRequiredMixin, CreateView): class UserViewSet(mixins.RetrieveModelMixin, GenericViewSet):
template_name = None # JavaScript-only view queryset = User.objects.all()
model = Image serializer_class = api.UserSerializer
form_class = ImageForm
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): class ImageViewSet(mixins.CreateModelMixin, GenericViewSet):
image = form.save() queryset = Image.objects.all()
for size in settings.IMAGE_SIZES: serializer_class = api.ImageSerializer
Thumbnail.objects.get_or_create_at_size(image.pk, size)
return self.render_json_response({
'success': {
'id': image.id
}
})
def form_invalid(self, form): def create(self, request, *args, **kwargs):
return self.render_json_response({'error': form.errors}) 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)

View File

@@ -14,6 +14,8 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'rest_framework',
'django_filters',
'taggit', 'taggit',
'compressor', 'compressor',
'django_images', 'django_images',
@@ -139,3 +141,21 @@ IS_TEST = False
# User custom settings # User custom settings
IMAGE_AUTO_DELETE = True 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,
}

View File

@@ -5,6 +5,42 @@
* Updated: Feb 26th, 2013 * Updated: Feb 26th, 2013
* Require: jQuery * 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) { function renderTemplate(templateId, context) {
@@ -25,21 +61,14 @@ function cleanTags(tags) {
return tags; return tags;
} }
function getImageData(imageId) {
var apiUrl = '/api/v1/image/'+imageId+'/?format=json';
return $.get(apiUrl);
}
function getPinData(pinId) { function getPinData(pinId) {
var apiUrl = '/api/v1/pin/'+pinId+'/?format=json'; var apiUrl = API_BASE + "pins/" + pinId + '/?format=json';
return $.get(apiUrl); return $.get(apiUrl);
} }
function deletePinData(pinId) { function deletePinData(pinId) {
var apiUrl = '/api/v1/pin/'+pinId+'/?format=json'; var apiUrl = API_BASE + 'pins/' +pinId + '/?format=json';
return $.ajax(apiUrl, { return $.ajax(apiUrl, {
type: 'DELETE' type: 'DELETE'
}); });
@@ -48,7 +77,7 @@ function deletePinData(pinId) {
function postPinData(data) { function postPinData(data) {
return $.ajax({ return $.ajax({
type: "post", type: "post",
url: "/api/v1/pin/", url: API_BASE + "pins/",
contentType: 'application/json', contentType: 'application/json',
data: JSON.stringify(data) data: JSON.stringify(data)
}); });

View File

@@ -15,7 +15,6 @@ $(window).load(function() {
// Start Helper Functions // Start Helper Functions
function getFormData() { function getFormData() {
return { return {
submitter: currentUser,
url: $('#pin-form-image-url').val(), url: $('#pin-form-image-url').val(),
referer: $('#pin-form-referer').val(), referer: $('#pin-form-referer').val(),
description: $('#pin-form-description').val(), description: $('#pin-form-description').val(),
@@ -25,7 +24,6 @@ $(window).load(function() {
function createPinPreviewFromForm() { function createPinPreviewFromForm() {
var context = {pins: [{ var context = {pins: [{
submitter: currentUser,
image: {thumbnail: {image: $('#pin-form-image-url').val()}}, image: {thumbnail: {image: $('#pin-form-image-url').val()}},
referer: $('#pin-form-referer').val(), referer: $('#pin-form-referer').val(),
description: $('#pin-form-description').val(), description: $('#pin-form-description').val(),
@@ -99,24 +97,25 @@ $(window).load(function() {
} }
// Drag and drop upload // Drag and drop upload
$('#pin-form-image-upload').dropzone({ $('#pin-form-image-upload').dropzone({
url: '/pins/create-image/', url: API_BASE + "images/",
paramName: 'qqfile', paramName: 'image',
parallelUploads: 1, parallelUploads: 1,
uploadMultiple: false, uploadMultiple: false,
maxFiles: 1, maxFiles: 1,
acceptedFiles: 'image/*', acceptedFiles: 'image/*',
headers: {
'X-CSRFToken': getCSRFToken(),
},
success: function(file, resp) { success: function(file, resp) {
$('#pin-form-image-url').parent().fadeOut(300); var image_url = $('#pin-form-image-url');
var promise = getImageData(resp.success.id); image_url.parent().fadeOut(300);
uploadedImage = resp.success.id; uploadedImage = resp.id;
promise.success(function(image) { image_url.val(resp.thumbnail.image);
$('#pin-form-image-url').val(image.thumbnail.image); createPinPreviewFromForm();
createPinPreviewFromForm(); },
}); error: function (error) {
promise.error(function() { message('Problem uploading image.', 'alert alert-error');
message('Problem uploading image.', 'alert alert-error'); },
});
}
}); });
// If bookmarklet submit // If bookmarklet submit
if (pinFromUrl) { if (pinFromUrl) {
@@ -136,13 +135,13 @@ $(window).load(function() {
$(this).off('click'); $(this).off('click');
$(this).addClass('disabled'); $(this).addClass('disabled');
if (editedPin) { if (editedPin) {
var apiUrl = '/api/v1/pin/'+editedPin.id+'/?format=json'; var apiUrl = API_BASE + 'pins/' + editedPin.id + '/?format=json';
var data = { var data = {
description: $('#pin-form-description').val(), description: $('#pin-form-description').val(),
tags: cleanTags($('#pin-form-tags').val()) tags: cleanTags($('#pin-form-tags').val())
} };
var promise = $.ajax({ var promise = $.ajax({
type: "put", type: "patch",
url: apiUrl, url: apiUrl,
contentType: 'application/json', contentType: 'application/json',
data: JSON.stringify(data) data: JSON.stringify(data)
@@ -165,13 +164,15 @@ $(window).load(function() {
}); });
} else { } else {
var data = { var data = {
submitter: '/api/v1/user/'+currentUser.id+'/',
referer: $('#pin-form-referer').val(), referer: $('#pin-form-referer').val(),
description: $('#pin-form-description').val(), description: $('#pin-form-description').val(),
tags: cleanTags($('#pin-form-tags').val()) tags: cleanTags($('#pin-form-tags').val())
}; };
if (uploadedImage) data.image = '/api/v1/image/'+uploadedImage+'/'; if (uploadedImage) {
else data.url = $('#pin-form-image-url').val(); data.image_by_id = uploadedImage;
} else {
data.url = $('#pin-form-image-url').val();
}
var promise = postPinData(data); var promise = postPinData(data);
promise.success(function(pin) { promise.success(function(pin) {
if (pinFromUrl) return window.close(); if (pinFromUrl) return window.close();

View File

@@ -10,7 +10,7 @@
$(window).load(function() { $(window).load(function() {
/** /**
* tileLayout will simply tile/retile the block/pin container when run. This * 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. * changes.
*/ */
window.tileLayout = function() { 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 * 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. * offset outside the function to keep a running tally of your location.
*/ */
function isPinEditable(pinObject) {
return pinObject.submitter.username === currentUser.username
}
function loadPins() { function loadPins() {
// Disable scroll // Disable scroll
$(window).off('scroll'); $(window).off('scroll');
@@ -112,21 +117,22 @@ $(window).load(function() {
$('.spinner').css('display', 'block'); $('.spinner').css('display', 'block');
// Fetch our pins from the api using our current offset // 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 (tagFilter) apiUrl = apiUrl + '&tags__name=' + tagFilter;
if (userFilter) apiUrl = apiUrl + '&submitter__username=' + userFilter; 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 // Set which items are editable by the current user
for (var i=0; i < pins.objects.length; i++) { var pins = pins_page.results;
pins.objects[i].editable = (pins.objects[i].submitter.username == currentUser.username); for (var i=0; i < pins.length; i++) {
pins.objects[i].tags.sort(function (a, b) { pins[i].editable = isPinEditable(pins[i]);
pins[i].tags.sort(function (a, b) {
return a.toLowerCase().localeCompare(b.toLowerCase()); return a.toLowerCase().localeCompare(b.toLowerCase());
}); });
} }
// Use the fetched pins as our context for our pins template // Use the fetched pins as our context for our pins template
var template = Handlebars.compile($('#pins-template').html()); 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 // Append the newly compiled data to our container
$('#pins').append(html); $('#pins').append(html);
@@ -140,7 +146,7 @@ $(window).load(function() {
}); });
}); });
if (pins.objects.length < apiLimitPerPage) { if (pins.length < apiLimitPerPage) {
$('.spinner').css('display', 'none'); $('.spinner').css('display', 'none');
if ($('#pins').length !== 0) { if ($('#pins').length !== 0) {
var theEnd = document.createElement('div'); var theEnd = document.createElement('div');

View File

@@ -36,7 +36,7 @@
}, },
pinFilter = "{{ request.resolver_match.kwargs.pin }}", pinFilter = "{{ request.resolver_match.kwargs.pin }}",
tagFilter = "{{ request.resolver_match.kwargs.tag }}", tagFilter = "{{ request.resolver_match.kwargs.tag }}",
userFilter = "{{ request.resolver_match.kwargs.user }}"; userFilter = "{{ request.resolver_match.kwargs.username }}";
</script> </script>
<!-- End JavaScript Variables --> <!-- End JavaScript Variables -->
</head> </head>

View File

@@ -19,7 +19,7 @@
{{#if tags}} {{#if tags}}
<br /><span class="dim">in</span> <br /><span class="dim">in</span>
{{#each tags}} {{#each tags}}
<span class="tag"><a href="/pins/tag/{{this}}/" class="btn btn-xs btn-primary">{{this}}</a></span> <span class="tag"><a href="/pins/tags/{{this}}/" class="btn btn-xs btn-primary">{{this}}</a></span>
{{/each}} {{/each}}
{{/if}} {{/if}}
</div> </div>

View File

@@ -26,11 +26,11 @@
</div> </div>
<div class="text pull-right"> <div class="text pull-right">
<span class="dim">pinned by</span> <span class="dim">pinned by</span>
<a href="/pins/user/{{submitter.username}}/">{{submitter.username}}</a> <a href="/pins/users/{{submitter.username}}/">{{submitter.username}}</a>
{{#if tags}} {{#if tags}}
<span class="dim">in</span> <span class="dim">in</span>
{{#each tags}} {{#each tags}}
<span class="tag"><a href="/pins/tag/{{this}}/">{{this}}</a></span> <span class="tag"><a href="/pins/tags/{{this}}/">{{this}}</a></span>
{{/each}} {{/each}}
{{/if}} {{/if}}
</div> </div>

View File

@@ -3,11 +3,21 @@ from django.conf.urls import include, url
from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.contrib import admin from django.contrib import admin
from django.views.static import serve from django.views.static import serve
from rest_framework.documentation import include_docs_urls
from core.views import drf_router
admin.autodiscover() admin.autodiscover()
urlpatterns = [ 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'^admin/', include(admin.site.urls)),
url(r'', include('core.urls', namespace='core')), url(r'', include('core.urls', namespace='core')),
url(r'', include('users.urls', namespace='users')), url(r'', include('users.urls', namespace='users')),

View File

@@ -5,7 +5,6 @@ from django.test.utils import override_settings
import mock import mock
from .auth.backends import CombinedAuthBackend from .auth.backends import CombinedAuthBackend
from core.models import Image, Pin
from .models import User from .models import User