Merge pull request #128 from winkidney/feature/upgrade2new-django

Feature:  Upgrade django to 1.11 LTS to fix webp saving error
This commit is contained in:
Ji Qu
2018-09-04 12:55:16 +08:00
committed by GitHub
28 changed files with 971 additions and 203 deletions

14
Makefile Normal file
View File

@@ -0,0 +1,14 @@
backup-images:
pipenv run python manage.py dumpdata django_images > db-backup.django_images.json
backup-all:
pipenv run python manage.py dumpdata > db-backup.all.json
migrate:
pipenv run python manage.py migrate
recover-all:
pipenv run python manage.py loaddata db-backup.all.json
serve:
pipenv run python manage.py runserver 0.0.0.0:8000
install:
pipenv install
test:
pipenv run python manage.py test

11
Pipfile
View File

@@ -1,25 +1,20 @@
[[source]]
url = "https://pypi.python.org/simple"
name = "pypi"
verify_ssl = true
[dev-packages]
"flake8" = "*"
qrcode = "*"
[packages]
django = "<1.9,>=1.8"
django = ">=1.11,<1.12"
pillow = "*"
requests = "*"
django-taggit = "*"
django-images = "*"
django-braces = "*"
django-compressor = "*"
django-tastypie = "==0.12.2"
django-tastypie = "*"
mock = "*"
factory-boy = "<2.0,>=1.3"
gunicorn = "*"

277
Pipfile.lock generated
View File

