mirror of
https://github.com/pinry/pinry.git
synced 2025-11-18 02:40:40 +01:00
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:
2
Makefile
2
Makefile
@@ -17,3 +17,5 @@ install:
|
||||
pipenv install
|
||||
test:
|
||||
pipenv run python manage.py test
|
||||
shell:
|
||||
pipenv run python manage.py shell
|
||||
|
||||
8
Pipfile
8
Pipfile
@@ -14,8 +14,10 @@ requests = "*"
|
||||
django-taggit = "*"
|
||||
django-braces = "*"
|
||||
django-compressor = "*"
|
||||
django-tastypie = "*"
|
||||
mock = "*"
|
||||
factory-boy = "<2.0,>=1.3"
|
||||
gunicorn = "*"
|
||||
"psycopg2" = "*"
|
||||
djangorestframework = "*"
|
||||
markdown = "*"
|
||||
django-filter = "*"
|
||||
coreapi = "*"
|
||||
psycopg2-binary = "*"
|
||||
|
||||
335
Pipfile.lock
generated
335
Pipfile.lock
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@ from .models import Pin
|
||||
class PinAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
admin.site.register(Pin, PinAdmin)
|
||||
|
||||
admin.site.register(Pin, PinAdmin)
|
||||
|
||||
227
core/api.py
227
core/api.py
@@ -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()
|
||||
@@ -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',)
|
||||
@@ -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):
|
||||
try:
|
||||
instance.image.delete()
|
||||
except Image.DoesNotExist:
|
||||
pass
|
||||
|
||||
42
core/permissions.py
Normal file
42
core/permissions.py
Normal 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
140
core/serializers.py
Normal 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)
|
||||
@@ -1,5 +1,3 @@
|
||||
from .api import *
|
||||
from .forms import *
|
||||
from .helpers import PinFactoryTest
|
||||
from .views import *
|
||||
|
||||
|
||||
@@ -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()]
|
||||
})
|
||||
|
||||
@@ -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))
|
||||
@@ -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):
|
||||
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(self.pk, size)
|
||||
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
|
||||
|
||||
@@ -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.'
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
23
core/urls.py
23
core/urls.py
@@ -1,31 +1,16 @@
|
||||
from django.conf.urls import include, url
|
||||
from django.conf.urls import url
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from tastypie.api import Api
|
||||
|
||||
from .api import ImageResource, ThumbnailResource, PinResource, UserResource
|
||||
from .views import CreateImage
|
||||
|
||||
|
||||
v1_api = Api(api_name='v1')
|
||||
v1_api.register(ImageResource())
|
||||
v1_api.register(ThumbnailResource())
|
||||
v1_api.register(PinResource())
|
||||
v1_api.register(UserResource())
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^api/', include(v1_api.urls, namespace='api')),
|
||||
|
||||
url(r'^pins/pin-form/$', TemplateView.as_view(template_name='core/pin_form.html'),
|
||||
name='pin-form'),
|
||||
url(r'^pins/create-image/$', CreateImage.as_view(), name='create-image'),
|
||||
|
||||
url(r'^pins/tag/(?P<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'),
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
var image_url = $('#pin-form-image-url');
|
||||
image_url.parent().fadeOut(300);
|
||||
uploadedImage = resp.id;
|
||||
image_url.val(resp.thumbnail.image);
|
||||
createPinPreviewFromForm();
|
||||
});
|
||||
promise.error(function() {
|
||||
},
|
||||
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();
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user