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
test:
pipenv run python manage.py test
shell:
pipenv run python manage.py shell

View File

@@ -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 = "*"

335
Pipfile.lock generated
View File

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

View File

@@ -6,5 +6,5 @@ from .models import Pin
class PinAdmin(admin.ModelAdmin):
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):
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

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 .forms import *
from .helpers import PinFactoryTest
from .views import *

View File

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

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

View File

@@ -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.'
]
}
)

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 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<tag>(\w|-)+)/$', TemplateView.as_view(template_name='core/pins.html'),
url(r'^pins/tags/(?P<tag>(\w|-)+)/$', TemplateView.as_view(template_name='core/pins.html'),
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'),
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'),
name='recent-pins'),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 }}";
</script>
<!-- End JavaScript Variables -->
</head>

View File

@@ -19,7 +19,7 @@
{{#if tags}}
<br /><span class="dim">in</span>
{{#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}}
{{/if}}
</div>

View File

@@ -26,11 +26,11 @@
</div>
<div class="text pull-right">
<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}}
<span class="dim">in</span>
{{#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}}
{{/if}}
</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 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')),

View File

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