@@ -1,20 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "03110b40f521ca462a63d092602bae16130febac8dfea6f7f536e19ab622d975"
},
"host-environment-markers": {
"implementation_name": "cpython",
"implementation_version": "3.5.2",
"os_name": "posix",
"platform_machine": "x86_64",
"platform_python_implementation": "CPython",
"platform_release": "4.4.0-43-Microsoft",
"platform_system": "Linux",
"platform_version": "#1-Microsoft Wed Dec 31 14:42:53 PST 2014",
"python_full_version": "3.5.2",
"python_version": "3.5",
"sys_platform": "linux"
"sha256": "68c12441be13a252f7fdd1b5532c7befdfa56fc8307ab75a2b197a1946a54bf2"
},
"pipfile-spec": 6,
"requires": {},
@@ -29,192 +16,194 @@
"default": {
"certifi": {
"hashes": [
"sha256:14131608ad2fd56836d33a71ee60fa1c82bc9d2c8d98b7bdbc631fe1b3cd1296",
"sha256:edbc3f203427eef571f79a7692bb160a2b0f7ccaa31953e99bd17e307cf63f7d"
"sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
"sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"
],
"version": "==2018.1.18"
"version": "==2018.8.24"
},
"chardet": {
"hashes": [
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691",
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"django": {
"hashes": [
"sha256:d8e2fd119756ab10b43a31052c3c8efbc262064b81eecb7871372de4d37b1a94",
"sha256:c7611cdd5e2539a443b7960c7cafd867d986c2720a1b44808deaa60ce3da50c7"
"sha256:8176ac7985fe6737ce3d6b2531b4a2453cb7c3377c9db00bacb2b3320f4a1311",
"sha256:b18235d82426f09733d2de9910cee975cf52ff05e5f836681eb957d105a05a40"
],
"version": "==1.8.18"
"index": "pypi",
"version": "==1.11.15"
},
"django-appconf": {
"hashes": [
"sha256:ddab987d14b26731352c01ee69c090a4ebfc9141ed223bef039d79587f22acd9",
"sha256:6a4d9aea683b4c224d97ab8ee11ad2d29a37072c0c6c509896dd9857466fb261"
"sha256:6a4d9aea683b4c224d97ab8ee11ad2d29a37072c0c6c509896dd9857466fb261",
"sha256:ddab987d14b26731352c01ee69c090a4ebfc9141ed223bef039d79587f22acd9"
],
"version": "==1.0.2"
},
"django-braces": {
"hashes": [
"sha256:7e0cb698f3cd17cc58d50aea4a663233f44228144b81d85e1b9bad05aa7f3f80",
"sha256:7c5b91c75240ccf2eb124fe3b09dd4978e8f4d412b553bb791920bd55839159c"
"sha256:a457d74ea29478123c0c4652272681b3cea0bf1232187fd9f9b6f1d97d32a890",
"sha256:ba68e98b817c6f01d71d10849f359979617b3fe4cefb7f289adefddced092ddc"
],
"version": "==1.12.0"
"index": "pypi",
"version": "==1.13.0"
},
"django-compressor": {
"hashes": [
"sha256:7732676cfb9d58498dfb522b036f75f3f253f72ea1345ac036434fdc418c2e57",
"sha256:9616570e5b08e92fa9eadc7a1b1b49639cce07ef392fc27c74230ab08075b30f"
],
"index": "pypi",
"version": "==2.2"
},
"django-images": {
"hashes": [
"sha256:0f6e079fa33683d765b6dec23e370750d8861d6de290b0346512d917da2d1461"
],
"version": "==0.4.3"
},
"django-taggit": {
"hashes": [
"sha256:58aa3e59e0643446e102523f22d137300298e2a537b1c5b0c310d99143f2c2b8",
"sha256:fd13e304ba37ff09e601c4797d893fb7d3e699a789b5afb0b09d686f94470441"
"sha256:a21cbe7e0879f1364eef1c88a2eda89d593bf000ebf51c3f00423c6927075dce",
"sha256:db4430ec99265341e05d0274edb0279163bd74357241f7b4d9274bdcb3338b17"
],
"version": "==0.22.2"
"index": "pypi",
"version": "==0.23.0"
},
"django-tastypie": {
"hashes": [
"sha256:7feccf811a8c2c60bc227e0d956015f0ea6097c804660e8f4a4ada0e74817c24",
"sha256:21625ce191ccb734f47b90ef92b603d04366bb67d0eb52bd060a8d76e6706018"
"sha256:1fbf61ec7467eec70bd1abcb14e3b1dc67e47cc3642ad16ed8a3709f4140678b"
],
"version": "==0.12.2"
"index": "pypi",
"version": "==0.14.1"
},
"factory-boy": {
"hashes": [
"sha256:bd5d87634946c8831c0d1389b5995da5dd64ccd97088eebc311eb0c9ef75ae3b"
],
"index": "pypi",
"version": "==1.3.0"
},
"gunicorn": {
"hashes": [
"sha256:75af03c99389535f218cc596c7de74df4763803f7b63eb09d77e92b3956b36c6",
"sha256:eee1169f0ca667be05db3351a0960765620dad53f53434262ff8901b68a1b622"
"sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471",
"sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3"
],
"version": "==19.7.1"
"index": "pypi",
"version": "==19.9.0"
},
"idna": {
"hashes": [
"sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4",
"sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f"
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
],
"version": "==2.6"
"version": "==2.7"
},
"mock": {
"hashes": [
"sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1",
"sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba"
],
"index": "pypi",
"version": "==2.0.0"
},
"pbr": {
"hashes": [
"sha256:60c25b7dfd054ef9bb0ae327af949dd4676aa09ac3a9471cdc871d8a9213f9ac",
"sha256:05f61c71aaefc02d8e37c0a3eeb9815ff526ea28b3b76324769e6158d7f95be1"
"sha256:1b8be50d938c9bb75d0eaf7eda111eec1bf6dc88a62a6412e33bf077457e0f45",
"sha256:b486975c0cafb6beeb50ca0e17ba047647f229087bd74e37f4a7e2cac17d2caa"
],
"version": "==3.1.1"
"version": "==4.2.0"
},
"pillow": {
"hashes": [
"sha256:718ec7a122b28d64afc5fbc3a9b99bb0545ef511373cac06fe7624520e82cb20",
"sha256:801cca8923508311bf5d6d0f7da5362552e8208ebd8ec0d7b9f2cd2ff5705734",
"sha256:43334f9581cd067945b8898cef9eb5714ee4883f8de0304c011f1dbdb1d4e2aa",
"sha256:153ec6f18f7b61641e0e6e502acfaf4a06c9aba2ea11c0b4b3578ea9f13a4a4a",
"sha256:25193f934d37d836a6b1f4c062ce574a96cbca7c6d9dc8ddfbbac7f9c54deaa4",
"sha256:b85f703c2ffe539313e39ce0676bed0f355cec45a16e58c9ab7417445843047c",
"sha256:8580fc58074a16b749905b26cf8363f7b628dd167ba0130f5382cdc91c86b509",
"sha256:2fcde9954c8882d1c7f93bb828caa34a4c5e3ee69dbc7895dc8652ad972b455a",
"sha256:1a5b93084e01328a1cb1ecdad99d11d75e881e89a95f88d85b523646553b36c2",
"sha256:b2240f298482f823576f397bb9f32ea913ad9456c526e141bc6f0a022b37a3e8",
"sha256:b1d33c63a55d0d85df0ad02b2c16158fb4d8153afa7b908f1a67330fac694cd6",
"sha256:6977cf073d83358b34f93abf5c1f1193b88675fe0e4441e0e28318bc3dcba7a0",
"sha256:1912b7230459fd53682dae32b83cbd8e5d642ba36d4be18566f00a9c063aa13d",
"sha256:4bd4a71501b6d51db4abc07e1f43f5a6fed0a1a9583cca0b401d6af50284b0db",
"sha256:0013f590a8f260df60bcfd65db19d18efc04e7f046c3c82a40e2e2b3292a937c",
"sha256:a224651a81e45ef4f1d0164e256c5f6b4abb49f2ae8f22ba2f3a9d0ff338e608",
"sha256:c793dfaa130847ccff958492b76ae8b9304e60b8a79a92962cb19e368276a22b",
"sha256:0b899ee80920bb533f26581af9b4660bc12aff4562555afe74e429101ebf3c94",
"sha256:9525cd680a6f9e80c6c0af03cf973e6505c59f60b4745f682cd1a449e54b31bb",
"sha256:35f7d998b8e82fb3fb51ff88b30485eb81cd7dd56ec7e1a8deba23eb88532d44",
"sha256:5b0d657460d9f3615876fec6306e97ca15a471f6169b622d76a47e270998acf1",
"sha256:ddd16ab250b4fc97db1c47407e78c25216a75c29d29d10ad37e51b7a2ec7b2c3",
"sha256:b9f63451084a718eccdeb1e382768c94647915653af4d6019f64560d9e98642b",
"sha256:a370d1c570f1d72e877099651e752332444b1c5009381f043c9da5fd47f3ebae",
"sha256:dc4b018d5c9b636f7546583c5591b9ea00c328c3e5871992ef5b95bac353f097",
"sha256:e126ff4fed71e78333840c07279e1617f63cfca76d63ad5b27d65a7277206a3d",
"sha256:fcf64c91fd44485100a2965d23bb0e227d093e91f7e776c5ca3b32574766eb56",
"sha256:2c042352b430d678db50c78c5214e19638eff8b688941271da2de21fd298dfe5",
"sha256:17fe25efc785194d48c38fad85dce470013ba19d2fb66639e149f14bccf1327f",
"sha256:2e818dbe445e86fc6c266973fe540c35125c42eb2cf13a6095e9adaa89c0deb5",
"sha256:135e9aa65150c53f7db85bf2bebb8a0e1a48ea850e80cf66e16dd04fa09d309c",
"sha256:7dfbefdb3fb911ca9faed307bf309861e9995e36cca6b761c7ba6d9b77a9744a",
"sha256:12f29d6c23424f704c66b5b68c02fe0b571504459605cfe36ab8158359b0e1bb",
"sha256:f8d49be8c282df8d2e1ab6ab53ab8abd859b1fa6fed384457ee85c9eff64ef97",
"sha256:82b172e3264e62372c01b5b009b5b1a02fbb9276cbe5cc57ab00a6d6e5ed9a18",
"sha256:57aa6198ba8acba1313c3b743e267d821a60cac77e6026caf0b55ca58d3d23be",
"sha256:d60c1625b108432ace8b1fa1a584017e5efa73f107d0f493c7f39c79bebf1d41",
"sha256:82d1ff571489765df2816785d532e243bde213752156c227fca595723ec5ff42",
"sha256:37cc0339abfa9e295c75d9a7f227d35cb44716feb95057f9449c4a9e9a17daf7",
"sha256:931030d1d6282b7900e6b0a7ff9ecdb503b5e1e6781800dab2b71a9f39405bff",
"sha256:5cd36804f9f06a914a883fe682df5711d16d7b4f44d43189c5f013e7cd91e149"
"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"
],
"version": "==5.0.0"
"index": "pypi",
"version": "==5.2.0"
},
"psycopg2": {
"hashes": [
"sha256:aeaba399254ca79c299d9fe6aa811d3c3eac61458dee10270de7f4e71c624998",
"sha256:1d90379d01d0dc50ae9b40c863933d87ff82d51dd7d52cea5d1cb7019afd72cd",
"sha256:36030ca7f4b4519ee4f52a74edc4ec73c75abfb6ea1d80ac7480953d1c0aa3c3",
"sha256:7cbc3b21ce2f681ca9ad2d8c0901090b23a30c955e980ebf1006d41f37068a95",
"sha256:b178e0923c93393e16646155794521e063ec17b7cc9f943f15b7d4b39776ea2c",
"sha256:fe6a7f87356116f5ea840c65b032af17deef0e1a5c34013a2962dd6f99b860dd",
"sha256:6f302c486132f8dd11f143e919e236ea4467d53bf18c451cac577e6988ecbd05",
"sha256:888bba7841116e529f407f15c6d28fe3ef0760df8c45257442ec2f14f161c871",
"sha256:932a4c101af007cb3132b1f8a9ffef23386acc53dad46536dc5ba43a3235ae02",
"sha256:179c52eb870110a8c1b460c86d4f696d58510ea025602cd3f81453746fccb94f",
"sha256:33f9e1032095e1436fa9ec424abcbd4c170da934fb70e391c5d78275d0307c75",
"sha256:092a80da1b052a181b6e6c765849c9b32d46c5dac3b81bf8c9b83e697f3cdbe8",
"sha256:f3d3a88128f0c219bdc5b2d9ccd496517199660cea021c560a3252116df91cbd",
"sha256:19983b77ec1fc2a210092aa0333ee48811fd9fb5f194c6cd5b927ed409aea5f8",
"sha256:027ae518d0e3b8fff41990e598bc7774c3d08a3a20e9ecc0b59fb2aaaf152f7f",
"sha256:363fbbf4189722fc46779be1fad2597e2c40b3f577dc618f353a46391cf5d235",
"sha256:d74cf9234ba76426add5e123449be08993a9b13ff434c6efa3a07caa305a619f",
"sha256:32702e3bd8bfe12b36226ba9846ed9e22336fc4bd710039d594b36bd432ae255",
"sha256:8eb94c0625c529215b53c08fb4e461546e2f3fc96a49c13d5474b5ad7aeab6cf",
"sha256:8ebba5314c609a05c6955e5773c7e0e57b8dd817e4f751f30de729be58fa5e78",
"sha256:27467fd5af1dcc0a82d72927113b8f92da8f44b2efbdb8906bd76face95b596d",
"sha256:b68e89bb086a9476fa85298caab43f92d0a6af135a5f433d1f6b6d82cafa7b55",
"sha256:0b9851e798bae024ed1a2a6377a8dab4b8a128a56ed406f572f9f06194e4b275",
"sha256:733166464598c239323142c071fa4c9b91c14359176e5ae7e202db6bcc1d2eb5",
"sha256:ad75fe10bea19ad2188c5cb5fc4cdf53ee808d9b44578c94a3cd1e9fc2beb656",
"sha256:8966829cb0d21a08a3c5ac971a2eb67c3927ae27c247300a8476554cc0ce2ae8",
"sha256:8bf51191d60f6987482ef0cfe8511bbf4877a5aa7f313d7b488b53189cf26209"
"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"
],
"version": "==2.7.4"
"index": "pypi",
"version": "==2.7.5"
},
"python-dateutil": {
"hashes": [
"sha256:95511bae634d69bc7329ba55e646499a842bc4ec342ad54a8cdb65645a0aad3c",
"sha256:891c38b2a02f5bb1be3e4793866c8df49c7d19baabf9c1bad62547e0b4866aca"
"sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0",
"sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8"
],
"version": "==2.6.1"
"version": "==2.7.3"
},
"python-mimeparse": {
"hashes": [
"sha256:a295f03ff20341491bfe4717a39cd0a8cc9afad619ba44b77e86b0ab8a2b8282",
"sha256:76e4b03d700a641fd7761d3cd4fdbbdcd787eade1ebfac43f877016328334f78"
"sha256:76e4b03d700a641fd7761d3cd4fdbbdcd787eade1ebfac43f877016328334f78",
"sha256:a295f03ff20341491bfe4717a39cd0a8cc9afad619ba44b77e86b0ab8a2b8282"
],
"version": "==1.6.0"
},
"pytz": {
"hashes": [
"sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053",
"sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277"
],
"version": "==2018.5"
},
"rcssmin": {
"hashes": [
"sha256:ca87b695d3d7864157773a61263e5abb96006e9ff0e021eff90cbe0e1ba18270"
@@ -223,10 +212,11 @@
},
"requests": {
"hashes": [
"sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b",
"sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e"
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
],
"version": "==2.18.4"
"index": "pypi",
"version": "==2.19.1"
},
"rjsmin": {
"hashes": [
@@ -236,25 +226,27 @@
},
"six": {
"hashes": [
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb",
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9"
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
],
"version": "==1.11.0"
},
"urllib3": {
"hashes": [
"sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b",
"sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
],
"version": "==1.22"
"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"
}
},
"develop": {
"flake8": {
"hashes": [
"sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37",
"sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0"
"sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0",
"sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"
],
"index": "pypi",
"version": "==3.5.0"
},
"mccabe": {
@@ -266,8 +258,8 @@
},
"pycodestyle": {
"hashes": [
"sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9",
"sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766"
"sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766",
"sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9"
],
"version": "==2.3.1"
},
@@ -277,6 +269,21 @@
"sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805"
],
"version": "==1.6.0"
},
"qrcode": {
"hashes": [
"sha256:037b0db4c93f44586e37f84c3da3f763874fcac85b2974a69a98e399ac78e1bf",
"sha256:de4ffc15065e6ff20a551ad32b6b41264f3c75275675406ddfa8e3530d154be3"
],
"index": "pypi",
"version": "==6.0"
},
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
],
"version": "==1.11.0"
}
}
}

