From 5baba68fee03f10aa35324cb42cf9d76bd79978e Mon Sep 17 00:00:00 2001 From: Raphael Odini Date: Wed, 25 May 2022 17:27:11 +0200 Subject: [PATCH 1/6] S3 settings config --- Pipfile | 1 + Pipfile.lock | 182 ++++++++++++++++++++++++++++-------------------- app/settings.py | 15 +++- 3 files changed, 122 insertions(+), 76 deletions(-) diff --git a/Pipfile b/Pipfile index 532b79de8..710e04444 100644 --- a/Pipfile +++ b/Pipfile @@ -34,6 +34,7 @@ PyYAML = "*" sentry-sdk = "*" sqlparse = "*" whitenoise = "*" +boto3 = "*" [dev-packages] black = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 022b42931..4fa34305f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1a914f669febd62ca7702b3d2b6cb8163aa693eb678c6f875e4fdc346185142a" + "sha256": "8f2b71b1c0a4f9bd22e4f434be9645b585459b071fcfd1ddb53bf3b02fd795a7" }, "pipfile-spec": 6, "requires": { @@ -54,6 +54,22 @@ "markers": "python_version >= '3.6'", "version": "==4.11.1" }, + "boto3": { + "hashes": [ + "sha256:2a4395e3241c20eef441d7443a5e6eaa0ee3f7114653fb9d9cef41587526f7bd", + "sha256:40d08614f17a69075e175c02c5d5aab69a6153fd50e40fa7057b913ac7bf40e7" + ], + "index": "pypi", + "version": "==1.23.10" + }, + "botocore": { + "hashes": [ + "sha256:5df2cf7ebe34377470172bd0bbc582cf98c5cbd02da0909a14e9e2885ab3ae9c", + "sha256:8a4a984bf901ccefe40037da11ba2abd1ddbcb3b490a492b7f218509c99fc12f" + ], + "markers": "python_version >= '3.6'", + "version": "==1.26.10" + }, "bs4": { "hashes": [ "sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a" @@ -228,11 +244,11 @@ }, "django-ckeditor": { "hashes": [ - "sha256:53f6489bfdfc98c2bafd312636db24db8f7964e16178a59153422200ac602de7", - "sha256:59130740564fe258fb50ea51ed7daff556129118ed282dc5ce831e0689662826" + "sha256:3f1e963376e0d79c0db8d1b44e649d394565e3a71c16c212dcc85647e529ce1b", + "sha256:db40bf9958a3f5a3e6053ff8d034a320b08050d5a385e5bd4defd6956d27e77e" ], "index": "pypi", - "version": "==6.4.1" + "version": "==6.4.2" }, "django-cors-headers": { "hashes": [ @@ -362,11 +378,11 @@ }, "ipython": { "hashes": [ - "sha256:341456643a764c28f670409bbd5d2518f9b82c013441084ff2c2fc999698f83b", - "sha256:807ae3cf43b84693c9272f70368440a9a7eaa2e7e6882dad943c32fbf7e51402" + "sha256:7ca74052a38fa25fe9bedf52da0be7d3fdd2fb027c3b778ea78dfe8c212937d1", + "sha256:f2db3a10254241d9b447232cec8b424847f338d9d36f9a577a6192c332a46abd" ], "index": "pypi", - "version": "==8.3.0" + "version": "==8.4.0" }, "jedi": { "hashes": [ @@ -376,6 +392,14 @@ "markers": "python_version >= '3.6'", "version": "==0.18.1" }, + "jmespath": { + "hashes": [ + "sha256:a490e280edd1f57d6de88636992d05b71e97d69a26a19f058ecf7d304474bf5e", + "sha256:e8dcd576ed616f14ec02eed0005c85973b5890083313860136657e24784e4c04" + ], + "markers": "python_version >= '3.7'", + "version": "==1.0.0" + }, "jsonschema": { "hashes": [ "sha256:71b5e39324422543546572954ce71c67728922c104902cb7ce252e522235b33f", @@ -404,31 +428,31 @@ }, "numpy": { "hashes": [ - "sha256:0791fbd1e43bf74b3502133207e378901272f3c156c4df4954cad833b1380207", - "sha256:1ce7ab2053e36c0a71e7a13a7475bd3b1f54750b4b433adc96313e127b870887", - "sha256:2d487e06ecbf1dc2f18e7efce82ded4f705f4bd0cd02677ffccfb39e5c284c7e", - "sha256:37431a77ceb9307c28382c9773da9f306435135fae6b80b62a11c53cfedd8802", - "sha256:3e1ffa4748168e1cc8d3cde93f006fe92b5421396221a02f2274aab6ac83b077", - "sha256:425b390e4619f58d8526b3dcf656dde069133ae5c240229821f01b5f44ea07af", - "sha256:43a8ca7391b626b4c4fe20aefe79fec683279e31e7c79716863b4b25021e0e74", - "sha256:4c6036521f11a731ce0648f10c18ae66d7143865f19f7299943c985cdc95afb5", - "sha256:59d55e634968b8f77d3fd674a3cf0b96e85147cd6556ec64ade018f27e9479e1", - "sha256:64f56fc53a2d18b1924abd15745e30d82a5782b2cab3429aceecc6875bd5add0", - "sha256:7228ad13744f63575b3a972d7ee4fd61815b2879998e70930d4ccf9ec721dce0", - "sha256:9ce7df0abeabe7fbd8ccbf343dc0db72f68549856b863ae3dd580255d009648e", - "sha256:a911e317e8c826ea632205e63ed8507e0dc877dcdc49744584dfc363df9ca08c", - "sha256:b89bf9b94b3d624e7bb480344e91f68c1c6c75f026ed6755955117de00917a7c", - "sha256:ba9ead61dfb5d971d77b6c131a9dbee62294a932bf6a356e48c75ae684e635b3", - "sha256:c1d937820db6e43bec43e8d016b9b3165dcb42892ea9f106c70fb13d430ffe72", - "sha256:cc7f00008eb7d3f2489fca6f334ec19ca63e31371be28fd5dad955b16ec285bd", - "sha256:d4c5d5eb2ec8da0b4f50c9a843393971f31f1d60be87e0fb0917a49133d257d6", - "sha256:e96d7f3096a36c8754207ab89d4b3282ba7b49ea140e4973591852c77d09eb76", - "sha256:f0725df166cf4785c0bc4cbfb320203182b1ecd30fee6e541c8752a92df6aa32", - "sha256:f3eb268dbd5cfaffd9448113539e44e2dd1c5ca9ce25576f7c04a5453edc26fa", - "sha256:fb7a980c81dd932381f8228a426df8aeb70d59bbcda2af075b627bbc50207cba" + "sha256:020218fc82390f1d537cb193d6f1449a919ec97df69b5a64c0a1d017486e0032", + "sha256:04e4dbe6b777e977813e7ff5f43aa030ef4f6f75cbc1a4504d3135942b5c12fe", + "sha256:1c881827ff0ad7d607047c19a075a7d7c7125cc103fb969a9200bad26175fb9d", + "sha256:1f22f6f3cb7094ad77c8d352e4bfd2c1db1c38bc08d0b6c74e9b46343c53b052", + "sha256:279dce16b143bc50d49bab52dc279d6ab5b0edc7f4d2cc7edaf6a547586bda7e", + "sha256:2e66decdea13ae8091ba480209dd5ce31261fa3b021ec06b30bd2f4a304861b7", + "sha256:548f4d86aa259a448f2da0c07df070bf1f71b68c1f84b1356d4a2ed832598758", + "sha256:6fbd492bead87ab83240c56b3490ac301595ab1399ace3e3c1b7c130e3529358", + "sha256:785d6520f7bf10ff188762bc460579d6a31c11f960976b2a29efc383b0346572", + "sha256:804293d9bdf33f9c9fb0b4a753f9e84114bb0ad538d184fc579b30782326c827", + "sha256:82e69890c394a4e1cbcaf12b47d8477bbac4635866fc46a77670abbe4bb4085d", + "sha256:8e8a88657c028b8b77f3df6f266a5e6ffb4419cbc3dfb525cbbb80ba710f5da2", + "sha256:9793feff4758c68502f7652fab08e5ec427d9973d26014767cc15c1b1d885f56", + "sha256:9810b840a751b6f0c73c21fb2a50e306d7d0be4114cded4c7d069e142ce488cf", + "sha256:ae7e8801b93124a6b0becedc06285ddbaca2daab2d30e35ea413d3bec252717a", + "sha256:b7be00b0a76384490845395714e62f597e64bc6dc8f8a14be0e96034dde3667c", + "sha256:c308afc8ec782badd073999385a6c93c27ee68e6c0991697394d4fd56566af1f", + "sha256:c335800064f04e0b474b64779ab234ae23c0a5b2f5a06284bb07d297d73692bd", + "sha256:d17f7feb2cca596daa4b3dae86b611a13e9ace061e6583a8db21841f529ca891", + "sha256:dbc987d14f46ae4c476068543d3ad2a20e7ebcb06b211eb4292224dc136eb01d", + "sha256:f5a1c7c45ff29db501f9e38a360aedd833e355c14c75155ba2bd46ee3799e30a", + "sha256:fde47931544086a648b12ee7c9ccf30edd6c6db776005fb07e4a019a04980042" ], "markers": "python_version < '3.10' and platform_machine != 'aarch64' and platform_machine != 'arm64'", - "version": "==1.22.4" + "version": "==1.23.0rc2" }, "odfpy": { "hashes": [ @@ -739,6 +763,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==2.27.1" }, + "s3transfer": { + "hashes": [ + "sha256:7a6f4c4d1fdb9a2b640244008e142cbc2cd3ae34b386584ef044dd0f27101971", + "sha256:95c58c194ce657a5f4fb0b9e60a84968c808888aed628cd98ab8771fe1db98ed" + ], + "markers": "python_version >= '3.6'", + "version": "==0.5.2" + }, "sentry-sdk": { "hashes": [ "sha256:259535ba66933eacf85ab46524188c84dcb4c39f40348455ce15e2c0aca68863", @@ -996,50 +1028,50 @@ }, "coverage": { "hashes": [ - "sha256:06f54765cdbce99901871d50fe9f41d58213f18e98b170a30ca34f47de7dd5e8", - "sha256:114944e6061b68a801c5da5427b9173a0dd9d32cd5fcc18a13de90352843737d", - "sha256:1414e8b124611bf4df8d77215bd32cba6e3425da8ce9c1f1046149615e3a9a31", - "sha256:2781c43bffbbec2b8867376d4d61916f5e9c4cc168232528562a61d1b4b01879", - "sha256:2ab88a01cd180b5640ccc9c47232e31924d5f9967ab7edd7e5c91c68eee47a69", - "sha256:338c417613f15596af9eb7a39353b60abec9d8ce1080aedba5ecee6a5d85f8d3", - "sha256:3401b0d2ed9f726fadbfa35102e00d1b3547b73772a1de5508ef3bdbcb36afe7", - "sha256:462105283de203df8de58a68c1bb4ba2a8a164097c2379f664fa81d6baf94b81", - "sha256:4cd696aa712e6cd16898d63cf66139dc70d998f8121ab558f0e1936396dbc579", - "sha256:4d06380e777dd6b35ee936f333d55b53dc4a8271036ff884c909cf6e94be8b6c", - "sha256:61f4fbf3633cb0713437291b8848634ea97f89c7e849c2be17a665611e433f53", - "sha256:6d4a6f30f611e657495cc81a07ff7aa8cd949144e7667c5d3e680d73ba7a70e4", - "sha256:6f5fee77ec3384b934797f1873758f796dfb4f167e1296dc00f8b2e023ce6ee9", - "sha256:75b5dbffc334e0beb4f6c503fb95e6d422770fd2d1b40a64898ea26d6c02742d", - "sha256:7835f76a081787f0ca62a53504361b3869840a1620049b56d803a8cb3a9eeea3", - "sha256:79bf405432428e989cad7b8bc60581963238f7645ae8a404f5dce90236cc0293", - "sha256:8329635c0781927a2c6ae068461e19674c564e05b86736ab8eb29c420ee7dc20", - "sha256:8586b177b4407f988731eb7f41967415b2197f35e2a6ee1a9b9b561f6323c8e9", - "sha256:892e7fe32191960da559a14536768a62e83e87bbb867e1b9c643e7e0fbce2579", - "sha256:91502bf27cbd5c83c95cfea291ef387469f2387508645602e1ca0fd8a4ba7548", - "sha256:93b16b08f94c92cab88073ffd185070cdcb29f1b98df8b28e6649145b7f2c90d", - "sha256:9c9441d57b0963cf8340268ad62fc83de61f1613034b79c2b1053046af0c5284", - "sha256:ad8f9068f5972a46d50fe5f32c09d6ee11da69c560fcb1b4c3baea246ca4109b", - "sha256:afb03f981fadb5aed1ac6e3dd34f0488e1a0875623d557b6fad09b97a942b38a", - "sha256:b5ba058610e8289a07db2a57bce45a1793ec0d3d11db28c047aae2aa1a832572", - "sha256:baa8be8aba3dd1e976e68677be68a960a633a6d44c325757aefaa4d66175050f", - "sha256:c06455121a089252b5943ea682187a4e0a5cf0a3fb980eb8e7ce394b144430a9", - "sha256:c1a9942e282cc9d3ed522cd3e3cab081149b27ea3bda72d6f61f84eaf88c1a63", - "sha256:c488db059848702aff30aa1d90ef87928d4e72e4f00717343800546fdbff0a94", - "sha256:cb5311d6ccbd22578c80028c5e292a7ab9adb91bd62c1982087fad75abe2e63d", - "sha256:cbe91bc84be4e5ef0b1480d15c7b18e29c73bdfa33e07d3725da7d18e1b0aff2", - "sha256:cc692c9ee18f0dd3214843779ba6b275ee4bb9b9a5745ba64265bce911aefd1a", - "sha256:cc972d829ad5ef4d4c5fcabd2bbe2add84ce8236f64ba1c0c72185da3a273130", - "sha256:ceb6534fcdfb5c503affb6b1130db7b5bfc8a0f77fa34880146f7a5c117987d0", - "sha256:d522f1dc49127eab0bfbba4e90fa068ecff0899bbf61bf4065c790ddd6c177fe", - "sha256:db094a6a4ae6329ed322a8973f83630b12715654c197dd392410400a5bfa1a73", - "sha256:df32ee0f4935a101e4b9a5f07b617d884a531ed5666671ff6ac66d2e8e8246d8", - "sha256:e5af1feee71099ae2e3b086ec04f57f9950e1be9ecf6c420696fea7977b84738", - "sha256:e814a4a5a1d95223b08cdb0f4f57029e8eab22ffdbae2f97107aeef28554517e", - "sha256:f8cabc5fd0091976ab7b020f5708335033e422de25e20ddf9416bdce2b7e07d8", - "sha256:fbc86ae8cc129c801e7baaafe3addf3c8d49c9c1597c44bdf2d78139707c3c62" + "sha256:00c8544510f3c98476bbd58201ac2b150ffbcce46a8c3e4fb89ebf01998f806a", + "sha256:016d7f5cf1c8c84f533a3c1f8f36126fbe00b2ec0ccca47cc5731c3723d327c6", + "sha256:03014a74023abaf5a591eeeaf1ac66a73d54eba178ff4cb1fa0c0a44aae70383", + "sha256:033ebec282793bd9eb988d0271c211e58442c31077976c19c442e24d827d356f", + "sha256:21e6686a95025927775ac501e74f5940cdf6fe052292f3a3f7349b0abae6d00f", + "sha256:26f8f92699756cb7af2b30720de0c5bb8d028e923a95b6d0c891088025a1ac8f", + "sha256:2e76bd16f0e31bc2b07e0fb1379551fcd40daf8cdf7e24f31a29e442878a827c", + "sha256:341e9c2008c481c5c72d0e0dbf64980a4b2238631a7f9780b0fe2e95755fb018", + "sha256:3cfd07c5889ddb96a401449109a8b97a165be9d67077df6802f59708bfb07720", + "sha256:4002f9e8c1f286e986fe96ec58742b93484195defc01d5cc7809b8f7acb5ece3", + "sha256:50ed480b798febce113709846b11f5d5ed1e529c88d8ae92f707806c50297abf", + "sha256:543e172ce4c0de533fa892034cce260467b213c0ea8e39da2f65f9a477425211", + "sha256:5a78cf2c43b13aa6b56003707c5203f28585944c277c1f3f109c7b041b16bd39", + "sha256:5cd698341626f3c77784858427bad0cdd54a713115b423d22ac83a28303d1d95", + "sha256:60c2147921da7f4d2d04f570e1838db32b95c5509d248f3fe6417e91437eaf41", + "sha256:62d382f7d77eeeaff14b30516b17bcbe80f645f5cf02bb755baac376591c653c", + "sha256:69432946f154c6add0e9ede03cc43b96e2ef2733110a77444823c053b1ff5166", + "sha256:727dafd7f67a6e1cad808dc884bd9c5a2f6ef1f8f6d2f22b37b96cb0080d4f49", + "sha256:742fb8b43835078dd7496c3c25a1ec8d15351df49fb0037bffb4754291ef30ce", + "sha256:750e13834b597eeb8ae6e72aa58d1d831b96beec5ad1d04479ae3772373a8088", + "sha256:7b546cf2b1974ddc2cb222a109b37c6ed1778b9be7e6b0c0bc0cf0438d9e45a6", + "sha256:83bd142cdec5e4a5c4ca1d4ff6fa807d28460f9db919f9f6a31babaaa8b88426", + "sha256:8d2e80dd3438e93b19e1223a9850fa65425e77f2607a364b6fd134fcd52dc9df", + "sha256:9229d074e097f21dfe0643d9d0140ee7433814b3f0fc3706b4abffd1e3038632", + "sha256:968ed5407f9460bd5a591cefd1388cc00a8f5099de9e76234655ae48cfdbe2c3", + "sha256:9c82f2cd69c71698152e943f4a5a6b83a3ab1db73b88f6e769fabc86074c3b08", + "sha256:a00441f5ea4504f5abbc047589d09e0dc33eb447dc45a1a527c8b74bfdd32c65", + "sha256:a022394996419142b33a0cf7274cb444c01d2bb123727c4bb0b9acabcb515dea", + "sha256:af5b9ee0fc146e907aa0f5fb858c3b3da9199d78b7bb2c9973d95550bd40f701", + "sha256:b5578efe4038be02d76c344007b13119b2b20acd009a88dde8adec2de4f630b5", + "sha256:b84ab65444dcc68d761e95d4d70f3cfd347ceca5a029f2ffec37d4f124f61311", + "sha256:c53ad261dfc8695062fc8811ac7c162bd6096a05a19f26097f411bdf5747aee7", + "sha256:cc173f1ce9ffb16b299f51c9ce53f66a62f4d975abe5640e976904066f3c835d", + "sha256:d548edacbf16a8276af13063a2b0669d58bbcfca7c55a255f84aac2870786a61", + "sha256:d55fae115ef9f67934e9f1103c9ba826b4c690e4c5bcf94482b8b2398311bf9c", + "sha256:d8099ea680201c2221f8468c372198ceba9338a5fec0e940111962b03b3f716a", + "sha256:e35217031e4b534b09f9b9a5841b9344a30a6357627761d4218818b865d45055", + "sha256:e4f52c272fdc82e7c65ff3f17a7179bc5f710ebc8ce8a5cadac81215e8326740", + "sha256:e637ae0b7b481905358624ef2e81d7fb0b1af55f5ff99f9ba05442a444b11e45", + "sha256:eef5292b60b6de753d6e7f2d128d5841c7915fb1e3321c3a1fe6acfe76c38052", + "sha256:fb45fe08e1abc64eb836d187b20a59172053999823f7f6ef4f18a819c44ba16f" ], "index": "pypi", - "version": "==6.3.3" + "version": "==6.4" }, "distlib": { "hashes": [ @@ -1074,11 +1106,11 @@ }, "faker": { "hashes": [ - "sha256:c6ff91847d7c820afc0a74d95e824b48aab71ddfd9003f300641e42d58ae886f", - "sha256:cad1f69d72a68878cd67855140b6fe3e44c11628971cd838595d289c98bc45de" + "sha256:1f6478011ac8a8273e0f9cd6da03d9ea6391c622db340eca015339512e9cde29", + "sha256:5cbb89fc6a16793b2bd98252c03a86098c7426beab0a20382709a815651b8804" ], "markers": "python_version >= '3.6'", - "version": "==13.11.1" + "version": "==13.12.0" }, "filelock": { "hashes": [ diff --git a/app/settings.py b/app/settings.py index 8d104302b..dfb3693cb 100644 --- a/app/settings.py +++ b/app/settings.py @@ -255,6 +255,19 @@ ) +# Object storage : Scaleway (S3-like) +# ------------------------------------------------------------------------------ + +S3_ENDPOINT = os.getenv("S3_ENDPOINT") +S3_BUCKET_NAME = os.getenv("S3_BUCKET_NAME") +S3_BUCKET_REGION = os.getenv("S3_BUCKET_REGION") +S3_ACCESS_KEY = os.getenv("S3_ACCESS_KEY") +S3_SECRET_KEY = os.getenv("S3_SECRET_KEY") + +QUESTION_FOLDER_NAME = "questions" +QUIZ_FOLDER_NAME = "quizs" + + # Django Bootstrap5 # https://django-bootstrap5.readthedocs.io/ # ------------------------------------------------------------------------------ @@ -318,7 +331,7 @@ "import csv, json, yaml", "from datetime import datetime, date, timedelta", "from core import constants", - "from core.utils import utilities, notion, github, sendinblue", + "from core.utils import utilities, notion, github, sendinblue, s3", "from stats import utilities as utilities_stats", ] From 78623f4f24ff448383a7f74604c4b7e973b1cc06 Mon Sep 17 00:00:00 2001 From: Raphael Odini Date: Tue, 31 May 2022 17:27:03 +0200 Subject: [PATCH 2/6] S3 utils --- core/utils/s3.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 core/utils/s3.py diff --git a/core/utils/s3.py b/core/utils/s3.py new file mode 100644 index 000000000..3461eb799 --- /dev/null +++ b/core/utils/s3.py @@ -0,0 +1,78 @@ +import boto3 +from botocore.client import Config +from django.conf import settings + + +# boto3 doc +# https://boto3.amazonaws.com/v1/documentation/api/latest/guide/collections.html + + +API_CONNECTION_DICT = { + "endpoint_url": settings.S3_ENDPOINT, + "aws_access_key_id": settings.S3_ACCESS_KEY, + "aws_secret_access_key": settings.S3_SECRET_KEY, + "region_name": settings.S3_BUCKET_REGION, + "config": Config(signature_version="s3v4"), +} + +CONTENT_TYPE_MAPPING = { + "png": "image/png", + "PNG": "image/png", + "svg": "image/svg+xml", + "gif": "image/gif", + "jpg": "image/jpg", + "JPG": "image/jpg", + "jpeg": "image/jpeg", +} # "jfif" + + +client = boto3.client("s3", **API_CONNECTION_DICT) +resource = boto3.resource("s3", **API_CONNECTION_DICT) +bucket = resource.Bucket(settings.S3_BUCKET_NAME) + + +def get_bucket(bucket_name=settings.S3_BUCKET_NAME): + bucket = resource.Bucket(bucket_name) + return bucket + + +def list_bucket_objects(bucket): + for obj in bucket.objects.all(): + print(obj.__dict__) + + +def get_object_metadata(bucket, object_key): + return client.head_object(Bucket=bucket.name, Key=object_key) + + +def get_object_url(bucket, object_key): + return f"{API_CONNECTION_DICT['endpoint_url']}/{bucket.name}/{object_key}" + + +def upload_object(bucket, object_file_path, s3_object_key): + """ + in read-mode instead of download-mode): https://stackoverflow.com/a/58641574/4293684 + # alternative + resource.meta.client.upload_file(object_file_path, bucket_name, s3_file_key, ExtraArgs={"ACL": "public-read", "ContentType": "image/png"}) # noqa + """ + object_extension = object_file_path.split(".")[1] + bucket.upload_file( + object_file_path, + s3_object_key, + ExtraArgs={"ACL": "public-read", "ContentType": CONTENT_TYPE_MAPPING[object_extension]}, + ) + + +def delete_object(bucket, object_key): + """ + to_delete = [{'Key':'IMG_20160807_150118.jpg'}, {'Key':'IMG_20160807_150124.jpg'}] + """ + bucket.delete_objects(Delete={"Objects": [{"Key": object_key}]}) + + +def delete_all_objects(bucket, prefix=None): + if prefix: + # Delete all objects inside a 'folder' + bucket.objects.filter(Prefix="myprefix/").delete() + else: + bucket.objects.delete() From 37e628cb8110ca6588615591988ee67edead342c Mon Sep 17 00:00:00 2001 From: Raphael Odini Date: Wed, 1 Jun 2022 01:17:14 +0200 Subject: [PATCH 3/6] cors & policy config --- app/settings.py | 3 +++ core/utils/s3.py | 66 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/app/settings.py b/app/settings.py index dfb3693cb..9ee2eb748 100644 --- a/app/settings.py +++ b/app/settings.py @@ -181,11 +181,14 @@ # ------------------------------------------------------------------------------ CORS_ORIGIN_WHITELIST = [ + "http://localhost:8000", "http://localhost:8080", "https://quiz-anthropocene.netlify.com", "https://quiz-anthropocene.netlify.app", "https://quiztaplanete.fr", "https://quizanthropocene.fr", + "https://admin.quizanthropocene.fr", + "https://quiz-anthropocene.osc-fr1.scalingo.io", ] CORS_ORIGIN_REGEX_WHITELIST = [ diff --git a/core/utils/s3.py b/core/utils/s3.py index 3461eb799..3695042c1 100644 --- a/core/utils/s3.py +++ b/core/utils/s3.py @@ -1,3 +1,5 @@ +import json + import boto3 from botocore.client import Config from django.conf import settings @@ -15,6 +17,47 @@ "config": Config(signature_version="s3v4"), } +DEFAULT_CORS_CONFIGURATION = { + "CORSRules": [ + { + "AllowedHeaders": [ + "Cache-Control", + "x-requested-with", + ], + "AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"], + "AllowedOrigins": settings.CORS_ORIGIN_WHITELIST, + "ExposeHeaders": ["ETag", "Location"], + } + ] +} + +DEFAULT_POLICY_CONFIGURATION = { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowPublicRead", + "Effect": "Allow", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": f"{settings.S3_BUCKET_NAME}/*", + }, + { + "Sid": "DenyPublicUpdate", + "Effect": "Deny", + "Principal": "*", + "Action": "s3:PutObject", + "Resource": f"{settings.S3_BUCKET_NAME}/*", + }, + { + "Sid": "DenyPublicDelete", + "Effect": "Deny", + "Principal": "*", + "Action": "s3:DeleteObject", + "Resource": f"{settings.S3_BUCKET_NAME}/*", + }, + ], +} + CONTENT_TYPE_MAPPING = { "png": "image/png", "PNG": "image/png", @@ -51,7 +94,7 @@ def get_object_url(bucket, object_key): def upload_object(bucket, object_file_path, s3_object_key): """ - in read-mode instead of download-mode): https://stackoverflow.com/a/58641574/4293684 + in read-mode instead of download-mode: https://stackoverflow.com/a/58641574/4293684 # alternative resource.meta.client.upload_file(object_file_path, bucket_name, s3_file_key, ExtraArgs={"ACL": "public-read", "ContentType": "image/png"}) # noqa """ @@ -64,9 +107,6 @@ def upload_object(bucket, object_file_path, s3_object_key): def delete_object(bucket, object_key): - """ - to_delete = [{'Key':'IMG_20160807_150118.jpg'}, {'Key':'IMG_20160807_150124.jpg'}] - """ bucket.delete_objects(Delete={"Objects": [{"Key": object_key}]}) @@ -76,3 +116,21 @@ def delete_all_objects(bucket, prefix=None): bucket.objects.filter(Prefix="myprefix/").delete() else: bucket.objects.delete() + + +def get_bucket_cors(bucket): + response = client.get_bucket_cors(Bucket=bucket.name) + print(response["CORSRules"]) + + +def update_bucket_cors(bucket, cors_configuration=DEFAULT_CORS_CONFIGURATION): + client.put_bucket_cors(Bucket=bucket.name, CORSConfiguration=cors_configuration) + + +def get_bucket_policy(bucket): + response = client.get_bucket_policy(Bucket=bucket.name) + print(response["Policy"]) + + +def update_bucket_policy(bucket, policy_configuration=DEFAULT_POLICY_CONFIGURATION): + client.put_bucket_policy(Bucket=bucket.name, Policy=json.dumps(policy_configuration)) From 21757909858a52ea8976e86f168071f1fb1f6cbe Mon Sep 17 00:00:00 2001 From: Raphael Odini Date: Fri, 3 Jun 2022 12:17:01 +0200 Subject: [PATCH 4/6] S3 upload workflow for Question answer_image_url --- app/settings.py | 18 ++ core/static/js/s3_upload.js | 171 ++++++++++++++++++ .../vendor/dropzone-5.9.3/dropzone.min.css | 1 + .../vendor/dropzone-5.9.3/dropzone.min.js | 1 + core/utils/s3.py | 45 +++++ questions/forms.py | 1 + templates/includes/_s3_upload_form.html | 18 ++ templates/questions/detail_edit.html | 40 ++++ www/questions/views.py | 6 + 9 files changed, 301 insertions(+) create mode 100644 core/static/js/s3_upload.js create mode 100644 core/static/vendor/dropzone-5.9.3/dropzone.min.css create mode 100644 core/static/vendor/dropzone-5.9.3/dropzone.min.js create mode 100644 templates/includes/_s3_upload_form.html diff --git a/app/settings.py b/app/settings.py index 9ee2eb748..7fd3dfb15 100644 --- a/app/settings.py +++ b/app/settings.py @@ -181,6 +181,7 @@ # ------------------------------------------------------------------------------ CORS_ORIGIN_WHITELIST = [ + "http://127.0.0.1:8000", "http://localhost:8000", "http://localhost:8080", "https://quiz-anthropocene.netlify.com", @@ -270,6 +271,23 @@ QUESTION_FOLDER_NAME = "questions" QUIZ_FOLDER_NAME = "quizs" +STORAGE_UPLOAD_KINDS = { + "default": { + "allowed_mime_types": ["image/png", "image/svg+xml", "image/gif", "image/jpg", "image/jpeg"], # ["image/*"] ? + "upload_expiration": 60 * 60, # in seconds + "key_path": "default", # appended before the file key. No backslash! + "max_files": 1, # 3, + "max_file_size": 5, # in mb + "timeout": 20000, # in ms + }, + "question_answer_image": { + "key_path": QUESTION_FOLDER_NAME, + }, + "quiz_image_background": { + "key_path": QUIZ_FOLDER_NAME, + }, +} + # Django Bootstrap5 # https://django-bootstrap5.readthedocs.io/ diff --git a/core/static/js/s3_upload.js b/core/static/js/s3_upload.js new file mode 100644 index 000000000..7054d38cd --- /dev/null +++ b/core/static/js/s3_upload.js @@ -0,0 +1,171 @@ +"use strict"; + +const extensionToContentTypeMapping = { + 'png': 'image/png', + 'PNG': 'image/png', + 'svg': 'image/svg+xml', + 'gif': 'image/gif', + 'jpg': 'image/jpg', + 'JPG': 'image/jpg', + 'jpeg': 'image/jpeg', +} + +// Arguments +// - callbackLocationSelector: input field where the location URL will be provided after success (ex. "#foo"). +// - dropzoneSelector: transform this element into a drag and drop zone. +// - s3FormValuesId: ID of DOM element that contains the JSON values of the form (eg. URL, fields, etc) +// - s3UploadConfigId: ID of DOM element that contains the JSON values of the S3 config (eg. max file size, timeout, etc). + +window.s3UploadInit = function s3UploadInit({ + dropzoneSelector = "#answer_image_form", + callbackLocationSelector = "", + s3FormValuesId = "s3-form-values", + s3UploadConfigId = "s3-upload-config", + sentryInternalUrl = "", + sentryCsrfToken = "", +} = {}) { + const formValues = JSON.parse( + document.getElementById(s3FormValuesId).textContent + ); + const uploadConfig = JSON.parse( + document.getElementById(s3UploadConfigId).textContent + ); + + // When a file is added to the drop zone, send a POST request to this URL. + const formUrl = formValues["url"]; + + // Submit button to be disabled during file processing + const submitButton = $("button[type='submit']"); + + // S3 form params sent when a new file is added to the drop zone. + let formParams = formValues["fields"]; + + // Appended before the file name. The final slash is added later. + const keyPath = uploadConfig["key_path"]; + + // Dropzone configuration + const dropzoneConfig = { + url: formUrl, + params: formParams, + maxFilesize: uploadConfig["max_file_size"], // in MB + timeout: uploadConfig["timeout"], // default 3000, in ms + maxFiles: uploadConfig["max_files"], + acceptedFiles: uploadConfig["allowed_mime_types"], + // UI config + addRemoveLinks: true, + // translations + dictFallbackMessage: "Ce navigateur n'est pas compatible", + dictFileTooBig: "Fichier trop volumineux", + dictInvalidFileType: "Type de fichier non pris en charge", + dictResponseError: "Erreur technique. Merci de recommencer.", + dictCancelUpload: "Annuler", + dictUploadCanceled: "Annulé", + dictCancelUploadConfirmation: "Voulez-vous vraiment annuler le transfert ?", + dictRemoveFile: "Supprimer", + dictRemoveFileConfirmation: "Voulez-vous vraiment supprimer le fichier ?", + dictMaxFilesExceeded: "Un seul fichier autorisé à la fois", + // the function will be used to rename the file.name before appending it to the formData + renameFile: function (file) { + const extension = file.name.split(".").pop(); + const filename = Dropzone.uuidv4(); + const fileKey = `${keyPath}/${filename}.${extension}`; + // Add a file key to options params so that it's sent as an input field on POST. + this.params["key"] = fileKey; + // We also send the Content-Type depending on the image type. + // Important because by default the Content-Type will be 'application/octet-stream' (when fetched, images would be downloaded automatically instead of being displayed) + this.params["Content-Type"] = extensionToContentTypeMapping[extension]; + return fileKey; + }, + }; + + // By default, Dropzone attaches to any component having the "dropzone" class. + // Turn off this behavior to control all the aspects "manually". + Dropzone.autoDiscover = false; + + const dropzone = new Dropzone(dropzoneSelector, dropzoneConfig); + + // Display a help message when the user tries to + // submit the form during file transfer. + // submitButton.tooltip({ title: "Veuillez attendre la fin du transfert" }); + // Enable it later, during file transfer. + // submitButton.tooltip("disable"); + + // Events + dropzone.on("addedfile", function (file) { + // submitButton.tooltip("enable"); + submitButton.prop("disabled", true); + submitButton.addClass("btn-secondary"); + }); + + // Called when the upload was either successful or erroneous. + dropzone.on("complete", function (file) { + console.log("complete") + // submitButton.tooltip("disable"); + submitButton.prop("disabled", false); + submitButton.removeClass("btn-secondary"); + }); + + dropzone.on("removedfile", function (file) { + $(callbackLocationSelector).val(""); + }); + + dropzone.on("success", function (file, xhr, formData) { + const location = `${formUrl}/${file.upload.filename}`; + // Prevent a selector mistake from being silent. + if ($(callbackLocationSelector).length === 0) { + this._handleUploadError( + [file], + xhr, + "Ce document n'a pas pu être envoyé à cause d'un problème technique. Nous vous invitons à contacter notre support." + ); + } + $(callbackLocationSelector).val(location); + }); + + dropzone.on("error", function (file, errorMessage, xhr) { + let statusCode = 500; + + if (typeof errorMessage === "string") { + if (errorMessage.includes("timedout")) { + // Override default English message and don't send the error to Sentry. + file.previewElement.querySelectorAll('[data-dz-errormessage]')[0].textContent = "Erreur technique. Merci de recommencer."; + return + } + } + else { + // errorMessage is a JSON object. Display a nice message to the user instead of [object Object]. + file.previewElement.querySelectorAll('[data-dz-errormessage]')[0].textContent = "Erreur technique. Merci de recommencer."; + } + + if (xhr) { + statusCode = xhr.status; + + if (statusCode === 0) { + // Don't send undefined errors to Sentry. + // Might be due to a firewall or to an unreachable network. + // See https://stackoverflow.com/questions/872206/what-does-it-mean-when-an-http-request-returns-status-code-0 + return + } + + if (xhr.responseText) { + let responseJson = JSON.parse(xhr.responseText); + errorMessage = responseJson["Message"]; + // User waited too long before sending the file. + // See base.py > STORAGE_UPLOAD_KINDS > upload expiration + if (errorMessage === "Policy expired") { + return + } + } + + // An error occurred with the request. Send it to Sentry to avoid silent bugs. + const sentryErrorMessage = + `Unable to upload "${file.upload.filename}" ` + + `(${file.upload.progress} of ${file.upload.total}) to S3 ${formUrl}: ${errorMessage}`; + $.post(sentryInternalUrl, { + status_code: statusCode, + error_message: sentryErrorMessage, + csrfmiddlewaretoken: sentryCsrfToken, + }); + } + }); +}; diff --git a/core/static/vendor/dropzone-5.9.3/dropzone.min.css b/core/static/vendor/dropzone-5.9.3/dropzone.min.css new file mode 100644 index 000000000..a839c4c03 --- /dev/null +++ b/core/static/vendor/dropzone-5.9.3/dropzone.min.css @@ -0,0 +1 @@ +@-webkit-keyframes passing-through{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%,70%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}100%{opacity:0;-webkit-transform:translateY(-40px);-moz-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}}@-moz-keyframes passing-through{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%,70%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}100%{opacity:0;-webkit-transform:translateY(-40px);-moz-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}}@keyframes passing-through{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%,70%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}100%{opacity:0;-webkit-transform:translateY(-40px);-moz-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}}@-webkit-keyframes slide-in{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}}@-moz-keyframes slide-in{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}}@keyframes slide-in{0%{opacity:0;-webkit-transform:translateY(40px);-moz-transform:translateY(40px);-ms-transform:translateY(40px);-o-transform:translateY(40px);transform:translateY(40px)}30%{opacity:1;-webkit-transform:translateY(0px);-moz-transform:translateY(0px);-ms-transform:translateY(0px);-o-transform:translateY(0px);transform:translateY(0px)}}@-webkit-keyframes pulse{0%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}10%{-webkit-transform:scale(1.1);-moz-transform:scale(1.1);-ms-transform:scale(1.1);-o-transform:scale(1.1);transform:scale(1.1)}20%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}}@-moz-keyframes pulse{0%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}10%{-webkit-transform:scale(1.1);-moz-transform:scale(1.1);-ms-transform:scale(1.1);-o-transform:scale(1.1);transform:scale(1.1)}20%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}}@keyframes pulse{0%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}10%{-webkit-transform:scale(1.1);-moz-transform:scale(1.1);-ms-transform:scale(1.1);-o-transform:scale(1.1);transform:scale(1.1)}20%{-webkit-transform:scale(1);-moz-transform:scale(1);-ms-transform:scale(1);-o-transform:scale(1);transform:scale(1)}}.dropzone,.dropzone *{box-sizing:border-box}.dropzone{min-height:150px;border:2px solid rgba(0,0,0,.3);background:#fff;padding:20px 20px}.dropzone.dz-clickable{cursor:pointer}.dropzone.dz-clickable *{cursor:default}.dropzone.dz-clickable .dz-message,.dropzone.dz-clickable .dz-message *{cursor:pointer}.dropzone.dz-started .dz-message{display:none}.dropzone.dz-drag-hover{border-style:solid}.dropzone.dz-drag-hover .dz-message{opacity:.5}.dropzone .dz-message{text-align:center;margin:2em 0}.dropzone .dz-message .dz-button{background:none;color:inherit;border:none;padding:0;font:inherit;cursor:pointer;outline:inherit}.dropzone .dz-preview{position:relative;display:inline-block;vertical-align:top;margin:16px;min-height:100px}.dropzone .dz-preview:hover{z-index:1000}.dropzone .dz-preview:hover .dz-details{opacity:1}.dropzone .dz-preview.dz-file-preview .dz-image{border-radius:20px;background:#999;background:linear-gradient(to bottom, #eee, #ddd)}.dropzone .dz-preview.dz-file-preview .dz-details{opacity:1}.dropzone .dz-preview.dz-image-preview{background:#fff}.dropzone .dz-preview.dz-image-preview .dz-details{-webkit-transition:opacity 0.2s linear;-moz-transition:opacity 0.2s linear;-ms-transition:opacity 0.2s linear;-o-transition:opacity 0.2s linear;transition:opacity 0.2s linear}.dropzone .dz-preview .dz-remove{font-size:14px;text-align:center;display:block;cursor:pointer;border:none}.dropzone .dz-preview .dz-remove:hover{text-decoration:underline}.dropzone .dz-preview:hover .dz-details{opacity:1}.dropzone .dz-preview .dz-details{z-index:20;position:absolute;top:0;left:0;opacity:0;font-size:13px;min-width:100%;max-width:100%;padding:2em 1em;text-align:center;color:rgba(0,0,0,.9);line-height:150%}.dropzone .dz-preview .dz-details .dz-size{margin-bottom:1em;font-size:16px}.dropzone .dz-preview .dz-details .dz-filename{white-space:nowrap}.dropzone .dz-preview .dz-details .dz-filename:hover span{border:1px solid rgba(200,200,200,.8);background-color:rgba(255,255,255,.8)}.dropzone .dz-preview .dz-details .dz-filename:not(:hover){overflow:hidden;text-overflow:ellipsis}.dropzone .dz-preview .dz-details .dz-filename:not(:hover) span{border:1px solid transparent}.dropzone .dz-preview .dz-details .dz-filename span,.dropzone .dz-preview .dz-details .dz-size span{background-color:rgba(255,255,255,.4);padding:0 .4em;border-radius:3px}.dropzone .dz-preview:hover .dz-image img{-webkit-transform:scale(1.05, 1.05);-moz-transform:scale(1.05, 1.05);-ms-transform:scale(1.05, 1.05);-o-transform:scale(1.05, 1.05);transform:scale(1.05, 1.05);-webkit-filter:blur(8px);filter:blur(8px)}.dropzone .dz-preview .dz-image{border-radius:20px;overflow:hidden;width:120px;height:120px;position:relative;display:block;z-index:10}.dropzone .dz-preview .dz-image img{display:block}.dropzone .dz-preview.dz-success .dz-success-mark{-webkit-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);-moz-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);-ms-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);-o-animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);animation:passing-through 3s cubic-bezier(0.77, 0, 0.175, 1)}.dropzone .dz-preview.dz-error .dz-error-mark{opacity:1;-webkit-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);-moz-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);-ms-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);-o-animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);animation:slide-in 3s cubic-bezier(0.77, 0, 0.175, 1)}.dropzone .dz-preview .dz-success-mark,.dropzone .dz-preview .dz-error-mark{pointer-events:none;opacity:0;z-index:500;position:absolute;display:block;top:50%;left:50%;margin-left:-27px;margin-top:-27px}.dropzone .dz-preview .dz-success-mark svg,.dropzone .dz-preview .dz-error-mark svg{display:block;width:54px;height:54px}.dropzone .dz-preview.dz-processing .dz-progress{opacity:1;-webkit-transition:all 0.2s linear;-moz-transition:all 0.2s linear;-ms-transition:all 0.2s linear;-o-transition:all 0.2s linear;transition:all 0.2s linear}.dropzone .dz-preview.dz-complete .dz-progress{opacity:0;-webkit-transition:opacity 0.4s ease-in;-moz-transition:opacity 0.4s ease-in;-ms-transition:opacity 0.4s ease-in;-o-transition:opacity 0.4s ease-in;transition:opacity 0.4s ease-in}.dropzone .dz-preview:not(.dz-processing) .dz-progress{-webkit-animation:pulse 6s ease infinite;-moz-animation:pulse 6s ease infinite;-ms-animation:pulse 6s ease infinite;-o-animation:pulse 6s ease infinite;animation:pulse 6s ease infinite}.dropzone .dz-preview .dz-progress{opacity:1;z-index:1000;pointer-events:none;position:absolute;height:16px;left:50%;top:50%;margin-top:-8px;width:80px;margin-left:-40px;background:rgba(255,255,255,.9);-webkit-transform:scale(1);border-radius:8px;overflow:hidden}.dropzone .dz-preview .dz-progress .dz-upload{background:#333;background:linear-gradient(to bottom, #666, #444);position:absolute;top:0;left:0;bottom:0;width:0;-webkit-transition:width 300ms ease-in-out;-moz-transition:width 300ms ease-in-out;-ms-transition:width 300ms ease-in-out;-o-transition:width 300ms ease-in-out;transition:width 300ms ease-in-out}.dropzone .dz-preview.dz-error .dz-error-message{display:block}.dropzone .dz-preview.dz-error:hover .dz-error-message{opacity:1;pointer-events:auto}.dropzone .dz-preview .dz-error-message{pointer-events:none;z-index:1000;position:absolute;display:block;display:none;opacity:0;-webkit-transition:opacity 0.3s ease;-moz-transition:opacity 0.3s ease;-ms-transition:opacity 0.3s ease;-o-transition:opacity 0.3s ease;transition:opacity 0.3s ease;border-radius:8px;font-size:13px;top:130px;left:-10px;width:140px;background:#be2626;background:linear-gradient(to bottom, #be2626, #a92222);padding:.5em 1.2em;color:#fff}.dropzone .dz-preview .dz-error-message:after{content:"";position:absolute;top:-6px;left:64px;width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #be2626} diff --git a/core/static/vendor/dropzone-5.9.3/dropzone.min.js b/core/static/vendor/dropzone-5.9.3/dropzone.min.js new file mode 100644 index 000000000..14a862d66 --- /dev/null +++ b/core/static/vendor/dropzone-5.9.3/dropzone.min.js @@ -0,0 +1 @@ +!function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var n=t();for(var r in n)("object"==typeof exports?exports:e)[r]=n[r]}}(self,(function(){return function(){var e={3099:function(e){e.exports=function(e){if("function"!=typeof e)throw TypeError(String(e)+" is not a function");return e}},6077:function(e,t,n){var r=n(111);e.exports=function(e){if(!r(e)&&null!==e)throw TypeError("Can't set "+String(e)+" as a prototype");return e}},1223:function(e,t,n){var r=n(5112),i=n(30),o=n(3070),a=r("unscopables"),u=Array.prototype;null==u[a]&&o.f(u,a,{configurable:!0,value:i(null)}),e.exports=function(e){u[a][e]=!0}},1530:function(e,t,n){"use strict";var r=n(8710).charAt;e.exports=function(e,t,n){return t+(n?r(e,t).length:1)}},5787:function(e){e.exports=function(e,t,n){if(!(e instanceof t))throw TypeError("Incorrect "+(n?n+" ":"")+"invocation");return e}},9670:function(e,t,n){var r=n(111);e.exports=function(e){if(!r(e))throw TypeError(String(e)+" is not an object");return e}},4019:function(e){e.exports="undefined"!=typeof ArrayBuffer&&"undefined"!=typeof DataView},260:function(e,t,n){"use strict";var r,i=n(4019),o=n(9781),a=n(7854),u=n(111),s=n(6656),l=n(648),c=n(8880),f=n(1320),p=n(3070).f,h=n(9518),d=n(7674),v=n(5112),y=n(9711),g=a.Int8Array,m=g&&g.prototype,b=a.Uint8ClampedArray,x=b&&b.prototype,w=g&&h(g),E=m&&h(m),k=Object.prototype,A=k.isPrototypeOf,S=v("toStringTag"),F=y("TYPED_ARRAY_TAG"),T=i&&!!d&&"Opera"!==l(a.opera),C=!1,L={Int8Array:1,Uint8Array:1,Uint8ClampedArray:1,Int16Array:2,Uint16Array:2,Int32Array:4,Uint32Array:4,Float32Array:4,Float64Array:8},R={BigInt64Array:8,BigUint64Array:8},I=function(e){if(!u(e))return!1;var t=l(e);return s(L,t)||s(R,t)};for(r in L)a[r]||(T=!1);if((!T||"function"!=typeof w||w===Function.prototype)&&(w=function(){throw TypeError("Incorrect invocation")},T))for(r in L)a[r]&&d(a[r],w);if((!T||!E||E===k)&&(E=w.prototype,T))for(r in L)a[r]&&d(a[r].prototype,E);if(T&&h(x)!==E&&d(x,E),o&&!s(E,S))for(r in C=!0,p(E,S,{get:function(){return u(this)?this[F]:void 0}}),L)a[r]&&c(a[r],F,r);e.exports={NATIVE_ARRAY_BUFFER_VIEWS:T,TYPED_ARRAY_TAG:C&&F,aTypedArray:function(e){if(I(e))return e;throw TypeError("Target is not a typed array")},aTypedArrayConstructor:function(e){if(d){if(A.call(w,e))return e}else for(var t in L)if(s(L,r)){var n=a[t];if(n&&(e===n||A.call(n,e)))return e}throw TypeError("Target is not a typed array constructor")},exportTypedArrayMethod:function(e,t,n){if(o){if(n)for(var r in L){var i=a[r];i&&s(i.prototype,e)&&delete i.prototype[e]}E[e]&&!n||f(E,e,n?t:T&&m[e]||t)}},exportTypedArrayStaticMethod:function(e,t,n){var r,i;if(o){if(d){if(n)for(r in L)(i=a[r])&&s(i,e)&&delete i[e];if(w[e]&&!n)return;try{return f(w,e,n?t:T&&g[e]||t)}catch(e){}}for(r in L)!(i=a[r])||i[e]&&!n||f(i,e,t)}},isView:function(e){if(!u(e))return!1;var t=l(e);return"DataView"===t||s(L,t)||s(R,t)},isTypedArray:I,TypedArray:w,TypedArrayPrototype:E}},3331:function(e,t,n){"use strict";var r=n(7854),i=n(9781),o=n(4019),a=n(8880),u=n(2248),s=n(7293),l=n(5787),c=n(9958),f=n(7466),p=n(7067),h=n(1179),d=n(9518),v=n(7674),y=n(8006).f,g=n(3070).f,m=n(1285),b=n(8003),x=n(9909),w=x.get,E=x.set,k="ArrayBuffer",A="DataView",S="Wrong index",F=r.ArrayBuffer,T=F,C=r.DataView,L=C&&C.prototype,R=Object.prototype,I=r.RangeError,U=h.pack,O=h.unpack,_=function(e){return[255&e]},M=function(e){return[255&e,e>>8&255]},z=function(e){return[255&e,e>>8&255,e>>16&255,e>>24&255]},P=function(e){return e[3]<<24|e[2]<<16|e[1]<<8|e[0]},j=function(e){return U(e,23,4)},D=function(e){return U(e,52,8)},N=function(e,t){g(e.prototype,t,{get:function(){return w(this)[t]}})},B=function(e,t,n,r){var i=p(n),o=w(e);if(i+t>o.byteLength)throw I(S);var a=w(o.buffer).bytes,u=i+o.byteOffset,s=a.slice(u,u+t);return r?s:s.reverse()},q=function(e,t,n,r,i,o){var a=p(n),u=w(e);if(a+t>u.byteLength)throw I(S);for(var s=w(u.buffer).bytes,l=a+u.byteOffset,c=r(+i),f=0;fG;)(W=Y[G++])in T||a(T,W,F[W]);H.constructor=T}v&&d(L)!==R&&v(L,R);var Q=new C(new T(2)),$=L.setInt8;Q.setInt8(0,2147483648),Q.setInt8(1,2147483649),!Q.getInt8(0)&&Q.getInt8(1)||u(L,{setInt8:function(e,t){$.call(this,e,t<<24>>24)},setUint8:function(e,t){$.call(this,e,t<<24>>24)}},{unsafe:!0})}else T=function(e){l(this,T,k);var t=p(e);E(this,{bytes:m.call(new Array(t),0),byteLength:t}),i||(this.byteLength=t)},C=function(e,t,n){l(this,C,A),l(e,T,A);var r=w(e).byteLength,o=c(t);if(o<0||o>r)throw I("Wrong offset");if(o+(n=void 0===n?r-o:f(n))>r)throw I("Wrong length");E(this,{buffer:e,byteLength:n,byteOffset:o}),i||(this.buffer=e,this.byteLength=n,this.byteOffset=o)},i&&(N(T,"byteLength"),N(C,"buffer"),N(C,"byteLength"),N(C,"byteOffset")),u(C.prototype,{getInt8:function(e){return B(this,1,e)[0]<<24>>24},getUint8:function(e){return B(this,1,e)[0]},getInt16:function(e){var t=B(this,2,e,arguments.length>1?arguments[1]:void 0);return(t[1]<<8|t[0])<<16>>16},getUint16:function(e){var t=B(this,2,e,arguments.length>1?arguments[1]:void 0);return t[1]<<8|t[0]},getInt32:function(e){return P(B(this,4,e,arguments.length>1?arguments[1]:void 0))},getUint32:function(e){return P(B(this,4,e,arguments.length>1?arguments[1]:void 0))>>>0},getFloat32:function(e){return O(B(this,4,e,arguments.length>1?arguments[1]:void 0),23)},getFloat64:function(e){return O(B(this,8,e,arguments.length>1?arguments[1]:void 0),52)},setInt8:function(e,t){q(this,1,e,_,t)},setUint8:function(e,t){q(this,1,e,_,t)},setInt16:function(e,t){q(this,2,e,M,t,arguments.length>2?arguments[2]:void 0)},setUint16:function(e,t){q(this,2,e,M,t,arguments.length>2?arguments[2]:void 0)},setInt32:function(e,t){q(this,4,e,z,t,arguments.length>2?arguments[2]:void 0)},setUint32:function(e,t){q(this,4,e,z,t,arguments.length>2?arguments[2]:void 0)},setFloat32:function(e,t){q(this,4,e,j,t,arguments.length>2?arguments[2]:void 0)},setFloat64:function(e,t){q(this,8,e,D,t,arguments.length>2?arguments[2]:void 0)}});b(T,k),b(C,A),e.exports={ArrayBuffer:T,DataView:C}},1048:function(e,t,n){"use strict";var r=n(7908),i=n(1400),o=n(7466),a=Math.min;e.exports=[].copyWithin||function(e,t){var n=r(this),u=o(n.length),s=i(e,u),l=i(t,u),c=arguments.length>2?arguments[2]:void 0,f=a((void 0===c?u:i(c,u))-l,u-s),p=1;for(l0;)l in n?n[s]=n[l]:delete n[s],s+=p,l+=p;return n}},1285:function(e,t,n){"use strict";var r=n(7908),i=n(1400),o=n(7466);e.exports=function(e){for(var t=r(this),n=o(t.length),a=arguments.length,u=i(a>1?arguments[1]:void 0,n),s=a>2?arguments[2]:void 0,l=void 0===s?n:i(s,n);l>u;)t[u++]=e;return t}},8533:function(e,t,n){"use strict";var r=n(2092).forEach,i=n(9341)("forEach");e.exports=i?[].forEach:function(e){return r(this,e,arguments.length>1?arguments[1]:void 0)}},8457:function(e,t,n){"use strict";var r=n(9974),i=n(7908),o=n(3411),a=n(7659),u=n(7466),s=n(6135),l=n(1246);e.exports=function(e){var t,n,c,f,p,h,d=i(e),v="function"==typeof this?this:Array,y=arguments.length,g=y>1?arguments[1]:void 0,m=void 0!==g,b=l(d),x=0;if(m&&(g=r(g,y>2?arguments[2]:void 0,2)),null==b||v==Array&&a(b))for(n=new v(t=u(d.length));t>x;x++)h=m?g(d[x],x):d[x],s(n,x,h);else for(p=(f=b.call(d)).next,n=new v;!(c=p.call(f)).done;x++)h=m?o(f,g,[c.value,x],!0):c.value,s(n,x,h);return n.length=x,n}},1318:function(e,t,n){var r=n(5656),i=n(7466),o=n(1400),a=function(e){return function(t,n,a){var u,s=r(t),l=i(s.length),c=o(a,l);if(e&&n!=n){for(;l>c;)if((u=s[c++])!=u)return!0}else for(;l>c;c++)if((e||c in s)&&s[c]===n)return e||c||0;return!e&&-1}};e.exports={includes:a(!0),indexOf:a(!1)}},2092:function(e,t,n){var r=n(9974),i=n(8361),o=n(7908),a=n(7466),u=n(5417),s=[].push,l=function(e){var t=1==e,n=2==e,l=3==e,c=4==e,f=6==e,p=7==e,h=5==e||f;return function(d,v,y,g){for(var m,b,x=o(d),w=i(x),E=r(v,y,3),k=a(w.length),A=0,S=g||u,F=t?S(d,k):n||p?S(d,0):void 0;k>A;A++)if((h||A in w)&&(b=E(m=w[A],A,x),e))if(t)F[A]=b;else if(b)switch(e){case 3:return!0;case 5:return m;case 6:return A;case 2:s.call(F,m)}else switch(e){case 4:return!1;case 7:s.call(F,m)}return f?-1:l||c?c:F}};e.exports={forEach:l(0),map:l(1),filter:l(2),some:l(3),every:l(4),find:l(5),findIndex:l(6),filterOut:l(7)}},6583:function(e,t,n){"use strict";var r=n(5656),i=n(9958),o=n(7466),a=n(9341),u=Math.min,s=[].lastIndexOf,l=!!s&&1/[1].lastIndexOf(1,-0)<0,c=a("lastIndexOf"),f=l||!c;e.exports=f?function(e){if(l)return s.apply(this,arguments)||0;var t=r(this),n=o(t.length),a=n-1;for(arguments.length>1&&(a=u(a,i(arguments[1]))),a<0&&(a=n+a);a>=0;a--)if(a in t&&t[a]===e)return a||0;return-1}:s},1194:function(e,t,n){var r=n(7293),i=n(5112),o=n(7392),a=i("species");e.exports=function(e){return o>=51||!r((function(){var t=[];return(t.constructor={})[a]=function(){return{foo:1}},1!==t[e](Boolean).foo}))}},9341:function(e,t,n){"use strict";var r=n(7293);e.exports=function(e,t){var n=[][e];return!!n&&r((function(){n.call(null,t||function(){throw 1},1)}))}},3671:function(e,t,n){var r=n(3099),i=n(7908),o=n(8361),a=n(7466),u=function(e){return function(t,n,u,s){r(n);var l=i(t),c=o(l),f=a(l.length),p=e?f-1:0,h=e?-1:1;if(u<2)for(;;){if(p in c){s=c[p],p+=h;break}if(p+=h,e?p<0:f<=p)throw TypeError("Reduce of empty array with no initial value")}for(;e?p>=0:f>p;p+=h)p in c&&(s=n(s,c[p],p,l));return s}};e.exports={left:u(!1),right:u(!0)}},5417:function(e,t,n){var r=n(111),i=n(3157),o=n(5112)("species");e.exports=function(e,t){var n;return i(e)&&("function"!=typeof(n=e.constructor)||n!==Array&&!i(n.prototype)?r(n)&&null===(n=n[o])&&(n=void 0):n=void 0),new(void 0===n?Array:n)(0===t?0:t)}},3411:function(e,t,n){var r=n(9670),i=n(9212);e.exports=function(e,t,n,o){try{return o?t(r(n)[0],n[1]):t(n)}catch(t){throw i(e),t}}},7072:function(e,t,n){var r=n(5112)("iterator"),i=!1;try{var o=0,a={next:function(){return{done:!!o++}},return:function(){i=!0}};a[r]=function(){return this},Array.from(a,(function(){throw 2}))}catch(e){}e.exports=function(e,t){if(!t&&!i)return!1;var n=!1;try{var o={};o[r]=function(){return{next:function(){return{done:n=!0}}}},e(o)}catch(e){}return n}},4326:function(e){var t={}.toString;e.exports=function(e){return t.call(e).slice(8,-1)}},648:function(e,t,n){var r=n(1694),i=n(4326),o=n(5112)("toStringTag"),a="Arguments"==i(function(){return arguments}());e.exports=r?i:function(e){var t,n,r;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(n=function(e,t){try{return e[t]}catch(e){}}(t=Object(e),o))?n:a?i(t):"Object"==(r=i(t))&&"function"==typeof t.callee?"Arguments":r}},9920:function(e,t,n){var r=n(6656),i=n(3887),o=n(1236),a=n(3070);e.exports=function(e,t){for(var n=i(t),u=a.f,s=o.f,l=0;l=74)&&(r=a.match(/Chrome\/(\d+)/))&&(i=r[1]),e.exports=i&&+i},748:function(e){e.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},2109:function(e,t,n){var r=n(7854),i=n(1236).f,o=n(8880),a=n(1320),u=n(3505),s=n(9920),l=n(4705);e.exports=function(e,t){var n,c,f,p,h,d=e.target,v=e.global,y=e.stat;if(n=v?r:y?r[d]||u(d,{}):(r[d]||{}).prototype)for(c in t){if(p=t[c],f=e.noTargetGet?(h=i(n,c))&&h.value:n[c],!l(v?c:d+(y?".":"#")+c,e.forced)&&void 0!==f){if(typeof p==typeof f)continue;s(p,f)}(e.sham||f&&f.sham)&&o(p,"sham",!0),a(n,c,p,e)}}},7293:function(e){e.exports=function(e){try{return!!e()}catch(e){return!0}}},7007:function(e,t,n){"use strict";n(4916);var r=n(1320),i=n(7293),o=n(5112),a=n(2261),u=n(8880),s=o("species"),l=!i((function(){var e=/./;return e.exec=function(){var e=[];return e.groups={a:"7"},e},"7"!=="".replace(e,"$")})),c="$0"==="a".replace(/./,"$0"),f=o("replace"),p=!!/./[f]&&""===/./[f]("a","$0"),h=!i((function(){var e=/(?:)/,t=e.exec;e.exec=function(){return t.apply(this,arguments)};var n="ab".split(e);return 2!==n.length||"a"!==n[0]||"b"!==n[1]}));e.exports=function(e,t,n,f){var d=o(e),v=!i((function(){var t={};return t[d]=function(){return 7},7!=""[e](t)})),y=v&&!i((function(){var t=!1,n=/a/;return"split"===e&&((n={}).constructor={},n.constructor[s]=function(){return n},n.flags="",n[d]=/./[d]),n.exec=function(){return t=!0,null},n[d](""),!t}));if(!v||!y||"replace"===e&&(!l||!c||p)||"split"===e&&!h){var g=/./[d],m=n(d,""[e],(function(e,t,n,r,i){return t.exec===a?v&&!i?{done:!0,value:g.call(t,n,r)}:{done:!0,value:e.call(n,t,r)}:{done:!1}}),{REPLACE_KEEPS_$0:c,REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE:p}),b=m[0],x=m[1];r(String.prototype,e,b),r(RegExp.prototype,d,2==t?function(e,t){return x.call(e,this,t)}:function(e){return x.call(e,this)})}f&&u(RegExp.prototype[d],"sham",!0)}},9974:function(e,t,n){var r=n(3099);e.exports=function(e,t,n){if(r(e),void 0===t)return e;switch(n){case 0:return function(){return e.call(t)};case 1:return function(n){return e.call(t,n)};case 2:return function(n,r){return e.call(t,n,r)};case 3:return function(n,r,i){return e.call(t,n,r,i)}}return function(){return e.apply(t,arguments)}}},5005:function(e,t,n){var r=n(857),i=n(7854),o=function(e){return"function"==typeof e?e:void 0};e.exports=function(e,t){return arguments.length<2?o(r[e])||o(i[e]):r[e]&&r[e][t]||i[e]&&i[e][t]}},1246:function(e,t,n){var r=n(648),i=n(7497),o=n(5112)("iterator");e.exports=function(e){if(null!=e)return e[o]||e["@@iterator"]||i[r(e)]}},8554:function(e,t,n){var r=n(9670),i=n(1246);e.exports=function(e){var t=i(e);if("function"!=typeof t)throw TypeError(String(e)+" is not iterable");return r(t.call(e))}},647:function(e,t,n){var r=n(7908),i=Math.floor,o="".replace,a=/\$([$&'`]|\d\d?|<[^>]*>)/g,u=/\$([$&'`]|\d\d?)/g;e.exports=function(e,t,n,s,l,c){var f=n+e.length,p=s.length,h=u;return void 0!==l&&(l=r(l),h=a),o.call(c,h,(function(r,o){var a;switch(o.charAt(0)){case"$":return"$";case"&":return e;case"`":return t.slice(0,n);case"'":return t.slice(f);case"<":a=l[o.slice(1,-1)];break;default:var u=+o;if(0===u)return r;if(u>p){var c=i(u/10);return 0===c?r:c<=p?void 0===s[c-1]?o.charAt(1):s[c-1]+o.charAt(1):r}a=s[u-1]}return void 0===a?"":a}))}},7854:function(e,t,n){var r=function(e){return e&&e.Math==Math&&e};e.exports=r("object"==typeof globalThis&&globalThis)||r("object"==typeof window&&window)||r("object"==typeof self&&self)||r("object"==typeof n.g&&n.g)||function(){return this}()||Function("return this")()},6656:function(e){var t={}.hasOwnProperty;e.exports=function(e,n){return t.call(e,n)}},3501:function(e){e.exports={}},490:function(e,t,n){var r=n(5005);e.exports=r("document","documentElement")},4664:function(e,t,n){var r=n(9781),i=n(7293),o=n(317);e.exports=!r&&!i((function(){return 7!=Object.defineProperty(o("div"),"a",{get:function(){return 7}}).a}))},1179:function(e){var t=Math.abs,n=Math.pow,r=Math.floor,i=Math.log,o=Math.LN2;e.exports={pack:function(e,a,u){var s,l,c,f=new Array(u),p=8*u-a-1,h=(1<>1,v=23===a?n(2,-24)-n(2,-77):0,y=e<0||0===e&&1/e<0?1:0,g=0;for((e=t(e))!=e||e===1/0?(l=e!=e?1:0,s=h):(s=r(i(e)/o),e*(c=n(2,-s))<1&&(s--,c*=2),(e+=s+d>=1?v/c:v*n(2,1-d))*c>=2&&(s++,c/=2),s+d>=h?(l=0,s=h):s+d>=1?(l=(e*c-1)*n(2,a),s+=d):(l=e*n(2,d-1)*n(2,a),s=0));a>=8;f[g++]=255&l,l/=256,a-=8);for(s=s<0;f[g++]=255&s,s/=256,p-=8);return f[--g]|=128*y,f},unpack:function(e,t){var r,i=e.length,o=8*i-t-1,a=(1<>1,s=o-7,l=i-1,c=e[l--],f=127&c;for(c>>=7;s>0;f=256*f+e[l],l--,s-=8);for(r=f&(1<<-s)-1,f>>=-s,s+=t;s>0;r=256*r+e[l],l--,s-=8);if(0===f)f=1-u;else{if(f===a)return r?NaN:c?-1/0:1/0;r+=n(2,t),f-=u}return(c?-1:1)*r*n(2,f-t)}}},8361:function(e,t,n){var r=n(7293),i=n(4326),o="".split;e.exports=r((function(){return!Object("z").propertyIsEnumerable(0)}))?function(e){return"String"==i(e)?o.call(e,""):Object(e)}:Object},9587:function(e,t,n){var r=n(111),i=n(7674);e.exports=function(e,t,n){var o,a;return i&&"function"==typeof(o=t.constructor)&&o!==n&&r(a=o.prototype)&&a!==n.prototype&&i(e,a),e}},2788:function(e,t,n){var r=n(5465),i=Function.toString;"function"!=typeof r.inspectSource&&(r.inspectSource=function(e){return i.call(e)}),e.exports=r.inspectSource},9909:function(e,t,n){var r,i,o,a=n(8536),u=n(7854),s=n(111),l=n(8880),c=n(6656),f=n(5465),p=n(6200),h=n(3501),d=u.WeakMap;if(a){var v=f.state||(f.state=new d),y=v.get,g=v.has,m=v.set;r=function(e,t){return t.facade=e,m.call(v,e,t),t},i=function(e){return y.call(v,e)||{}},o=function(e){return g.call(v,e)}}else{var b=p("state");h[b]=!0,r=function(e,t){return t.facade=e,l(e,b,t),t},i=function(e){return c(e,b)?e[b]:{}},o=function(e){return c(e,b)}}e.exports={set:r,get:i,has:o,enforce:function(e){return o(e)?i(e):r(e,{})},getterFor:function(e){return function(t){var n;if(!s(t)||(n=i(t)).type!==e)throw TypeError("Incompatible receiver, "+e+" required");return n}}}},7659:function(e,t,n){var r=n(5112),i=n(7497),o=r("iterator"),a=Array.prototype;e.exports=function(e){return void 0!==e&&(i.Array===e||a[o]===e)}},3157:function(e,t,n){var r=n(4326);e.exports=Array.isArray||function(e){return"Array"==r(e)}},4705:function(e,t,n){var r=n(7293),i=/#|\.prototype\./,o=function(e,t){var n=u[a(e)];return n==l||n!=s&&("function"==typeof t?r(t):!!t)},a=o.normalize=function(e){return String(e).replace(i,".").toLowerCase()},u=o.data={},s=o.NATIVE="N",l=o.POLYFILL="P";e.exports=o},111:function(e){e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},1913:function(e){e.exports=!1},7850:function(e,t,n){var r=n(111),i=n(4326),o=n(5112)("match");e.exports=function(e){var t;return r(e)&&(void 0!==(t=e[o])?!!t:"RegExp"==i(e))}},9212:function(e,t,n){var r=n(9670);e.exports=function(e){var t=e.return;if(void 0!==t)return r(t.call(e)).value}},3383:function(e,t,n){"use strict";var r,i,o,a=n(7293),u=n(9518),s=n(8880),l=n(6656),c=n(5112),f=n(1913),p=c("iterator"),h=!1;[].keys&&("next"in(o=[].keys())?(i=u(u(o)))!==Object.prototype&&(r=i):h=!0);var d=null==r||a((function(){var e={};return r[p].call(e)!==e}));d&&(r={}),f&&!d||l(r,p)||s(r,p,(function(){return this})),e.exports={IteratorPrototype:r,BUGGY_SAFARI_ITERATORS:h}},7497:function(e){e.exports={}},133:function(e,t,n){var r=n(7293);e.exports=!!Object.getOwnPropertySymbols&&!r((function(){return!String(Symbol())}))},590:function(e,t,n){var r=n(7293),i=n(5112),o=n(1913),a=i("iterator");e.exports=!r((function(){var e=new URL("b?a=1&b=2&c=3","http://a"),t=e.searchParams,n="";return e.pathname="c%20d",t.forEach((function(e,r){t.delete("b"),n+=r+e})),o&&!e.toJSON||!t.sort||"http://a/c%20d?a=1&c=3"!==e.href||"3"!==t.get("c")||"a=1"!==String(new URLSearchParams("?a=1"))||!t[a]||"a"!==new URL("https://a@b").username||"b"!==new URLSearchParams(new URLSearchParams("a=b")).get("a")||"xn--e1aybc"!==new URL("http://тест").host||"#%D0%B1"!==new URL("http://a#б").hash||"a1c3"!==n||"x"!==new URL("http://x",void 0).host}))},8536:function(e,t,n){var r=n(7854),i=n(2788),o=r.WeakMap;e.exports="function"==typeof o&&/native code/.test(i(o))},1574:function(e,t,n){"use strict";var r=n(9781),i=n(7293),o=n(1956),a=n(5181),u=n(5296),s=n(7908),l=n(8361),c=Object.assign,f=Object.defineProperty;e.exports=!c||i((function(){if(r&&1!==c({b:1},c(f({},"a",{enumerable:!0,get:function(){f(this,"b",{value:3,enumerable:!1})}}),{b:2})).b)return!0;var e={},t={},n=Symbol(),i="abcdefghijklmnopqrst";return e[n]=7,i.split("").forEach((function(e){t[e]=e})),7!=c({},e)[n]||o(c({},t)).join("")!=i}))?function(e,t){for(var n=s(e),i=arguments.length,c=1,f=a.f,p=u.f;i>c;)for(var h,d=l(arguments[c++]),v=f?o(d).concat(f(d)):o(d),y=v.length,g=0;y>g;)h=v[g++],r&&!p.call(d,h)||(n[h]=d[h]);return n}:c},30:function(e,t,n){var r,i=n(9670),o=n(6048),a=n(748),u=n(3501),s=n(490),l=n(317),c=n(6200)("IE_PROTO"),f=function(){},p=function(e){return" + +{{ s3_form_values_answer_image|json_script:"s3-form-values-question-image" }} +{{ s3_upload_config_answer_image|json_script:"s3-upload-config-question-image" }} + {% endblock %} diff --git a/www/questions/views.py b/www/questions/views.py index 3c3889cf9..301d1b527 100644 --- a/www/questions/views.py +++ b/www/questions/views.py @@ -14,6 +14,7 @@ from contributions.tables import ContributionTable from core.forms import form_filters_cleaned_dict, form_filters_to_list from core.mixins import ContributorUserRequiredMixin +from core.utils.s3 import S3Upload from history.utilities import get_diff_between_two_history_records from questions.filters import QuestionFilter from questions.forms import QUESTION_FORM_FIELDS, QuestionCreateForm, QuestionEditForm @@ -78,6 +79,11 @@ def get_form(self, *args, **kwargs): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) question = self.get_object() + # S3 Upload form + s3_upload = S3Upload(kind="question_answer_image") + context["s3_form_values_answer_image"] = s3_upload.form_values + context["s3_upload_config_answer_image"] = s3_upload.config + # User can edit? context["user_can_edit_question"] = self.request.user.can_edit_question(question) return context From e4d584bea836bd6015095e9df800a8e579eb65a2 Mon Sep 17 00:00:00 2001 From: Raphael Odini Date: Fri, 3 Jun 2022 14:35:16 +0200 Subject: [PATCH 5/6] S3 upload workflow for Quiz image --- core/static/js/s3_upload.js | 2 +- questions/forms.py | 1 + quizs/forms.py | 5 ++- templates/questions/detail_edit.html | 17 ++++++---- templates/quizs/detail_edit.html | 49 +++++++++++++++++++++++++++- www/questions/views.py | 6 ++-- www/quizs/views.py | 6 ++++ 7 files changed, 73 insertions(+), 13 deletions(-) diff --git a/core/static/js/s3_upload.js b/core/static/js/s3_upload.js index 7054d38cd..250b554a8 100644 --- a/core/static/js/s3_upload.js +++ b/core/static/js/s3_upload.js @@ -17,7 +17,7 @@ const extensionToContentTypeMapping = { // - s3UploadConfigId: ID of DOM element that contains the JSON values of the S3 config (eg. max file size, timeout, etc). window.s3UploadInit = function s3UploadInit({ - dropzoneSelector = "#answer_image_form", + dropzoneSelector = "#image_form", callbackLocationSelector = "", s3FormValuesId = "s3-form-values", s3UploadConfigId = "s3-upload-config", diff --git a/questions/forms.py b/questions/forms.py index c44ef8301..9d5200caa 100644 --- a/questions/forms.py +++ b/questions/forms.py @@ -29,6 +29,7 @@ class Meta: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["tags"].queryset = Tag.objects.all().order_by("name") + self.fields["answer_image_url"].label = "Image pour illustrer la réponse" for field_name in QUESTION_READONLY_FORM_FIELDS: self.fields[field_name].disabled = True for field_name in QUESTION_REQUIRED_FORM_FIELDS: diff --git a/quizs/forms.py b/quizs/forms.py index f3ea04c45..41dfff612 100644 --- a/quizs/forms.py +++ b/quizs/forms.py @@ -22,11 +22,14 @@ class QuizCreateForm(forms.ModelForm): class Meta: model = Quiz fields = QUIZ_CREATE_FORM_FIELDS + QUIZ_READONLY_FORM_FIELDS - widgets = {} + widgets = { + "image_background_url": forms.HiddenInput(), + } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["tags"].queryset = Tag.objects.all().order_by("name") + self.fields["image_background_url"].label = "Image pour illustrer le quiz" for field_name in QUIZ_READONLY_FORM_FIELDS: self.fields[field_name].disabled = True diff --git a/templates/questions/detail_edit.html b/templates/questions/detail_edit.html index acbacaa3c..f1cbdd4d5 100644 --- a/templates/questions/detail_edit.html +++ b/templates/questions/detail_edit.html @@ -1,6 +1,5 @@ {% extends "questions/detail_base.html" %} -{% load static %} -{% load django_bootstrap5 %} +{% load static django_bootstrap5 %} {% block extra_css %} @@ -17,14 +16,18 @@
{% bootstrap_form form alert_error_type="all" %}
+
+
L'ajout de l'image d'explication se fait à la fin du formulaire 👇
+
+
{# {{ form.answer_image_url.as_hidden }} #} - - {% include "includes/_s3_upload_form.html" with dropzone_form_id="answer_image_form" %} + + {% include "includes/_s3_upload_form.html" with dropzone_form_id="image_form" %}
@@ -69,12 +72,12 @@ {{ form.media.js }} -{{ s3_form_values_answer_image|json_script:"s3-form-values-question-image" }} -{{ s3_upload_config_answer_image|json_script:"s3-upload-config-question-image" }} +{{ s3_form_values|json_script:"s3-form-values-question-image" }} +{{ s3_upload_config|json_script:"s3-upload-config-question-image" }} + +{{ s3_form_values|json_script:"s3-form-values-quiz-image" }} +{{ s3_upload_config|json_script:"s3-upload-config-quiz-image" }} + {% endblock %} diff --git a/www/questions/views.py b/www/questions/views.py index 301d1b527..fbee4492b 100644 --- a/www/questions/views.py +++ b/www/questions/views.py @@ -81,9 +81,9 @@ def get_context_data(self, **kwargs): question = self.get_object() # S3 Upload form s3_upload = S3Upload(kind="question_answer_image") - context["s3_form_values_answer_image"] = s3_upload.form_values - context["s3_upload_config_answer_image"] = s3_upload.config - # User can edit? + context["s3_form_values"] = s3_upload.form_values + context["s3_upload_config"] = s3_upload.config + # User authorizations context["user_can_edit_question"] = self.request.user.can_edit_question(question) return context diff --git a/www/quizs/views.py b/www/quizs/views.py index d6499c0bb..c1233fe4b 100644 --- a/www/quizs/views.py +++ b/www/quizs/views.py @@ -13,6 +13,7 @@ from contributions.tables import ContributionTable from core.forms import form_filters_cleaned_dict, form_filters_to_list from core.mixins import ContributorUserRequiredMixin +from core.utils.s3 import S3Upload from history.utilities import get_diff_between_two_history_records from quizs.filters import QuizFilter from quizs.forms import QUIZ_FORM_FIELDS, QuizCreateForm, QuizEditForm, QuizQuestionFormSet @@ -78,6 +79,11 @@ def get_form(self, *args, **kwargs): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) quiz = self.get_object() + # S3 Upload form + s3_upload = S3Upload(kind="quiz_image_background") + context["s3_form_values"] = s3_upload.form_values + context["s3_upload_config"] = s3_upload.config + # User authorizations context["user_can_edit_quiz"] = self.request.user.can_edit_quiz(quiz) return context From 4047bd168d038525e094b753be22d450675d4c5e Mon Sep 17 00:00:00 2001 From: Raphael Odini Date: Fri, 3 Jun 2022 14:44:01 +0200 Subject: [PATCH 6/6] Limit image size. Link to tinypng --- app/settings.py | 4 ++-- core/static/js/s3_upload.js | 2 +- templates/questions/detail_edit.html | 3 ++- templates/quizs/detail_edit.html | 3 ++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/settings.py b/app/settings.py index 7fd3dfb15..2810007ca 100644 --- a/app/settings.py +++ b/app/settings.py @@ -276,8 +276,8 @@ "allowed_mime_types": ["image/png", "image/svg+xml", "image/gif", "image/jpg", "image/jpeg"], # ["image/*"] ? "upload_expiration": 60 * 60, # in seconds "key_path": "default", # appended before the file key. No backslash! - "max_files": 1, # 3, - "max_file_size": 5, # in mb + "max_files": 1, + "max_file_size": 2, # in mb "timeout": 20000, # in ms }, "question_answer_image": { diff --git a/core/static/js/s3_upload.js b/core/static/js/s3_upload.js index 250b554a8..bfc25f82d 100644 --- a/core/static/js/s3_upload.js +++ b/core/static/js/s3_upload.js @@ -67,7 +67,7 @@ window.s3UploadInit = function s3UploadInit({ // the function will be used to rename the file.name before appending it to the formData renameFile: function (file) { const extension = file.name.split(".").pop(); - const filename = Dropzone.uuidv4(); + const filename = Dropzone.uuidv4().substring(0, 8); const fileKey = `${keyPath}/${filename}.${extension}`; // Add a file key to options params so that it's sent as an input field on POST. this.params["key"] = fileKey; diff --git a/templates/questions/detail_edit.html b/templates/questions/detail_edit.html index f1cbdd4d5..220cb2076 100644 --- a/templates/questions/detail_edit.html +++ b/templates/questions/detail_edit.html @@ -29,10 +29,11 @@ {% include "includes/_s3_upload_form.html" with dropzone_form_id="image_form" %}
+ Image trop grosse ? outil pour réduire sa taille :
tinypng.com
- + {% if form.answer_image_url.value %}
diff --git a/templates/quizs/detail_edit.html b/templates/quizs/detail_edit.html index d9bf603c5..664de1f0f 100644 --- a/templates/quizs/detail_edit.html +++ b/templates/quizs/detail_edit.html @@ -31,10 +31,11 @@ {% include "includes/_s3_upload_form.html" with dropzone_form_id="image_form" %}
+ Image trop grosse ? outil pour réduire sa taille : tinypng.com
- + {% if form.image_background_url.value %}