├── s3upload ├── __init__.py ├── src │ ├── .babelrc │ ├── app │ │ ├── reducers │ │ │ ├── index.js │ │ │ ├── awsUploadsParams.js │ │ │ └── appStatus.js │ │ ├── store │ │ │ ├── index.js │ │ │ └── connect.js │ │ ├── index.js │ │ ├── constants │ │ │ └── index.js │ │ ├── utils │ │ │ └── index.js │ │ ├── components │ │ │ └── index.js │ │ └── actions │ │ │ └── index.js │ └── package.json ├── urls.py ├── apps.py ├── templates │ └── s3upload │ │ └── s3upload-widget.tpl ├── static │ └── s3upload │ │ ├── css │ │ ├── styles.css │ │ └── bootstrap-progress.min.css │ │ └── js │ │ ├── django-s3-uploads.min.js │ │ └── django-s3-uploads.js ├── fields.py ├── views.py ├── widgets.py └── utils.py ├── example ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── __init__.py ├── apps.py ├── admin.py ├── urls.py ├── templates │ ├── form.html │ └── async_form.html ├── views.py ├── models.py └── forms.py ├── pytest.ini ├── poetry.toml ├── screenshot.png ├── .editorconfig ├── tests ├── urls.py ├── test_utils.py ├── settings.py └── test_widgets.py ├── .gitignore ├── manage.py ├── .prettierrc ├── mypy.ini ├── .pre-commit-config.yaml ├── LICENSE ├── tox.ini ├── pyproject.toml ├── .github └── workflows │ └── tox.yml ├── .ruff.toml └── README.md /s3upload/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /s3upload/src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = tests.settings 3 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | create = true 3 | in-project = true 4 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = "example.apps.ExampleAppConfig" 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunojuno/django-s3-upload/HEAD/screenshot.png -------------------------------------------------------------------------------- /s3upload/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import get_upload_params 4 | 5 | urlpatterns = [path("get_upload_params/", get_upload_params, name="s3upload")] 6 | -------------------------------------------------------------------------------- /example/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ExampleAppConfig(AppConfig): 5 | name = "example" 6 | verbose_name = "Sample Cats app " 7 | default_auto_field = "django.db.models.AutoField" 8 | -------------------------------------------------------------------------------- /s3upload/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class S3UploadAppConfig(AppConfig): 5 | name = "s3upload" 6 | verbose_name = "S3 Uploads" 7 | default_auto_field = "django.db.models.AutoField" 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path 3 | 4 | admin.autodiscover() 5 | 6 | urlpatterns = [ 7 | path("admin/", admin.site.urls), 8 | path("s3upload/", include("s3upload.urls")), 9 | ] 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | .idea 3 | .tox 4 | *.DS_Store 5 | *.egg-info 6 | *.pyc 7 | .eggs 8 | build 9 | dist 10 | *.sqlite3 11 | example/example/settings-test.py 12 | example/static/ 13 | poetry.lock 14 | s3upload/src/node_modules/ 15 | tests/static 16 | venv 17 | venv3 18 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "proseWrap": "always", 11 | "endOfLine": "auto" 12 | } 13 | -------------------------------------------------------------------------------- /example/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Cat, Kitten 4 | 5 | 6 | class KittenAdminInline(admin.StackedInline): 7 | model = Kitten 8 | extra = 1 9 | 10 | 11 | class CatAdmin(admin.ModelAdmin): 12 | inlines = [KittenAdminInline] 13 | 14 | 15 | admin.site.register(Cat, CatAdmin) 16 | -------------------------------------------------------------------------------- /example/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import AsyncForm, MultiForm, MyView 4 | 5 | app_name = "cat" 6 | 7 | urlpatterns = [ 8 | path("async/", AsyncForm.as_view(), name="async_form"), 9 | path("multi/", MultiForm.as_view(), name="multi_form"), 10 | path("", MyView.as_view(), name="form"), 11 | ] 12 | -------------------------------------------------------------------------------- /example/templates/form.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |"]
6 | license = "MIT"
7 | maintainers = ["YunoJuno "]
8 | readme = "README.md"
9 | homepage = "https://github.com/yunojuno/django-s3-upload"
10 | repository = "https://github.com/yunojuno/django-s3-upload"
11 | documentation = "https://github.com/yunojuno/django-s3-upload"
12 | classifiers = [
13 | "Environment :: Web Environment",
14 | "Framework :: Django",
15 | "Framework :: Django :: 4.2",
16 | "Framework :: Django :: 5.0",
17 | "Framework :: Django :: 5.1",
18 | "Framework :: Django :: 5.2",
19 | "License :: OSI Approved :: MIT License",
20 | "Operating System :: OS Independent",
21 | "Programming Language :: Python :: 3 :: Only",
22 | "Programming Language :: Python :: 3.10",
23 | "Programming Language :: Python :: 3.11",
24 | "Programming Language :: Python :: 3.12",
25 | ]
26 | packages = [{ include = "s3upload" }]
27 |
28 | [tool.poetry.dependencies]
29 | python = "^3.10"
30 | django = "^4.2 || ^5.0"
31 | boto3 = "^1.14"
32 |
33 | [tool.poetry.group.test.dependencies]
34 | coverage = "*"
35 | pytest = "*"
36 | pytest-cov = "*"
37 | pytest-django = "*"
38 | tox = "*"
39 |
40 | [tool.poetry.group.dev.dependencies]
41 | coverage = "*"
42 | mypy = "*"
43 | pre-commit = "*"
44 | ruff = "*"
45 |
46 | [build-system]
47 | requires = ["poetry>=0.12"]
48 | build-backend = "poetry.masonry.api"
49 |
--------------------------------------------------------------------------------
/s3upload/fields.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import Any
4 |
5 | from django.conf import settings
6 | from django.db.models import Field, Model
7 |
8 | from .utils import get_s3_path_from_url
9 | from .widgets import S3UploadWidget
10 |
11 |
12 | class S3UploadField(Field):
13 | def __init__(self, dest: str | None = None, *args: Any, **kwargs: Any) -> None:
14 | if not dest:
15 | raise ValueError("S3UploadField must be initialised with a destination")
16 | self.dest = dest
17 | self.widget = S3UploadWidget(self.dest)
18 | super(S3UploadField, self).__init__(*args, **kwargs)
19 |
20 | def deconstruct(self) -> tuple[str, str, Any, Any]:
21 | name, path, args, kwargs = super(S3UploadField, self).deconstruct()
22 | kwargs["dest"] = self.dest
23 | return name, path, args, kwargs
24 |
25 | def get_internal_type(self) -> str:
26 | return "TextField"
27 |
28 | def formfield(self, *args: Any, **kwargs: Any) -> Any:
29 | kwargs["widget"] = self.widget
30 | return super(S3UploadField, self).formfield(*args, **kwargs)
31 |
32 | def pre_save(self, model_instance: Model, add: bool) -> str:
33 | file_url = getattr(model_instance, self.attname)
34 |
35 | if file_url:
36 | setattr(model_instance, self.attname, file_url)
37 | bucket_name = settings.S3UPLOAD_DESTINATIONS[self.dest].get(
38 | "bucket", settings.AWS_STORAGE_BUCKET_NAME
39 | )
40 | return get_s3_path_from_url(file_url, bucket_name=bucket_name)
41 |
42 | return file_url
43 |
--------------------------------------------------------------------------------
/s3upload/src/app/reducers/appStatus.js:
--------------------------------------------------------------------------------
1 | import constants from '../constants';
2 |
3 | export default (state = {}, action) => {
4 | switch (action.type) {
5 | case constants.BEGIN_UPLOAD_TO_AWS:
6 | return Object.assign({}, state, {
7 | isUploading: true
8 | });
9 | case constants.COMPLETE_UPLOAD_TO_AWS:
10 | return Object.assign({}, state, {
11 | isUploading: false,
12 | uploadProgress: 0,
13 | filename: action.filename,
14 | url: action.url
15 | });
16 | case constants.DID_NOT_COMPLETE_UPLOAD_TO_AWS:
17 | return Object.assign({}, state, {
18 | isUploading: false
19 | });
20 | case constants.REMOVE_UPLOAD:
21 | return Object.assign({}, state, {
22 | filename: null,
23 | url: null
24 | });
25 | case constants.ADD_ERROR:
26 | return Object.assign({}, state, {
27 | error: action.error
28 | });
29 | case constants.CLEAR_ERRORS:
30 | return Object.assign({}, state, {
31 | error: null
32 | });
33 | case constants.UPDATE_PROGRESS:
34 | return Object.assign({}, state, {
35 | uploadProgress: action.progress
36 | });
37 | case constants.RECEIVE_SIGNED_URL: {
38 | return Object.assign({}, state, {
39 | signedURL: action.signedURL
40 | });
41 | }
42 |
43 | default:
44 | return state;
45 | }
46 | }
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from s3upload.utils import get_bucket_endpoint_url, get_s3_path_from_url
4 |
5 | TEST_BUCKET = "test-bucket-name"
6 | TEST_KEY = "folder1/folder2/file1.json"
7 | TEST_REGION = "test-region"
8 |
9 |
10 | @pytest.mark.parametrize(
11 | "url",
12 | [
13 | f"s3://{TEST_BUCKET}/{TEST_KEY}",
14 | f"https://{TEST_BUCKET}.s3.aws-region.amazonaws.com/{TEST_KEY}?test=1&test1=2",
15 | f"https://{TEST_BUCKET}.s3.amazonaws.com:443/{TEST_KEY}",
16 | f"https://s3-aws-region.amazonaws.com/{TEST_BUCKET}/{TEST_KEY}",
17 | f"https://s3-aws-region.amazonaws.com:443/{TEST_BUCKET}/{TEST_KEY}",
18 | f"https%3a%2f%2fs3-aws-region.amazonaws.com%3a443%2f{TEST_BUCKET}%2ffolder1%2ffolder2%2ffile1.json",
19 | ],
20 | )
21 | def test_get_s3_path_from_url(url: str) -> None:
22 | assert get_s3_path_from_url(url, bucket_name=TEST_BUCKET) == TEST_KEY
23 |
24 |
25 | @pytest.mark.parametrize(
26 | "url,expected",
27 | [
28 | (None, f"https://{TEST_BUCKET}.s3.{TEST_REGION}.amazonaws.com"),
29 | (
30 | "https://{bucket}.s3.{region}.amazonaws.com",
31 | f"https://{TEST_BUCKET}.s3.{TEST_REGION}.amazonaws.com",
32 | ),
33 | (
34 | "https://s3.{region}.amazonaws.com",
35 | f"https://s3.{TEST_REGION}.amazonaws.com",
36 | ),
37 | (
38 | "https://{bucket}.s3.amazonaws.com",
39 | f"https://{TEST_BUCKET}.s3.amazonaws.com",
40 | ),
41 | ],
42 | )
43 | def test_get_bucket_endpoint_url(url: str, expected: str) -> None:
44 | kwargs = {}
45 | if url:
46 | kwargs["default"] = url
47 | assert (
48 | get_bucket_endpoint_url(bucket_name=TEST_BUCKET, region=TEST_REGION, **kwargs)
49 | == expected
50 | )
51 |
--------------------------------------------------------------------------------
/example/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1 on 2020-08-25 12:15
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 | import s3upload.fields
7 |
8 |
9 | class Migration(migrations.Migration):
10 | initial = True
11 |
12 | dependencies = []
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name="Cat",
17 | fields=[
18 | (
19 | "id",
20 | models.AutoField(
21 | auto_created=True,
22 | primary_key=True,
23 | serialize=False,
24 | verbose_name="ID",
25 | ),
26 | ),
27 | (
28 | "custom_filename",
29 | s3upload.fields.S3UploadField(blank=True, dest="custom_filename"),
30 | ),
31 | ],
32 | ),
33 | migrations.CreateModel(
34 | name="Kitten",
35 | fields=[
36 | (
37 | "id",
38 | models.AutoField(
39 | auto_created=True,
40 | primary_key=True,
41 | serialize=False,
42 | verbose_name="ID",
43 | ),
44 | ),
45 | ("video", s3upload.fields.S3UploadField(blank=True, dest="vids")),
46 | ("image", s3upload.fields.S3UploadField(blank=True, dest="imgs")),
47 | ("pdf", s3upload.fields.S3UploadField(blank=True, dest="files")),
48 | (
49 | "mother",
50 | models.ForeignKey(
51 | on_delete=django.db.models.deletion.CASCADE, to="example.cat"
52 | ),
53 | ),
54 | ],
55 | ),
56 | ]
57 |
--------------------------------------------------------------------------------
/s3upload/src/app/utils/index.js:
--------------------------------------------------------------------------------
1 | import {i18n_strings} from '../constants';
2 |
3 | export const getCookie = function(name) {
4 | var value = '; ' + document.cookie,
5 | parts = value.split('; ' + name + '=');
6 | if (parts.length == 2) return parts.pop().split(';').shift();
7 | }
8 |
9 | export const request = function(method, url, data, headers, onProgress, onLoad, onError) {
10 | var request = new XMLHttpRequest();
11 | request.open(method, url, true);
12 |
13 | Object.keys(headers).forEach(function(key){
14 | request.setRequestHeader(key, headers[key]);
15 | });
16 |
17 | request.onload = function() {
18 | onLoad(request.status, request.responseText);
19 | }
20 |
21 | if (onError) {
22 | request.onerror = request.onabort = function() {
23 | onError(request.status, request.responseText);
24 | }
25 | }
26 |
27 | if (onProgress) {
28 | request.upload.onprogress = function(data) {
29 | onProgress(data);
30 | }
31 | }
32 |
33 | request.send(data);
34 | }
35 |
36 | export const parseURL = function(text) {
37 | var xml = new DOMParser().parseFromString(text, 'text/xml'),
38 | tag = xml.getElementsByTagName('Location')[0],
39 | url = decodeURIComponent(tag.childNodes[0].nodeValue);
40 |
41 | return url;
42 | }
43 |
44 | export const parseNameFromUrl = function(url) {
45 | return decodeURIComponent((url + '').replace(/\+/g, '%20'));
46 | }
47 |
48 | export const parseJson = function(json) {
49 | var data;
50 |
51 | try {
52 | data = JSON.parse(json);
53 | }
54 | catch(error) {
55 | data = null;
56 | };
57 |
58 | return data;
59 | }
60 |
61 | export const raiseEvent = function(element, name, detail) {
62 | if (window.CustomEvent) {
63 | var event = new CustomEvent(name, {detail, bubbles: true});
64 | element.dispatchEvent(event);
65 | }
66 | }
67 |
68 | export const observeStore = function(store, select, onChange) {
69 | let currentState;
70 |
71 | function handleChange() {
72 | let nextState = select(store.getState());
73 |
74 | if (nextState !== currentState) {
75 | currentState = nextState;
76 | onChange(currentState);
77 | }
78 | }
79 |
80 | let unsubscribe = store.subscribe(handleChange);
81 | handleChange();
82 | return unsubscribe;
83 | }
--------------------------------------------------------------------------------
/.github/workflows/tox.yml:
--------------------------------------------------------------------------------
1 | name: Python / Django
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | pull_request:
9 | types: [opened, synchronize, reopened]
10 |
11 | jobs:
12 | format:
13 | name: Check formatting
14 | runs-on: ubuntu-latest
15 | strategy:
16 | matrix:
17 | toxenv: [fmt, lint, mypy]
18 | env:
19 | TOXENV: ${{ matrix.toxenv }}
20 |
21 | steps:
22 | - name: Check out the repository
23 | uses: actions/checkout@v4
24 |
25 | - name: Set up Python (3.12)
26 | uses: actions/setup-python@v4
27 | with:
28 | python-version: "3.12"
29 |
30 | - name: Install and run tox
31 | run: |
32 | pip install tox
33 | tox
34 |
35 | checks:
36 | name: Run Django checks
37 | runs-on: ubuntu-latest
38 | strategy:
39 | matrix:
40 | toxenv: ["django-checks"]
41 | env:
42 | TOXENV: ${{ matrix.toxenv }}
43 |
44 | steps:
45 | - name: Check out the repository
46 | uses: actions/checkout@v4
47 |
48 | - name: Set up Python (3.12)
49 | uses: actions/setup-python@v4
50 | with:
51 | python-version: "3.12"
52 |
53 | - name: Install and run tox
54 | run: |
55 | pip install tox
56 | tox
57 |
58 | test:
59 | name: Run tests
60 | runs-on: ubuntu-latest
61 | strategy:
62 | matrix:
63 | python: ["3.10", "3.11", "3.12"]
64 | # build LTS version, next version, HEAD
65 | django: ["42", "50", "51", "52", "main"]
66 | exclude:
67 | - python: "3.10"
68 | django: "main"
69 | - python: "3.11"
70 | django: "main"
71 |
72 | env:
73 | TOXENV: django${{ matrix.django }}-py${{ matrix.python }}
74 |
75 | steps:
76 | - name: Check out the repository
77 | uses: actions/checkout@v4
78 |
79 | - name: Set up Python ${{ matrix.python }}
80 | uses: actions/setup-python@v4
81 | with:
82 | python-version: ${{ matrix.python }}
83 |
84 | - name: Install and run tox
85 | run: |
86 | pip install tox
87 | tox
88 |
--------------------------------------------------------------------------------
/.ruff.toml:
--------------------------------------------------------------------------------
1 | line-length = 88
2 |
3 | [lint]
4 | ignore = [
5 | "D100", # Missing docstring in public module
6 | "D101", # Missing docstring in public class
7 | "D102", # Missing docstring in public method
8 | "D103", # Missing docstring in public function
9 | "D104", # Missing docstring in public package
10 | "D105", # Missing docstring in magic method
11 | "D106", # Missing docstring in public nested class
12 | "D107", # Missing docstring in __init__
13 | "D203", # 1 blank line required before class docstring
14 | "D212", # Multi-line docstring summary should start at the first line
15 | "D213", # Multi-line docstring summary should start at the second line
16 | "D404", # First word of the docstring should not be "This"
17 | "D405", # Section name should be properly capitalized
18 | "D406", # Section name should end with a newline
19 | "D407", # Missing dashed underline after section
20 | "D410", # Missing blank line after section
21 | "D411", # Missing blank line before section
22 | "D412", # No blank lines allowed between a section header and its content
23 | "D416", # Section name should end with a colon
24 | "D417",
25 | "D417", # Missing argument description in the docstring
26 | ]
27 | select = [
28 | "A", # flake8 builtins
29 | "C9", # mcabe
30 | "D", # pydocstyle
31 | "E", # pycodestyle (errors)
32 | "F", # Pyflakes
33 | "I", # isort
34 | "S", # flake8-bandit
35 | "T2", # flake8-print
36 | "W", # pycodestype (warnings)
37 | ]
38 |
39 | [lint.isort]
40 | combine-as-imports = true
41 |
42 | [lint.mccabe]
43 | max-complexity = 8
44 |
45 | [lint.per-file-ignores]
46 | "*tests/*" = [
47 | "D205", # 1 blank line required between summary line and description
48 | "D400", # First line should end with a period
49 | "D401", # First line should be in imperative mood
50 | "D415", # First line should end with a period, question mark, or exclamation point
51 | "E501", # Line too long
52 | "E731", # Do not assign a lambda expression, use a def
53 | "S101", # Use of assert detected
54 | "S105", # Possible hardcoded password
55 | "S106", # Possible hardcoded password
56 | "S113", # Probable use of requests call with timeout set to {value}
57 | ]
58 | "*/migrations/*" = [
59 | "E501", # Line too long
60 | ]
61 | "*/settings.py" = [
62 | "F403", # from {name} import * used; unable to detect undefined names
63 | "F405", # {name} may be undefined, or defined from star imports:
64 | ]
65 | "*/settings/*" = [
66 | "F403", # from {name} import * used; unable to detect undefined names
67 | "F405", # {name} may be undefined, or defined from star imports:
68 | ]
69 |
--------------------------------------------------------------------------------
/s3upload/views.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from os.path import splitext
4 |
5 | from django.conf import settings
6 | from django.http import HttpRequest, JsonResponse
7 | from django.utils.text import get_valid_filename
8 | from django.views.decorators.http import require_POST
9 |
10 | from .utils import create_upload_data, get_signed_download_url
11 |
12 |
13 | @require_POST
14 | def get_upload_params(request: HttpRequest) -> JsonResponse: # noqa: C901
15 | content_type = request.POST["type"]
16 | filename = get_valid_filename(request.POST["name"])
17 | dest = settings.S3UPLOAD_DESTINATIONS[request.POST["dest"]]
18 |
19 | if not dest:
20 | return JsonResponse({"error": "File destination does not exist."}, status=400)
21 |
22 | key = dest.get("key")
23 | auth = dest.get("auth")
24 | allowed_types = dest.get("allowed_types")
25 | acl = dest.get("acl")
26 | bucket = dest.get("bucket")
27 | cache_control = dest.get("cache_control")
28 | content_disposition = dest.get("content_disposition")
29 | content_length_range = dest.get("content_length_range")
30 | allowed_extensions = dest.get("allowed_extensions")
31 | server_side_encryption = dest.get("server_side_encryption")
32 |
33 | if not acl:
34 | acl = "public-read"
35 |
36 | if not key:
37 | return JsonResponse({"error": "Missing destination path."}, status=403)
38 |
39 | if auth and not auth(request.user):
40 | return JsonResponse({"error": "Permission denied."}, status=403)
41 |
42 | if (allowed_types and content_type not in allowed_types) and allowed_types != "*":
43 | return JsonResponse(
44 | {"error": "Invalid file type (%s)." % content_type}, status=400
45 | )
46 |
47 | original_ext = splitext(filename)[1]
48 | lowercased_ext = original_ext.lower()
49 | if (
50 | allowed_extensions and lowercased_ext not in allowed_extensions
51 | ) and allowed_extensions != "*":
52 | return JsonResponse(
53 | {"error": "Forbidden file extension (%s)." % original_ext}, status=415
54 | )
55 |
56 | if hasattr(key, "__call__"):
57 | key = key(filename)
58 | elif key == "/":
59 | key = filename
60 | else:
61 | key = "{}/{}".format(key, filename)
62 |
63 | aws_payload = create_upload_data(
64 | content_type=content_type,
65 | key=key,
66 | acl=acl,
67 | bucket=bucket,
68 | cache_control=cache_control,
69 | content_disposition=content_disposition,
70 | content_length_range=content_length_range,
71 | server_side_encryption=server_side_encryption,
72 | )
73 |
74 | url = None
75 |
76 | # Generate signed URL for private document access
77 | if acl == "private":
78 | url = get_signed_download_url(
79 | key=key.replace("${filename}", filename),
80 | bucket_name=bucket,
81 | ttl=int(5 * 60), # 5 mins
82 | )
83 |
84 | data = {
85 | "aws_payload": aws_payload,
86 | "private_access_url": url,
87 | }
88 |
89 | return JsonResponse(data, content_type="application/json")
90 |
--------------------------------------------------------------------------------
/s3upload/widgets.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import os
4 | from typing import Any
5 | from urllib.parse import unquote_plus as urlunquote_plus
6 |
7 | from django.conf import settings
8 | from django.core.exceptions import ImproperlyConfigured
9 | from django.forms import widgets
10 | from django.template.loader import render_to_string
11 | from django.urls import reverse
12 | from django.utils.safestring import mark_safe
13 |
14 | from .utils import (
15 | get_bucket_endpoint_url,
16 | get_s3_path_from_url,
17 | get_signed_download_url,
18 | )
19 |
20 |
21 | class S3UploadWidget(widgets.TextInput):
22 | class Media:
23 | js = ("s3upload/js/django-s3-uploads.min.js",)
24 | css = {
25 | "all": (
26 | "s3upload/css/bootstrap-progress.min.css",
27 | "s3upload/css/styles.css",
28 | )
29 | }
30 |
31 | def __init__(self, dest: str, **kwargs: Any) -> None:
32 | if not dest:
33 | raise ValueError("S3UploadWidget must be initialised with a destination")
34 | if dest not in settings.S3UPLOAD_DESTINATIONS:
35 | raise ImproperlyConfigured(
36 | "S3UploadWidget destination '%s' is not configured. "
37 | "Please check settings.S3UPLOAD_DESTINATIONS." % dest
38 | )
39 | self.acl = settings.S3UPLOAD_DESTINATIONS[dest].get("acl", "public-read")
40 | self.dest = dest
41 | super(S3UploadWidget, self).__init__(**kwargs)
42 |
43 | def get_file_url(self, value: str) -> str:
44 | if value:
45 | bucket_name = settings.S3UPLOAD_DESTINATIONS[self.dest].get("bucket")
46 | if self.acl == "private":
47 | return get_signed_download_url(value, bucket_name)
48 | else:
49 | # Default to virtual-hosted–style URL
50 | bucket_url = get_bucket_endpoint_url(bucket_name)
51 | return "{}/{}".format(bucket_url, value)
52 | else:
53 | return ""
54 |
55 | def get_attr(
56 | self, attrs: dict[str, Any] | None, key: str, default: str = ""
57 | ) -> str:
58 | return self.build_attrs(attrs).get(key, default) if attrs else default
59 |
60 | def render(
61 | self, name: str, value: str, attrs: dict[str, Any] | None = None, **kwargs: Any
62 | ) -> str:
63 | path = get_s3_path_from_url(value) if value else ""
64 | file_name = os.path.basename(urlunquote_plus(path))
65 | tpl = os.path.join("s3upload", "s3upload-widget.tpl")
66 | output = render_to_string(
67 | tpl,
68 | {
69 | "policy_url": reverse("s3upload"),
70 | "element_id": self.get_attr(attrs, "id"),
71 | "file_name": file_name,
72 | "dest": self.dest,
73 | "file_url": self.get_file_url(path),
74 | "name": name,
75 | "style": self.get_attr(attrs, "style"),
76 | },
77 | )
78 | # TODO: review use of mark_safe - will the template render
79 | # cover cases of bad filenames?
80 | return mark_safe(output) # noqa: S308
81 |
--------------------------------------------------------------------------------
/tests/settings.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from os import getenv, path
3 |
4 | DEBUG = True
5 | TEMPLATE_DEBUG = True
6 | USE_TZ = True
7 | USE_L10N = True
8 |
9 | DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "test.db"}}
10 |
11 | INSTALLED_APPS = (
12 | "django.contrib.admin",
13 | "django.contrib.auth",
14 | "django.contrib.contenttypes",
15 | "django.contrib.sessions",
16 | "django.contrib.messages",
17 | "django.contrib.staticfiles",
18 | "s3upload",
19 | "example",
20 | )
21 |
22 | MIDDLEWARE = [
23 | # default django middleware
24 | "django.contrib.sessions.middleware.SessionMiddleware",
25 | "django.middleware.common.CommonMiddleware",
26 | "django.middleware.csrf.CsrfViewMiddleware",
27 | "django.contrib.auth.middleware.AuthenticationMiddleware",
28 | "django.contrib.messages.middleware.MessageMiddleware",
29 | ]
30 |
31 | PROJECT_DIR = path.abspath(path.join(path.dirname(__file__)))
32 |
33 | TEMPLATES = [
34 | {
35 | "BACKEND": "django.template.backends.django.DjangoTemplates",
36 | "DIRS": [path.join(PROJECT_DIR, "templates")],
37 | "APP_DIRS": True,
38 | "OPTIONS": {
39 | "context_processors": [
40 | "django.contrib.messages.context_processors.messages",
41 | "django.contrib.auth.context_processors.auth",
42 | "django.template.context_processors.request",
43 | ]
44 | },
45 | }
46 | ]
47 |
48 | STATIC_URL = "/static/"
49 |
50 | STATIC_ROOT = path.join(PROJECT_DIR, "static")
51 |
52 | SECRET_KEY = "secret" # noqa: S105
53 |
54 | LOGGING = {
55 | "version": 1,
56 | "disable_existing_loggers": False,
57 | "formatters": {"simple": {"format": "%(levelname)s %(message)s"}},
58 | "handlers": {
59 | "console": {
60 | "level": "DEBUG",
61 | "class": "logging.StreamHandler",
62 | "formatter": "simple",
63 | }
64 | },
65 | "loggers": {
66 | "": {"handlers": ["console"], "propagate": True, "level": "DEBUG"},
67 | # 'django': {
68 | # 'handlers': ['console'],
69 | # 'propagate': True,
70 | # 'level': 'WARNING',
71 | # },
72 | },
73 | }
74 |
75 | ROOT_URLCONF = "tests.urls"
76 |
77 |
78 | # used by the example app
79 | def create_filename(filename: str) -> str:
80 | ext = filename.split(".")[-1]
81 | filename = "{}.{}".format(uuid.uuid4().hex, ext)
82 | return path.join("custom", filename)
83 |
84 |
85 | AWS_ACCESS_KEY_ID = getenv("AWS_ACCESS_KEY_ID", "")
86 | AWS_SECRET_ACCESS_KEY = getenv("AWS_SECRET_ACCESS_KEY", "")
87 | AWS_STORAGE_BUCKET_NAME = getenv("AWS_STORAGE_BUCKET_NAME", "")
88 | S3UPLOAD_REGION = getenv("S3UPLOAD_REGION", "")
89 |
90 | S3UPLOAD_DESTINATIONS = {
91 | "misc": {"key": lambda original_filename: "images/unique.jpg"},
92 | "files": {"key": "uploads/files", "auth": lambda u: u.is_staff},
93 | "imgs": {
94 | "key": "uploads/imgs",
95 | "auth": lambda u: True,
96 | "allowed_types": ["image/jpeg", "image/png"],
97 | "content_length_range": (5000, 20000000), # 5kb - 20mb
98 | "allowed_extensions": (".jpg", ".jpeg", ".png"),
99 | },
100 | "vids": {
101 | "key": "uploads/vids",
102 | "auth": lambda u: u.is_authenticated,
103 | "allowed_types": ["video/mp4"],
104 | },
105 | "cached": {
106 | "key": "uploads/vids",
107 | "auth": lambda u: True,
108 | "allowed_types": "*",
109 | "acl": "authenticated-read",
110 | "bucket": "astoragebucketname",
111 | "cache_control": "max-age=2592000",
112 | "content_disposition": "attachment",
113 | "server_side_encryption": "AES256",
114 | },
115 | # Allow anybody to upload any MIME type with a custom name function
116 | "custom_filename": {"key": create_filename},
117 | }
118 |
119 | if not DEBUG:
120 | raise Exception("This settings file can only be used with DEBUG=True")
121 |
--------------------------------------------------------------------------------
/s3upload/src/app/components/index.js:
--------------------------------------------------------------------------------
1 | import {removeUpload, getUploadURL, clearErrors, updateProgress} from '../actions';
2 | import {getFilename, getUrl, getError, getUploadProgress} from '../store';
3 | import {observeStore, raiseEvent} from '../utils';
4 |
5 |
6 | const View = function(element, store) {
7 | return {
8 | renderFilename: function() {
9 | const filename = getFilename(store),
10 | url = getUrl(store);
11 |
12 | if (filename && url) {
13 | this.$link.innerHTML = filename;
14 | this.$link.setAttribute('href', url);
15 | this.$url.value = url.split("?")[0];
16 |
17 | this.$element.classList.add('link-active');
18 | this.$element.classList.remove('form-active');
19 | }
20 | else {
21 | this.$url.value = '';
22 | this.$input.value = '';
23 |
24 | this.$element.classList.add('form-active');
25 | this.$element.classList.remove('link-active');
26 | }
27 | },
28 |
29 | renderError: function() {
30 | const error = getError(store);
31 |
32 | if (error) {
33 | this.$element.classList.add('has-error');
34 | this.$element.classList.add('form-active');
35 | this.$element.classList.remove('link-active');
36 |
37 | this.$element.querySelector('.s3upload__file-input').value = '';
38 | this.$element.querySelector('.s3upload__error').innerHTML = error;
39 | }
40 | else {
41 | this.$element.classList.remove('has-error');
42 | this.$element.querySelector('.s3upload__error').innerHTML = '';
43 | }
44 | },
45 |
46 | renderUploadProgress: function() {
47 | const uploadProgress = getUploadProgress(store);
48 |
49 | if (uploadProgress > 0) {
50 | this.$element.classList.add('progress-active');
51 | this.$bar.style.width = uploadProgress + '%';
52 | }
53 | else {
54 | this.$element.classList.remove('progress-active');
55 | this.$bar.style.width = '0';
56 | }
57 | },
58 |
59 | removeUpload: function(event) {
60 | event.preventDefault();
61 |
62 | store.dispatch(updateProgress());
63 | store.dispatch(removeUpload());
64 | raiseEvent(this.$element, 's3upload:clear-upload');
65 | },
66 |
67 | getUploadURL: function(event) {
68 | const file = this.$input.files[0],
69 | dest = this.$dest.value,
70 | url = this.$element.getAttribute('data-policy-url');
71 |
72 | store.dispatch(clearErrors());
73 | store.dispatch(getUploadURL(file, dest, url, store));
74 | },
75 |
76 | init: function() {
77 | // cache all the query selectors
78 | // $variables represent DOM elements
79 | this.$element = element;
80 | this.$url = element.querySelector('.s3upload__file-url');
81 | this.$input = element.querySelector('.s3upload__file-input');
82 | this.$remove = element.querySelector('.s3upload__file-remove');
83 | this.$dest = element.querySelector('.s3upload__file-dest');
84 | this.$link = element.querySelector('.s3upload__file-link');
85 | this.$error = element.querySelector('.s3upload__error');
86 | this.$bar = element.querySelector('.s3upload__bar');
87 |
88 | // set initial DOM states3upload__
89 | const status = (this.$url.value === '') ? 'form' : 'link';
90 | this.$element.classList.add(status + '-active');
91 |
92 | // add event listeners
93 | this.$remove.addEventListener('click', this.removeUpload.bind(this))
94 | this.$input.addEventListener('change', this.getUploadURL.bind(this))
95 |
96 | // these three observers subscribe to the store, but only trigger their
97 | // callbacks when the specific piece of state they observe changes.
98 | // this allows for a less naive approach to rendering changes than a
99 | // render method subscribed to the whole state.
100 | const filenameObserver = observeStore(store, state => state.appStatus.filename, this.renderFilename.bind(this));
101 |
102 | const errorObserver = observeStore(store, state => state.appStatus.error, this.renderError.bind(this));
103 |
104 | const uploadProgressObserver = observeStore(store, state => state.appStatus.uploadProgress, this.renderUploadProgress.bind(this));
105 | }
106 | }
107 | }
108 |
109 | export {View};
--------------------------------------------------------------------------------
/s3upload/utils.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import hashlib
4 | import hmac
5 | import json
6 | from base64 import b64encode
7 | from datetime import datetime, timedelta, timezone
8 | from typing import Any
9 | from urllib.parse import unquote, urlparse
10 |
11 | import boto3
12 | from botocore.config import Config
13 | from django.conf import settings
14 |
15 |
16 | def create_upload_data( # noqa: C901
17 | *,
18 | content_type: str,
19 | key: str,
20 | acl: str,
21 | bucket: str | None = None,
22 | cache_control: str | None = None,
23 | content_disposition: str | None = None,
24 | content_length_range: str | None = None,
25 | server_side_encryption: str | None = None,
26 | token: str | None = None,
27 | ) -> dict[str, Any]:
28 | """Generate AWS upload payload."""
29 | access_key = settings.AWS_ACCESS_KEY_ID
30 | secret_access_key = settings.AWS_SECRET_ACCESS_KEY
31 | bucket = bucket or settings.AWS_STORAGE_BUCKET_NAME
32 | region = settings.S3UPLOAD_REGION
33 | bucket_url = get_bucket_endpoint_url(bucket, region)
34 | expires_in = datetime.now(timezone.utc) + timedelta(seconds=60 * 5)
35 | expires = expires_in.strftime("%Y-%m-%dT%H:%M:%S.000Z")
36 | now_date = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S000Z")
37 | raw_date = datetime.now(timezone.utc).strftime("%Y%m%d")
38 |
39 | policy_dict: dict[str, Any] = {
40 | "expiration": expires,
41 | "conditions": [
42 | {"bucket": bucket},
43 | {"acl": acl},
44 | ["starts-with", "$key", ""],
45 | {"success_action_status": "201"},
46 | {"x-amz-credential": f"{access_key}/{raw_date}/{region}/s3/aws4_request"},
47 | {"x-amz-algorithm": "AWS4-HMAC-SHA256"},
48 | {"x-amz-date": now_date},
49 | {"content-type": content_type},
50 | ],
51 | }
52 |
53 | if token:
54 | policy_dict["conditions"].append({"x-amz-security-token": token})
55 |
56 | if cache_control:
57 | policy_dict["conditions"].append({"Cache-Control": cache_control})
58 |
59 | if content_disposition:
60 | policy_dict["conditions"].append({"Content-Disposition": content_disposition})
61 |
62 | if server_side_encryption:
63 | policy_dict["conditions"].append(
64 | {"x-amz-server-side-encryption": server_side_encryption}
65 | )
66 |
67 | if content_length_range:
68 | policy_dict["conditions"].append(
69 | ["content-length-range", content_length_range[0], content_length_range[1]]
70 | )
71 |
72 | policy_object = json.dumps(policy_dict)
73 |
74 | policy = b64encode(policy_object.replace("\n", "").replace("\r", "").encode())
75 |
76 | date_key = hmac.new(
77 | b"AWS4" + secret_access_key.encode("utf-8"),
78 | msg=raw_date.encode("utf-8"),
79 | digestmod=hashlib.sha256,
80 | ).digest()
81 |
82 | date_region_key = hmac.new(
83 | date_key, msg=region.encode("utf-8"), digestmod=hashlib.sha256
84 | ).digest()
85 |
86 | date_region_service_key = hmac.new(
87 | date_region_key, msg=b"s3", digestmod=hashlib.sha256
88 | ).digest()
89 |
90 | signing_key = hmac.new(
91 | date_region_service_key, msg=b"aws4_request", digestmod=hashlib.sha256
92 | ).digest()
93 |
94 | signature = hmac.new(signing_key, msg=policy, digestmod=hashlib.sha256).hexdigest()
95 | return_dict = {
96 | "policy": policy.decode(), # decode to make it JSON serializable
97 | "success_action_status": 201,
98 | "x-amz-credential": f"{access_key}/{raw_date}/{region}/s3/aws4_request",
99 | "x-amz-date": now_date,
100 | "x-amz-signature": signature,
101 | "x-amz-algorithm": "AWS4-HMAC-SHA256",
102 | "form_action": bucket_url,
103 | "key": key,
104 | "acl": acl,
105 | "content-type": content_type,
106 | }
107 |
108 | if token:
109 | return_dict["x-amz-security-token"] = token
110 |
111 | if server_side_encryption:
112 | return_dict["x-amz-server-side-encryption"] = server_side_encryption
113 |
114 | if cache_control:
115 | return_dict["Cache-Control"] = cache_control
116 |
117 | if content_disposition:
118 | return_dict["Content-Disposition"] = content_disposition
119 |
120 | return return_dict
121 |
122 |
123 | def get_s3_path_from_url(
124 | url: str, bucket_name: str = settings.AWS_STORAGE_BUCKET_NAME
125 | ) -> str:
126 | decoded = unquote(url)
127 | path = urlparse(decoded).path
128 |
129 | # The bucket name might be part of the path,
130 | # so get the path that comes after the bucket name
131 | key_path = path.split(bucket_name)[-1]
132 |
133 | # Remove slash prefix if present
134 | if key_path[0] == "/":
135 | key_path = key_path[1:]
136 |
137 | return key_path
138 |
139 |
140 | def get_signed_download_url(
141 | key: str,
142 | bucket_name: str | None = None,
143 | ttl: int = 60,
144 | ) -> str:
145 | bucket_name = bucket_name or settings.AWS_STORAGE_BUCKET_NAME
146 | s3 = boto3.client(
147 | "s3",
148 | aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
149 | aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
150 | config=Config(signature_version="s3v4"),
151 | region_name=settings.S3UPLOAD_REGION,
152 | )
153 | download_url = s3.generate_presigned_url(
154 | "get_object", Params={"Bucket": bucket_name, "Key": key}, ExpiresIn=ttl
155 | )
156 | return download_url
157 |
158 |
159 | # virtual-hosted-style URLs are now the default
160 | # Example: https://bucket-name.s3.Region.amazonaws.com/key name
161 | # See: https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-bucket-intro.html
162 | def get_bucket_endpoint_url(
163 | bucket_name: str | None = None,
164 | region: str | None = None,
165 | default: str = "https://{bucket}.s3.{region}.amazonaws.com",
166 | ) -> str:
167 | bucket_name = bucket_name or settings.AWS_STORAGE_BUCKET_NAME
168 | region = region or settings.S3UPLOAD_REGION
169 | bucket_endpoint = getattr(
170 | settings,
171 | "S3UPLOAD_BUCKET_ENDPOINT",
172 | default,
173 | )
174 | return bucket_endpoint.format(bucket=bucket_name, region=region)
175 |
--------------------------------------------------------------------------------
/s3upload/src/app/actions/index.js:
--------------------------------------------------------------------------------
1 | import constants from '../constants';
2 | import {i18n_strings} from '../constants';
3 | import {request, parseJson, getCookie, parseURL, parseNameFromUrl, raiseEvent} from '../utils';
4 | import {getElement, getAWSPayload} from '../store';
5 |
6 | export const getUploadURL = (file, dest, url, store) => {
7 |
8 | const form = new FormData(),
9 | headers = {'X-CSRFToken': getCookie('csrftoken')};
10 |
11 | form.append('type', file.type);
12 | form.append('name', file.name);
13 | form.append('dest', dest);
14 |
15 | const onLoad = function(status, json) {
16 | const data = parseJson(json);
17 |
18 | switch(status) {
19 | case 200:
20 | store.dispatch(receiveSignedURL(data.private_access_url));
21 | store.dispatch(receiveAWSUploadParams(data.aws_payload));
22 | store.dispatch(beginUploadToAWS(file, store));
23 | break;
24 | case 400:
25 | case 403:
26 | case 415:
27 | console.error('Error uploading', status, data.error);
28 | raiseEvent(getElement(store), 's3upload:error', {status, error: data});
29 | store.dispatch(addError(data.error));
30 | store.dispatch(didNotReceivAWSUploadParams());
31 | break;
32 | default:
33 | console.error('Error uploading', status, i18n_strings.no_upload_url);
34 | raiseEvent(getElement(store), 's3upload:error', {status, error: data});
35 | store.dispatch(addError(i18n_strings.no_upload_url));
36 | store.dispatch(didNotReceivAWSUploadParams());
37 | }
38 | }
39 |
40 | const onError = function(status, json) {
41 | const data = parseJson(json);
42 |
43 | console.error('Error uploading', status, i18n_strings.no_upload_url);
44 | raiseEvent(getElement(store), 's3upload:error', {status, error: data});
45 |
46 | store.dispatch(addError(i18n_strings.no_upload_url));
47 | }
48 |
49 | request('POST', url, form, headers, false, onLoad, onError);
50 |
51 | return {
52 | type: constants.REQUEST_AWS_UPLOAD_PARAMS
53 | }
54 | }
55 |
56 | export const receiveAWSUploadParams = (aws_payload) => {
57 | return {
58 | type: constants.RECEIVE_AWS_UPLOAD_PARAMS,
59 | aws_payload: aws_payload
60 | }
61 | }
62 |
63 | export const receiveSignedURL = (signedURL) => {
64 | return {
65 | type: constants.RECEIVE_SIGNED_URL,
66 | signedURL,
67 | }
68 | }
69 |
70 | export const didNotReceivAWSUploadParams = () => {
71 | return {
72 | type: constants.DID_NOT_RECEIVE_AWS_UPLOAD_PARAMS,
73 | }
74 | }
75 |
76 | export const removeUpload = () => {
77 | return {
78 | type: constants.REMOVE_UPLOAD
79 | }
80 | }
81 |
82 | export const beginUploadToAWS = (file, store) => {
83 | const AWSPayload = getAWSPayload(store),
84 | url = AWSPayload.form_action,
85 | headers = {};
86 |
87 | let form = new FormData();
88 |
89 | // we need to remove this key because otherwise S3 will trigger a 403
90 | // when we send the payload along with the file.
91 | delete AWSPayload['form_action'];
92 |
93 | Object.keys(AWSPayload).forEach(function(key){
94 | form.append(key, AWSPayload[key]);
95 | });
96 |
97 | // the file has to be appended at the end, or else S3 will throw a wobbly
98 | form.append('file', file);
99 |
100 | const onLoad = function(status, xml) {
101 | switch(status) {
102 | case 201:
103 | const url = parseURL(xml),
104 | filename = parseNameFromUrl(url).split('/').pop();
105 |
106 | store.dispatch(completeUploadToAWS(filename, url));
107 | raiseEvent(getElement(store), 's3upload:file-uploaded', {filename, url});
108 | break;
109 | default:
110 | console.error('Error uploading', status, xml);
111 | raiseEvent(getElement(store), 's3upload:error', {status, error: xml});
112 |
113 | store.dispatch(didNotCompleteUploadToAWS());
114 |
115 | if (xml.indexOf('') > -1) {
116 | store.dispatch(addError(i18n_strings.no_file_too_small));
117 | }
118 | else if (xml.indexOf('') > -1) {
119 | store.dispatch(addError(i18n_strings.no_file_too_large));
120 | }
121 | else {
122 | store.dispatch(addError(i18n_strings.no_upload_failed));
123 | }
124 |
125 | break;
126 | }
127 | };
128 |
129 | const onError = function(status, xml) {
130 | console.error('Error uploading', status, xml);
131 | raiseEvent(getElement(store), 's3upload:error', {status, xml});
132 |
133 | store.dispatch(didNotCompleteUploadToAWS());
134 | store.dispatch(addError(i18n_strings.no_upload_failed));
135 | }
136 |
137 | const onProgress = function(data) {
138 | let progress = null;
139 |
140 | if (data.lengthComputable) {
141 | progress = Math.round(data.loaded * 100 / data.total);
142 | }
143 |
144 | store.dispatch(updateProgress(progress));
145 | raiseEvent(getElement(store), 's3upload:progress-updated', {progress});
146 | }
147 |
148 | request('POST', url, form, headers, onProgress, onLoad, onError);
149 |
150 | return {
151 | type: constants.BEGIN_UPLOAD_TO_AWS
152 | }
153 | }
154 |
155 | export const completeUploadToAWS = (filename, url) => {
156 | return {
157 | type: constants.COMPLETE_UPLOAD_TO_AWS,
158 | url,
159 | filename
160 | }
161 | }
162 |
163 | export const didNotCompleteUploadToAWS = () => {
164 | return {
165 | type: constants.DID_NOT_COMPLETE_UPLOAD_TO_AWS
166 | }
167 | }
168 |
169 | export const addError = (error) => {
170 | return {
171 | type: constants.ADD_ERROR,
172 | error
173 | }
174 | }
175 |
176 | export const clearErrors = () => {
177 | return {
178 | type: constants.CLEAR_ERRORS
179 | }
180 | }
181 |
182 | export const updateProgress = (progress = {}) => {
183 | return {
184 | type: constants.UPDATE_PROGRESS,
185 | progress
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # django-s3-upload
2 |
3 | ## Compatibility
4 |
5 | This library supports Python 3.10+ and Django 4.2+ only.
6 |
7 | [](https://travis-ci.org/yunojuno/django-s3upload)
8 |
9 | **Allows direct uploading of a file from the browser to AWS S3 via a file input field rendered by
10 | Django.**
11 |
12 | The uploaded file's URL is then saveable as the value of that field in the database.
13 |
14 | This avoids the problem of uploads timing out when they go via a web server before being handed off
15 | to S3.
16 |
17 | Features include:
18 |
19 | - displaying a progress bar
20 | - support for ACLs (eg, private uploads)
21 | - support for encrypted-at-rest S3 buckets
22 | - mimetype and file extension whitelisting
23 | - specifying different bucket destinations on a per-field basis
24 |
25 | ## Installation
26 |
27 | Install with Pip:
28 |
29 | `pip install django-s3-upload`
30 |
31 | ## AWS Setup
32 |
33 | ### Access Credentials
34 |
35 | You have two options of providing access to AWS resources:
36 |
37 | 1. Add credentials of an IAM user to your Django settings (see below)
38 | 2. Use the EC2 instance profile and its attached IAM role
39 |
40 | Whether you are using an IAM user or a role, there needs to be an IAM policy in effect that grants
41 | permission to upload to S3:
42 |
43 | ```json
44 | "Statement": [
45 | {
46 | "Effect": "Allow",
47 | "Action": ["s3:PutObject", "s3:PutObjectAcl"],
48 | "Resource": "arn:aws:s3:::your-bucket-name/*"
49 | }
50 | ]
51 | ```
52 |
53 | If the instance profile is to be used, the IAM role needs to have a Trust Relationship configuration
54 | applied:
55 |
56 | ```json
57 | "Statement": [
58 | {
59 | "Effect": "Allow",
60 | "Principal": {
61 | "Service": "ec2.amazonaws.com"
62 | },
63 | "Action": "sts:AssumeRole"
64 | }
65 | ]
66 | ```
67 |
68 | Note that in order to use the EC2 instance profile, django-s3-upload needs to query the EC2 instance
69 | metadata using utility functions from the [botocore] [] package. You already have `botocore`
70 | installed if `boto3` is a dependency of your project.
71 |
72 | ### S3 CORS
73 |
74 | Setup a CORS policy on your S3 bucket.
75 |
76 | ```xml
77 |
78 |
79 | http://yourdomain.com:8080
80 | POST
81 | PUT
82 | 3000
83 | *
84 |
85 |
86 | ```
87 |
88 | ## Django Setup
89 |
90 | ### settings.py
91 |
92 | ```python
93 | INSTALLED_APPS = [
94 | ...
95 | 's3upload',
96 | ...
97 | ]
98 |
99 | TEMPLATES = [{
100 | ...
101 | 'APP_DIRS': True,
102 | ...
103 | }]
104 |
105 | # AWS
106 |
107 | # If these are not defined, the EC2 instance profile and IAM role are used.
108 | # This requires you to add boto3 (or botocore, which is a dependency of boto3)
109 | # to your project dependencies.
110 | AWS_ACCESS_KEY_ID = ''
111 | AWS_SECRET_ACCESS_KEY = ''
112 |
113 | AWS_STORAGE_BUCKET_NAME = ''
114 |
115 | # The region of your bucket, more info:
116 | # http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
117 | S3UPLOAD_REGION = 'us-east-1'
118 |
119 | # [Optional] Custom bucket endpoint url, with following keys (also optional):
120 | # region - region of your bucket
121 | # bucket - your bucket name
122 | # S3UPLOAD_BUCKET_ENDPOINT = 'https://{region}.non-amazon-s3-url.com/{bucket}'
123 |
124 | # Destinations, with the following keys:
125 | #
126 | # key [required] Where to upload the file to, can be either:
127 | # 1. '/' = Upload to root with the original filename.
128 | # 2. 'some/path' = Upload to some/path with the original filename.
129 | # 3. functionName = Pass a function and create your own path/filename.
130 | # auth [optional] An ACL function to whether the current Django user can perform this action.
131 | # allowed [optional] List of allowed MIME types.
132 | # acl [optional] Give the object another ACL rather than 'public-read'.
133 | # cache_control [optional] Cache control headers, eg 'max-age=2592000'.
134 | # content_disposition [optional] Useful for sending files as attachments.
135 | # bucket [optional] Specify a different bucket for this particular object.
136 | # server_side_encryption [optional] Encryption headers for buckets that require it.
137 |
138 | S3UPLOAD_DESTINATIONS = {
139 | 'example_destination': {
140 | # REQUIRED
141 | 'key': 'uploads/images',
142 |
143 | # OPTIONAL
144 | 'auth': lambda u: u.is_staff, # Default allow anybody to upload
145 | 'allowed_types': ['image/jpeg', 'image/png', 'video/mp4'], # Default allow all mime types
146 | 'allowed_extensions': ('.jpg', '.jpeg', '.png'), # Defaults to all extensions
147 | 'bucket': 'pdf-bucket', # Default is 'AWS_STORAGE_BUCKET_NAME'
148 | 'acl': 'private', # Defaults to 'public-read'
149 | 'cache_control': 'max-age=2592000', # Default no cache-control
150 | 'content_disposition': 'attachment', # Default no content disposition
151 | 'content_length_range': (5000, 20000000), # Default allow any size
152 | 'server_side_encryption': 'AES256', # Default no encryption
153 | }
154 | }
155 | ```
156 |
157 | ### urls.py
158 |
159 | ```python
160 | urlpatterns = [
161 | path(r'^s3upload/', include('s3upload.urls')),
162 | ]
163 | ```
164 |
165 | Run `python manage.py collectstatic` if required.
166 |
167 | ## Use in Django admin
168 |
169 | ### models.py
170 |
171 | ```python
172 | from django.db import models
173 | from s3upload.fields import S3UploadField
174 |
175 | class Example(models.Model):
176 | video = S3UploadField(dest='example_destination')
177 | ```
178 |
179 | ## Use the widget in a custom form
180 |
181 | ### forms.py
182 |
183 | ```python
184 | from django import forms
185 | from s3upload.widgets import S3UploadWidget
186 |
187 | class S3UploadForm(forms.Form):
188 | images = forms.URLField(widget=S3UploadWidget(dest='example_destination'))
189 | ```
190 |
191 | **\*Optional.** You can modify the HTML of the widget by overiding template
192 | **s3upload/templates/s3upload-widget.tpl**
193 |
194 | ### views.py
195 |
196 | ```python
197 | from django.views.generic import FormView
198 | from .forms import S3UploadForm
199 |
200 | class MyView(FormView):
201 | template_name = 'form.html'
202 | form_class = S3UploadForm
203 | ```
204 |
205 | ### templates/form.html
206 |
207 | ```html
208 |
209 |
210 |
211 | s3upload
212 | {{ form.media }}
213 |
214 |
215 |
216 |
217 |
218 | ```
219 |
220 | ## Examples
221 |
222 | Examples of both approaches can be found in the examples folder. To run them:
223 |
224 | ```shell
225 | $ git clone git@github.com:yunojuno/django-s3-upload.git
226 | $ cd django-s3-upload
227 |
228 | # Add your AWS keys to your environment
229 | export AWS_ACCESS_KEY_ID='...'
230 | export AWS_SECRET_ACCESS_KEY='...'
231 | export AWS_STORAGE_BUCKET_NAME='...'
232 | export S3UPLOAD_REGION='...' # e.g. 'eu-west-1'
233 |
234 | $ docker-compose up
235 | ```
236 |
237 | Visit `http://localhost:8000/admin` to view the admin widget and `http://localhost:8000/form` to
238 | view the custom form widget.
239 |
240 | [botocore]: https://github.com/boto/botocore
241 |
--------------------------------------------------------------------------------
/tests/test_widgets.py:
--------------------------------------------------------------------------------
1 | import json
2 | from base64 import b64decode
3 | from urllib.parse import parse_qs, urlparse
4 |
5 | from django.conf import settings
6 | from django.contrib.auth.models import User
7 | from django.core.exceptions import ImproperlyConfigured
8 | from django.test import TestCase
9 | from django.test.utils import override_settings
10 | from django.urls import resolve, reverse
11 |
12 | from s3upload import widgets
13 |
14 | HTML_OUTPUT = """
31 | """
32 |
33 | FOO_RESPONSE = {
34 | "x-amz-algorithm": "AWS4-HMAC-SHA256",
35 | "form_action": "https://s3.amazonaws.com/{}".format(
36 | settings.AWS_STORAGE_BUCKET_NAME
37 | ),
38 | "success_action_status": 201,
39 | "acl": "public-read",
40 | "key": "uploads/imgs/image.jpg",
41 | "content-type": "image/jpeg",
42 | }
43 |
44 |
45 | class WidgetTests(TestCase):
46 | def setUp(self) -> None:
47 | admin = User.objects.create_superuser("admin", "u@email.com", "admin")
48 | admin.save()
49 |
50 | def test_init(self) -> None:
51 | # Test initialising the widget without an invalid destination
52 | self.assertRaises(ImproperlyConfigured, widgets.S3UploadWidget, "foo")
53 | self.assertRaises(ValueError, widgets.S3UploadWidget, None)
54 | self.assertRaises(ValueError, widgets.S3UploadWidget, "")
55 | with override_settings(S3UPLOAD_DESTINATIONS={"foo": {}}):
56 | widgets.S3UploadWidget("foo")
57 |
58 | def test_check_urls(self) -> None:
59 | reversed_url = reverse("s3upload")
60 | resolved_url = resolve("/s3upload/get_upload_params/")
61 | self.assertEqual(reversed_url, "/s3upload/get_upload_params/")
62 | self.assertEqual(resolved_url.view_name, "s3upload")
63 |
64 | @override_settings(S3UPLOAD_DESTINATIONS={"foo": {}})
65 | def test_check_widget_html(self) -> None:
66 | widget = widgets.S3UploadWidget(dest="foo")
67 | html = widget.render("filename", "")
68 | self.assertEqual(html, HTML_OUTPUT)
69 |
70 | def test_check_signing_logged_in(self) -> None:
71 | self.client.login(username="admin", password="admin")
72 | data = {"dest": "files", "name": "image.jpg", "type": "image/jpeg"}
73 | response = self.client.post(reverse("s3upload"), data)
74 | self.assertEqual(response.status_code, 200)
75 |
76 | def test_check_signing_logged_out(self) -> None:
77 | data = {"dest": "files", "name": "image.jpg", "type": "image/jpeg"}
78 | response = self.client.post(reverse("s3upload"), data)
79 | self.assertEqual(response.status_code, 403)
80 |
81 | def test_check_allowed_type(self) -> None:
82 | data = {"dest": "imgs", "name": "image.jpg", "type": "image/jpeg"}
83 | response = self.client.post(reverse("s3upload"), data)
84 | self.assertEqual(response.status_code, 200)
85 |
86 | def test_check_disallowed_type(self) -> None:
87 | data = {"dest": "imgs", "name": "image.mp4", "type": "video/mp4"}
88 | response = self.client.post(reverse("s3upload"), data)
89 | self.assertEqual(response.status_code, 400)
90 |
91 | def test_check_allowed_type_logged_in(self) -> None:
92 | self.client.login(username="admin", password="admin")
93 | data = {"dest": "vids", "name": "video.mp4", "type": "video/mp4"}
94 | response = self.client.post(reverse("s3upload"), data)
95 | self.assertEqual(response.status_code, 200)
96 |
97 | def test_check_disallowed_type_logged_out(self) -> None:
98 | data = {"dest": "vids", "name": "video.mp4", "type": "video/mp4"}
99 | response = self.client.post(reverse("s3upload"), data)
100 | self.assertEqual(response.status_code, 403)
101 |
102 | def test_check_disallowed_extensions(self) -> None:
103 | data = {"dest": "imgs", "name": "image.jfif", "type": "image/jpeg"}
104 | response = self.client.post(reverse("s3upload"), data)
105 | self.assertEqual(response.status_code, 415)
106 |
107 | def test_check_allowed_extensions(self) -> None:
108 | data = {"dest": "imgs", "name": "image.jpg", "type": "image/jpeg"}
109 | response = self.client.post(reverse("s3upload"), data)
110 | self.assertEqual(response.status_code, 200)
111 |
112 | def test_check_disallowed_extensions__uppercase(self) -> None:
113 | data = {"dest": "imgs", "name": "image.JFIF", "type": "image/jpeg"}
114 | response = self.client.post(reverse("s3upload"), data)
115 | self.assertEqual(response.status_code, 415)
116 |
117 | def test_check_allowed_extensions__uppercase(self) -> None:
118 | data = {"dest": "imgs", "name": "image.JPG", "type": "image/jpeg"}
119 | response = self.client.post(reverse("s3upload"), data)
120 | self.assertEqual(response.status_code, 200)
121 |
122 | def test_check_signing_fields(self) -> None:
123 | self.client.login(username="admin", password="admin")
124 | data = {"dest": "imgs", "name": "image.jpg", "type": "image/jpeg"}
125 | response = self.client.post(reverse("s3upload"), data)
126 | response_dict = json.loads(response.content.decode())
127 | aws_payload = response_dict["aws_payload"]
128 | self.assertTrue("x-amz-signature" in aws_payload)
129 | self.assertTrue("x-amz-credential" in aws_payload)
130 | self.assertTrue("policy" in aws_payload)
131 | self.assertEqual(aws_payload["x-amz-algorithm"], "AWS4-HMAC-SHA256")
132 | self.assertEqual(
133 | aws_payload["form_action"],
134 | f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.S3UPLOAD_REGION}.amazonaws.com",
135 | )
136 | self.assertEqual(aws_payload["success_action_status"], 201)
137 | self.assertEqual(aws_payload["acl"], "public-read")
138 | self.assertEqual(aws_payload["key"], "uploads/imgs/image.jpg")
139 | self.assertEqual(aws_payload["content-type"], "image/jpeg")
140 |
141 | def test_check_signing_fields_unique_filename(self) -> None:
142 | data = {"dest": "misc", "name": "image.jpg", "type": "image/jpeg"}
143 | response = self.client.post(reverse("s3upload"), data)
144 | response_dict = json.loads(response.content.decode())
145 | aws_payload = response_dict["aws_payload"]
146 | self.assertTrue("x-amz-signature" in aws_payload)
147 | self.assertTrue("x-amz-credential" in aws_payload)
148 | self.assertTrue("policy" in aws_payload)
149 | self.assertEqual(aws_payload["x-amz-algorithm"], "AWS4-HMAC-SHA256")
150 | self.assertEqual(
151 | aws_payload["form_action"],
152 | f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.S3UPLOAD_REGION}.amazonaws.com",
153 | )
154 | self.assertEqual(aws_payload["success_action_status"], 201)
155 | self.assertEqual(aws_payload["acl"], "public-read")
156 | self.assertEqual(aws_payload["key"], "images/unique.jpg")
157 | self.assertEqual(aws_payload["content-type"], "image/jpeg")
158 |
159 | def test_check_policy_conditions(self) -> None:
160 | self.client.login(username="admin", password="admin")
161 | data = {"dest": "cached", "name": "video.mp4", "type": "video/mp4"}
162 | response = self.client.post(reverse("s3upload"), data)
163 | self.assertEqual(response.status_code, 200)
164 | response_dict = json.loads(response.content.decode())
165 | aws_payload = response_dict["aws_payload"]
166 | self.assertTrue("policy" in aws_payload)
167 | policy_dict = json.loads(b64decode(aws_payload["policy"]).decode("utf-8"))
168 | self.assertTrue("conditions" in policy_dict)
169 | conditions_dict = policy_dict["conditions"]
170 | self.assertEqual(conditions_dict[0]["bucket"], "astoragebucketname")
171 | self.assertEqual(conditions_dict[1]["acl"], "authenticated-read")
172 | self.assertEqual(conditions_dict[8]["Cache-Control"], "max-age=2592000")
173 | self.assertEqual(conditions_dict[9]["Content-Disposition"], "attachment")
174 | self.assertEqual(conditions_dict[10]["x-amz-server-side-encryption"], "AES256")
175 |
176 | @override_settings(
177 | S3UPLOAD_DESTINATIONS={
178 | "misc": {
179 | "key": "/",
180 | "auth": lambda u: True,
181 | "acl": "private",
182 | "bucket": "test-bucket",
183 | }
184 | },
185 | S3UPLOAD_REGION="eu-west-1",
186 | )
187 | def test_check_signed_url(self) -> None:
188 | data = {"dest": "misc", "name": "image.jpg", "type": "image/jpeg"}
189 | response = self.client.post(reverse("s3upload"), data)
190 | response_dict = json.loads(response.content.decode())
191 | parsed_url = urlparse(response_dict["private_access_url"])
192 | parsed_qs = parse_qs(parsed_url.query)
193 | self.assertEqual(parsed_url.scheme, "https")
194 | self.assertEqual(parsed_url.netloc, "test-bucket.s3.amazonaws.com")
195 | self.assertTrue("X-Amz-Signature" in parsed_qs)
196 | self.assertTrue("X-Amz-Expires" in parsed_qs)
197 |
198 | def test_content_length_range(self) -> None:
199 | # Content_length_range setting is always sent as part of policy.
200 | # Initial request data doesn't affect it.
201 | data = {"dest": "imgs", "name": "image.jpg", "type": "image/jpeg"}
202 | response = self.client.post(reverse("s3upload"), data)
203 | self.assertEqual(response.status_code, 200)
204 |
205 | response_dict = json.loads(response.content.decode())
206 | aws_payload = response_dict["aws_payload"]
207 |
208 | self.assertTrue("policy" in aws_payload)
209 | policy_dict = json.loads(b64decode(aws_payload["policy"]).decode("utf-8"))
210 | self.assertTrue("conditions" in policy_dict)
211 | conditions_dict = policy_dict["conditions"]
212 | self.assertEqual(conditions_dict[-1], ["content-length-range", 5000, 20000000])
213 |
--------------------------------------------------------------------------------
/s3upload/static/s3upload/css/bootstrap-progress.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap v2.3.1
3 | *
4 | * Copyright 2012 Twitter, Inc
5 | * Licensed under the Apache License v2.0
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | *
8 | * Designed and built with all the love in the world @twitter by @mdo and @fat.
9 | */
10 | .clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0;}
11 | .clearfix:after{clear:both;}
12 | .hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0;}
13 | .input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;}
14 | @-webkit-keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}@-o-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}.s3upload__progress{overflow:hidden;height:20px;margin-bottom:20px;background-color:#f7f7f7;background-image:-moz-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9));background-image:-webkit-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-o-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:linear-gradient(to bottom, #f5f5f5, #f9f9f9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
15 | .s3upload__progress .s3upload__bar{width:0%;height:100%;color:#ffffff;float:left;font-size:12px;text-align:center;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top, #149bdf, #0480be);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be));background-image:-webkit-linear-gradient(top, #149bdf, #0480be);background-image:-o-linear-gradient(top, #149bdf, #0480be);background-image:linear-gradient(to bottom, #149bdf, #0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width 0.6s ease;-moz-transition:width 0.6s ease;-o-transition:width 0.6s ease;transition:width 0.6s ease;}
16 | .s3upload__progress .s3upload__bar+.s3upload__bar{-webkit-box-shadow:inset 1px 0 0 rgba(0,0,0,.15), inset 0 -1px 0 rgba(0,0,0,.15);-moz-box-shadow:inset 1px 0 0 rgba(0,0,0,.15), inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 1px 0 0 rgba(0,0,0,.15), inset 0 -1px 0 rgba(0,0,0,.15);}
17 | .s3upload__progress-striped .s3upload__bar{background-color:#149bdf;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px;}
18 | .s3upload__progress.active .s3upload__bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite;}
19 | .s3upload__progress-danger .s3upload__bar,.s3upload__progress .s3upload__bar-danger{background-color:#dd514c;background-image:-moz-linear-gradient(top, #ee5f5b, #c43c35);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35));background-image:-webkit-linear-gradient(top, #ee5f5b, #c43c35);background-image:-o-linear-gradient(top, #ee5f5b, #c43c35);background-image:linear-gradient(to bottom, #ee5f5b, #c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffc43c35', GradientType=0);}
20 | .s3upload__progress-danger.s3upload__progress-striped .s3upload__bar,.s3upload__progress-striped .s3upload__bar-danger{background-color:#ee5f5b;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
21 | .s3upload__progress-success .s3upload__bar,.s3upload__progress .s3upload__bar-success{background-color:#5eb95e;background-image:-moz-linear-gradient(top, #62c462, #57a957);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957));background-image:-webkit-linear-gradient(top, #62c462, #57a957);background-image:-o-linear-gradient(top, #62c462, #57a957);background-image:linear-gradient(to bottom, #62c462, #57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff57a957', GradientType=0);}
22 | .s3upload__progress-success.s3upload__progress-striped .s3upload__bar,.s3upload__progress-striped .s3upload__bar-success{background-color:#62c462;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
23 | .s3upload__progress-info .s3upload__bar,.s3upload__progress .s3upload__bar-info{background-color:#4bb1cf;background-image:-moz-linear-gradient(top, #5bc0de, #339bb9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9));background-image:-webkit-linear-gradient(top, #5bc0de, #339bb9);background-image:-o-linear-gradient(top, #5bc0de, #339bb9);background-image:linear-gradient(to bottom, #5bc0de, #339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff339bb9', GradientType=0);}
24 | .s3upload__progress-info.s3upload__progress-striped .s3upload__bar,.s3upload__progress-striped .s3upload__bar-info{background-color:#5bc0de;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
25 | .s3upload__progress-warning .s3upload__bar,.s3upload__progress .s3upload__bar-warning{background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(to bottom, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0);}
26 | .s3upload__progress-warning.s3upload__progress-striped .s3upload__bar,.s3upload__progress-striped .s3upload__bar-warning{background-color:#fbb450;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
27 |
--------------------------------------------------------------------------------
/s3upload/static/s3upload/js/django-s3-uploads.min.js:
--------------------------------------------------------------------------------
1 | !function(){return function e(t,r,n){function o(i,s){if(!r[i]){if(!t[i]){var u="function"==typeof require&&require;if(!s&&u)return u(i,!0);if(a)return a(i,!0);var l=new Error("Cannot find module '"+i+"'");throw l.code="MODULE_NOT_FOUND",l}var d=r[i]={exports:{}};t[i][0].call(d.exports,function(e){var r=t[i][1][e];return o(r||e)},d,d.exports,e,t,r,n)}return r[i].exports}for(var a="function"==typeof require&&require,i=0;i")>-1?t.dispatch(_(o.i18n_strings.no_file_too_small)):r.indexOf("")>-1?t.dispatch(_(o.i18n_strings.no_file_too_large)):t.dispatch(_(o.i18n_strings.no_upload_failed))}},function(e,r){console.error("Error uploading",e,r),(0,i.raiseEvent)((0,s.getElement)(t),"s3upload:error",{status:e,xml:r}),t.dispatch(p()),t.dispatch(_(o.i18n_strings.no_upload_failed))}),{type:a.default.BEGIN_UPLOAD_TO_AWS}}),f=r.completeUploadToAWS=function(e,t){return{type:a.default.COMPLETE_UPLOAD_TO_AWS,url:t,filename:e}},p=r.didNotCompleteUploadToAWS=function(){return{type:a.default.DID_NOT_COMPLETE_UPLOAD_TO_AWS}},_=r.addError=function(e){return{type:a.default.ADD_ERROR,error:e}},v=(r.clearErrors=function(){return{type:a.default.CLEAR_ERRORS}},r.updateProgress=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return{type:a.default.UPDATE_PROGRESS,progress:e}})},{"../constants":3,"../store":8,"../utils":9}],2:[function(e,t,r){"use strict";Object.defineProperty(r,"__esModule",{value:!0}),r.View=void 0;var n=e("../actions"),o=e("../store"),a=e("../utils");r.View=function(e,t){return{renderFilename:function(){var e=(0,o.getFilename)(t),r=(0,o.getUrl)(t);e&&r?(this.$link.innerHTML=e,this.$link.setAttribute("href",r),this.$url.value=r.split("?")[0],this.$element.classList.add("link-active"),this.$element.classList.remove("form-active")):(this.$url.value="",this.$input.value="",this.$element.classList.add("form-active"),this.$element.classList.remove("link-active"))},renderError:function(){var e=(0,o.getError)(t);e?(this.$element.classList.add("has-error"),this.$element.classList.add("form-active"),this.$element.classList.remove("link-active"),this.$element.querySelector(".s3upload__file-input").value="",this.$element.querySelector(".s3upload__error").innerHTML=e):(this.$element.classList.remove("has-error"),this.$element.querySelector(".s3upload__error").innerHTML="")},renderUploadProgress:function(){var e=(0,o.getUploadProgress)(t);e>0?(this.$element.classList.add("progress-active"),this.$bar.style.width=e+"%"):(this.$element.classList.remove("progress-active"),this.$bar.style.width="0")},removeUpload:function(e){e.preventDefault(),t.dispatch((0,n.updateProgress)()),t.dispatch((0,n.removeUpload)()),(0,a.raiseEvent)(this.$element,"s3upload:clear-upload")},getUploadURL:function(e){var r=this.$input.files[0],o=this.$dest.value,a=this.$element.getAttribute("data-policy-url");t.dispatch((0,n.clearErrors)()),t.dispatch((0,n.getUploadURL)(r,o,a,t))},init:function(){this.$element=e,this.$url=e.querySelector(".s3upload__file-url"),this.$input=e.querySelector(".s3upload__file-input"),this.$remove=e.querySelector(".s3upload__file-remove"),this.$dest=e.querySelector(".s3upload__file-dest"),this.$link=e.querySelector(".s3upload__file-link"),this.$error=e.querySelector(".s3upload__error"),this.$bar=e.querySelector(".s3upload__bar");var r=""===this.$url.value?"form":"link";this.$element.classList.add(r+"-active"),this.$remove.addEventListener("click",this.removeUpload.bind(this)),this.$input.addEventListener("change",this.getUploadURL.bind(this)),(0,a.observeStore)(t,function(e){return e.appStatus.filename},this.renderFilename.bind(this)),(0,a.observeStore)(t,function(e){return e.appStatus.error},this.renderError.bind(this)),(0,a.observeStore)(t,function(e){return e.appStatus.uploadProgress},this.renderUploadProgress.bind(this))}}}},{"../actions":1,"../store":8,"../utils":9}],3:[function(e,t,r){"use strict";Object.defineProperty(r,"__esModule",{value:!0}),r.default={REQUEST_AWS_UPLOAD_PARAMS:"REQUEST_AWS_UPLOAD_PARAMS",RECEIVE_AWS_UPLOAD_PARAMS:"RECEIVE_AWS_UPLOAD_PARAMS",DID_NOT_RECEIVE_AWS_UPLOAD_PARAMS:"DID_NOT_RECEIVE_AWS_UPLOAD_PARAMS",REMOVE_UPLOAD:"REMOVE_UPLOAD",BEGIN_UPLOAD_TO_AWS:"BEGIN_UPLOAD_TO_AWS",COMPLETE_UPLOAD_TO_AWS:"COMPLETE_UPLOAD_TO_AWS",DID_NOT_COMPLETE_UPLOAD_TO_AWS:"DID_NOT_COMPLETE_UPLOAD_TO_AWS",ADD_ERROR:"ADD_ERROR",CLEAR_ERRORS:"CLEAR_ERRORS",UPDATE_PROGRESS:"UPDATE_PROGRESS",RECEIVE_SIGNED_URL:"RECEIVE_SIGNED_URL"};var n=void 0;try{r.i18n_strings=n=djangoS3Upload.i18n_strings}catch(e){r.i18n_strings=n={no_upload_failed:"Sorry, failed to upload file.",no_upload_url:"Sorry, could not get upload URL.",no_file_too_large:"Sorry, the file is too large to be uploaded.",no_file_too_small:"Sorry, the file is too small to be uploaded."}}r.i18n_strings=n},{}],4:[function(e,t,r){"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n,o=e("../constants"),a=(n=o)&&n.__esModule?n:{default:n};r.default=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments[1];switch(t.type){case a.default.BEGIN_UPLOAD_TO_AWS:return Object.assign({},e,{isUploading:!0});case a.default.COMPLETE_UPLOAD_TO_AWS:return Object.assign({},e,{isUploading:!1,uploadProgress:0,filename:t.filename,url:t.url});case a.default.DID_NOT_COMPLETE_UPLOAD_TO_AWS:return Object.assign({},e,{isUploading:!1});case a.default.REMOVE_UPLOAD:return Object.assign({},e,{filename:null,url:null});case a.default.ADD_ERROR:return Object.assign({},e,{error:t.error});case a.default.CLEAR_ERRORS:return Object.assign({},e,{error:null});case a.default.UPDATE_PROGRESS:return Object.assign({},e,{uploadProgress:t.progress});case a.default.RECEIVE_SIGNED_URL:return Object.assign({},e,{signedURL:t.signedURL});default:return e}}},{"../constants":3}],5:[function(e,t,r){"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n,o=e("../constants"),a=(n=o)&&n.__esModule?n:{default:n};r.default=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments[1];switch(t.type){case a.default.REQUEST_AWS_UPLOAD_PARAMS:return Object.assign({},e,{isLoading:!0});case a.default.RECEIVE_AWS_UPLOAD_PARAMS:return Object.assign({},e,{isLoading:!1,AWSPayload:t.aws_payload});case a.default.DID_NOT_RECEIVE_AWS_UPLOAD_PARAMS:return Object.assign({},e,{isLoading:!1});default:return e}}},{"../constants":3}],6:[function(e,t,r){"use strict";Object.defineProperty(r,"__esModule",{value:!0});var n=e("redux"),o=i(e("./awsUploadsParams")),a=i(e("./appStatus"));function i(e){return e&&e.__esModule?e:{default:e}}r.default=(0,n.combineReducers)({AWSUploadParams:o.default,appStatus:a.default,element:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};arguments[1];return e}})},{"./appStatus":4,"./awsUploadsParams":5,redux:26}],7:[function(e,t,r){"use strict";Object.defineProperty(r,"__esModule",{value:!0}),r.getFilename=function(e){return e.getState().appStatus.filename},r.getUrl=function(e){return e.getState().appStatus.signedURL||e.getState().appStatus.url},r.getError=function(e){return e.getState().appStatus.error},r.getUploadProgress=function(e){return e.getState().appStatus.uploadProgress},r.getElement=function(e){return e.getState().element},r.getAWSPayload=function(e){return e.getState().AWSUploadParams.AWSPayload}},{}],8:[function(e,t,r){"use strict";Object.defineProperty(r,"__esModule",{value:!0}),r.default=function(e){return(0,a.createStore)(s.default,e,u&&u())};var n=e("./connect");Object.keys(n).forEach(function(e){"default"!==e&&"__esModule"!==e&&Object.defineProperty(r,e,{enumerable:!0,get:function(){return n[e]}})});var o,a=e("redux"),i=e("../reducers"),s=(o=i)&&o.__esModule?o:{default:o};var u=window.devToolsExtension},{"../reducers":6,"./connect":7,redux:26}],9:[function(e,t,r){"use strict";Object.defineProperty(r,"__esModule",{value:!0}),r.observeStore=r.raiseEvent=r.parseJson=r.parseNameFromUrl=r.parseURL=r.request=r.getCookie=void 0;e("../constants"),r.getCookie=function(e){var t=("; "+document.cookie).split("; "+e+"=");if(2==t.length)return t.pop().split(";").shift()},r.request=function(e,t,r,n,o,a,i){var s=new XMLHttpRequest;s.open(e,t,!0),Object.keys(n).forEach(function(e){s.setRequestHeader(e,n[e])}),s.onload=function(){a(s.status,s.responseText)},i&&(s.onerror=s.onabort=function(){i(s.status,s.responseText)}),o&&(s.upload.onprogress=function(e){o(e)}),s.send(r)},r.parseURL=function(e){var t=(new DOMParser).parseFromString(e,"text/xml").getElementsByTagName("Location")[0];return decodeURIComponent(t.childNodes[0].nodeValue)},r.parseNameFromUrl=function(e){return decodeURIComponent((e+"").replace(/\+/g,"%20"))},r.parseJson=function(e){var t;try{t=JSON.parse(e)}catch(e){t=null}return t},r.raiseEvent=function(e,t,r){if(window.CustomEvent){var n=new CustomEvent(t,{detail:r,bubbles:!0});e.dispatchEvent(n)}},r.observeStore=function(e,t,r){var n=void 0;function o(){var o=t(e.getState());o!==n&&r(n=o)}var a=e.subscribe(o);return o(),a}},{"../constants":3}],10:[function(e,t,r){"use strict";var n,o=e("./store"),a=(n=o)&&n.__esModule?n:{default:n},i=e("./components");function s(e){var t=".s3upload";e.detail&&e.detail.selector&&(t=e.detail.selector);for(var r=document.querySelectorAll(t),n=0;n0&&void 0!==arguments[0]?arguments[0]:{},t=arguments[1];if(s)throw s;for(var n,o,a,u=!1,l={},d=0;d') > -1) {
129 | store.dispatch(addError(_constants.i18n_strings.no_file_too_small));
130 | } else if (xml.indexOf('') > -1) {
131 | store.dispatch(addError(_constants.i18n_strings.no_file_too_large));
132 | } else {
133 | store.dispatch(addError(_constants.i18n_strings.no_upload_failed));
134 | }
135 |
136 | break;
137 | }
138 | };
139 |
140 | var onError = function onError(status, xml) {
141 | console.error('Error uploading', status, xml);
142 | (0, _utils.raiseEvent)((0, _store.getElement)(store), 's3upload:error', { status: status, xml: xml });
143 |
144 | store.dispatch(didNotCompleteUploadToAWS());
145 | store.dispatch(addError(_constants.i18n_strings.no_upload_failed));
146 | };
147 |
148 | var onProgress = function onProgress(data) {
149 | var progress = null;
150 |
151 | if (data.lengthComputable) {
152 | progress = Math.round(data.loaded * 100 / data.total);
153 | }
154 |
155 | store.dispatch(updateProgress(progress));
156 | (0, _utils.raiseEvent)((0, _store.getElement)(store), 's3upload:progress-updated', { progress: progress });
157 | };
158 |
159 | (0, _utils.request)('POST', url, form, headers, onProgress, onLoad, onError);
160 |
161 | return {
162 | type: _constants2.default.BEGIN_UPLOAD_TO_AWS
163 | };
164 | };
165 |
166 | var completeUploadToAWS = exports.completeUploadToAWS = function completeUploadToAWS(filename, url) {
167 | return {
168 | type: _constants2.default.COMPLETE_UPLOAD_TO_AWS,
169 | url: url,
170 | filename: filename
171 | };
172 | };
173 |
174 | var didNotCompleteUploadToAWS = exports.didNotCompleteUploadToAWS = function didNotCompleteUploadToAWS() {
175 | return {
176 | type: _constants2.default.DID_NOT_COMPLETE_UPLOAD_TO_AWS
177 | };
178 | };
179 |
180 | var addError = exports.addError = function addError(error) {
181 | return {
182 | type: _constants2.default.ADD_ERROR,
183 | error: error
184 | };
185 | };
186 |
187 | var clearErrors = exports.clearErrors = function clearErrors() {
188 | return {
189 | type: _constants2.default.CLEAR_ERRORS
190 | };
191 | };
192 |
193 | var updateProgress = exports.updateProgress = function updateProgress() {
194 | var progress = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
195 |
196 | return {
197 | type: _constants2.default.UPDATE_PROGRESS,
198 | progress: progress
199 | };
200 | };
201 |
202 | },{"../constants":3,"../store":8,"../utils":9}],2:[function(require,module,exports){
203 | 'use strict';
204 |
205 | Object.defineProperty(exports, "__esModule", {
206 | value: true
207 | });
208 | exports.View = undefined;
209 |
210 | var _actions = require('../actions');
211 |
212 | var _store = require('../store');
213 |
214 | var _utils = require('../utils');
215 |
216 | var View = function View(element, store) {
217 | return {
218 | renderFilename: function renderFilename() {
219 | var filename = (0, _store.getFilename)(store),
220 | url = (0, _store.getUrl)(store);
221 |
222 | if (filename && url) {
223 | this.$link.innerHTML = filename;
224 | this.$link.setAttribute('href', url);
225 | this.$url.value = url.split("?")[0];
226 |
227 | this.$element.classList.add('link-active');
228 | this.$element.classList.remove('form-active');
229 | } else {
230 | this.$url.value = '';
231 | this.$input.value = '';
232 |
233 | this.$element.classList.add('form-active');
234 | this.$element.classList.remove('link-active');
235 | }
236 | },
237 |
238 | renderError: function renderError() {
239 | var error = (0, _store.getError)(store);
240 |
241 | if (error) {
242 | this.$element.classList.add('has-error');
243 | this.$element.classList.add('form-active');
244 | this.$element.classList.remove('link-active');
245 |
246 | this.$element.querySelector('.s3upload__file-input').value = '';
247 | this.$element.querySelector('.s3upload__error').innerHTML = error;
248 | } else {
249 | this.$element.classList.remove('has-error');
250 | this.$element.querySelector('.s3upload__error').innerHTML = '';
251 | }
252 | },
253 |
254 | renderUploadProgress: function renderUploadProgress() {
255 | var uploadProgress = (0, _store.getUploadProgress)(store);
256 |
257 | if (uploadProgress > 0) {
258 | this.$element.classList.add('progress-active');
259 | this.$bar.style.width = uploadProgress + '%';
260 | } else {
261 | this.$element.classList.remove('progress-active');
262 | this.$bar.style.width = '0';
263 | }
264 | },
265 |
266 | removeUpload: function removeUpload(event) {
267 | event.preventDefault();
268 |
269 | store.dispatch((0, _actions.updateProgress)());
270 | store.dispatch((0, _actions.removeUpload)());
271 | (0, _utils.raiseEvent)(this.$element, 's3upload:clear-upload');
272 | },
273 |
274 | getUploadURL: function getUploadURL(event) {
275 | var file = this.$input.files[0],
276 | dest = this.$dest.value,
277 | url = this.$element.getAttribute('data-policy-url');
278 |
279 | store.dispatch((0, _actions.clearErrors)());
280 | store.dispatch((0, _actions.getUploadURL)(file, dest, url, store));
281 | },
282 |
283 | init: function init() {
284 | // cache all the query selectors
285 | // $variables represent DOM elements
286 | this.$element = element;
287 | this.$url = element.querySelector('.s3upload__file-url');
288 | this.$input = element.querySelector('.s3upload__file-input');
289 | this.$remove = element.querySelector('.s3upload__file-remove');
290 | this.$dest = element.querySelector('.s3upload__file-dest');
291 | this.$link = element.querySelector('.s3upload__file-link');
292 | this.$error = element.querySelector('.s3upload__error');
293 | this.$bar = element.querySelector('.s3upload__bar');
294 |
295 | // set initial DOM states3upload__
296 | var status = this.$url.value === '' ? 'form' : 'link';
297 | this.$element.classList.add(status + '-active');
298 |
299 | // add event listeners
300 | this.$remove.addEventListener('click', this.removeUpload.bind(this));
301 | this.$input.addEventListener('change', this.getUploadURL.bind(this));
302 |
303 | // these three observers subscribe to the store, but only trigger their
304 | // callbacks when the specific piece of state they observe changes.
305 | // this allows for a less naive approach to rendering changes than a
306 | // render method subscribed to the whole state.
307 | var filenameObserver = (0, _utils.observeStore)(store, function (state) {
308 | return state.appStatus.filename;
309 | }, this.renderFilename.bind(this));
310 |
311 | var errorObserver = (0, _utils.observeStore)(store, function (state) {
312 | return state.appStatus.error;
313 | }, this.renderError.bind(this));
314 |
315 | var uploadProgressObserver = (0, _utils.observeStore)(store, function (state) {
316 | return state.appStatus.uploadProgress;
317 | }, this.renderUploadProgress.bind(this));
318 | }
319 | };
320 | };
321 |
322 | exports.View = View;
323 |
324 | },{"../actions":1,"../store":8,"../utils":9}],3:[function(require,module,exports){
325 | 'use strict';
326 |
327 | Object.defineProperty(exports, "__esModule", {
328 | value: true
329 | });
330 | exports.default = {
331 | REQUEST_AWS_UPLOAD_PARAMS: 'REQUEST_AWS_UPLOAD_PARAMS',
332 | RECEIVE_AWS_UPLOAD_PARAMS: 'RECEIVE_AWS_UPLOAD_PARAMS',
333 | DID_NOT_RECEIVE_AWS_UPLOAD_PARAMS: 'DID_NOT_RECEIVE_AWS_UPLOAD_PARAMS',
334 | REMOVE_UPLOAD: 'REMOVE_UPLOAD',
335 | BEGIN_UPLOAD_TO_AWS: 'BEGIN_UPLOAD_TO_AWS',
336 | COMPLETE_UPLOAD_TO_AWS: 'COMPLETE_UPLOAD_TO_AWS',
337 | DID_NOT_COMPLETE_UPLOAD_TO_AWS: 'DID_NOT_COMPLETE_UPLOAD_TO_AWS',
338 | ADD_ERROR: 'ADD_ERROR',
339 | CLEAR_ERRORS: 'CLEAR_ERRORS',
340 | UPDATE_PROGRESS: 'UPDATE_PROGRESS',
341 | RECEIVE_SIGNED_URL: 'RECEIVE_SIGNED_URL'
342 | };
343 |
344 |
345 | var i18n_strings = void 0;
346 |
347 | try {
348 | exports.i18n_strings = i18n_strings = djangoS3Upload.i18n_strings;
349 | } catch (e) {
350 | exports.i18n_strings = i18n_strings = {
351 | "no_upload_failed": "Sorry, failed to upload file.",
352 | "no_upload_url": "Sorry, could not get upload URL.",
353 | "no_file_too_large": "Sorry, the file is too large to be uploaded.",
354 | "no_file_too_small": "Sorry, the file is too small to be uploaded."
355 | };
356 | }
357 |
358 | exports.i18n_strings = i18n_strings;
359 |
360 | },{}],4:[function(require,module,exports){
361 | 'use strict';
362 |
363 | Object.defineProperty(exports, "__esModule", {
364 | value: true
365 | });
366 |
367 | var _constants = require('../constants');
368 |
369 | var _constants2 = _interopRequireDefault(_constants);
370 |
371 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
372 |
373 | exports.default = function () {
374 | var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
375 | var action = arguments[1];
376 |
377 | switch (action.type) {
378 | case _constants2.default.BEGIN_UPLOAD_TO_AWS:
379 | return Object.assign({}, state, {
380 | isUploading: true
381 | });
382 | case _constants2.default.COMPLETE_UPLOAD_TO_AWS:
383 | return Object.assign({}, state, {
384 | isUploading: false,
385 | uploadProgress: 0,
386 | filename: action.filename,
387 | url: action.url
388 | });
389 | case _constants2.default.DID_NOT_COMPLETE_UPLOAD_TO_AWS:
390 | return Object.assign({}, state, {
391 | isUploading: false
392 | });
393 | case _constants2.default.REMOVE_UPLOAD:
394 | return Object.assign({}, state, {
395 | filename: null,
396 | url: null
397 | });
398 | case _constants2.default.ADD_ERROR:
399 | return Object.assign({}, state, {
400 | error: action.error
401 | });
402 | case _constants2.default.CLEAR_ERRORS:
403 | return Object.assign({}, state, {
404 | error: null
405 | });
406 | case _constants2.default.UPDATE_PROGRESS:
407 | return Object.assign({}, state, {
408 | uploadProgress: action.progress
409 | });
410 | case _constants2.default.RECEIVE_SIGNED_URL:
411 | {
412 | return Object.assign({}, state, {
413 | signedURL: action.signedURL
414 | });
415 | }
416 |
417 | default:
418 | return state;
419 | }
420 | };
421 |
422 | },{"../constants":3}],5:[function(require,module,exports){
423 | 'use strict';
424 |
425 | Object.defineProperty(exports, "__esModule", {
426 | value: true
427 | });
428 |
429 | var _constants = require('../constants');
430 |
431 | var _constants2 = _interopRequireDefault(_constants);
432 |
433 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
434 |
435 | exports.default = function () {
436 | var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
437 | var action = arguments[1];
438 |
439 | switch (action.type) {
440 | case _constants2.default.REQUEST_AWS_UPLOAD_PARAMS:
441 | return Object.assign({}, state, {
442 | isLoading: true
443 | });
444 | case _constants2.default.RECEIVE_AWS_UPLOAD_PARAMS:
445 | return Object.assign({}, state, {
446 | isLoading: false,
447 | AWSPayload: action.aws_payload
448 | });
449 | case _constants2.default.DID_NOT_RECEIVE_AWS_UPLOAD_PARAMS:
450 | // Returns current state and sets loading to false
451 | return Object.assign({}, state, {
452 | isLoading: false
453 | });
454 | default:
455 | return state; // reducer must return by default
456 | }
457 | };
458 |
459 | },{"../constants":3}],6:[function(require,module,exports){
460 | 'use strict';
461 |
462 | Object.defineProperty(exports, "__esModule", {
463 | value: true
464 | });
465 |
466 | var _redux = require('redux');
467 |
468 | var _awsUploadsParams = require('./awsUploadsParams');
469 |
470 | var _awsUploadsParams2 = _interopRequireDefault(_awsUploadsParams);
471 |
472 | var _appStatus = require('./appStatus');
473 |
474 | var _appStatus2 = _interopRequireDefault(_appStatus);
475 |
476 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
477 |
478 | var element = function element() {
479 | var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
480 | var action = arguments[1];
481 |
482 | return state; // reducer must return by default
483 | };
484 |
485 | exports.default = (0, _redux.combineReducers)({
486 | AWSUploadParams: _awsUploadsParams2.default,
487 | appStatus: _appStatus2.default,
488 | element: element
489 | });
490 |
491 | },{"./appStatus":4,"./awsUploadsParams":5,"redux":26}],7:[function(require,module,exports){
492 | "use strict";
493 |
494 | Object.defineProperty(exports, "__esModule", {
495 | value: true
496 | });
497 | exports.getFilename = getFilename;
498 | exports.getUrl = getUrl;
499 | exports.getError = getError;
500 | exports.getUploadProgress = getUploadProgress;
501 | exports.getElement = getElement;
502 | exports.getAWSPayload = getAWSPayload;
503 | function getFilename(store) {
504 | return store.getState().appStatus.filename;
505 | }
506 |
507 | function getUrl(store) {
508 | var url = store.getState().appStatus.signedURL || store.getState().appStatus.url;
509 |
510 | return url;
511 | }
512 |
513 | function getError(store) {
514 | return store.getState().appStatus.error;
515 | }
516 |
517 | function getUploadProgress(store) {
518 | return store.getState().appStatus.uploadProgress;
519 | }
520 |
521 | function getElement(store) {
522 | return store.getState().element;
523 | }
524 |
525 | function getAWSPayload(store) {
526 | return store.getState().AWSUploadParams.AWSPayload;
527 | }
528 |
529 | },{}],8:[function(require,module,exports){
530 | 'use strict';
531 |
532 | Object.defineProperty(exports, "__esModule", {
533 | value: true
534 | });
535 | exports.default = configureStore;
536 |
537 | var _connect = require('./connect');
538 |
539 | Object.keys(_connect).forEach(function (key) {
540 | if (key === "default" || key === "__esModule") return;
541 | Object.defineProperty(exports, key, {
542 | enumerable: true,
543 | get: function get() {
544 | return _connect[key];
545 | }
546 | });
547 | });
548 |
549 | var _redux = require('redux');
550 |
551 | var _reducers = require('../reducers');
552 |
553 | var _reducers2 = _interopRequireDefault(_reducers);
554 |
555 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
556 |
557 | var _window = window,
558 | devToolsExtension = _window.devToolsExtension;
559 | function configureStore(initialState) {
560 | return (0, _redux.createStore)(_reducers2.default, initialState, devToolsExtension && devToolsExtension());
561 | }
562 |
563 | },{"../reducers":6,"./connect":7,"redux":26}],9:[function(require,module,exports){
564 | 'use strict';
565 |
566 | Object.defineProperty(exports, "__esModule", {
567 | value: true
568 | });
569 | exports.observeStore = exports.raiseEvent = exports.parseJson = exports.parseNameFromUrl = exports.parseURL = exports.request = exports.getCookie = undefined;
570 |
571 | var _constants = require('../constants');
572 |
573 | var getCookie = exports.getCookie = function getCookie(name) {
574 | var value = '; ' + document.cookie,
575 | parts = value.split('; ' + name + '=');
576 | if (parts.length == 2) return parts.pop().split(';').shift();
577 | };
578 |
579 | var request = exports.request = function request(method, url, data, headers, onProgress, onLoad, onError) {
580 | var request = new XMLHttpRequest();
581 | request.open(method, url, true);
582 |
583 | Object.keys(headers).forEach(function (key) {
584 | request.setRequestHeader(key, headers[key]);
585 | });
586 |
587 | request.onload = function () {
588 | onLoad(request.status, request.responseText);
589 | };
590 |
591 | if (onError) {
592 | request.onerror = request.onabort = function () {
593 | onError(request.status, request.responseText);
594 | };
595 | }
596 |
597 | if (onProgress) {
598 | request.upload.onprogress = function (data) {
599 | onProgress(data);
600 | };
601 | }
602 |
603 | request.send(data);
604 | };
605 |
606 | var parseURL = exports.parseURL = function parseURL(text) {
607 | var xml = new DOMParser().parseFromString(text, 'text/xml'),
608 | tag = xml.getElementsByTagName('Location')[0],
609 | url = decodeURIComponent(tag.childNodes[0].nodeValue);
610 |
611 | return url;
612 | };
613 |
614 | var parseNameFromUrl = exports.parseNameFromUrl = function parseNameFromUrl(url) {
615 | return decodeURIComponent((url + '').replace(/\+/g, '%20'));
616 | };
617 |
618 | var parseJson = exports.parseJson = function parseJson(json) {
619 | var data;
620 |
621 | try {
622 | data = JSON.parse(json);
623 | } catch (error) {
624 | data = null;
625 | };
626 |
627 | return data;
628 | };
629 |
630 | var raiseEvent = exports.raiseEvent = function raiseEvent(element, name, detail) {
631 | if (window.CustomEvent) {
632 | var event = new CustomEvent(name, { detail: detail, bubbles: true });
633 | element.dispatchEvent(event);
634 | }
635 | };
636 |
637 | var observeStore = exports.observeStore = function observeStore(store, select, onChange) {
638 | var currentState = void 0;
639 |
640 | function handleChange() {
641 | var nextState = select(store.getState());
642 |
643 | if (nextState !== currentState) {
644 | currentState = nextState;
645 | onChange(currentState);
646 | }
647 | }
648 |
649 | var unsubscribe = store.subscribe(handleChange);
650 | handleChange();
651 | return unsubscribe;
652 | };
653 |
654 | },{"../constants":3}],10:[function(require,module,exports){
655 | 'use strict';
656 |
657 | var _store = require('./store');
658 |
659 | var _store2 = _interopRequireDefault(_store);
660 |
661 | var _components = require('./components');
662 |
663 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
664 |
665 | // by default initHandler inits on '.s3upload', but if passed a custom
666 | // selector in the event data, it will init on that instead.
667 | function initHandler(event) {
668 | var selector = '.s3upload';
669 |
670 | if (event.detail && event.detail.selector) {
671 | selector = event.detail.selector;
672 | }
673 |
674 | var elements = document.querySelectorAll(selector);
675 |
676 | // safari doesn't like forEach on nodeList objects
677 | for (var i = 0; i < elements.length; i++) {
678 | // initialise instance for each element
679 | var element = elements[i];
680 | var store = (0, _store2.default)({ element: element });
681 | var view = new _components.View(element, store);
682 | view.init();
683 | }
684 | }
685 |
686 | // default global init on document ready
687 | document.addEventListener('DOMContentLoaded', initHandler);
688 |
689 | // custom event listener for use in async init
690 | document.addEventListener('s3upload:init', initHandler);
691 |
692 | },{"./components":2,"./store":8}],11:[function(require,module,exports){
693 | var root = require('./_root');
694 |
695 | /** Built-in value references. */
696 | var Symbol = root.Symbol;
697 |
698 | module.exports = Symbol;
699 |
700 | },{"./_root":18}],12:[function(require,module,exports){
701 | var Symbol = require('./_Symbol'),
702 | getRawTag = require('./_getRawTag'),
703 | objectToString = require('./_objectToString');
704 |
705 | /** `Object#toString` result references. */
706 | var nullTag = '[object Null]',
707 | undefinedTag = '[object Undefined]';
708 |
709 | /** Built-in value references. */
710 | var symToStringTag = Symbol ? Symbol.toStringTag : undefined;
711 |
712 | /**
713 | * The base implementation of `getTag` without fallbacks for buggy environments.
714 | *
715 | * @private
716 | * @param {*} value The value to query.
717 | * @returns {string} Returns the `toStringTag`.
718 | */
719 | function baseGetTag(value) {
720 | if (value == null) {
721 | return value === undefined ? undefinedTag : nullTag;
722 | }
723 | return (symToStringTag && symToStringTag in Object(value))
724 | ? getRawTag(value)
725 | : objectToString(value);
726 | }
727 |
728 | module.exports = baseGetTag;
729 |
730 | },{"./_Symbol":11,"./_getRawTag":15,"./_objectToString":16}],13:[function(require,module,exports){
731 | (function (global){
732 | /** Detect free variable `global` from Node.js. */
733 | var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;
734 |
735 | module.exports = freeGlobal;
736 |
737 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
738 | },{}],14:[function(require,module,exports){
739 | var overArg = require('./_overArg');
740 |
741 | /** Built-in value references. */
742 | var getPrototype = overArg(Object.getPrototypeOf, Object);
743 |
744 | module.exports = getPrototype;
745 |
746 | },{"./_overArg":17}],15:[function(require,module,exports){
747 | var Symbol = require('./_Symbol');
748 |
749 | /** Used for built-in method references. */
750 | var objectProto = Object.prototype;
751 |
752 | /** Used to check objects for own properties. */
753 | var hasOwnProperty = objectProto.hasOwnProperty;
754 |
755 | /**
756 | * Used to resolve the
757 | * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
758 | * of values.
759 | */
760 | var nativeObjectToString = objectProto.toString;
761 |
762 | /** Built-in value references. */
763 | var symToStringTag = Symbol ? Symbol.toStringTag : undefined;
764 |
765 | /**
766 | * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values.
767 | *
768 | * @private
769 | * @param {*} value The value to query.
770 | * @returns {string} Returns the raw `toStringTag`.
771 | */
772 | function getRawTag(value) {
773 | var isOwn = hasOwnProperty.call(value, symToStringTag),
774 | tag = value[symToStringTag];
775 |
776 | try {
777 | value[symToStringTag] = undefined;
778 | var unmasked = true;
779 | } catch (e) {}
780 |
781 | var result = nativeObjectToString.call(value);
782 | if (unmasked) {
783 | if (isOwn) {
784 | value[symToStringTag] = tag;
785 | } else {
786 | delete value[symToStringTag];
787 | }
788 | }
789 | return result;
790 | }
791 |
792 | module.exports = getRawTag;
793 |
794 | },{"./_Symbol":11}],16:[function(require,module,exports){
795 | /** Used for built-in method references. */
796 | var objectProto = Object.prototype;
797 |
798 | /**
799 | * Used to resolve the
800 | * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
801 | * of values.
802 | */
803 | var nativeObjectToString = objectProto.toString;
804 |
805 | /**
806 | * Converts `value` to a string using `Object.prototype.toString`.
807 | *
808 | * @private
809 | * @param {*} value The value to convert.
810 | * @returns {string} Returns the converted string.
811 | */
812 | function objectToString(value) {
813 | return nativeObjectToString.call(value);
814 | }
815 |
816 | module.exports = objectToString;
817 |
818 | },{}],17:[function(require,module,exports){
819 | /**
820 | * Creates a unary function that invokes `func` with its argument transformed.
821 | *
822 | * @private
823 | * @param {Function} func The function to wrap.
824 | * @param {Function} transform The argument transform.
825 | * @returns {Function} Returns the new function.
826 | */
827 | function overArg(func, transform) {
828 | return function(arg) {
829 | return func(transform(arg));
830 | };
831 | }
832 |
833 | module.exports = overArg;
834 |
835 | },{}],18:[function(require,module,exports){
836 | var freeGlobal = require('./_freeGlobal');
837 |
838 | /** Detect free variable `self`. */
839 | var freeSelf = typeof self == 'object' && self && self.Object === Object && self;
840 |
841 | /** Used as a reference to the global object. */
842 | var root = freeGlobal || freeSelf || Function('return this')();
843 |
844 | module.exports = root;
845 |
846 | },{"./_freeGlobal":13}],19:[function(require,module,exports){
847 | /**
848 | * Checks if `value` is object-like. A value is object-like if it's not `null`
849 | * and has a `typeof` result of "object".
850 | *
851 | * @static
852 | * @memberOf _
853 | * @since 4.0.0
854 | * @category Lang
855 | * @param {*} value The value to check.
856 | * @returns {boolean} Returns `true` if `value` is object-like, else `false`.
857 | * @example
858 | *
859 | * _.isObjectLike({});
860 | * // => true
861 | *
862 | * _.isObjectLike([1, 2, 3]);
863 | * // => true
864 | *
865 | * _.isObjectLike(_.noop);
866 | * // => false
867 | *
868 | * _.isObjectLike(null);
869 | * // => false
870 | */
871 | function isObjectLike(value) {
872 | return value != null && typeof value == 'object';
873 | }
874 |
875 | module.exports = isObjectLike;
876 |
877 | },{}],20:[function(require,module,exports){
878 | var baseGetTag = require('./_baseGetTag'),
879 | getPrototype = require('./_getPrototype'),
880 | isObjectLike = require('./isObjectLike');
881 |
882 | /** `Object#toString` result references. */
883 | var objectTag = '[object Object]';
884 |
885 | /** Used for built-in method references. */
886 | var funcProto = Function.prototype,
887 | objectProto = Object.prototype;
888 |
889 | /** Used to resolve the decompiled source of functions. */
890 | var funcToString = funcProto.toString;
891 |
892 | /** Used to check objects for own properties. */
893 | var hasOwnProperty = objectProto.hasOwnProperty;
894 |
895 | /** Used to infer the `Object` constructor. */
896 | var objectCtorString = funcToString.call(Object);
897 |
898 | /**
899 | * Checks if `value` is a plain object, that is, an object created by the
900 | * `Object` constructor or one with a `[[Prototype]]` of `null`.
901 | *
902 | * @static
903 | * @memberOf _
904 | * @since 0.8.0
905 | * @category Lang
906 | * @param {*} value The value to check.
907 | * @returns {boolean} Returns `true` if `value` is a plain object, else `false`.
908 | * @example
909 | *
910 | * function Foo() {
911 | * this.a = 1;
912 | * }
913 | *
914 | * _.isPlainObject(new Foo);
915 | * // => false
916 | *
917 | * _.isPlainObject([1, 2, 3]);
918 | * // => false
919 | *
920 | * _.isPlainObject({ 'x': 0, 'y': 0 });
921 | * // => true
922 | *
923 | * _.isPlainObject(Object.create(null));
924 | * // => true
925 | */
926 | function isPlainObject(value) {
927 | if (!isObjectLike(value) || baseGetTag(value) != objectTag) {
928 | return false;
929 | }
930 | var proto = getPrototype(value);
931 | if (proto === null) {
932 | return true;
933 | }
934 | var Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor;
935 | return typeof Ctor == 'function' && Ctor instanceof Ctor &&
936 | funcToString.call(Ctor) == objectCtorString;
937 | }
938 |
939 | module.exports = isPlainObject;
940 |
941 | },{"./_baseGetTag":12,"./_getPrototype":14,"./isObjectLike":19}],21:[function(require,module,exports){
942 | 'use strict';
943 |
944 | exports.__esModule = true;
945 |
946 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
947 |
948 | exports['default'] = applyMiddleware;
949 |
950 | var _compose = require('./compose');
951 |
952 | var _compose2 = _interopRequireDefault(_compose);
953 |
954 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
955 |
956 | /**
957 | * Creates a store enhancer that applies middleware to the dispatch method
958 | * of the Redux store. This is handy for a variety of tasks, such as expressing
959 | * asynchronous actions in a concise manner, or logging every action payload.
960 | *
961 | * See `redux-thunk` package as an example of the Redux middleware.
962 | *
963 | * Because middleware is potentially asynchronous, this should be the first
964 | * store enhancer in the composition chain.
965 | *
966 | * Note that each middleware will be given the `dispatch` and `getState` functions
967 | * as named arguments.
968 | *
969 | * @param {...Function} middlewares The middleware chain to be applied.
970 | * @returns {Function} A store enhancer applying the middleware.
971 | */
972 | function applyMiddleware() {
973 | for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
974 | middlewares[_key] = arguments[_key];
975 | }
976 |
977 | return function (createStore) {
978 | return function (reducer, preloadedState, enhancer) {
979 | var store = createStore(reducer, preloadedState, enhancer);
980 | var _dispatch = store.dispatch;
981 | var chain = [];
982 |
983 | var middlewareAPI = {
984 | getState: store.getState,
985 | dispatch: function dispatch(action) {
986 | return _dispatch(action);
987 | }
988 | };
989 | chain = middlewares.map(function (middleware) {
990 | return middleware(middlewareAPI);
991 | });
992 | _dispatch = _compose2['default'].apply(undefined, chain)(store.dispatch);
993 |
994 | return _extends({}, store, {
995 | dispatch: _dispatch
996 | });
997 | };
998 | };
999 | }
1000 | },{"./compose":24}],22:[function(require,module,exports){
1001 | 'use strict';
1002 |
1003 | exports.__esModule = true;
1004 | exports['default'] = bindActionCreators;
1005 | function bindActionCreator(actionCreator, dispatch) {
1006 | return function () {
1007 | return dispatch(actionCreator.apply(undefined, arguments));
1008 | };
1009 | }
1010 |
1011 | /**
1012 | * Turns an object whose values are action creators, into an object with the
1013 | * same keys, but with every function wrapped into a `dispatch` call so they
1014 | * may be invoked directly. This is just a convenience method, as you can call
1015 | * `store.dispatch(MyActionCreators.doSomething())` yourself just fine.
1016 | *
1017 | * For convenience, you can also pass a single function as the first argument,
1018 | * and get a function in return.
1019 | *
1020 | * @param {Function|Object} actionCreators An object whose values are action
1021 | * creator functions. One handy way to obtain it is to use ES6 `import * as`
1022 | * syntax. You may also pass a single function.
1023 | *
1024 | * @param {Function} dispatch The `dispatch` function available on your Redux
1025 | * store.
1026 | *
1027 | * @returns {Function|Object} The object mimicking the original object, but with
1028 | * every action creator wrapped into the `dispatch` call. If you passed a
1029 | * function as `actionCreators`, the return value will also be a single
1030 | * function.
1031 | */
1032 | function bindActionCreators(actionCreators, dispatch) {
1033 | if (typeof actionCreators === 'function') {
1034 | return bindActionCreator(actionCreators, dispatch);
1035 | }
1036 |
1037 | if (typeof actionCreators !== 'object' || actionCreators === null) {
1038 | throw new Error('bindActionCreators expected an object or a function, instead received ' + (actionCreators === null ? 'null' : typeof actionCreators) + '. ' + 'Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?');
1039 | }
1040 |
1041 | var keys = Object.keys(actionCreators);
1042 | var boundActionCreators = {};
1043 | for (var i = 0; i < keys.length; i++) {
1044 | var key = keys[i];
1045 | var actionCreator = actionCreators[key];
1046 | if (typeof actionCreator === 'function') {
1047 | boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);
1048 | }
1049 | }
1050 | return boundActionCreators;
1051 | }
1052 | },{}],23:[function(require,module,exports){
1053 | 'use strict';
1054 |
1055 | exports.__esModule = true;
1056 | exports['default'] = combineReducers;
1057 |
1058 | var _createStore = require('./createStore');
1059 |
1060 | var _isPlainObject = require('lodash/isPlainObject');
1061 |
1062 | var _isPlainObject2 = _interopRequireDefault(_isPlainObject);
1063 |
1064 | var _warning = require('./utils/warning');
1065 |
1066 | var _warning2 = _interopRequireDefault(_warning);
1067 |
1068 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
1069 |
1070 | function getUndefinedStateErrorMessage(key, action) {
1071 | var actionType = action && action.type;
1072 | var actionName = actionType && '"' + actionType.toString() + '"' || 'an action';
1073 |
1074 | return 'Given action ' + actionName + ', reducer "' + key + '" returned undefined. ' + 'To ignore an action, you must explicitly return the previous state. ' + 'If you want this reducer to hold no value, you can return null instead of undefined.';
1075 | }
1076 |
1077 | function getUnexpectedStateShapeWarningMessage(inputState, reducers, action, unexpectedKeyCache) {
1078 | var reducerKeys = Object.keys(reducers);
1079 | var argumentName = action && action.type === _createStore.ActionTypes.INIT ? 'preloadedState argument passed to createStore' : 'previous state received by the reducer';
1080 |
1081 | if (reducerKeys.length === 0) {
1082 | return 'Store does not have a valid reducer. Make sure the argument passed ' + 'to combineReducers is an object whose values are reducers.';
1083 | }
1084 |
1085 | if (!(0, _isPlainObject2['default'])(inputState)) {
1086 | return 'The ' + argumentName + ' has unexpected type of "' + {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] + '". Expected argument to be an object with the following ' + ('keys: "' + reducerKeys.join('", "') + '"');
1087 | }
1088 |
1089 | var unexpectedKeys = Object.keys(inputState).filter(function (key) {
1090 | return !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key];
1091 | });
1092 |
1093 | unexpectedKeys.forEach(function (key) {
1094 | unexpectedKeyCache[key] = true;
1095 | });
1096 |
1097 | if (unexpectedKeys.length > 0) {
1098 | return 'Unexpected ' + (unexpectedKeys.length > 1 ? 'keys' : 'key') + ' ' + ('"' + unexpectedKeys.join('", "') + '" found in ' + argumentName + '. ') + 'Expected to find one of the known reducer keys instead: ' + ('"' + reducerKeys.join('", "') + '". Unexpected keys will be ignored.');
1099 | }
1100 | }
1101 |
1102 | function assertReducerShape(reducers) {
1103 | Object.keys(reducers).forEach(function (key) {
1104 | var reducer = reducers[key];
1105 | var initialState = reducer(undefined, { type: _createStore.ActionTypes.INIT });
1106 |
1107 | if (typeof initialState === 'undefined') {
1108 | throw new Error('Reducer "' + key + '" returned undefined during initialization. ' + 'If the state passed to the reducer is undefined, you must ' + 'explicitly return the initial state. The initial state may ' + 'not be undefined. If you don\'t want to set a value for this reducer, ' + 'you can use null instead of undefined.');
1109 | }
1110 |
1111 | var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.');
1112 | if (typeof reducer(undefined, { type: type }) === 'undefined') {
1113 | throw new Error('Reducer "' + key + '" returned undefined when probed with a random type. ' + ('Don\'t try to handle ' + _createStore.ActionTypes.INIT + ' or other actions in "redux/*" ') + 'namespace. They are considered private. Instead, you must return the ' + 'current state for any unknown actions, unless it is undefined, ' + 'in which case you must return the initial state, regardless of the ' + 'action type. The initial state may not be undefined, but can be null.');
1114 | }
1115 | });
1116 | }
1117 |
1118 | /**
1119 | * Turns an object whose values are different reducer functions, into a single
1120 | * reducer function. It will call every child reducer, and gather their results
1121 | * into a single state object, whose keys correspond to the keys of the passed
1122 | * reducer functions.
1123 | *
1124 | * @param {Object} reducers An object whose values correspond to different
1125 | * reducer functions that need to be combined into one. One handy way to obtain
1126 | * it is to use ES6 `import * as reducers` syntax. The reducers may never return
1127 | * undefined for any action. Instead, they should return their initial state
1128 | * if the state passed to them was undefined, and the current state for any
1129 | * unrecognized action.
1130 | *
1131 | * @returns {Function} A reducer function that invokes every reducer inside the
1132 | * passed object, and builds a state object with the same shape.
1133 | */
1134 | function combineReducers(reducers) {
1135 | var reducerKeys = Object.keys(reducers);
1136 | var finalReducers = {};
1137 | for (var i = 0; i < reducerKeys.length; i++) {
1138 | var key = reducerKeys[i];
1139 |
1140 | if ("production" !== 'production') {
1141 | if (typeof reducers[key] === 'undefined') {
1142 | (0, _warning2['default'])('No reducer provided for key "' + key + '"');
1143 | }
1144 | }
1145 |
1146 | if (typeof reducers[key] === 'function') {
1147 | finalReducers[key] = reducers[key];
1148 | }
1149 | }
1150 | var finalReducerKeys = Object.keys(finalReducers);
1151 |
1152 | var unexpectedKeyCache = void 0;
1153 | if ("production" !== 'production') {
1154 | unexpectedKeyCache = {};
1155 | }
1156 |
1157 | var shapeAssertionError = void 0;
1158 | try {
1159 | assertReducerShape(finalReducers);
1160 | } catch (e) {
1161 | shapeAssertionError = e;
1162 | }
1163 |
1164 | return function combination() {
1165 | var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
1166 | var action = arguments[1];
1167 |
1168 | if (shapeAssertionError) {
1169 | throw shapeAssertionError;
1170 | }
1171 |
1172 | if ("production" !== 'production') {
1173 | var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache);
1174 | if (warningMessage) {
1175 | (0, _warning2['default'])(warningMessage);
1176 | }
1177 | }
1178 |
1179 | var hasChanged = false;
1180 | var nextState = {};
1181 | for (var _i = 0; _i < finalReducerKeys.length; _i++) {
1182 | var _key = finalReducerKeys[_i];
1183 | var reducer = finalReducers[_key];
1184 | var previousStateForKey = state[_key];
1185 | var nextStateForKey = reducer(previousStateForKey, action);
1186 | if (typeof nextStateForKey === 'undefined') {
1187 | var errorMessage = getUndefinedStateErrorMessage(_key, action);
1188 | throw new Error(errorMessage);
1189 | }
1190 | nextState[_key] = nextStateForKey;
1191 | hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
1192 | }
1193 | return hasChanged ? nextState : state;
1194 | };
1195 | }
1196 | },{"./createStore":25,"./utils/warning":27,"lodash/isPlainObject":20}],24:[function(require,module,exports){
1197 | "use strict";
1198 |
1199 | exports.__esModule = true;
1200 | exports["default"] = compose;
1201 | /**
1202 | * Composes single-argument functions from right to left. The rightmost
1203 | * function can take multiple arguments as it provides the signature for
1204 | * the resulting composite function.
1205 | *
1206 | * @param {...Function} funcs The functions to compose.
1207 | * @returns {Function} A function obtained by composing the argument functions
1208 | * from right to left. For example, compose(f, g, h) is identical to doing
1209 | * (...args) => f(g(h(...args))).
1210 | */
1211 |
1212 | function compose() {
1213 | for (var _len = arguments.length, funcs = Array(_len), _key = 0; _key < _len; _key++) {
1214 | funcs[_key] = arguments[_key];
1215 | }
1216 |
1217 | if (funcs.length === 0) {
1218 | return function (arg) {
1219 | return arg;
1220 | };
1221 | }
1222 |
1223 | if (funcs.length === 1) {
1224 | return funcs[0];
1225 | }
1226 |
1227 | return funcs.reduce(function (a, b) {
1228 | return function () {
1229 | return a(b.apply(undefined, arguments));
1230 | };
1231 | });
1232 | }
1233 | },{}],25:[function(require,module,exports){
1234 | 'use strict';
1235 |
1236 | exports.__esModule = true;
1237 | exports.ActionTypes = undefined;
1238 | exports['default'] = createStore;
1239 |
1240 | var _isPlainObject = require('lodash/isPlainObject');
1241 |
1242 | var _isPlainObject2 = _interopRequireDefault(_isPlainObject);
1243 |
1244 | var _symbolObservable = require('symbol-observable');
1245 |
1246 | var _symbolObservable2 = _interopRequireDefault(_symbolObservable);
1247 |
1248 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
1249 |
1250 | /**
1251 | * These are private action types reserved by Redux.
1252 | * For any unknown actions, you must return the current state.
1253 | * If the current state is undefined, you must return the initial state.
1254 | * Do not reference these action types directly in your code.
1255 | */
1256 | var ActionTypes = exports.ActionTypes = {
1257 | INIT: '@@redux/INIT'
1258 |
1259 | /**
1260 | * Creates a Redux store that holds the state tree.
1261 | * The only way to change the data in the store is to call `dispatch()` on it.
1262 | *
1263 | * There should only be a single store in your app. To specify how different
1264 | * parts of the state tree respond to actions, you may combine several reducers
1265 | * into a single reducer function by using `combineReducers`.
1266 | *
1267 | * @param {Function} reducer A function that returns the next state tree, given
1268 | * the current state tree and the action to handle.
1269 | *
1270 | * @param {any} [preloadedState] The initial state. You may optionally specify it
1271 | * to hydrate the state from the server in universal apps, or to restore a
1272 | * previously serialized user session.
1273 | * If you use `combineReducers` to produce the root reducer function, this must be
1274 | * an object with the same shape as `combineReducers` keys.
1275 | *
1276 | * @param {Function} [enhancer] The store enhancer. You may optionally specify it
1277 | * to enhance the store with third-party capabilities such as middleware,
1278 | * time travel, persistence, etc. The only store enhancer that ships with Redux
1279 | * is `applyMiddleware()`.
1280 | *
1281 | * @returns {Store} A Redux store that lets you read the state, dispatch actions
1282 | * and subscribe to changes.
1283 | */
1284 | };function createStore(reducer, preloadedState, enhancer) {
1285 | var _ref2;
1286 |
1287 | if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
1288 | enhancer = preloadedState;
1289 | preloadedState = undefined;
1290 | }
1291 |
1292 | if (typeof enhancer !== 'undefined') {
1293 | if (typeof enhancer !== 'function') {
1294 | throw new Error('Expected the enhancer to be a function.');
1295 | }
1296 |
1297 | return enhancer(createStore)(reducer, preloadedState);
1298 | }
1299 |
1300 | if (typeof reducer !== 'function') {
1301 | throw new Error('Expected the reducer to be a function.');
1302 | }
1303 |
1304 | var currentReducer = reducer;
1305 | var currentState = preloadedState;
1306 | var currentListeners = [];
1307 | var nextListeners = currentListeners;
1308 | var isDispatching = false;
1309 |
1310 | function ensureCanMutateNextListeners() {
1311 | if (nextListeners === currentListeners) {
1312 | nextListeners = currentListeners.slice();
1313 | }
1314 | }
1315 |
1316 | /**
1317 | * Reads the state tree managed by the store.
1318 | *
1319 | * @returns {any} The current state tree of your application.
1320 | */
1321 | function getState() {
1322 | return currentState;
1323 | }
1324 |
1325 | /**
1326 | * Adds a change listener. It will be called any time an action is dispatched,
1327 | * and some part of the state tree may potentially have changed. You may then
1328 | * call `getState()` to read the current state tree inside the callback.
1329 | *
1330 | * You may call `dispatch()` from a change listener, with the following
1331 | * caveats:
1332 | *
1333 | * 1. The subscriptions are snapshotted just before every `dispatch()` call.
1334 | * If you subscribe or unsubscribe while the listeners are being invoked, this
1335 | * will not have any effect on the `dispatch()` that is currently in progress.
1336 | * However, the next `dispatch()` call, whether nested or not, will use a more
1337 | * recent snapshot of the subscription list.
1338 | *
1339 | * 2. The listener should not expect to see all state changes, as the state
1340 | * might have been updated multiple times during a nested `dispatch()` before
1341 | * the listener is called. It is, however, guaranteed that all subscribers
1342 | * registered before the `dispatch()` started will be called with the latest
1343 | * state by the time it exits.
1344 | *
1345 | * @param {Function} listener A callback to be invoked on every dispatch.
1346 | * @returns {Function} A function to remove this change listener.
1347 | */
1348 | function subscribe(listener) {
1349 | if (typeof listener !== 'function') {
1350 | throw new Error('Expected listener to be a function.');
1351 | }
1352 |
1353 | var isSubscribed = true;
1354 |
1355 | ensureCanMutateNextListeners();
1356 | nextListeners.push(listener);
1357 |
1358 | return function unsubscribe() {
1359 | if (!isSubscribed) {
1360 | return;
1361 | }
1362 |
1363 | isSubscribed = false;
1364 |
1365 | ensureCanMutateNextListeners();
1366 | var index = nextListeners.indexOf(listener);
1367 | nextListeners.splice(index, 1);
1368 | };
1369 | }
1370 |
1371 | /**
1372 | * Dispatches an action. It is the only way to trigger a state change.
1373 | *
1374 | * The `reducer` function, used to create the store, will be called with the
1375 | * current state tree and the given `action`. Its return value will
1376 | * be considered the **next** state of the tree, and the change listeners
1377 | * will be notified.
1378 | *
1379 | * The base implementation only supports plain object actions. If you want to
1380 | * dispatch a Promise, an Observable, a thunk, or something else, you need to
1381 | * wrap your store creating function into the corresponding middleware. For
1382 | * example, see the documentation for the `redux-thunk` package. Even the
1383 | * middleware will eventually dispatch plain object actions using this method.
1384 | *
1385 | * @param {Object} action A plain object representing “what changed”. It is
1386 | * a good idea to keep actions serializable so you can record and replay user
1387 | * sessions, or use the time travelling `redux-devtools`. An action must have
1388 | * a `type` property which may not be `undefined`. It is a good idea to use
1389 | * string constants for action types.
1390 | *
1391 | * @returns {Object} For convenience, the same action object you dispatched.
1392 | *
1393 | * Note that, if you use a custom middleware, it may wrap `dispatch()` to
1394 | * return something else (for example, a Promise you can await).
1395 | */
1396 | function dispatch(action) {
1397 | if (!(0, _isPlainObject2['default'])(action)) {
1398 | throw new Error('Actions must be plain objects. ' + 'Use custom middleware for async actions.');
1399 | }
1400 |
1401 | if (typeof action.type === 'undefined') {
1402 | throw new Error('Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?');
1403 | }
1404 |
1405 | if (isDispatching) {
1406 | throw new Error('Reducers may not dispatch actions.');
1407 | }
1408 |
1409 | try {
1410 | isDispatching = true;
1411 | currentState = currentReducer(currentState, action);
1412 | } finally {
1413 | isDispatching = false;
1414 | }
1415 |
1416 | var listeners = currentListeners = nextListeners;
1417 | for (var i = 0; i < listeners.length; i++) {
1418 | var listener = listeners[i];
1419 | listener();
1420 | }
1421 |
1422 | return action;
1423 | }
1424 |
1425 | /**
1426 | * Replaces the reducer currently used by the store to calculate the state.
1427 | *
1428 | * You might need this if your app implements code splitting and you want to
1429 | * load some of the reducers dynamically. You might also need this if you
1430 | * implement a hot reloading mechanism for Redux.
1431 | *
1432 | * @param {Function} nextReducer The reducer for the store to use instead.
1433 | * @returns {void}
1434 | */
1435 | function replaceReducer(nextReducer) {
1436 | if (typeof nextReducer !== 'function') {
1437 | throw new Error('Expected the nextReducer to be a function.');
1438 | }
1439 |
1440 | currentReducer = nextReducer;
1441 | dispatch({ type: ActionTypes.INIT });
1442 | }
1443 |
1444 | /**
1445 | * Interoperability point for observable/reactive libraries.
1446 | * @returns {observable} A minimal observable of state changes.
1447 | * For more information, see the observable proposal:
1448 | * https://github.com/tc39/proposal-observable
1449 | */
1450 | function observable() {
1451 | var _ref;
1452 |
1453 | var outerSubscribe = subscribe;
1454 | return _ref = {
1455 | /**
1456 | * The minimal observable subscription method.
1457 | * @param {Object} observer Any object that can be used as an observer.
1458 | * The observer object should have a `next` method.
1459 | * @returns {subscription} An object with an `unsubscribe` method that can
1460 | * be used to unsubscribe the observable from the store, and prevent further
1461 | * emission of values from the observable.
1462 | */
1463 | subscribe: function subscribe(observer) {
1464 | if (typeof observer !== 'object') {
1465 | throw new TypeError('Expected the observer to be an object.');
1466 | }
1467 |
1468 | function observeState() {
1469 | if (observer.next) {
1470 | observer.next(getState());
1471 | }
1472 | }
1473 |
1474 | observeState();
1475 | var unsubscribe = outerSubscribe(observeState);
1476 | return { unsubscribe: unsubscribe };
1477 | }
1478 | }, _ref[_symbolObservable2['default']] = function () {
1479 | return this;
1480 | }, _ref;
1481 | }
1482 |
1483 | // When a store is created, an "INIT" action is dispatched so that every
1484 | // reducer returns their initial state. This effectively populates
1485 | // the initial state tree.
1486 | dispatch({ type: ActionTypes.INIT });
1487 |
1488 | return _ref2 = {
1489 | dispatch: dispatch,
1490 | subscribe: subscribe,
1491 | getState: getState,
1492 | replaceReducer: replaceReducer
1493 | }, _ref2[_symbolObservable2['default']] = observable, _ref2;
1494 | }
1495 | },{"lodash/isPlainObject":20,"symbol-observable":28}],26:[function(require,module,exports){
1496 | 'use strict';
1497 |
1498 | exports.__esModule = true;
1499 | exports.compose = exports.applyMiddleware = exports.bindActionCreators = exports.combineReducers = exports.createStore = undefined;
1500 |
1501 | var _createStore = require('./createStore');
1502 |
1503 | var _createStore2 = _interopRequireDefault(_createStore);
1504 |
1505 | var _combineReducers = require('./combineReducers');
1506 |
1507 | var _combineReducers2 = _interopRequireDefault(_combineReducers);
1508 |
1509 | var _bindActionCreators = require('./bindActionCreators');
1510 |
1511 | var _bindActionCreators2 = _interopRequireDefault(_bindActionCreators);
1512 |
1513 | var _applyMiddleware = require('./applyMiddleware');
1514 |
1515 | var _applyMiddleware2 = _interopRequireDefault(_applyMiddleware);
1516 |
1517 | var _compose = require('./compose');
1518 |
1519 | var _compose2 = _interopRequireDefault(_compose);
1520 |
1521 | var _warning = require('./utils/warning');
1522 |
1523 | var _warning2 = _interopRequireDefault(_warning);
1524 |
1525 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
1526 |
1527 | /*
1528 | * This is a dummy function to check if the function name has been altered by minification.
1529 | * If the function has been minified and NODE_ENV !== 'production', warn the user.
1530 | */
1531 | function isCrushed() {}
1532 |
1533 | if ("production" !== 'production' && typeof isCrushed.name === 'string' && isCrushed.name !== 'isCrushed') {
1534 | (0, _warning2['default'])('You are currently using minified code outside of NODE_ENV === \'production\'. ' + 'This means that you are running a slower development build of Redux. ' + 'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' + 'or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) ' + 'to ensure you have the correct code for your production build.');
1535 | }
1536 |
1537 | exports.createStore = _createStore2['default'];
1538 | exports.combineReducers = _combineReducers2['default'];
1539 | exports.bindActionCreators = _bindActionCreators2['default'];
1540 | exports.applyMiddleware = _applyMiddleware2['default'];
1541 | exports.compose = _compose2['default'];
1542 | },{"./applyMiddleware":21,"./bindActionCreators":22,"./combineReducers":23,"./compose":24,"./createStore":25,"./utils/warning":27}],27:[function(require,module,exports){
1543 | 'use strict';
1544 |
1545 | exports.__esModule = true;
1546 | exports['default'] = warning;
1547 | /**
1548 | * Prints a warning in the console if it exists.
1549 | *
1550 | * @param {String} message The warning message.
1551 | * @returns {void}
1552 | */
1553 | function warning(message) {
1554 | /* eslint-disable no-console */
1555 | if (typeof console !== 'undefined' && typeof console.error === 'function') {
1556 | console.error(message);
1557 | }
1558 | /* eslint-enable no-console */
1559 | try {
1560 | // This error was thrown as a convenience so that if you enable
1561 | // "break on all exceptions" in your console,
1562 | // it would pause the execution at this line.
1563 | throw new Error(message);
1564 | /* eslint-disable no-empty */
1565 | } catch (e) {}
1566 | /* eslint-enable no-empty */
1567 | }
1568 | },{}],28:[function(require,module,exports){
1569 | (function (global){
1570 | 'use strict';
1571 |
1572 | Object.defineProperty(exports, "__esModule", {
1573 | value: true
1574 | });
1575 |
1576 | var _ponyfill = require('./ponyfill.js');
1577 |
1578 | var _ponyfill2 = _interopRequireDefault(_ponyfill);
1579 |
1580 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
1581 |
1582 | var root; /* global window */
1583 |
1584 |
1585 | if (typeof self !== 'undefined') {
1586 | root = self;
1587 | } else if (typeof window !== 'undefined') {
1588 | root = window;
1589 | } else if (typeof global !== 'undefined') {
1590 | root = global;
1591 | } else if (typeof module !== 'undefined') {
1592 | root = module;
1593 | } else {
1594 | root = Function('return this')();
1595 | }
1596 |
1597 | var result = (0, _ponyfill2['default'])(root);
1598 | exports['default'] = result;
1599 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
1600 | },{"./ponyfill.js":29}],29:[function(require,module,exports){
1601 | 'use strict';
1602 |
1603 | Object.defineProperty(exports, "__esModule", {
1604 | value: true
1605 | });
1606 | exports['default'] = symbolObservablePonyfill;
1607 | function symbolObservablePonyfill(root) {
1608 | var result;
1609 | var _Symbol = root.Symbol;
1610 |
1611 | if (typeof _Symbol === 'function') {
1612 | if (_Symbol.observable) {
1613 | result = _Symbol.observable;
1614 | } else {
1615 | result = _Symbol('observable');
1616 | _Symbol.observable = result;
1617 | }
1618 | } else {
1619 | result = '@@observable';
1620 | }
1621 |
1622 | return result;
1623 | };
1624 | },{}]},{},[10]);
1625 |
--------------------------------------------------------------------------------