View File

@@ -8,6 +8,15 @@ to skim through format.
For more information and a working demo board visit `getpinry.com`_.
Upgrade from 1.x
------------------
For source code users:
Read our `online doc <doc/upgrade_from_1.x.md>`_ about how to upgrade to 2.x
For docker users, please contact us for help: )
Requirements
------------

View File

@@ -10,36 +10,101 @@ 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 update_detail(self, object_list, bundle):
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)
permission = '%s.change_%s' % (klass._meta.app_label, klass._meta.model_name)
if not bundle.request.user.has_perm(permission, bundle.obj):
raise Unauthorized("You are not allowed to access that resource.")
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):
klass = self.base_checks(bundle.request, bundle.obj.__class__)
return self._is_obj_owner(object_list, bundle)
if klass is False:
raise Unauthorized("You are not allowed to access that resource.")
def update_list(self, object_list, bundle):
return _is_authenticated_and_owner(object_list, bundle)
permission = '%s.delete_%s' % (klass._meta.app_label, klass._meta.model_name)
def delete_list(self, object_list, bundle):
return _is_authenticated_and_owner(object_list, bundle)
if not bundle.request.user.has_perm(permission, bundle.obj):
raise Unauthorized("You are not allowed to access that resource.")
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)
@@ -80,19 +145,28 @@ class ThumbnailResource(ModelResource):
class ImageResource(ModelResource):
standard = fields.ToOneField(ThumbnailResource, full=True,
attribute=lambda bundle: filter_generator_for('standard')(bundle))
thumbnail = fields.ToOneField(ThumbnailResource, full=True,
attribute=lambda bundle: filter_generator_for('thumbnail')(bundle))
square = fields.ToOneField(ThumbnailResource, full=True,
attribute=lambda bundle: filter_generator_for('square')(bundle))
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 = DjangoAuthorization()
authorization = ImageAuthorization()
class PinResource(ModelResource):
@@ -127,8 +201,8 @@ class PinResource(ModelResource):
def dehydrate_tags(self, bundle):
return list(map(str, bundle.obj.tags.all()))
def build_filters(self, filters=None):
orm_filters = super(PinResource, self).build_filters(filters)
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

View File

@@ -1,4 +1,4 @@
from django.conf.urls import patterns, include, url
from django.conf.urls import include, url
from django.views.generic import TemplateView
from tastypie.api import Api
@@ -13,7 +13,7 @@ v1_api.register(ThumbnailResource())
v1_api.register(PinResource())
v1_api.register(UserResource())
urlpatterns = patterns('',
urlpatterns = [
url(r'^api/', include(v1_api.urls, namespace='api')),
url(r'^pins/pin-form/$', TemplateView.as_view(template_name='core/pin_form.html'),
@@ -28,4 +28,4 @@ urlpatterns = patterns('',
name='recent-pins'),
url(r'^$', TemplateView.as_view(template_name='core/pins.html'),
name='recent-pins'),
)
]

View File

View File

@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django_images.models
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='Image',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('image', models.ImageField(height_field=b'height', width_field=b'width', max_length=255, upload_to=django_images.models.hashed_upload_to)),
('height', models.PositiveIntegerField(default=0, editable=False)),
('width', models.PositiveIntegerField(default=0, editable=False)),
],
),
migrations.CreateModel(
name='Thumbnail',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('image', models.ImageField(height_field=b'height', width_field=b'width', max_length=255, upload_to=django_images.models.hashed_upload_to)),
('size', models.CharField(max_length=100)),
('height', models.PositiveIntegerField(default=0, editable=False)),
('width', models.PositiveIntegerField(default=0, editable=False)),
('original', models.ForeignKey(to='django_images.Image')),
],
),
migrations.AlterUniqueTogether(
name='thumbnail',
unique_together=set([('original', 'size')]),
),
]

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import core.utils
class Migration(migrations.Migration):
dependencies = [
('django_images', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='image',
name='image',
field=models.ImageField(upload_to=core.utils.upload_path, width_field='width', max_length=255, height_field='height'),
),
migrations.AlterField(
model_name='thumbnail',
name='image',
field=models.ImageField(upload_to=core.utils.upload_path, width_field='width', max_length=255, height_field='height'),
),
]

View File

132
django_images/models.py Normal file
View File

@@ -0,0 +1,132 @@
import hashlib
import os.path
from io import BytesIO
from django.db import models
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.core.urlresolvers import reverse
from django.dispatch import receiver
import PIL
try:
from importlib import import_module
except ImportError:
from django.utils.importlib import import_module
from . import utils
from .settings import IMAGE_SIZES, IMAGE_PATH, IMAGE_AUTO_DELETE
def hashed_upload_to(instance, filename, **kwargs):
image_type = 'original' if isinstance(instance, Image) else 'thumbnail'
prefix = 'image/%s/by-md5/' % (image_type,)
hasher = hashlib.md5()
for chunk in instance.image.chunks():
hasher.update(chunk)
hash_ = hasher.hexdigest()
base, ext = os.path.splitext(filename)
return '%(prefix)s%(first)s/%(second)s/%(hash)s/%(base)s%(ext)s' % {
'prefix': prefix,
'first': hash_[0],
'second': hash_[1],
'hash': hash_,
'base': base,
'ext': ext,
}
if IMAGE_PATH is None:
upload_to = hashed_upload_to
else:
if callable(IMAGE_PATH):
upload_to = IMAGE_PATH
else:
parts = IMAGE_PATH.split('.')
module_name = '.'.join(parts[:-1])
module = import_module(module_name)
upload_to = getattr(module, parts[-1])
class Image(models.Model):
image = models.ImageField(upload_to=upload_to,
height_field='height', width_field='width',
max_length=255)
height = models.PositiveIntegerField(default=0, editable=False)
width = models.PositiveIntegerField(default=0, editable=False)
def get_by_size(self, size):
return self.thumbnail_set.get(size=size)
def get_absolute_url(self, size=None):
if not size:
return self.image.url
try:
return self.get_by_size(size).image.url
except Thumbnail.DoesNotExist:
return reverse('image-thumbnail', args=(self.id, size))
class ThumbnailManager(models.Manager):
def get_or_create_at_size(self, image_id, size):
image = Image.objects.get(id=image_id)
if size not in IMAGE_SIZES:
raise ValueError("Received unknown size: %s" % size)
try:
thumbnail = image.get_by_size(size)
except Thumbnail.DoesNotExist:
img = utils.scale_and_crop(image.image, **IMAGE_SIZES[size])
# save to memory
buf = BytesIO()
try:
img.save(buf, img.format, **img.info)
except IOError:
if img.info.get('progression'):
orig_MAXBLOCK = PIL.ImageFile.MAXBLOCK
temp_MAXBLOCK = 1048576
if orig_MAXBLOCK >= temp_MAXBLOCK:
raise
PIL.ImageFile.MAXBLOCK = temp_MAXBLOCK
try:
img.save(buf, img.format, **img.info)
finally:
PIL.ImageFile.MAXBLOCK = orig_MAXBLOCK
else:
raise
# and save to storage
original_dir, original_file = os.path.split(image.image.name)
thumb_file = InMemoryUploadedFile(buf, "image", original_file,
None, buf.tell(), None)
thumbnail, created = image.thumbnail_set.get_or_create(
size=size, defaults={'image': thumb_file})
return thumbnail
class Thumbnail(models.Model):
original = models.ForeignKey(Image)
image = models.ImageField(upload_to=upload_to,
height_field='height', width_field='width',
max_length=255)
size = models.CharField(max_length=100)
height = models.PositiveIntegerField(default=0, editable=False)
width = models.PositiveIntegerField(default=0, editable=False)
objects = ThumbnailManager()
class Meta:
unique_together = ('original', 'size')
def get_absolute_url(self):
return self.image.url
@receiver(models.signals.post_save)
def original_changed(sender, instance, created, **kwargs):
if isinstance(instance, Image):
instance.thumbnail_set.all().delete()
@receiver(models.signals.post_delete)
def delete_image_files(sender, instance, **kwargs):
if isinstance(instance, (Image, Thumbnail)) and IMAGE_AUTO_DELETE:
if instance.image.storage.exists(instance.image.name):
instance.image.delete(save=False)

View File

@@ -0,0 +1,5 @@
from django.conf import settings
IMAGE_PATH = getattr(settings, 'IMAGE_PATH', None)
IMAGE_SIZES = getattr(settings, 'IMAGE_SIZES', {})
IMAGE_AUTO_DELETE = getattr(settings, 'IMAGE_AUTO_DELETE', True)

View File

@@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'Image'
db.create_table(u'django_images_image', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('image', self.gf('django.db.models.fields.files.ImageField')(max_length=255)),
('height', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
('width', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
))
db.send_create_signal(u'django_images', ['Image'])
# Adding model 'Thumbnail'
db.create_table(u'django_images_thumbnail', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('original', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['django_images.Image'])),
('image', self.gf('django.db.models.fields.files.ImageField')(max_length=255)),
('size', self.gf('django.db.models.fields.CharField')(max_length=100)),
('height', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
('width', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
))
db.send_create_signal(u'django_images', ['Thumbnail'])
# Adding unique constraint on 'Thumbnail', fields ['image', 'size']
db.create_unique(u'django_images_thumbnail', ['image', 'size'])
def backwards(self, orm):
# Removing unique constraint on 'Thumbnail', fields ['image', 'size']
db.delete_unique(u'django_images_thumbnail', ['image', 'size'])
# Deleting model 'Image'
db.delete_table(u'django_images_image')
# Deleting model 'Thumbnail'
db.delete_table(u'django_images_thumbnail')
models = {
u'django_images.image': {
'Meta': {'object_name': 'Image'},
'height': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '255'}),
'width': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
},
u'django_images.thumbnail': {
'Meta': {'unique_together': "(('image', 'size'),)", 'object_name': 'Thumbnail'},
'height': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '255'}),
'original': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['django_images.Image']"}),
'size': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'width': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
}
}
complete_apps = ['django_images']

View File

@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Removing unique constraint on 'Thumbnail', fields ['image', 'size']
db.delete_unique(u'django_images_thumbnail', ['image', 'size'])
# Adding unique constraint on 'Thumbnail', fields ['original', 'size']
db.create_unique(u'django_images_thumbnail', ['original_id', 'size'])
def backwards(self, orm):
# Removing unique constraint on 'Thumbnail', fields ['original', 'size']
db.delete_unique(u'django_images_thumbnail', ['original_id', 'size'])
# Adding unique constraint on 'Thumbnail', fields ['image', 'size']
db.create_unique(u'django_images_thumbnail', ['image', 'size'])
models = {
u'django_images.image': {
'Meta': {'object_name': 'Image'},
'height': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '255'}),
'width': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
},
u'django_images.thumbnail': {
'Meta': {'unique_together': "(('original', 'size'),)", 'object_name': 'Thumbnail'},
'height': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '255'}),
'original': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['django_images.Image']"}),
'size': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'width': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
}
}
complete_apps = ['django_images']

View File

View File

@@ -0,0 +1,8 @@
from django import template
register = template.Library()
@register.filter
def at_size(image, size):
return image.get_absolute_url(size=size)

208
django_images/tests.py Normal file
View File

@@ -0,0 +1,208 @@
import mock
import qrcode
from django.test import TestCase
from django.core.files.images import ImageFile
from django.conf import settings
from django.utils.six import BytesIO
from django.core.urlresolvers import reverse
from django_images.models import Image, Thumbnail
from django_images.templatetags.images import at_size
from django_images.utils import scale_and_crop
class ImageModelTest(TestCase):
def setUp(self):
image_obj = BytesIO()
qrcode_obj = qrcode.make('https://mirumee.com/')
qrcode_obj.save(image_obj)
self.image = Image.objects.create(width=370, height=370,
image=ImageFile(image_obj, '01.png'))
def test_get_by_size(self):
size = list(settings.IMAGE_SIZES.keys())[0]
thumb = Thumbnail.objects.get_or_create_at_size(self.image.id, size)
self.image.get_by_size(size)
def test_get_absolute_url(self):
url = self.image.get_absolute_url()
self.assertEqual(url, self.image.image.url)
# For thumbnail
size = list(settings.IMAGE_SIZES.keys())[0]
thumb = Thumbnail.objects.get_or_create_at_size(self.image.id, size)
url = self.image.get_absolute_url(size)
self.assertEqual(url, thumb.image.url)
# Fallback on creation url
size = list(settings.IMAGE_SIZES.keys())[1]
url = self.image.get_absolute_url(size)
fallback_url = reverse('image-thumbnail', args=(self.image.id, size))
self.assertEqual(url, fallback_url)
class ThumbnailManagerModelTest(TestCase):
def setUp(self):
image_obj = BytesIO()
qrcode_obj = qrcode.make('https://mirumee.com/')
qrcode_obj.save(image_obj)
self.image = Image.objects.create(width=370, height=370,
image=ImageFile(image_obj, '01.png'))
self.size = list(settings.IMAGE_SIZES.keys())[0]
def test_unknown_size(self):
self.assertRaises(ValueError, Thumbnail.objects.get_or_create_at_size,
self.image.id, 'foo')
# TODO: Test the image object and data
def test_create(self):
thumb = Thumbnail.objects.get_or_create_at_size(self.image.id, self.size)
self.assertEqual(self.image.thumbnail_set.count(), 1)
def test_get(self):
thumb = Thumbnail.objects.get_or_create_at_size(self.image.id, self.size)
thumb2 = Thumbnail.objects.get_or_create_at_size(self.image.id, self.size)
self.assertEqual(thumb.id, thumb2.id)
class ThumbnailModelTest(TestCase):
def setUp(self):
image_obj = BytesIO()
qrcode_obj = qrcode.make('https://mirumee.com/')
qrcode_obj.save(image_obj)
self.image = Image.objects.create(width=370, height=370,
image=ImageFile(image_obj, '01.png'))
size = list(settings.IMAGE_SIZES.keys())[0]
self.thumb = Thumbnail.objects.get_or_create_at_size(self.image.id, size)
def test_get_absolute_url(self):
url = self.thumb.get_absolute_url()
self.assertEqual(url, self.thumb.image.url)
class PostSaveSignalOriginalChangedTestCase(TestCase):
def setUp(self):
image_obj = BytesIO()
qrcode_obj = qrcode.make('https://mirumee.com/')
qrcode_obj.save(image_obj)
self.image = Image.objects.create(width=370, height=370,
image=ImageFile(image_obj, '01.png'))
size = list(settings.IMAGE_SIZES.keys())[0]
self.thumb = Thumbnail.objects.get_or_create_at_size(self.image.id, size)
def test_post_save_signal_original_changed(self):
size = list(settings.IMAGE_SIZES.keys())[0]
thumb = Thumbnail.objects.get_or_create_at_size(self.image.id, size)
self.image.delete()
self.assertFalse(Thumbnail.objects.exists())
class PostDeleteSignalDeleteImageFileTest(TestCase):
def setUp(self):
image_obj = BytesIO()
qrcode_obj = qrcode.make('https://mirumee.com/')
qrcode_obj.save(image_obj)
self.image = Image.objects.create(width=370, height=370,
image=ImageFile(image_obj, '01.png'))
size = list(settings.IMAGE_SIZES.keys())[0]
self.thumb = Thumbnail.objects.get_or_create_at_size(self.image.id, size)
@mock.patch('django_images.models.IMAGE_AUTO_DELETE', True)
def test_post_delete_signal_delete_image_files_enabled(self):
storage = self.image.image.storage
image_name = self.image.image.name
thumb_name = self.thumb.image.name
self.image.delete()
self.assertFalse(storage.exists(image_name))
self.assertFalse(storage.exists(thumb_name))
@mock.patch('django_images.models.IMAGE_AUTO_DELETE', False)
def test_post_delete_signal_delete_image_files_disabled(self):
storage = self.image.image.storage
image_name = self.image.image.name
thumb_name = self.thumb.image.name
# Delete thumb
self.thumb.delete()
self.assertTrue(storage.exists(image_name))
self.assertTrue(storage.exists(thumb_name))
# Delete image
self.image.delete()
self.assertTrue(storage.exists(image_name))
self.assertTrue(storage.exists(thumb_name))
class AtSizeTemplateTagTest(TestCase):
def setUp(self):
image_obj = BytesIO()
qrcode_obj = qrcode.make('https://mirumee.com/')
qrcode_obj.save(image_obj)
self.image = Image.objects.create(width=370, height=370,
image=ImageFile(image_obj, '01.png'))
size = list(settings.IMAGE_SIZES.keys())[0]
self.thumb = Thumbnail.objects.get_or_create_at_size(self.image.id, size)
def test_at_size(self):
size = list(settings.IMAGE_SIZES.keys())[0]
url = at_size(self.image, size)
self.assertEqual(url, self.thumb.image.url)
class ThumbnailViewTest(TestCase):
def setUp(self):
image_obj = BytesIO()
qrcode_obj = qrcode.make('https://mirumee.com/')
qrcode_obj.save(image_obj)
self.image = Image.objects.create(width=370, height=370,
image=ImageFile(image_obj, '01.png'))
self.size = list(settings.IMAGE_SIZES.keys())[0]
self.thumb = Thumbnail.objects.get_or_create_at_size(self.image.id, self.size)
def test_redirect(self):
url = reverse('image-thumbnail', args=[self.image.id, self.size])
response = self.client.get(url)
self.assertNotEqual(url, self.thumb.image.url)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, self.thumb.image.url)
def test_not_found(self):
url = reverse('image-thumbnail', args=['42', self.size])
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_size_not_found(self):
url = reverse('image-thumbnail', args=[self.image.id, '42'])
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
class UtilsScaleAndDropTest(TestCase):
def setUp(self):
image_obj = BytesIO()
qrcode_obj = qrcode.make('https://mirumee.com/')
qrcode_obj.save(image_obj)
self.imagefile = ImageFile(image_obj, '01.png')
def test_change_size(self):
new_size = (10, 10)
image = scale_and_crop(self.imagefile, new_size)
self.assertEqual(new_size, image.im.size)
def test_crop(self):
new_size = (10, 10)
image = scale_and_crop(self.imagefile, new_size, crop=True)
self.assertEqual(new_size, image.im.size)
def test_disabled_upscale(self):
image = scale_and_crop(self.imagefile, (740, 740), upscale=False)
self.assertLess(image.im.size[0], 371)
self.assertLess(image.im.size[1], 371)
def test_enaabled_upscale(self):
image = scale_and_crop(self.imagefile, (740, 740), upscale=True)
self.assertGreater(image.im.size[0], 371)
self.assertGreater(image.im.size[1], 371)
def test_not_change_quality(self):
image = scale_and_crop(self.imagefile, (10, 10), quality=None)
self.assertEqual(image.info.get('quality'), None)
def test_change_quality(self):
image = scale_and_crop(self.imagefile, (10, 10), quality=50)
self.assertEqual(image.info.get('quality'), 50)

6
django_images/urls.py Normal file
View File

@@ -0,0 +1,6 @@
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^thumbnail/(?P<image_id>\d+)/(?P<size>[^/]+)/$', views.thumbnail, name='image-thumbnail'),
]

73
django_images/utils.py Normal file
View File

@@ -0,0 +1,73 @@
from PIL import Image
# this neat function is based on easy-thumbnails
def scale_and_crop(image, size, crop=False, upscale=False, quality=None):
"""
Resize, crop and/or change quality of an image.
:param image: Source image file
:param type: :class:`django.core.files.images.ImageFile`
:param size: Size as width & height, zero as either means unrestricted
:type size: tuple of two int
:param crop: Truncate image or not
:type crop: bool
:param upscale: Enable scale up
:type upscale: bool
:param quality: Value between 1 to 95, or None for keep the same
:type quality: int or NoneType
:return: Handled image
:rtype: class:`PIL.Image`
"""
# Open image and store format/metadata.
image.open()
im = Image.open(image)
im_format, im_info = im.format, im.info
if quality:
im_info['quality'] = quality
# Force PIL to load image data.
im.load()
source_x, source_y = [float(v) for v in im.size]
target_x, target_y = [float(v) for v in size]
if crop or not target_x or not target_y:
scale = max(target_x / source_x, target_y / source_y)
else:
scale = min(target_x / source_x, target_y / source_y)
# Handle one-dimensional targets.
if not target_x:
target_x = source_x * scale
elif not target_y:
target_y = source_y * scale
if scale < 1.0 or (scale > 1.0 and upscale):
im = im.resize((int(source_x * scale), int(source_y * scale)),
resample=Image.ANTIALIAS)
if crop:
# Use integer values now.
source_x, source_y = im.size
# Difference between new image size and requested size.
diff_x = int(source_x - min(source_x, target_x))
diff_y = int(source_y - min(source_y, target_y))
if diff_x or diff_y:
# Center cropping (default).
halfdiff_x, halfdiff_y = diff_x // 2, diff_y // 2
box = [halfdiff_x, halfdiff_y,
min(source_x, int(target_x) + halfdiff_x),
min(source_y, int(target_y) + halfdiff_y)]
# Finally, crop the image!
im = im.crop(box)
# Close image and replace format/metadata, as PIL blows this away.
im.format, im.info = im_format, im_info
image.close()
return im

14
django_images/views.py Normal file
View File

@@ -0,0 +1,14 @@
from django.http import HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect
from . import models
from .settings import IMAGE_SIZES
def thumbnail(request, image_id, size):
image = get_object_or_404(models.Image, id=image_id)
if size not in IMAGE_SIZES:
return HttpResponseNotFound()
return redirect(models.Thumbnail.objects.get_or_create_at_size(image.id,
size))

59
doc/upgrade_from_1.x.md Normal file
View File

@@ -0,0 +1,59 @@
Guide of Upgrade from 1.x to 2.x
---------------------------------------
If you have Pinry installed by srouce code, it's easy for
you to upgrade from 1.x to 2.x, just follow these steps.
At first you should checkout the old branch.
```bash
# it is not required if you are currently the old version
git checkout 1.x version
# install and enter the shell
pipenv install
pipenv shell
# Upgrade to lastest version of django-images
pip install -U git+https://github.com/mirumee/django-images.git
python manage.py makemigrations
python manage.py migrate --fake-initial
# clean action
pip uninstall django-images
# It will ask if you want to remove an extra file named like
# Uninstalling django-images-0.4.3:
# -------------------------------------------
# Would remove:
# /path_to_your_python_packages/django_images-0.4.3.dist-info/*
# /path_to_your_python_packages/django_images/*
# Would not remove (might be manually added):
# /path_to_your_python_packages/django_images/migrations/0002_auto_20180826_0845.py
# Proceed (y/n)?
# ------------------------------------------
# Please remove the file it by hand if possible (this operation is optional)
rm /path_to_your_python_packages/django_images/migrations/0002_auto_20180826_0845.py
# exit pipenv's virtualenv
exit
```
Ant then you should checkout to version 2.x (our current master)
```bash
# If you are not the lastest version ,just call "git pull --rebase"
# to upgrade to lastest version of Pinry (2.x)
git checkout master
make install
make migrate
# Try to run as development server
make serve
# If no error occurs, just enjoy it
```
And now, you can just run your server in the way you like.

View File

@@ -6,5 +6,8 @@ if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pinry.settings.development")
from django.core.management import execute_from_command_line
if 'test' in sys.argv:
from django.conf import settings
settings.IS_TEST = True
execute_from_command_line(sys.argv)

View File

@@ -134,5 +134,8 @@ IMAGE_SIZES = {
'square': {'crop': True, 'size': [125, 125]},
}
# IS_TEST is a variable to mark if the test is running
IS_TEST = False
# User custom settings
IMAGE_AUTO_DELETE = True

View File

@@ -1,21 +1,28 @@
from django.conf import settings
from django.conf.urls import patterns, include, url
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
admin.autodiscover()
urlpatterns = patterns('',
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'', include('core.urls', namespace='core')),
url(r'', include('users.urls', namespace='users')),
)
]
if settings.DEBUG:
urlpatterns += staticfiles_urlpatterns()
urlpatterns += patterns('', url(r'^media/(?P<path>.*)$',
'django.views.static.serve', {'document_root': settings.MEDIA_ROOT,}),)
urlpatterns += [
url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT, }),
]
if settings.IS_TEST:
urlpatterns += staticfiles_urlpatterns()
# For test running of django_images
urlpatterns += [
url(r'^__images/', include('django_images.urls')),
]

View File

@@ -33,11 +33,3 @@ class CombinedAuthBackend(object):
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
def has_perm(self, user, perm, obj=None):
"""
A very simplistic authorization mechanism for now. Basically a pin owner can do anything with the pin.
"""
if obj and isinstance(obj, Pin):
return obj.submitter == user
return False

View File

@@ -34,21 +34,6 @@ class CombinedAuthBackendTest(TestCase):
def test_authenticate_unknown_user(self):
self.assertIsNone(self.backend.authenticate(username='wrong-username', password='wrong-password'))
@mock.patch('requests.get', mock_requests_get)
def test_has_perm_on_pin(self):
image = Image.objects.create_for_url('http://testserver/mocked/screenshot.png')
user = User.objects.get(username=self.username)
pin = Pin.objects.create(submitter=user, image=image)
self.assertTrue(self.backend.has_perm(user, 'add_pin', pin))
@mock.patch('requests.get', mock_requests_get)
def test_has_perm_on_pin_unauthorized(self):
image = Image.objects.create_for_url('http://testserver/mocked/screenshot.png')
user = User.objects.get(username=self.username)
other_user = User.objects.create_user('test', 'test@example.com', 'test')
pin = Pin.objects.create(submitter=user, image=image)
self.assertFalse(self.backend.has_perm(other_user, 'add_pin', pin))
class CreateUserTest(TestCase):
def test_create_post(self):

View File

@@ -1,11 +1,12 @@
from django.conf.urls import patterns, url
from django.conf.urls import url
from django.contrib.auth.views import login
from .views import CreateUser
from . import views
urlpatterns = patterns('',
url(r'^private/$', 'users.views.private', name='private'),
url(r'^register/$', CreateUser.as_view(), name='register'),
url(r'^login/$', 'django.contrib.auth.views.login',
urlpatterns = [
url(r'^private/$', views.private, name='private'),
url(r'^register/$', views.CreateUser.as_view(), name='register'),
url(r'^login/$', login,
{'template_name': 'users/login.html'}, name='login'),
url(r'^logout/$', 'users.views.logout_user', name='logout'),
)
url(r'^logout/$', views.logout_user, name='logout'),
]