├── .babelrc
├── .ebextensions
├── 01_packages.config
├── newrelic.config
└── python.config
├── .gitignore
├── .travis.yml
├── Dockerfile
├── Dockerfile-webpack
├── LICENSE
├── README.md
├── datastore
└── fingerprinting
│ ├── hashers.py
│ ├── pytest.ini
│ ├── requirements.txt
│ ├── samples
│ ├── color-bw.jpeg
│ ├── color-sepia.jpg
│ ├── cropped-150x150.jpg
│ ├── cropped-240x160.jpg
│ ├── manifest.md
│ ├── memed-notext.jpg
│ ├── memed-text.jpg
│ ├── providers-500px.jpg
│ ├── providers-flickr.jpg
│ ├── similar-donkey.jpg
│ └── similar-horse.jpg
│ └── test_hashers.py
├── docker-compose.yml
├── fabfile.py
├── imageledger
├── __init__.py
├── admin.py
├── apps.py
├── forms.py
├── handlers
│ ├── __init__.py
│ ├── handler_500px.py
│ ├── handler_europeana.py
│ ├── handler_flickr.py
│ ├── handler_met.py
│ ├── handler_nypl.py
│ ├── handler_rijks.py
│ ├── handler_wikimedia.py
│ └── utils.py
├── jinja2
│ ├── 404.html
│ ├── about.html
│ ├── base.html
│ ├── detail.html
│ ├── includes
│ │ ├── favorite.html
│ │ ├── image-result.html
│ │ ├── license-logo.html
│ │ ├── lists.html
│ │ ├── metadata.html
│ │ ├── pagination.html
│ │ ├── photoswipe.html
│ │ ├── search-form.html
│ │ └── tags.html
│ ├── list-public.html
│ ├── list.html
│ ├── lists.html
│ ├── profile.html
│ ├── results.html
│ ├── robots.txt
│ ├── user-tags-list.html
│ └── user-tags.html
├── licenses.py
├── management
│ └── commands
│ │ ├── all_english_words.txt
│ │ ├── handlers.py
│ │ ├── indexer.py
│ │ ├── loader.py
│ │ ├── mocker.py
│ │ ├── remap.py
│ │ ├── syncer.py
│ │ └── util.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_auto_20161111_1812.py
│ ├── 0003_auto_20161116_1812.py
│ ├── 0004_auto_20161116_2041.py
│ ├── 0005_auto_20161117_1512.py
│ ├── 0006_image_last_synced_with_source.py
│ ├── 0007_auto_20161128_1847.py
│ ├── 0008_image_removed_from_source.py
│ ├── 0009_auto_20161128_2019.py
│ ├── 0010_auto_20161130_1814.py
│ ├── 0011_auto_20161205_1424.py
│ ├── 0012_add_user_tags.py
│ ├── 0013_add-slug-to-tag.py
│ ├── 0014_increase-slug-size.py
│ ├── 0015_auto_20161219_1955.py
│ └── __init__.py
├── models.py
├── search.py
├── signals.py
├── tests
│ ├── 500px-response.json
│ ├── __init__.py
│ ├── elasticsearch
│ │ └── elasticsearch.yml
│ ├── flickr-response.json
│ ├── image.png
│ ├── rijks-response.json
│ ├── settings.py
│ ├── test_api.py
│ ├── test_auth.py
│ ├── test_favorites.py
│ ├── test_handlers.py
│ ├── test_licenses.py
│ ├── test_lists.py
│ ├── test_models.py
│ ├── test_search.py
│ ├── test_tags.py
│ ├── utils.py
│ ├── wikimedia-data-response.json
│ └── wikimedia-entities-response.json
├── urls.py
└── views
│ ├── __init__.py
│ ├── api_views.py
│ ├── favorite_views.py
│ ├── list_views.py
│ ├── search_views.py
│ ├── site_views.py
│ └── tag_views.py
├── manage.py
├── newrelic.ini
├── npm-shrinkwrap.json
├── openledger
├── __init__.py
├── jinja2.py
├── local.py.example
├── settings.py
├── test_settings.py
├── urls.py
└── wsgi.py
├── package.json
├── requirements-test.txt
├── requirements.txt
├── static
├── css
│ ├── app.css
│ ├── app.css.map
│ ├── default-skin.css
│ ├── default-skin.png
│ ├── default-skin.svg
│ ├── foundation-icons.css
│ └── preloader.gif
├── fonts
│ ├── foundation-icons.eot
│ ├── foundation-icons.svg
│ ├── foundation-icons.ttf
│ └── foundation-icons.woff
├── images
│ ├── by-nc-nd.png
│ ├── by-nc-sa.png
│ ├── by-nc.png
│ ├── by-nd.png
│ ├── by-sa.png
│ ├── by.png
│ ├── by.svg
│ ├── cc.logo.white.svg
│ ├── cc.svg
│ ├── cc0.png
│ ├── cc0.svg
│ ├── loading.svg
│ ├── nc-eu.svg
│ ├── nc-jp.svg
│ ├── nc.svg
│ ├── nd.svg
│ ├── pdm.png
│ ├── pdm.svg
│ ├── sa.svg
│ ├── share.svg
│ ├── title-search.svg
│ └── zero.svg
├── js
│ ├── api.js
│ ├── attributions.js
│ ├── favorite.js
│ ├── form.js
│ ├── grid.js
│ ├── index.js
│ ├── list.js
│ ├── photoswipe-ui-default.js
│ ├── search.js
│ ├── tags.js
│ ├── test
│ │ ├── global.js
│ │ ├── test-utils.js
│ │ ├── testFavorite.js
│ │ ├── testForm.js
│ │ └── testList.js
│ └── utils.js
└── scss
│ ├── _about.scss
│ ├── _base.scss
│ ├── _detail.scss
│ ├── _favorite.scss
│ ├── _footer.scss
│ ├── _forms.scss
│ ├── _licenses.scss
│ ├── _lists.scss
│ ├── _nav.scss
│ ├── _photoswipe.scss
│ ├── _photoswipe_skin.scss
│ ├── _photoswipe_skin_custom.scss
│ ├── _result.scss
│ ├── _tags.scss
│ └── app.scss
├── util
├── __init__.py
├── list-es-snapshots.py
├── make-es-snapshot.py
├── register-es-snapshots.py
├── restore-es-snapshot.py
└── scheduled-snapshots
│ ├── aws_requests_auth
│ ├── __init__.py
│ └── aws_auth.py
│ ├── requests
│ ├── __init__.py
│ ├── _internal_utils.py
│ ├── adapters.py
│ ├── api.py
│ ├── auth.py
│ ├── cacert.pem
│ ├── certs.py
│ ├── compat.py
│ ├── cookies.py
│ ├── exceptions.py
│ ├── hooks.py
│ ├── models.py
│ ├── packages
│ │ ├── __init__.py
│ │ ├── chardet
│ │ │ ├── __init__.py
│ │ │ ├── big5freq.py
│ │ │ ├── big5prober.py
│ │ │ ├── chardetect.py
│ │ │ ├── chardistribution.py
│ │ │ ├── charsetgroupprober.py
│ │ │ ├── charsetprober.py
│ │ │ ├── codingstatemachine.py
│ │ │ ├── compat.py
│ │ │ ├── constants.py
│ │ │ ├── cp949prober.py
│ │ │ ├── escprober.py
│ │ │ ├── escsm.py
│ │ │ ├── eucjpprober.py
│ │ │ ├── euckrfreq.py
│ │ │ ├── euckrprober.py
│ │ │ ├── euctwfreq.py
│ │ │ ├── euctwprober.py
│ │ │ ├── gb2312freq.py
│ │ │ ├── gb2312prober.py
│ │ │ ├── hebrewprober.py
│ │ │ ├── jisfreq.py
│ │ │ ├── jpcntx.py
│ │ │ ├── langbulgarianmodel.py
│ │ │ ├── langcyrillicmodel.py
│ │ │ ├── langgreekmodel.py
│ │ │ ├── langhebrewmodel.py
│ │ │ ├── langhungarianmodel.py
│ │ │ ├── langthaimodel.py
│ │ │ ├── latin1prober.py
│ │ │ ├── mbcharsetprober.py
│ │ │ ├── mbcsgroupprober.py
│ │ │ ├── mbcssm.py
│ │ │ ├── sbcharsetprober.py
│ │ │ ├── sbcsgroupprober.py
│ │ │ ├── sjisprober.py
│ │ │ ├── universaldetector.py
│ │ │ └── utf8prober.py
│ │ ├── idna
│ │ │ ├── __init__.py
│ │ │ ├── codec.py
│ │ │ ├── compat.py
│ │ │ ├── core.py
│ │ │ ├── idnadata.py
│ │ │ ├── intranges.py
│ │ │ └── uts46data.py
│ │ └── urllib3
│ │ │ ├── __init__.py
│ │ │ ├── _collections.py
│ │ │ ├── connection.py
│ │ │ ├── connectionpool.py
│ │ │ ├── contrib
│ │ │ ├── __init__.py
│ │ │ ├── appengine.py
│ │ │ ├── ntlmpool.py
│ │ │ ├── pyopenssl.py
│ │ │ └── socks.py
│ │ │ ├── exceptions.py
│ │ │ ├── fields.py
│ │ │ ├── filepost.py
│ │ │ ├── packages
│ │ │ ├── __init__.py
│ │ │ ├── backports
│ │ │ │ ├── __init__.py
│ │ │ │ └── makefile.py
│ │ │ ├── ordered_dict.py
│ │ │ ├── six.py
│ │ │ └── ssl_match_hostname
│ │ │ │ ├── __init__.py
│ │ │ │ └── _implementation.py
│ │ │ ├── poolmanager.py
│ │ │ ├── request.py
│ │ │ ├── response.py
│ │ │ └── util
│ │ │ ├── __init__.py
│ │ │ ├── connection.py
│ │ │ ├── request.py
│ │ │ ├── response.py
│ │ │ ├── retry.py
│ │ │ ├── selectors.py
│ │ │ ├── ssl_.py
│ │ │ ├── timeout.py
│ │ │ ├── url.py
│ │ │ └── wait.py
│ ├── sessions.py
│ ├── status_codes.py
│ ├── structures.py
│ └── utils.py
│ └── setup.cfg
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015"
4 | ],
5 | "plugins": [
6 | "transform-object-rest-spread"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.ebextensions/01_packages.config:
--------------------------------------------------------------------------------
1 | packages:
2 | yum:
3 | git: []
4 | postgresql93-devel: []
5 | libjpeg-turbo-devel: []
6 | gcc-c++: []
7 | httpd24-devel-2.4.27-3.75.amzn1.x86_64: []
8 |
9 | files:
10 | "/tmp/update-wsgi.sh" :
11 | mode: "000755"
12 | owner: root
13 | group: root
14 | content: |
15 | # update mod_wsgi
16 | cd /tmp
17 | wget -q "https://github.com/GrahamDumpleton/mod_wsgi/archive/4.4.21.tar.gz" && \
18 | tar -xzf '4.4.21.tar.gz' && \
19 | cd ./mod_wsgi-4.4.21 && \
20 | sudo ./configure --with-python=/usr/bin/python3.6 && \
21 | sudo make && \
22 | sudo make install && \
23 | sudo service httpd restart
24 |
25 | commands:
26 | mod_wsgi_update:
27 | command: /tmp/update-wsgi.sh
28 | cwd: /tmp
29 |
--------------------------------------------------------------------------------
/.ebextensions/newrelic.config:
--------------------------------------------------------------------------------
1 | packages:
2 | yum:
3 | newrelic-sysmond: []
4 | rpm:
5 | newrelic: http://yum.newrelic.com/pub/newrelic/el5/x86_64/newrelic-repo-5-3.noarch.rpm
6 |
7 | container_commands:
8 | "01":
9 | command: nrsysmond-config --set license_key=${NEW_RELIC_LICENSE_KEY}
10 | "02":
11 | command: echo hostname=ccsearch.creativecommons.org >> /etc/newrelic/nrsysmond.cfg
12 | "03":
13 | command: /etc/init.d/newrelic-sysmond start
14 |
--------------------------------------------------------------------------------
/.ebextensions/python.config:
--------------------------------------------------------------------------------
1 | packages:
2 | yum:
3 | python34-devel: []
4 |
5 | files:
6 | "/etc/httpd/conf.d/ssl_rewrite.conf":
7 | mode: "000644"
8 | owner: root
9 | group: root
10 | content: |
11 | RewriteEngine On
12 |
13 | RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R,L]
14 |
15 |
16 | "/etc/httpd/conf.d/eb_healthcheck.conf":
17 | mode: "000644"
18 | owner: root
19 | group: root
20 | content: |
21 |
22 | RequestHeader set Host "check.elasticbeanstalk.com"
23 |
24 |
25 | "/etc/httpd/conf.d/wsgadditional.conf":
26 | mode: "000644"
27 | owner: root
28 | group: root
29 | content: |
30 | WSGIPassAuthorization On
31 |
32 | option_settings:
33 | "aws:elasticbeanstalk:container:python:staticfiles":
34 | "/static/": "deploy/"
35 | "aws:elasticbeanstalk:container:python":
36 | WSGIPath: "openledger/wsgi.py"
37 | "aws:elasticbeanstalk:application:environment":
38 | DJANGO_SETTINGS_MODULE: "openledger.settings"
39 |
40 | commands:
41 | 01_node_install:
42 | cwd: /tmp
43 | test: '[ ! -f /usr/bin/node ] && echo "node not installed"'
44 | command: 'curl --silent --location https://rpm.nodesource.com/setup_7.x | bash - '
45 | 02_npm_install:
46 | cwd: /tmp
47 | test: '[ ! -f /usr/bin/npm ] && echo "npm not installed"'
48 | command: 'yum install -y nodejs npm'
49 | 03_node_update:
50 | cwd: /tmp
51 | test: '[ ! -f /usr/bin/n ] && echo "node not updated"'
52 | command: 'npm install -g n && n stable'
53 |
54 | container_commands:
55 | 00_create_dir:
56 | command: 'mkdir -p /var/log/app-logs'
57 | 01_change_permissions:
58 | command: 'chmod g+s /var/log/app-logs'
59 | 02_change_owner:
60 | command: 'chown wsgi:wsgi /var/log/app-logs'
61 | 03_touch_file:
62 | command: 'touch /var/log/app-logs/app.log'
63 | 04_change_file_owner:
64 | command: 'chown wsgi:wsgi /var/log/app-logs/app.log'
65 | 05_npm_build:
66 | command: 'npm install'
67 | 06_webpack_build:
68 | command: 'NODE_ENV=production node_modules/.bin/webpack'
69 | 07_database_migrate:
70 | command: 'django-admin.py migrate'
71 | leader_only: true
72 | 08_database_cache:
73 | command: 'django-admin.py createcachetable'
74 | leader_only: true
75 | 09_collect_static:
76 | command: 'django-admin.py collectstatic --noinput --clear'
77 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # local/configs
2 | config.py
3 | secret.py
4 |
5 | # Byte-compiled / optimized / DLL files
6 | __pycache__/
7 | *.py[cod]
8 | *$py.class
9 |
10 | # C extensions
11 | *.so
12 |
13 | # Distribution / packaging
14 | .Python
15 | env/
16 | build/
17 | develop-eggs/
18 | dist/
19 | downloads/
20 | eggs/
21 | .eggs/
22 | lib/
23 | lib64/
24 | parts/
25 | sdist/
26 | var/
27 | *.egg-info/
28 | .installed.cfg
29 | *.egg
30 |
31 | # PyInstaller
32 | # Usually these files are written by a python script from a template
33 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
34 | *.manifest
35 | *.spec
36 |
37 | # Installer logs
38 | pip-log.txt
39 | pip-delete-this-directory.txt
40 |
41 | # Unit test / coverage reports
42 | htmlcov/
43 | .tox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *,cover
50 | .hypothesis/
51 |
52 | # Translations
53 | *.mo
54 | *.pot
55 |
56 | # Django stuff:
57 | *.log
58 | local_settings.py
59 |
60 | # Flask stuff:
61 | instance/
62 | .webassets-cache
63 |
64 | # Scrapy stuff:
65 | .scrapy
66 |
67 | # Sphinx documentation
68 | docs/_build/
69 |
70 | # PyBuilder
71 | target/
72 |
73 | # IPython Notebook
74 | .ipynb_checkpoints
75 |
76 | # pyenv
77 | .python-version
78 |
79 | # celery beat schedule file
80 | celerybeat-schedule
81 |
82 | # dotenv
83 | .env
84 |
85 | # virtualenv
86 | venv/
87 | ENV/
88 |
89 | # Spyder project settings
90 | .spyderproject
91 |
92 | # Rope project settings
93 | .ropeproject
94 | secret
95 | .DS_Store
96 | node_modules
97 |
98 | # Elastic Beanstalk Files
99 | .elasticbeanstalk/*
100 | !.elasticbeanstalk/*.cfg.yml
101 | !.elasticbeanstalk/*.global.yml
102 | .sass-cache
103 |
104 | local.py
105 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - '3.6'
4 | sudo: required
5 | before_install:
6 | - sudo service postgresql stop
7 | - while sudo lsof -Pi :5432 -sTCP:LISTEN -t; do sleep 1; done
8 | script:
9 | - sudo service postgresql stop && docker-compose run web python manage.py test
10 | services:
11 | - docker
12 | notifications:
13 | slack:
14 | secure: o4VCZ5u1KPvm+1HwL5vWAFCVv57u5ILNOIqUUwUpfiLuItwcObindwHBKeBMYmXC3J5hSz/pwRT1jR1dT58PfIVnAau5v10uadVNL1s01Gy/2VfYun4oBZRRuhMOAIDmIMSemx16FgbY08IabcHqt+rZQ4dfezXN2Jr82xNrnFeS5dqMNMx/YiFstdJ7jYOYzM0qGYKOvk74AxZWGnc9FsJgnxHkGybjEtE9tlkA4+rPHylQ5zoo3B4KX6N/9XYVONzwqIGRJl2Uv1G1i5bwKXiENEzwRX9FLB1kXyFyTZ1C0P1s+yMx6KnE6rv4XkstNbeOwMSbis0hDWgoYIp2A1vdd5FHF21DlH1pXRf+xA/hs4dfXfKC22OK1MvEO8eOQ71rBO/TD0Jf4ZsEtIH/SysTGRyDWyBEacPHUG+od2E5A3sLsF1HGEerInZNvjIndBRgMUoSBx0Qwm5YYssURNQ+o7jc6hqf+DdVsfXx9QMpGKPrpEpg8WbA9r/43zp4TeR7bLknxrV7Ef/dQbio5ucxfbemfDdXaubff6i3ZYftyP+MxIguCE8de51CzXqWNZgy/8WKzIpflOyrWyM6WRLbZjOYOyH0nI1dxyktx4kFkbXMd9t/p/nygMREWVK6R1OLi4Wl1oPjiu96sA2trwwa9NzPEsf1J7K3mBrRS2s=
15 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.6
2 |
3 | ENV PYTHONUNBUFFERED 1
4 |
5 | RUN mkdir /django-app
6 | WORKDIR /django-app
7 |
8 | ADD requirements.txt /django-app/
9 | RUN pip install -r requirements.txt
10 | ADD requirements-test.txt /django-app/
11 | RUN pip install -r requirements-test.txt
12 |
--------------------------------------------------------------------------------
/Dockerfile-webpack:
--------------------------------------------------------------------------------
1 | FROM node:8
2 |
3 | ENV PATH="/webpack/node_modules/.bin:${PATH}"
4 |
5 | RUN mkdir /webpack
6 | WORKDIR /webpack
7 |
8 | ADD package.json /webpack/
9 | RUN npm install
10 |
11 | CMD webpack --watch
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Liza Daly
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/datastore/fingerprinting/hashers.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | import subprocess
3 |
4 | from PIL import Image
5 |
6 | def hamming_distance(s1, s2):
7 | """Return the Hamming distance between equal-length sequences, a measure of hash similarity"""
8 | # Transform into a fixed-size binary string first
9 | s1bin = ' '.join('{0:08b}'.format(ord(x), 'b') for x in s1)
10 | s2bin = ' '.join('{0:08b}'.format(ord(x), 'b') for x in s2)
11 | if len(s1bin) != len(s2bin):
12 | raise ValueError("Undefined for sequences of unequal length")
13 | return sum(el1 != el2 for el1, el2 in zip(s1bin, s2bin))
14 |
15 | def random(imgfile):
16 | """This 'strategy' simply returns a unique random value and should always fail to
17 | assign the same hash to two different images. This is to ensure that our tests are honest"""
18 | return str(uuid.uuid4())
19 |
20 | def phash(imgfile):
21 | # This is a common algorithm but is deliberately not included in this package
22 | # as it is no longer maintained and considered to have unaddressed security
23 | # flaws: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=751916
24 | raise NotImplementedError("Not intending to include")
25 |
26 | def blockhash(imgfile):
27 | """Use the hashing algorithm from https://github.com/creativecommons/blockhash-python"""
28 | # This fails on images that are cropped/resized
29 | from blockhash import process_images
30 | options = {
31 | 'quick': False,
32 | 'bits': 16,
33 | 'size': "256x256",
34 | 'interpolation': 1,
35 | 'debug': False,
36 | }
37 | return process_images([imgfile], options=options)
38 |
39 | def imagehash(imgfile, method="phash"):
40 | """Use the imagehashing algorithm from https://github.com/JohannesBuchner/imagehash/,
41 | which includes multiple hashing mechanisms:
42 | ahash: Average hash
43 | phash: Perceptual hash
44 | dhash: Difference hash
45 | whash-haar: Haar wavelet hash
46 | whash-db4: Daubechies wavelet hash
47 |
48 | Subtracting two of these values will return a Hamming distance value between them.
49 | """
50 | import imagehash
51 | im = Image.open(imgfile)
52 | hashfunc = getattr(imagehash, method)
53 | return {imgfile: hashfunc(im)}
54 |
--------------------------------------------------------------------------------
/datastore/fingerprinting/pytest.ini:
--------------------------------------------------------------------------------
1 | # content of pytest.ini
2 | [pytest]
3 | norecursedirs = .git venv ve
4 |
--------------------------------------------------------------------------------
/datastore/fingerprinting/requirements.txt:
--------------------------------------------------------------------------------
1 | Pillow
2 | pytest
3 | git+git://github.com/creativecommons/blockhash-python.git#egg=blockhash-python
4 | imagehash
5 |
--------------------------------------------------------------------------------
/datastore/fingerprinting/samples/color-bw.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/datastore/fingerprinting/samples/color-bw.jpeg
--------------------------------------------------------------------------------
/datastore/fingerprinting/samples/color-sepia.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/datastore/fingerprinting/samples/color-sepia.jpg
--------------------------------------------------------------------------------
/datastore/fingerprinting/samples/cropped-150x150.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/datastore/fingerprinting/samples/cropped-150x150.jpg
--------------------------------------------------------------------------------
/datastore/fingerprinting/samples/cropped-240x160.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/datastore/fingerprinting/samples/cropped-240x160.jpg
--------------------------------------------------------------------------------
/datastore/fingerprinting/samples/manifest.md:
--------------------------------------------------------------------------------
1 | # Image credits
2 |
3 | ## Images that differ by size
4 | cropped*.jpg
5 | https://www.flickr.com/photos/lizadaly/28390955302/ (CC-BY, liza31337)
6 |
7 | ## Same image, different content provider
8 | providers*.jpg
9 | https://500px.com/photo/128635675/yellow-orange-leavy-s-by-stephan-visser (CC-BY, Stephan Visser)
10 | https://www.flickr.com/photos/svis82/22978275201/ (CC-BY-NC-SA, Stephan Visser)
11 |
12 | ## Different images, but flagged as "similar" by Google image search
13 | similar-donkey.jpg
14 | https://www.flickr.com/photos/lizadaly/28390955302/ (CC-BY, liza31337)
15 | https://pixabay.com/en/horse-white-horse-ascension-714084/ (CC0)
16 |
--------------------------------------------------------------------------------
/datastore/fingerprinting/samples/memed-notext.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/datastore/fingerprinting/samples/memed-notext.jpg
--------------------------------------------------------------------------------
/datastore/fingerprinting/samples/memed-text.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/datastore/fingerprinting/samples/memed-text.jpg
--------------------------------------------------------------------------------
/datastore/fingerprinting/samples/providers-500px.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/datastore/fingerprinting/samples/providers-500px.jpg
--------------------------------------------------------------------------------
/datastore/fingerprinting/samples/providers-flickr.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/datastore/fingerprinting/samples/providers-flickr.jpg
--------------------------------------------------------------------------------
/datastore/fingerprinting/samples/similar-donkey.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/datastore/fingerprinting/samples/similar-donkey.jpg
--------------------------------------------------------------------------------
/datastore/fingerprinting/samples/similar-horse.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/datastore/fingerprinting/samples/similar-horse.jpg
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | db:
4 | image: postgres:10.3-alpine
5 | image: postgres
6 | ports:
7 | - "5432:5432"
8 | environment:
9 | POSTGRES_DB: "openledger"
10 | POSTGRES_USER: "deploy"
11 | POSTGRES_PASSWORD: "deploy"
12 | healthcheck:
13 | test: "pg_isready -U deploy -d openledger"
14 |
15 | es:
16 | image: docker.elastic.co/elasticsearch/elasticsearch:5.3.3
17 | ports:
18 | - "9200:9200"
19 | environment:
20 | # disable XPack
21 | # https://www.elastic.co/guide/en/elasticsearch/reference/5.3/docker.html#_security_note
22 | xpack.security.enabled: "false"
23 | healthcheck:
24 | test: ["CMD", "curl", "-f", "http://es:9200"]
25 | web:
26 | build: .
27 | command: python manage.py runserver 0.0.0.0:8000
28 | volumes:
29 | - .:/django-app
30 | ports:
31 | - "8000:8000"
32 | depends_on:
33 | - db
34 | - es
35 | webpack:
36 | build:
37 | context: .
38 | dockerfile: Dockerfile-webpack
39 | volumes:
40 | - .:/webpack/
41 | - /webpack/node_modules
42 |
--------------------------------------------------------------------------------
/imageledger/__init__.py:
--------------------------------------------------------------------------------
1 | default_app_config = 'imageledger.apps.ImageledgerConfig'
2 |
--------------------------------------------------------------------------------
/imageledger/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.contrib.contenttypes.admin import GenericTabularInline
3 | from imageledger import models
4 | from django.contrib.admin.widgets import AdminFileWidget
5 | from django.utils.translation import ugettext as _
6 | from django.utils.safestring import mark_safe
7 |
8 |
9 | class ImageAdmin(admin.ModelAdmin):
10 | list_display = ('title', 'creator', 'provider', 'created_on', 'last_synced_with_source')
11 | fields = ( 'image_tag', 'title', 'provider', 'license', 'license_version', 'created_on', 'last_synced_with_source',
12 | 'foreign_landing_url', 'foreign_identifier')
13 | readonly_fields = ('image_tag', 'created_on', 'last_synced_with_source', )
14 | search_fields = ('title', 'creator')
15 | ordering = ('-id', )
16 | # list_filter = ('provider', 'license') # RDS database falls down and dies on this
17 |
18 | class ListAdmin(admin.ModelAdmin):
19 | empty_value_display = '-blank-'
20 | list_display = ('title', 'owner', 'num_images', 'is_public', 'created_on', 'updated_on')
21 | readonly_fields = ('images', 'slug')
22 | ordering = ('-created_on', )
23 | list_filter = ('is_public', )
24 | search_fields = ('title', 'owner__username', 'owner__email')
25 |
26 | def num_images(self, obj):
27 | return obj.images.all().count()
28 |
29 | admin.site.register(models.List, ListAdmin)
30 | admin.site.register(models.Image, ImageAdmin)
31 |
--------------------------------------------------------------------------------
/imageledger/apps.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.db.models.signals import pre_save
3 |
4 | from django.apps import AppConfig
5 |
6 | class ImageledgerConfig(AppConfig):
7 | name = 'imageledger'
8 | verbose_name = "Image Ledger"
9 |
10 | def ready(self):
11 | from imageledger.signals import set_identifier, create_slug
12 | from imageledger import search
13 | search.init()
14 |
--------------------------------------------------------------------------------
/imageledger/handlers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/imageledger/handlers/__init__.py
--------------------------------------------------------------------------------
/imageledger/handlers/handler_500px.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from urllib.parse import parse_qs
3 | from pprint import pprint
4 |
5 | import requests
6 | from requests_oauthlib import OAuth1Session, OAuth1
7 | from oauthlib.oauth2 import BackendApplicationClient
8 | from django.conf import settings
9 | from django.db.utils import IntegrityError
10 | from django.utils import timezone
11 |
12 | from imageledger.licenses import license_match
13 | from imageledger.handlers.utils import *
14 |
15 | BASE_URL = 'https://api.500px.com'
16 | ENDPOINT_PHOTOS = BASE_URL + '/v1/photos/search'
17 |
18 | PROVIDER_NAME = '500px'
19 | SOURCE_NAME = '500px'
20 |
21 | IMAGE_SIZE_THUMBNAIL = 3 # 200x200
22 | IMAGE_SIZE_FULL = 1080 # 1080x
23 |
24 | log = logging.getLogger(__name__)
25 |
26 | # 500px will return these values as integers, so keep them as integers
27 | LICENSES = {
28 | "BY": 4,
29 | "BY-NC": 1,
30 | "BY-ND": 5,
31 | "BY-SA": 6,
32 | "BY-NC-ND": 2,
33 | "BY-NC-SA": 3,
34 | "PDM": 7,
35 | "CC0": 8,
36 | }
37 | LICENSE_VERSION = "3.0"
38 |
39 | LICENSE_LOOKUP = {v: k for k, v in LICENSES.items()}
40 |
41 | def auth():
42 | return OAuth1(settings.API_500PX_KEY, client_secret=settings.API_500PX_SECRET)
43 |
44 | def photos(search=None, licenses=["ALL"], page=1, per_page=20, extra={}, **kwargs):
45 | params = {
46 | 'license_type': license_match(licenses, LICENSES),
47 | 'term': search,
48 | 'page': page,
49 | 'rpp': per_page,
50 | 'nsfw': False,
51 | 'image_size': "%s,%s" % (IMAGE_SIZE_THUMBNAIL, IMAGE_SIZE_FULL)
52 | }
53 | params.update(extra)
54 | r = requests.get(ENDPOINT_PHOTOS, params=params, auth=auth())
55 | assert r.status_code == 200
56 | return r.json()
57 |
58 | def serialize(result):
59 | """For a given 500px result, map that to our database"""
60 | url = result['images'][1]['https_url']
61 | image = models.Image(url=url)
62 | image.thumbnail = result['images'][0]['https_url']
63 | image.provider = PROVIDER_NAME
64 | image.source = SOURCE_NAME
65 | image.creator = result['user']['username']
66 | try:
67 | image.license = LICENSE_LOOKUP[result['license_type']].lower()
68 | except KeyError:
69 | # We got an unknown license, so just skip this
70 | return None
71 | image.license_version = LICENSE_VERSION
72 | image.foreign_landing_url = "https://500px.com/" + result['url']
73 | image.foreign_identifier = result['id']
74 | image.title = result['name']
75 | image.identifier = signals.create_identifier(image.url)
76 | image.last_synced_with_source = timezone.now()
77 | return image
78 |
79 | def walk(page=1, per_page=100):
80 | """Walk through a set of search results and collect items to serialize"""
81 |
82 | has_more = True
83 | while has_more:
84 | results = photos(page=page, per_page=per_page, extra={'sort': 'created_at'})
85 | for result in results.get('photos'):
86 | yield result
87 | page += 1
88 | time.sleep(2)
89 |
--------------------------------------------------------------------------------
/imageledger/handlers/handler_flickr.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | import logging
4 |
5 | requests_log = logging.getLogger("flickrapi")
6 | requests_log.propagate = False
7 |
8 | import flickrapi
9 |
10 | from django.conf import settings
11 | from imageledger.licenses import license_match
12 |
13 | log = logging.getLogger(__name__)
14 |
15 | LICENSES = {
16 | "BY": 4,
17 | "BY-NC": 2,
18 | "BY-ND": 6,
19 | "BY-SA": 5,
20 | "BY-NC-ND": 3,
21 | "BY-NC-SA": 1,
22 | "PDM": 7,
23 | "CC0": 9,
24 | }
25 | LICENSE_VERSION = "2.0"
26 |
27 | LICENSE_LOOKUP = {v: k for k, v in LICENSES.items()}
28 |
29 | def auth():
30 | return flickrapi.FlickrAPI(settings.FLICKR_KEY,
31 | settings.FLICKR_SECRET,
32 | format='parsed-json',
33 | store_token=False,
34 | cache=True)
35 |
36 | def photos(search=None, licenses=["ALL"], page=1, per_page=20, **kwargs):
37 | flickr = auth()
38 | photos = flickr.photos.search(safe_search=1, # safe-search on
39 | content_type=1, # Photos only, no screenshots
40 | license=license_match(licenses, LICENSES),
41 | text=search,
42 | extras='url_l,url_m,url_s,owner_name,license',
43 | sort='relevance',
44 | page=page,
45 | per_page=per_page)
46 | photos['photos']['total'] = int(photos['photos']['total']) # seriously why is this a string
47 | photos['photos']['pages'] = int(photos['photos']['pages'])
48 | for p in photos['photos']['photo']:
49 | p['url'] = p.get('url_l') or p.get('url_m') or p.get('url_s') or ""
50 | p['thumbnail'] = p.get('url_s') or ""
51 | return photos
52 |
--------------------------------------------------------------------------------
/imageledger/handlers/utils.py:
--------------------------------------------------------------------------------
1 | import itertools
2 | import logging
3 | import time
4 |
5 | from django.conf import settings
6 | from django.db.utils import IntegrityError
7 | import elasticsearch
8 | import requests
9 |
10 | from imageledger import models, signals, search
11 |
12 | log = logging.getLogger(__name__)
13 |
14 | def grouper_it(n, iterable):
15 | it = iter(iterable)
16 | while True:
17 | chunk_it = itertools.islice(it, n)
18 | try:
19 | first_el = next(chunk_it)
20 | except StopIteration:
21 | return
22 | yield itertools.chain((first_el,), chunk_it)
23 |
24 | def insert_image(walk_func, serialize_func, chunk_size, max_results=5000, **kwargs):
25 | count = 0
26 | success_count = 0
27 | es = search.init()
28 | search.Image.init()
29 | mapping = search.Image._doc_type.mapping
30 | mapping.save(settings.ELASTICSEARCH_INDEX)
31 |
32 | for chunk in grouper_it(chunk_size, walk_func(**kwargs)):
33 | if max_results is not None and count >= max_results:
34 | break
35 | else:
36 | images = []
37 | for result in chunk:
38 | image = serialize_func(result)
39 | if image:
40 | images.append(image)
41 | if len(images) > 0:
42 | try:
43 | # Bulk update the search engine too
44 | if not settings.DEBUG:
45 | es.cluster.health(wait_for_status='green', request_timeout=2000)
46 | search_objs = [search.db_image_to_index(img).to_dict(include_meta=True) for img in images]
47 | elasticsearch.helpers.bulk(es, search_objs)
48 | models.Image.objects.bulk_create(images)
49 | log.debug("*** Committed set of %d images", len(images))
50 | success_count += len(images)
51 | except (requests.exceptions.ReadTimeout,
52 | elasticsearch.exceptions.TransportError,
53 | elasticsearch.helpers.BulkIndexError,
54 | IntegrityError) as e:
55 | log.warn("Got one or more integrity errors on batch: %s", e)
56 | finally:
57 | count += len(images)
58 | return success_count
59 |
--------------------------------------------------------------------------------
/imageledger/jinja2/404.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block body %}
4 |
5 |
6 |
7 |
Oops! That page can’t be found.
8 |
9 | That page may no longer exist, or you may need to be logged in to see it.
10 |
11 |
12 |
13 | {% endblock body %}
14 |
--------------------------------------------------------------------------------
/imageledger/jinja2/about.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block body %}
4 |
5 |
6 |
7 |
8 |
About CC Search
9 |
10 |
11 |
12 |
13 | Source
14 |
15 |
16 | Number of works
17 |
18 |
19 | {% for provider in providers %}
20 |
21 |
22 | {{ provider['display_name'] }}
23 |
24 |
25 | {{ provider['hits']}}
26 |
27 |
28 | {% endfor %}
29 |
30 |
31 |
32 | There is no larger compendium of shared human knowledge and creativity than the Commons, including over 1.1 billion digital works available under CC licenses. Despite the tremendous growth of the Commons, and the widespread use of the CC licenses and public domain marks, there is no simple way to maximize use of, and engagement with, all of that content. There is no front door — no tool designed for the general public to facilitate discovery for the purpose of reuse and remix, to simplify the license terms, make attribution easy, or support curation, and crowdsourced metadata.
33 |
34 |
35 | Creative Commons’ “CC Search” project will develop and release an open online search and re-use tool that will allow high-quality content from the commons to surface in a more seamless and accessible way. Our first prototype relies on open APIs and focuses on photos as its first media type. It is meant to elicit discussion and inform our development as we build out the full set of tools. “CC Search” will enable users to curate, tag, and remix that content. It will go beyond simple search to aggregate results from across the hundreds of public repositories into a single ledger, and also facilitate the use and re-use through tools like curated lists, saved searches, one- or no-click attribution, and provenance.
36 |
37 |
38 |
39 | {% endblock body %}
40 |
--------------------------------------------------------------------------------
/imageledger/jinja2/includes/favorite.html:
--------------------------------------------------------------------------------
1 | {% macro add(image, request, size) -%}
2 |
3 |
4 |
19 |
20 |
21 | {% endmacro -%}
22 |
--------------------------------------------------------------------------------
/imageledger/jinja2/includes/image-result.html:
--------------------------------------------------------------------------------
1 | {% import "includes/license-logo.html" as license_logo %}
2 | {% import "includes/lists.html" as lists %}
3 | {% import "includes/favorite.html" as favorite %}
4 | {% set skip_attributes = ['removed_from_source', 'thumbnail'] %}
5 |
6 | {% macro show(detail_url, image, request, providers) -%}
7 |
50 | {%- endmacro %}
51 |
--------------------------------------------------------------------------------
/imageledger/jinja2/includes/license-logo.html:
--------------------------------------------------------------------------------
1 | {% macro license(l, with_cc=False) -%}
2 | {% if with_cc %}
3 |
4 | {% endif %}
5 | {# Temporary fix for data bug #112 #}
6 | {% for license in l.split('-') %}
7 | {% set license = license.replace("('", "").replace("',)", "") %}
8 |
9 | {% endfor %}
10 |
11 | {%- endmacro %}
12 |
--------------------------------------------------------------------------------
/imageledger/jinja2/includes/lists.html:
--------------------------------------------------------------------------------
1 | {% macro add(image, request, size) -%}
2 |
3 |
6 | Save to List
7 |
8 | {% else %}
9 | class="button success tiny fi-list-bullet">
10 |
11 | {% endif %}
12 |
13 |
14 |
20 |
21 |
22 | {% endmacro -%}
23 |
--------------------------------------------------------------------------------
/imageledger/jinja2/includes/metadata.html:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/imageledger/jinja2/includes/pagination.html:
--------------------------------------------------------------------------------
1 |
2 | {% macro paginate_api(provider, page, pages, form) -%}
3 |
4 |
22 |
23 |
24 | {%- endmacro %}
25 |
26 | {% macro paginate_openledger(page, pages, form) -%}
27 |
28 |
46 |
47 |
48 | {%- endmacro %}
49 |
--------------------------------------------------------------------------------
/imageledger/jinja2/includes/search-form.html:
--------------------------------------------------------------------------------
1 |
76 |
--------------------------------------------------------------------------------
/imageledger/jinja2/includes/tags.html:
--------------------------------------------------------------------------------
1 | {% macro add(image, request, size) -%}
2 |
21 | {% endmacro -%}
22 |
--------------------------------------------------------------------------------
/imageledger/jinja2/list-public.html:
--------------------------------------------------------------------------------
1 | {% import "includes/image-result.html" as image_result %}
2 |
3 | {% extends "base.html" %}
4 |
5 | {% block body %}
6 |
7 |
8 |
9 | {{ object.title }}
10 |
11 |
12 | {% if object.description %}
13 |
14 | {{ object.description }}
15 |
16 | {% endif %}
17 |
18 |
19 | {% for image in object.images.all() %}
20 | {% set detail_url = url('detail', image.identifier) %}
21 | {% call image_result.show(detail_url, image, request) %}
22 | {% endcall %}
23 | {% endfor %}
24 |
25 |
26 |
27 |
28 |
29 | {% endblock body %}
30 |
--------------------------------------------------------------------------------
/imageledger/jinja2/list.html:
--------------------------------------------------------------------------------
1 | {% import "includes/image-result.html" as image_result %}
2 |
3 | {% extends "base.html" %}
4 |
5 | {% block body %}
6 |
7 |
8 |
9 |
40 |
41 |
45 |
46 |
47 | {% for image in object.images.all() %}
48 | {% set detail_url = url('detail', image.identifier) %}
49 | {% call image_result.show(detail_url, image, request) %}
50 |
55 | {% endcall %}
56 | {% endfor %}
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | {% endblock body %}
66 |
--------------------------------------------------------------------------------
/imageledger/jinja2/lists.html:
--------------------------------------------------------------------------------
1 | {% import "includes/image-result.html" as image_result %}
2 |
3 | {% extends "base.html" %}
4 |
5 | {% block body %}
6 |
7 |
8 |
9 |
My Lists and Favorites
10 | {% if favorites %}
11 |
Favorite images
12 |
13 | {% for favorite in favorites %}
14 | {% set detail_url = url('detail', favorite.image.identifier) %}
15 | {% call image_result.show(detail_url, favorite.image, request) %}
16 | {% endcall %}
17 | {% endfor %}
18 |
19 |
20 | {% else %}
21 |
22 | You haven't added any images as favorites.
23 |
24 | {% endif %}
25 |
26 |
27 | {% if object_list %}
28 |
Lists
29 | {% for lst in object_list %}
30 |
31 |
35 |
36 | Created on {{ lst.created_on.strftime('%a, %d %b %Y') }}, last updated {{ lst.updated_on.strftime('%d %b %Y')}}
37 |
38 |
39 | {{ lst.description or "" }}
40 |
41 |
42 | {% for image in lst.images.all() %}
43 | {% set detail_url = url('detail', image.identifier) %}
44 | {% call image_result.show(detail_url, image, request) %}
45 |
52 | {% endcall %}
53 | {% endfor %}
54 |
55 |
56 |
57 | {% endfor %}
58 | {% else %}
59 |
60 | You have no lists at the moment. Search for images and add them to lists to collect or share them with others.
61 |
62 | {% endif %}
63 |
64 |
65 | {% endblock body %}
66 |
--------------------------------------------------------------------------------
/imageledger/jinja2/profile.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block body %}
4 |
5 |
31 |
32 | {% endblock body %}
33 |
--------------------------------------------------------------------------------
/imageledger/jinja2/results.html:
--------------------------------------------------------------------------------
1 | {% import "includes/pagination.html" as pagination %}
2 | {% import "includes/image-result.html" as image_result %}
3 |
4 | {% extends "base.html" %}
5 |
6 | {% block body %}
7 |
8 |
9 | {% with %}
10 | {% set endpoint = '' %}
11 | {% include "includes/search-form.html" %}
12 | {% endwith %}
13 |
14 |
15 |
16 |
17 | {% if form.cleaned_data and form.cleaned_data.search and results.items|length == 0 %}
18 |
No results were found for '{{ form.cleaned_data.search }}'
19 |
20 | {% else %}
21 |
22 | {% if results.pages > 1 %}
23 |
Page {{ results.page }} of {{ results.pages }} page{{ results.pages|pluralize }}
24 | {% endif %}
25 |
26 | {% if results.items %}
27 |
28 | {{ pagination.paginate_openledger(results.page, results.pages, form) }}
29 |
30 |
31 |
32 | {% for result in results.items %}
33 |
34 | {% set detail_url = url('detail', result.identifier) %}
35 | {% call image_result.show(detail_url, result, request, results.providers) %}{% endcall %}
36 |
37 | {% endfor %}
38 |
39 | {{ pagination.paginate_openledger(results.page, results.pages, form) }}
40 | {% endif %}
41 | {% endif %}
42 |
43 |
44 |
45 | {% include "includes/photoswipe.html" %}
46 |
47 | {% endblock %}
48 |
--------------------------------------------------------------------------------
/imageledger/jinja2/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /list/
3 |
--------------------------------------------------------------------------------
/imageledger/jinja2/user-tags-list.html:
--------------------------------------------------------------------------------
1 | {% import "includes/image-result.html" as image_result %}
2 |
3 | {% extends "base.html" %}
4 |
5 | {% block body %}
6 |
37 |
38 | {% endblock body %}
39 |
--------------------------------------------------------------------------------
/imageledger/jinja2/user-tags.html:
--------------------------------------------------------------------------------
1 | {% import "includes/image-result.html" as image_result %}
2 |
3 | {% extends "base.html" %}
4 |
5 | {% block body %}
6 |
7 |
8 |
9 | {{ tag.name }}
10 |
11 |
12 |
13 | {% for image in object_list %}
14 | {% set detail_url = url('detail', image.identifier) %}
15 | {% call image_result.show(detail_url, image, request) %}
16 | {% endcall %}
17 | {% endfor %}
18 |
19 |
20 |
21 |
22 |
23 | {% endblock body %}
24 |
--------------------------------------------------------------------------------
/imageledger/management/commands/syncer.py:
--------------------------------------------------------------------------------
1 | from collections import namedtuple
2 | import itertools
3 | import logging
4 | from multiprocessing.dummy import Pool as ThreadPool
5 |
6 | from elasticsearch import helpers
7 | from django.core.management.base import BaseCommand, CommandError
8 | from django.db import connection, transaction
9 |
10 | from imageledger import models, search
11 |
12 |
13 | console = logging.StreamHandler()
14 | log = logging.getLogger(__name__)
15 | log.setLevel(logging.INFO)
16 |
17 |
18 | MAX_CONNECTION_RETRIES = 10
19 | RETRY_WAIT = 5 # Number of sections to wait before retrying
20 |
21 | DEFAULT_CHUNK_SIZE = 1000
22 | DEFAULT_NUM_ITERATIONS = 100
23 |
24 | class Command(BaseCommand):
25 | can_import_settings = True
26 | requires_migrations_checks = True
27 |
28 | def add_arguments(self, parser):
29 | parser.add_argument("--verbose",
30 | action="store_true",
31 | default=False,
32 | help="Be very chatty and run logging at DEBUG")
33 | parser.add_argument("--chunk-size",
34 | dest="chunk_size",
35 | default=DEFAULT_CHUNK_SIZE,
36 | type=int,
37 | help="The number of records to batch process at once")
38 | parser.add_argument("--with-fingerprinting",
39 | dest="with_fingerprinting",
40 | action="store_true",
41 | help="Whether to run the expensive perceptual hash routine as part of syncing")
42 |
43 | parser.add_argument("--num-iterations",
44 | dest="num_iterations",
45 | default=DEFAULT_NUM_ITERATIONS,
46 | type=int,
47 | help="The number of times to loop through `chunk_size` records")
48 |
49 | def handle(self, *args, **options):
50 | if options['verbose']:
51 | log.addHandler(console)
52 | log.setLevel(logging.DEBUG)
53 | self.sync_all_images(chunk_size=options['chunk_size'],
54 | with_fingerprinting=options['with_fingerprinting'],
55 | num_iterations=options['num_iterations'])
56 |
57 | def sync_all_images(self, chunk_size=DEFAULT_CHUNK_SIZE, with_fingerprinting=False, num_iterations=DEFAULT_NUM_ITERATIONS):
58 | """Sync all of the images, sorting from least-recently-synced"""
59 | with ThreadPool(4) as pool:
60 | starts = [i * chunk_size for i in range(0, num_iterations)]
61 | pool.starmap(do_sync, zip(starts,
62 | itertools.repeat(chunk_size, len(starts)),
63 | itertools.repeat(with_fingerprinting, len(starts))))
64 |
65 | def do_sync(start, chunk_size, with_fingerprinting):
66 | end = start + chunk_size
67 | log.info("Starting sync in range from %d to %d...", start, end)
68 | imgs = models.Image.objects.all().order_by('-last_synced_with_source')[start:end]
69 | for img in imgs:
70 | img.sync(attempt_perceptual_hash=with_fingerprinting)
71 |
--------------------------------------------------------------------------------
/imageledger/management/commands/util.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 |
4 | from django.conf import settings
5 | from django.core.management.base import BaseCommand, CommandError
6 |
7 | from elasticsearch_dsl import Search, Q
8 | from imageledger import models, search
9 |
10 | # This is a catch-all management command for one-off queries that need to be executed
11 | # in the Django environment. It's expected that most of these functions will be short-lived,
12 | # but are retained in source code for future reference.
13 |
14 | console = logging.StreamHandler()
15 | log = logging.getLogger(__name__)
16 | log.setLevel(logging.INFO)
17 |
18 | class Command(BaseCommand):
19 | can_import_settings = True
20 | requires_migrations_checks = True
21 |
22 | def add_arguments(self, parser):
23 | parser.add_argument("--verbose",
24 | action="store_true",
25 | default=False,
26 | help="Be very chatty and run logging at DEBUG")
27 | parser.add_argument("func",
28 | help="The function to be run")
29 |
30 | def handle(self, *args, **options):
31 | if options['verbose']:
32 | log.setLevel(logging.DEBUG)
33 | getattr(self, options['func'])()
34 |
35 |
36 | def correct_orphan_records(self, provider='europeana', end=None):
37 | """[#185] Delete records from the search engine which aren't found in the database"""
38 | s = Search()
39 | q = Q('term', provider=provider)
40 | s = s.query(q)
41 | response = s.execute()
42 | total = response.hits.total
43 | # A file extracted from the production database listing all of the europeana identifiers
44 | identifier_file = '/tmp/europeana-identifiers.json'
45 | db_identifiers = set(json.load(open(identifier_file)))
46 | total_in_db = len(db_identifiers)
47 | log.info("Using search engine instance %s", settings.ELASTICSEARCH_URL)
48 | log.info("Total records: %d (search engine), %d (database) [diff=%d]", total, total_in_db, total - total_in_db)
49 | deleted_count = 0
50 | for r in s.scan():
51 | if r.identifier not in db_identifiers:
52 | img = search.Image.get(id=r.identifier)
53 | log.debug("Going to delete image %s", img)
54 | deleted_count += 1
55 | log.info("Deleted %d from search engine", deleted_count)
56 |
57 | def correct_license_capitalization(self, provider='europeana', end=None):
58 | """[#186] Correct license capitalization"""
59 | s = Search()
60 | q = Q('term', provider=provider)
61 | s = s.query(q)
62 | response = s.execute()
63 | total = response.hits.total
64 | log.info("Using search engine instance %s", settings.ELASTICSEARCH_URL)
65 | mod_count = 0
66 | count = 0
67 | for r in s.scan():
68 | if not r.license.islower():
69 | img = search.Image.get(id=r.identifier)
70 | log.debug("[%d] Changing license %s to %s", count, img.license, img.license.lower())
71 | img.update(license=img.license.lower())
72 | mod_count += 1
73 | count += 1
74 | log.info("Modified %d records in search engine", mod_count)
75 |
--------------------------------------------------------------------------------
/imageledger/migrations/0002_auto_20161111_1812.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.3 on 2016-11-11 18:12
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('imageledger', '0001_initial'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterUniqueTogether(
16 | name='imagetags',
17 | unique_together=set([('tag', 'image')]),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/imageledger/migrations/0003_auto_20161116_1812.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.3 on 2016-11-16 18:12
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('imageledger', '0002_auto_20161111_1812'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterModelOptions(
16 | name='image',
17 | options={'ordering': ['-created_on']},
18 | ),
19 | migrations.AlterField(
20 | model_name='list',
21 | name='images',
22 | field=models.ManyToManyField(related_name='lists', to='imageledger.Image'),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/imageledger/migrations/0004_auto_20161116_2041.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.3 on 2016-11-16 20:41
3 | from __future__ import unicode_literals
4 |
5 | from django.conf import settings
6 | from django.db import migrations, models
7 | import django.db.models.deletion
8 |
9 |
10 | class Migration(migrations.Migration):
11 |
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ('imageledger', '0003_auto_20161116_1812'),
15 | ]
16 |
17 | operations = [
18 | migrations.AddField(
19 | model_name='image',
20 | name='tags',
21 | field=models.ManyToManyField(through='imageledger.ImageTags', to='imageledger.Tag'),
22 | ),
23 | migrations.AddField(
24 | model_name='list',
25 | name='owner',
26 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/imageledger/migrations/0005_auto_20161117_1512.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.3 on 2016-11-17 15:12
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('imageledger', '0004_auto_20161116_2041'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='list',
17 | name='is_public',
18 | field=models.BooleanField(default=False),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/imageledger/migrations/0006_image_last_synced_with_source.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.3 on 2016-11-28 18:43
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('imageledger', '0005_auto_20161117_1512'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='image',
17 | name='last_synced_with_source',
18 | field=models.DateTimeField(blank=True, null=True),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/imageledger/migrations/0007_auto_20161128_1847.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.3 on 2016-11-28 18:47
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('imageledger', '0006_image_last_synced_with_source'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='image',
17 | name='last_synced_with_source',
18 | field=models.DateTimeField(blank=True, db_index=True, null=True),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/imageledger/migrations/0008_image_removed_from_source.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.3 on 2016-11-28 18:53
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('imageledger', '0007_auto_20161128_1847'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='image',
17 | name='removed_from_source',
18 | field=models.BooleanField(default=False),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/imageledger/migrations/0009_auto_20161128_2019.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.3 on 2016-11-28 20:19
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('imageledger', '0008_image_removed_from_source'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='image',
17 | name='perceptual_hash',
18 | field=models.CharField(blank=True, db_index=True, max_length=255, null=True),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/imageledger/migrations/0010_auto_20161130_1814.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.3 on 2016-11-30 18:14
3 | from __future__ import unicode_literals
4 |
5 | from django.conf import settings
6 | from django.db import migrations, models
7 | import django.db.models.deletion
8 |
9 |
10 | class Migration(migrations.Migration):
11 |
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ('imageledger', '0009_auto_20161128_2019'),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name='Favorite',
20 | fields=[
21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22 | ('created_on', models.DateTimeField(auto_now_add=True)),
23 | ('updated_on', models.DateTimeField(auto_now=True)),
24 | ('image', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorites', to='imageledger.Image')),
25 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
26 | ],
27 | options={
28 | 'db_table': 'favorite',
29 | },
30 | ),
31 | migrations.AlterUniqueTogether(
32 | name='favorite',
33 | unique_together=set([('user', 'image')]),
34 | ),
35 | ]
36 |
--------------------------------------------------------------------------------
/imageledger/migrations/0011_auto_20161205_1424.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.3 on 2016-12-05 14:24
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('imageledger', '0010_auto_20161130_1814'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterModelOptions(
16 | name='favorite',
17 | options={'ordering': ['-updated_on']},
18 | ),
19 | migrations.AlterModelOptions(
20 | name='list',
21 | options={'ordering': ['-updated_on']},
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/imageledger/migrations/0012_add_user_tags.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.3 on 2016-12-05 15:17
3 | from __future__ import unicode_literals
4 |
5 | from django.conf import settings
6 | from django.db import migrations, models
7 | import django.db.models.deletion
8 |
9 |
10 | class Migration(migrations.Migration):
11 |
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ('imageledger', '0011_auto_20161205_1424'),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name='UserTags',
20 | fields=[
21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22 | ('created_on', models.DateTimeField(auto_now_add=True)),
23 | ('updated_on', models.DateTimeField(auto_now=True)),
24 | ('image', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_tags', to='imageledger.Image')),
25 | ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='imageledger.Tag')),
26 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
27 | ],
28 | options={
29 | 'db_table': 'user_tags',
30 | },
31 | ),
32 | migrations.AlterUniqueTogether(
33 | name='usertags',
34 | unique_together=set([('tag', 'image', 'user')]),
35 | ),
36 | ]
37 |
--------------------------------------------------------------------------------
/imageledger/migrations/0013_add-slug-to-tag.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.3 on 2016-12-05 21:52
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 | import django.db.models.deletion
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | dependencies = [
12 | ('imageledger', '0012_add_user_tags'),
13 | ]
14 |
15 | operations = [
16 | migrations.AddField(
17 | model_name='tag',
18 | name='slug',
19 | field=models.SlugField(blank=True, null=True),
20 | ),
21 | migrations.AlterField(
22 | model_name='usertags',
23 | name='tag',
24 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_tags', to='imageledger.Tag'),
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/imageledger/migrations/0014_increase-slug-size.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.3 on 2016-12-05 21:58
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('imageledger', '0013_add-slug-to-tag'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='tag',
17 | name='slug',
18 | field=models.SlugField(blank=True, max_length=255, null=True),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/imageledger/migrations/0015_auto_20161219_1955.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.10.3 on 2016-12-19 19:55
3 | from __future__ import unicode_literals
4 |
5 | from django.conf import settings
6 | from django.db import migrations
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | dependencies = [
12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13 | ('imageledger', '0014_increase-slug-size'),
14 | ]
15 |
16 | operations = [
17 | migrations.AlterUniqueTogether(
18 | name='list',
19 | unique_together=set([('title', 'owner')]),
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/imageledger/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/imageledger/migrations/__init__.py
--------------------------------------------------------------------------------
/imageledger/signals.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import hashlib
3 | import logging
4 | import uuid
5 |
6 | from django.db.models.signals import pre_save, post_save, post_delete
7 | from django.contrib.auth import get_user_model
8 | from django.dispatch import receiver
9 | from django.utils.text import slugify
10 | from django.conf import settings
11 |
12 | from imageledger import models, search
13 |
14 | log = logging.getLogger(__name__)
15 |
16 | @receiver(pre_save, sender=models.Image)
17 | def set_identifier(sender, instance, **kwargs):
18 | if instance.identifier is None:
19 | instance.identifier = create_identifier(instance.url)
20 |
21 | @receiver(post_save, sender=models.Image)
22 | def update_search_index(sender, instance, **kwargs):
23 | """When an Image instance is saved, tell the search engine about it."""
24 | if not settings.TESTING:
25 | _update_search_index(instance)
26 |
27 | def _update_search_index(img):
28 | # FIXME This may result in a lot of concurrent requests during batch updates;
29 | # in those cases consider unregistering this signal and manually batching requests
30 | # (note that Django's bulk_create will not fire this signal, which is good)
31 | search_obj = search.db_image_to_index(img)
32 | if (search_obj.removed_from_source):
33 | log.debug("Removing image %s from search index", img.identifier)
34 | search_obj.delete(ignore=404)
35 | else:
36 | log.debug("Indexing image %s", img.identifier)
37 | search_obj.save()
38 |
39 | def create_identifier(key):
40 | """Create a unique, stable identifier for a key"""
41 | m = hashlib.md5()
42 | m.update(bytes(key.encode('utf-8')))
43 | return base64.urlsafe_b64encode(m.digest()).decode('utf-8')
44 |
45 | @receiver(pre_save, sender=models.Tag)
46 | @receiver(pre_save, sender=models.List)
47 | def set_slug(sender, instance, **kwargs):
48 | if instance.slug is None:
49 | uniquish = str(uuid.uuid4())[:8]
50 | title_field = instance.title if hasattr(instance, 'title') else instance.name
51 | slug = create_slug([title_field, uniquish])
52 | instance.slug = slug
53 |
54 | def create_slug(el):
55 | """For the list of items el, create a unique slug out of them"""
56 | return '-'.join([slugify(str(i)) for i in el])
57 |
--------------------------------------------------------------------------------
/imageledger/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/imageledger/tests/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/imageledger/tests/image.png
--------------------------------------------------------------------------------
/imageledger/tests/settings.py:
--------------------------------------------------------------------------------
1 | SECRET_KEY = 'XXX'
2 | DEBUG=False
3 |
4 | # API-specific
5 | API_500PX_KEY = 'XXX'
6 | API_500PX_SECRET = 'XXX'
7 | API_RIJKS = 'XXX'
8 | FLICKR_KEY = 'XXX'
9 | FLICKR_SECRET = 'XXX'
10 |
11 | # Database-specific
12 | SQLALCHEMY_DATABASE_URI = 'postgresql://{}:{}@{}:{}/{}'.format('cctest',
13 | 'cctest',
14 | 'localhost',
15 | '5432',
16 | 'openledgertest')
17 |
18 | SQLALCHEMY_TRACK_MODIFICATIONS = False
19 |
20 | DEBUG_TB_ENABLED = False
21 |
22 | TESTING=True
23 |
--------------------------------------------------------------------------------
/imageledger/tests/test_favorites.py:
--------------------------------------------------------------------------------
1 | import os
2 | import logging
3 |
4 | from django.urls import reverse
5 | from django.conf import settings
6 | from django.contrib.auth import get_user_model
7 |
8 | from imageledger import models
9 | from imageledger.tests.utils import *
10 |
11 | class TestFavoritesViews(TestImageledgerApp):
12 |
13 | def setUp(self):
14 | super().setUp()
15 | self.img1 = models.Image.objects.create(title='image1', url='http://example.com/1', license='CC0')
16 | self.img2 = models.Image.objects.create(title='image2', url='http://example.com/2', license='CC0')
17 | self.username = 'testuser'
18 | self.user = get_user_model().objects.create_user(self.username, password=self.username)
19 | self.client.force_login(self.user)
20 |
21 | def test_favorites_shown(self):
22 | """When an image is favorited, it should show up on the My Lists page"""
23 | models.Favorite.objects.create(user=self.user, image=self.img1)
24 | models.Favorite.objects.create(user=self.user, image=self.img2)
25 | resp = self.client.get(reverse('my-lists'))
26 | self.assertEqual(2, len(select_nodes(resp, '.t-image-result')))
27 | assert 'image1' in str(resp.content)
28 | assert 'image2' in str(resp.content)
29 |
30 | def test_favorites_removed(self):
31 | """When a favorite is removed, it should not appear on the My Lists page"""
32 | fave = models.Favorite.objects.create(user=self.user, image=self.img1)
33 | resp = self.client.get(reverse('my-lists'))
34 | self.assertEqual(1, len(select_nodes(resp, '.t-image-result')))
35 | fave.delete()
36 | resp = self.client.get(reverse('my-lists'))
37 | self.assertEqual(0, len(select_nodes(resp, '.t-image-result')))
38 |
--------------------------------------------------------------------------------
/imageledger/tests/test_tags.py:
--------------------------------------------------------------------------------
1 | import os
2 | import logging
3 |
4 | from django.urls import reverse
5 | from django.conf import settings
6 | from django.contrib.auth import get_user_model
7 |
8 | from imageledger import models
9 | from imageledger.tests.utils import *
10 |
11 | class TestTagsViews(TestImageledgerApp):
12 |
13 | def setUp(self):
14 | super().setUp()
15 | self.img1 = models.Image.objects.create(title='image1', url='http://example.com/1', license='CC0')
16 | self.img2 = models.Image.objects.create(title='image2', url='http://example.com/2', license='CC0')
17 | self.username = 'testuser'
18 | self.user = get_user_model().objects.create_user(self.username, password=self.username)
19 |
20 | def test_tag_view_owned(self):
21 | """A user should be able to view their own tag on a page"""
22 | tag = models.Tag.objects.create(name='My Tag')
23 | user_tag = models.UserTags.objects.create(tag=tag, image=self.img1, user=self.user)
24 | self.client.force_login(self.user)
25 | resp = self.client.get(reverse('my-tags-detail', kwargs={'slug': tag.slug}))
26 | self.assertEquals(200, resp.status_code)
27 | self.assertEquals(1, len(select_nodes(resp, '.image-result')))
28 |
29 | def test_tag_view_not_found(self):
30 | """A logged-in request for a tag that doesn't exist should 404"""
31 | self.client.force_login(self.user)
32 | resp = self.client.get(reverse('my-tags-detail', kwargs={'slug': 'notfound'}))
33 | self.assertEquals(404, resp.status_code)
34 |
35 | def test_tag_exists_but_not_logged_in(self):
36 | """[#131] An anon request for a tag should redirect to login"""
37 | tag = models.Tag.objects.create(name='My Tag')
38 | user_tag = models.UserTags.objects.create(tag=tag, image=self.img1, user=self.user)
39 | # no log in
40 | resp = self.client.get(reverse('my-tags-detail', kwargs={'slug': tag.slug}))
41 | self.assertEquals(302, resp.status_code)
42 |
--------------------------------------------------------------------------------
/imageledger/tests/utils.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 |
4 | import jinja2
5 | from django.test import TestCase
6 | import html5lib
7 | from lxml.html import tostring, html5parser
8 | import responses
9 |
10 | from imageledger import models
11 |
12 | TESTING_CONFIG = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'settings.py')
13 |
14 | class TestImageledgerApp(TestCase):
15 | """Setup/teardown for app test cases"""
16 | def setUp(self):
17 | activate_all_provider_mocks()
18 |
19 | def activate_all_provider_mocks():
20 | """Mock all the responses we know about from all the providers"""
21 | activate_500px_mocks()
22 | activate_rijks_mocks()
23 | activate_flickr_mocks()
24 | activate_wikimedia_mocks()
25 |
26 | def activate_flickr_mocks():
27 | from imageledger.handlers.handler_flickr import auth
28 | flickr = auth()
29 | responses.add(responses.POST, flickr.REST_URL, status=200, content_type='application/json',
30 | json=load_json_data('flickr-response.json'))
31 |
32 | def activate_500px_mocks():
33 | from imageledger.handlers.handler_500px import ENDPOINT_PHOTOS
34 | responses.add(responses.GET, ENDPOINT_PHOTOS, status=200, content_type='application/json',
35 | json=load_json_data('500px-response.json'))
36 |
37 | def activate_rijks_mocks():
38 | from imageledger.handlers.handler_rijks import ENDPOINT_PHOTOS
39 | responses.add(responses.GET, ENDPOINT_PHOTOS, status=200, content_type='application/json',
40 | json=load_json_data('rijks-response.json'))
41 |
42 | def activate_wikimedia_mocks():
43 | from imageledger.handlers.handler_wikimedia import BASE_URL, WIKIDATA_URL
44 | entities_template = load_json_data('wikimedia-entities-response.json')
45 | wikidata_template = load_json_data('wikimedia-data-response.json')
46 | responses.add(responses.GET, BASE_URL, status=200, content_type='application/json',
47 | json=entities_template)
48 | responses.add(responses.GET, WIKIDATA_URL, status=200, content_type='application/json',
49 | json=wikidata_template)
50 |
51 | def load_json_data(datafile):
52 | """Load testing data in JSON format relative to the path where the test lives"""
53 | dir_path = os.path.dirname(os.path.realpath(__file__))
54 | return json.loads(open(os.path.join(dir_path, datafile)).read())
55 |
56 | def select_node(resp, selector):
57 | """Give a response from the app, return just the HTML fragment defined by `selector`.
58 | Guaranteed to return one node or None"""
59 | r = select_nodes(resp, selector)
60 | if r and len(r) > 0:
61 | return r[0]
62 | return None
63 |
64 | def select_nodes(resp, selector):
65 | """Give a response from the app, return just the HTML fragment defined by `selector`"""
66 | h = html5lib.parse(resp.content.decode('utf-8'), treebuilder='lxml', namespaceHTMLElements=False)
67 | return h.getroot().cssselect(selector)
68 |
--------------------------------------------------------------------------------
/imageledger/tests/wikimedia-entities-response.json:
--------------------------------------------------------------------------------
1 | {
2 | "searchinfo": {
3 | "search": "cat"
4 | },
5 | "search": [{
6 | "id": "Q146",
7 | "concepturi": "http://www.wikidata.org/entity/Q146",
8 | "url": "//www.wikidata.org/wiki/Q146",
9 | "title": "Q146",
10 | "pageid": 282,
11 | "label": "cat",
12 | "description": "domesticated species of feline",
13 | "match": {
14 | "type": "label",
15 | "language": "en",
16 | "text": "cat"
17 | }
18 | }],
19 | "search-continue": 1,
20 | "success": 1
21 | }
22 |
--------------------------------------------------------------------------------
/imageledger/views/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/imageledger/views/__init__.py
--------------------------------------------------------------------------------
/imageledger/views/favorite_views.py:
--------------------------------------------------------------------------------
1 | from django.db.models import Q
2 | from django.contrib.auth.mixins import LoginRequiredMixin, AccessMixin
3 | from django.views.generic.edit import CreateView, UpdateView, DeleteView
4 | from django.views.generic.list import ListView
5 | from django.views.generic.detail import DetailView
6 | from django.urls import reverse_lazy
7 | from django.http import HttpResponseRedirect, Http404
8 |
9 | from imageledger import models
10 |
11 | class FavoriteList(LoginRequiredMixin, ListView):
12 | model = models.Favorite
13 | template_name = "favorites.html"
14 | raise_exception = False
15 |
--------------------------------------------------------------------------------
/imageledger/views/list_views.py:
--------------------------------------------------------------------------------
1 | from django.db.models import Q
2 | from django.contrib.auth.mixins import LoginRequiredMixin, AccessMixin
3 | from django.views.generic.edit import CreateView, UpdateView, DeleteView
4 | from django.views.generic.list import ListView
5 | from django.views.generic.detail import DetailView
6 | from django.urls import reverse_lazy
7 | from django.http import HttpResponseRedirect, Http404
8 |
9 | from imageledger import models, forms
10 |
11 | class OwnedListMixin(object):
12 |
13 | def get_queryset(self):
14 | """Lists owned by the current user only"""
15 | qs = super().get_queryset()
16 | qs = qs.filter(owner=self.request.user)
17 | return qs
18 |
19 | class OLListDetail(DetailView):
20 | model = models.List
21 | template_name = "list-public.html"
22 | fields = ['title', 'description', 'creator_displayname', 'images']
23 |
24 | def render_to_response(self, context):
25 | if not self.request.user.is_anonymous and self.object.owner == self.request.user:
26 | return HttpResponseRedirect(reverse_lazy('my-list-update', kwargs={'slug': self.object.slug}))
27 | return super().render_to_response(context)
28 |
29 | def get_queryset(self):
30 | """Public lists only, or the user's own list"""
31 | qs = super().get_queryset()
32 |
33 | if self.request.user.is_anonymous:
34 | qs = qs.filter(is_public=True)
35 | else:
36 | qs = qs.filter(Q(owner=self.request.user) | Q(is_public=True))
37 | return qs
38 |
39 |
40 | class OLListCreate(LoginRequiredMixin, CreateView):
41 | model = models.List
42 | template_name = "list.html"
43 | fields = ['title', 'description', 'is_public', 'creator_displayname']
44 |
45 | class OLListUpdate(LoginRequiredMixin, OwnedListMixin, UpdateView):
46 | model = models.List
47 | form_class = forms.ListForm
48 | template_name = "list.html"
49 |
50 | def get_form_kwargs(self):
51 | kw = super().get_form_kwargs()
52 | kw['request'] = self.request
53 | return kw
54 |
55 | def get_object(self):
56 | try:
57 | obj = super().get_object()
58 | return obj
59 | except Http404:
60 | return None # Don't raise 404 here, do that in render_to_response so we can redirect
61 |
62 | def render_to_response(self, context):
63 | if not context.get('object'):
64 | return HttpResponseRedirect(reverse_lazy('list-detail', kwargs=self.kwargs))
65 | return super().render_to_response(context)
66 |
67 | def handle_no_permission(self):
68 | return HttpResponseRedirect(reverse_lazy('list-detail', kwargs=self.kwargs))
69 |
70 | class OLListDelete(LoginRequiredMixin, OwnedListMixin, DeleteView):
71 | model = models.List
72 | success_url = reverse_lazy('my-lists')
73 |
74 | class OLOwnedListList(LoginRequiredMixin, OwnedListMixin, ListView):
75 | model = models.List
76 | template_name = "lists.html"
77 | raise_exception = False
78 |
79 | def get_context_data(self, **kwargs):
80 | """Get the "list" of favorites as well"""
81 | context = super().get_context_data(**kwargs)
82 | context['favorites'] = models.Favorite.objects.filter(user=self.request.user).select_related('image')
83 | return context
84 |
--------------------------------------------------------------------------------
/imageledger/views/search_views.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from functools import reduce
3 |
4 | from django.conf import settings
5 | from django.shortcuts import render, get_object_or_404
6 | from django.views.decorators.csrf import ensure_csrf_cookie
7 | from elasticsearch_dsl import Search, Q
8 | from elasticsearch_dsl.connections import connections
9 |
10 | from imageledger import forms, search, licenses, models
11 |
12 | log = logging.getLogger(__name__)
13 |
14 | @ensure_csrf_cookie
15 | def index(request):
16 | res = search.do_search(request);
17 | return render(request, 'results.html',
18 | {'form': res['form'],
19 | 'work_types': settings.WORK_TYPES,
20 | 'results': res['results'],})
21 |
22 | def by_image(request):
23 | """Load an image in detail view, passing parameters by query string so that
24 | we can either load an image from an external provider or from our own datastore."""
25 | license_version = licenses.license_map_from_partners()[request.GET.get('provider')]['version']
26 | license_url = licenses.get_license_url(request.GET.get('license'), license_version)
27 | remaining = dict((k, request.GET[k]) for k in request.GET) # The vals in request.GET are lists, so flatten
28 | remaining.update({'license_version': license_version})
29 | return render(request, 'detail.html',
30 | {'image': remaining,
31 | 'license_url': license_url,
32 | })
33 |
34 |
35 | def detail(request, identifier):
36 | obj = get_object_or_404(models.Image, identifier=identifier)
37 | if request.user.is_authenticated:
38 | # Is it a favorite?
39 | obj.is_favorite = models.Favorite.objects.filter(user=request.user, image=obj).exists()
40 | license_url = licenses.get_license_url(obj.license, obj.license_version)
41 | return render(request, 'detail.html',
42 | {'image': obj,
43 | 'license_url': license_url,})
44 |
--------------------------------------------------------------------------------
/imageledger/views/site_views.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import re
3 | from django.core.cache import cache
4 | from django.conf import settings
5 | from django.contrib.auth.decorators import login_required
6 | from django.views.decorators.http import require_POST
7 | from django.template.loader import get_template
8 |
9 | from django.http import HttpResponse
10 | from django.contrib.auth import get_user_model, logout
11 | from django.shortcuts import render, redirect
12 | from django.urls import reverse
13 | from django.views.decorators.cache import cache_page
14 | from elasticsearch_dsl import Search, Q
15 |
16 | from imageledger import forms, search, licenses, models
17 |
18 | log = logging.getLogger(__name__)
19 |
20 | CACHE_STATS_DURATION = 60 * 60 # 1 hour
21 | CACHE_STATS_NAME = 'provider-stats'
22 |
23 | def about(request):
24 | """Information about the current site, its goals, and what content is loaded"""
25 | # Provider counts
26 | providers = cache.get_or_set(CACHE_STATS_NAME, [], CACHE_STATS_DURATION)
27 | if not providers:
28 | for provider in sorted(settings.PROVIDERS.keys()):
29 | s = Search()
30 | q = Q('term', provider=provider)
31 | s = s.query(q)
32 | response = s.execute()
33 | if response.hits.total > 0:
34 | data = settings.PROVIDERS[provider]
35 | total = intcomma(response.hits.total)
36 | data.update({'hits': total})
37 | providers.append(data)
38 | # All results
39 | s = Search()
40 | response = s.execute()
41 | total = intcomma(response.hits.total)
42 | providers.append({'display_name': 'Total', 'hits': total})
43 | cache.set(CACHE_STATS_NAME, providers)
44 | return render(request, "about.html", {'providers': providers})
45 |
46 | @login_required
47 | def profile(request):
48 | counts = {}
49 | counts['lists'] = models.List.objects.filter(owner=request.user).count()
50 | counts['favorites'] = models.Favorite.objects.filter(user=request.user).count()
51 | counts['tags'] = models.UserTags.objects.filter(user=request.user).count()
52 | return render(request, "profile.html", {'user': request.user,
53 | 'counts': counts})
54 |
55 |
56 | @login_required
57 | @require_POST
58 | def delete_account(request):
59 | user = get_user_model().objects.get(username=request.user.username)
60 | logout(request)
61 | user.delete()
62 | return redirect(reverse('index'))
63 |
64 | def health(request):
65 | return HttpResponse('OK')
66 |
67 | @cache_page(60 * 60) # 1 hour
68 | def robots(request):
69 | out = get_template("robots.txt").render()
70 | return HttpResponse(out, content_type="text/plain")
71 |
72 | def intcomma(value):
73 | # Adapted from https://github.com/django/django/blob/master/django/contrib/humanize/templatetags/humanize.py
74 | orig = str(value)
75 | new = re.sub(r"^(-?\d+)(\d{3})", r'\g<1>,\g<2>', orig)
76 | if orig == new:
77 | return new
78 | else:
79 | return intcomma(new)
80 |
--------------------------------------------------------------------------------
/imageledger/views/tag_views.py:
--------------------------------------------------------------------------------
1 | from django.db.models import Q
2 | from django.contrib.auth.mixins import LoginRequiredMixin, AccessMixin
3 | from django.views.generic.edit import CreateView, UpdateView, DeleteView
4 | from django.views.generic.list import ListView
5 | from django.views.generic.detail import DetailView
6 | from django.urls import reverse_lazy
7 | from django.http import HttpResponseRedirect, Http404
8 | from django.shortcuts import get_object_or_404
9 |
10 | from imageledger import models
11 |
12 | class OwnedTagsMixin(object):
13 |
14 | def get_queryset(self):
15 | """Tags owned by the current user only"""
16 | qs = super().get_queryset()
17 | # Get the distinct set of tags
18 | qs = qs.filter(user_tags__user=self.request.user).distinct()
19 | return qs
20 |
21 | class UserTagsList(LoginRequiredMixin, OwnedTagsMixin, ListView):
22 | model = models.Tag
23 | template_name = "user-tags-list.html"
24 | raise_exception = False
25 |
26 |
27 | class UserTagsDetail(LoginRequiredMixin, ListView):
28 | template_name = "user-tags.html"
29 | model = models.Tag
30 |
31 | def get_queryset(self):
32 | """Images owned by the current user only; 302 if anon"""
33 | # Get the distinct set of images
34 | qs = models.Image.objects.filter(user_tags__user=self.request.user,
35 | user_tags__tag__slug=self.kwargs.get('slug')).distinct()
36 | return qs
37 |
38 | def render_to_response(self, context):
39 | tag = get_object_or_404(models.Tag, slug=self.kwargs.get('slug'))
40 | context['tag'] = tag
41 | return super().render_to_response(context)
42 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | if len(sys.argv) > 1 and sys.argv[1] == 'test':
7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openledger.test_settings")
8 | else:
9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openledger.settings")
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError:
13 | # The above import may fail for some other reason. Ensure that the
14 | # issue is really that Django is missing to avoid masking other
15 | # exceptions on Python 2.
16 | try:
17 | import django
18 | except ImportError:
19 | raise ImportError(
20 | "Couldn't import Django. Are you sure it's installed and "
21 | "available on your PYTHONPATH environment variable? Did you "
22 | "forget to activate a virtual environment?"
23 | )
24 | raise
25 |
26 | execute_from_command_line(sys.argv)
27 |
--------------------------------------------------------------------------------
/openledger/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/openledger/__init__.py
--------------------------------------------------------------------------------
/openledger/jinja2.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from urllib.parse import urlencode, parse_qs
3 |
4 |
5 | from django.contrib.staticfiles.storage import staticfiles_storage
6 | from django.urls import reverse
7 |
8 | from jinja2 import Environment
9 |
10 | log = logging.getLogger(__name__)
11 |
12 | def environment(**options):
13 | env = Environment(**options)
14 | env.globals.update({
15 | 'static': staticfiles_storage.url,
16 | 'url': url_tag,
17 | 'url_for': url_tag,
18 | 'url_with_form': url_with_form,
19 | })
20 | return env
21 |
22 | def url_with_form(view, form, args, kwargs):
23 | """Expects a view name, a form, and optional arguments. The form's data will be
24 | serialized, with any overrides from kwargs applied. Args are passed through to `reverse`"""
25 | url = reverse(view, args=args)
26 | qs = form.data.urlencode()
27 | parsed = parse_qs(qs)
28 | if kwargs:
29 | parsed.update(kwargs)
30 | url = url + '?' + urlencode(parsed, doseq=True)
31 |
32 | return url
33 |
34 | def url_tag(view, *args, **kwargs):
35 | url = reverse(view, args=args)
36 | if kwargs:
37 | url += '?' + urlencode(kwargs)
38 | return url
39 |
40 | def pluralize(number, singular='', plural='s'):
41 | try:
42 | number = int(number)
43 | except ValueError:
44 | number = 0
45 | finally:
46 | return singular if number == 1 else plural
47 |
--------------------------------------------------------------------------------
/openledger/local.py.example:
--------------------------------------------------------------------------------
1 | # Example local.py for development
2 | DEBUG = True
3 | SECRET_KEY = 'CHANGEME'
4 |
5 | CSRF_COOKIE_SECURE = False
6 |
7 | API_500PX_KEY = 'CHANGEME'
8 | API_500PX_SECRET = 'CHANGEME'
9 | API_RIJKS = 'CHANGEME'
10 | FLICKR_KEY = 'CHANGEME'
11 | FLICKR_SECRET = 'CHANGEME'
12 | AKISMET_KEY = 'CHANGEME'
13 | EUROPEANA_API_KEY = 'CHANGEME'
14 | EUROPEANA_PRIVATE_KEY = 'CHANGEME'
15 |
16 | ELASTICSEARCH_URL = 'es'
17 | ELASTICSEARCH_PORT = 9200
18 |
19 | AWS_ACCESS_KEY_ID = 'CHANGEME'
20 | AWS_SECRET_ACCESS_KEY = 'CHANGEME'
21 |
22 |
23 | DATABASES = {
24 | 'default': {
25 | 'ENGINE': 'django.db.backends.postgresql',
26 | 'NAME': 'openledger',
27 | 'USER': 'postgres',
28 | # 'PASSWORD': 'CHANGEME',
29 | 'HOST': 'db',
30 | 'PORT': 5432,
31 | }
32 | }
33 | ALLOWED_HOSTS = ['localhost']
34 |
35 | LOGGING = {
36 | 'version': 1,
37 | 'disable_existing_loggers': False,
38 | 'filters': {
39 | 'require_debug_false': {
40 | '()': 'django.utils.log.RequireDebugFalse'
41 | }
42 | },
43 | 'handlers': {
44 | 'mail_admins': {
45 | 'level': 'ERROR',
46 | 'filters': ['require_debug_false'],
47 | 'class': 'django.utils.log.AdminEmailHandler'
48 | },
49 | 'console': {
50 | 'level': 'DEBUG',
51 | 'class': 'logging.StreamHandler'
52 | },
53 | },
54 | 'loggers': {
55 | 'django.request': {
56 | 'handlers': ['mail_admins'],
57 | 'level': 'ERROR',
58 | 'propagate': True,
59 | },
60 | 'imageledger': {
61 | 'handlers': ['console'],
62 | 'level': 'DEBUG'
63 | },
64 |
65 | }
66 | }
67 | from openledger.settings import INSTALLED_APPS, MIDDLEWARE
68 |
69 | MIDDLEWARE += [
70 | 'debug_toolbar.middleware.DebugToolbarMiddleware',
71 | ]
72 | INSTALLED_APPS += [
73 | 'debug_toolbar',
74 | ]
75 | INTERNAL_IPS = ['127.0.0.1']
76 |
77 | DEBUG_TOOLBAR_PANELS = [
78 | 'ddt_request_history.panels.request_history.RequestHistoryPanel', # Here it is
79 | 'debug_toolbar.panels.versions.VersionsPanel',
80 | 'debug_toolbar.panels.timer.TimerPanel',
81 | 'debug_toolbar.panels.settings.SettingsPanel',
82 | 'debug_toolbar.panels.headers.HeadersPanel',
83 | 'debug_toolbar.panels.request.RequestPanel',
84 | 'debug_toolbar.panels.sql.SQLPanel',
85 | 'debug_toolbar.panels.templates.TemplatesPanel',
86 | 'debug_toolbar.panels.staticfiles.StaticFilesPanel',
87 | 'debug_toolbar.panels.cache.CachePanel',
88 | 'debug_toolbar.panels.signals.SignalsPanel',
89 | 'debug_toolbar.panels.logging.LoggingPanel',
90 | 'debug_toolbar.panels.redirects.RedirectsPanel',
91 | 'debug_toolbar.panels.profiling.ProfilingPanel',
92 | ]
93 | DEBUG_TOOLBAR_CONFIG = {
94 | 'SHOW_TOOLBAR_CALLBACK': 'ddt_request_history.panels.request_history.allow_ajax',
95 | }
96 |
97 | ENABLE_BASIC_AUTH = False
98 |
--------------------------------------------------------------------------------
/openledger/test_settings.py:
--------------------------------------------------------------------------------
1 | from openledger.settings import *
2 |
3 | STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage'
4 | SECRET_KEY = 'SECRET_KEY_FOR_TESTING'
5 |
6 | API_500PX_KEY = 'TESTING'
7 | API_500PX_SECRET = 'TESTING'
8 |
9 | API_RIJKS = 'TESTING'
10 | NYPL_KEY = 'TESTING'
11 | FLICKR_KEY = 'TESTING'
12 | FLICKR_SECRET = 'TESTING'
13 |
14 | ELASTICSEARCH_URL = 'es'
15 | ELASTICSEARCH_PORT = 9200
16 | ELASTICSEARCH_INDEX = 'testing'
17 |
18 | AWS_ACCESS_KEY_ID = "TESTING"
19 | AWS_SECRET_ACCESS_KEY = "TESTING"
20 |
21 | ALLOWED_HOSTS = ['localhost']
22 | DATABASES = {
23 | 'default': {
24 | 'ENGINE': 'django.db.backends.postgresql',
25 | 'NAME': 'openledger',
26 | 'USER': 'deploy',
27 | 'HOST': 'db',
28 | 'PASSWORD': 'deploy',
29 | 'PORT': 5432,
30 | }
31 | }
32 | LOGGING = {
33 | 'version': 1,
34 | 'disable_existing_loggers': False,
35 | 'filters': {
36 | 'require_debug_false': {
37 | '()': 'django.utils.log.RequireDebugFalse'
38 | }
39 | },
40 | 'handlers': {
41 | 'mail_admins': {
42 | 'level': 'ERROR',
43 | 'filters': ['require_debug_false'],
44 | 'class': 'django.utils.log.AdminEmailHandler'
45 | },
46 | 'console': {
47 | 'level': 'DEBUG',
48 | 'class': 'logging.StreamHandler'
49 | },
50 | },
51 | 'loggers': {
52 | 'django.request': {
53 | 'handlers': ['mail_admins'],
54 | 'level': 'ERROR',
55 | 'propagate': True,
56 | },
57 | 'imageledger': {
58 | 'handlers': ['console'],
59 | 'level': 'DEBUG'
60 | },
61 |
62 | }
63 | }
64 |
65 | TESTING=True
66 |
--------------------------------------------------------------------------------
/openledger/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import include, path
2 | from django.contrib import admin
3 | from django.conf.urls.static import static
4 | from django.conf import settings
5 |
6 | urlpatterns = [
7 | path('', include('imageledger.urls')),
8 | path('admin/', admin.site.urls),
9 | ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
10 |
11 | if settings.DEBUG:
12 | import debug_toolbar
13 | urlpatterns += [
14 | path('__debug__/', include(debug_toolbar.urls)),
15 | ]
16 |
--------------------------------------------------------------------------------
/openledger/wsgi.py:
--------------------------------------------------------------------------------
1 | import os
2 | import newrelic.agent
3 |
4 | newrelic.agent.initialize(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
5 | 'newrelic.ini'))
6 | from django.core.wsgi import get_wsgi_application
7 |
8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openledger.settings")
9 |
10 | application = get_wsgi_application()
11 |
12 | from wsgi_basic_auth import BasicAuth
13 | application = BasicAuth(application)
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "open-ledger",
3 | "version": "1.0.0",
4 | "description": "A prototype repository for reuse of freely licensed imagery",
5 | "scripts": {
6 | "test": "_mocha --require static/js/test/global --compilers js:babel-core/register --recursive 'static/js/test/*.js'"
7 | },
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/creativecommons/open-ledger.git"
11 | },
12 | "author": [
13 | "Liza Daly",
14 | "Paola Villarreal"
15 | ],
16 | "license": "MIT",
17 | "bugs": {
18 | "url": "https://github.com/creativecommons/open-ledger/issues"
19 | },
20 | "homepage": "https://github.com/creativecommons/open-ledger#readme",
21 | "dependencies": {
22 | "clipboard": "^1.5.15",
23 | "dom4": "^1.8.5",
24 | "imagesloaded": "^4.1.1",
25 | "js-cookie": "^2.1.3",
26 | "masonry-layout": "^4.1.1",
27 | "promise-polyfill": "^6.0.2",
28 | "underscore": "^1.8.3",
29 | "whatwg-fetch": "^1.0.0",
30 | "photoswipe": "^4.1.2"
31 | },
32 | "devDependencies": {
33 | "babel": "^6.5.2",
34 | "babel-cli": "^6.16.0",
35 | "babel-core": "^6.17.0",
36 | "babel-loader": "^6.2.5",
37 | "babel-plugin-transform-object-rest-spread": "^6.16.0",
38 | "babel-preset-es2015": "^6.16.0",
39 | "babel-preset-es2015-webpack": "^6.4.3",
40 | "chai": "^3.5.0",
41 | "jsdom": "9.8.3",
42 | "jsdom-global": "2.1.0",
43 | "mocha": "^5.2.0",
44 | "sass": "^0.5.0",
45 | "sinon": "^1.17.6",
46 | "webpack": "^1.15.0"
47 | },
48 | "main": "openledger/static/js/index.js"
49 | }
50 |
--------------------------------------------------------------------------------
/requirements-test.txt:
--------------------------------------------------------------------------------
1 | lxml
2 | html5lib==0.9999999
3 | cssselect
4 | mock
5 | awsebcli
6 | Fabric3==1.12.post1
7 | responses
8 | git+git://github.com/lizadaly/fabtools.git#egg=fabtools
9 | django-debug-toolbar
10 | git+https://github.com/djsutho/django-debug-toolbar-request-history.git
11 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | flickrapi
2 | Jinja2
3 | oauthlib
4 | psycopg2
5 | rdflib
6 | requests
7 | requests-oauthlib
8 | boto3
9 | elasticsearch-dsl>=5.0.0,<6.0.0
10 | aws-requests-auth
11 | django==2.0.1
12 | git+git://github.com/mingchen/django-cas-ng.git@5a370af14b8023bda0ae34af001228f15ba86f51
13 | djangorestframework
14 | django-filter
15 | newrelic
16 | wsgi-basic-auth
17 | python-akismet
18 | wordfilter
19 |
--------------------------------------------------------------------------------
/static/css/default-skin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/static/css/default-skin.png
--------------------------------------------------------------------------------
/static/css/default-skin.svg:
--------------------------------------------------------------------------------
1 | default-skin 2
--------------------------------------------------------------------------------
/static/css/preloader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/static/css/preloader.gif
--------------------------------------------------------------------------------
/static/fonts/foundation-icons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/static/fonts/foundation-icons.eot
--------------------------------------------------------------------------------
/static/fonts/foundation-icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/static/fonts/foundation-icons.ttf
--------------------------------------------------------------------------------
/static/fonts/foundation-icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/static/fonts/foundation-icons.woff
--------------------------------------------------------------------------------
/static/images/by-nc-nd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/static/images/by-nc-nd.png
--------------------------------------------------------------------------------
/static/images/by-nc-sa.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/static/images/by-nc-sa.png
--------------------------------------------------------------------------------
/static/images/by-nc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/static/images/by-nc.png
--------------------------------------------------------------------------------
/static/images/by-nd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/static/images/by-nd.png
--------------------------------------------------------------------------------
/static/images/by-sa.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/static/images/by-sa.png
--------------------------------------------------------------------------------
/static/images/by.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/static/images/by.png
--------------------------------------------------------------------------------
/static/images/by.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/static/images/cc.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
8 |
9 |
10 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/static/images/cc0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/static/images/cc0.png
--------------------------------------------------------------------------------
/static/images/cc0.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
13 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/static/images/loading.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/nc-eu.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/static/images/nc-jp.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/static/images/nc.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/static/images/nd.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/static/images/pdm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/static/images/pdm.png
--------------------------------------------------------------------------------
/static/images/pdm.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/static/images/sa.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/static/images/share.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/static/images/zero.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
13 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/static/js/api.js:
--------------------------------------------------------------------------------
1 | export const API_BASE = '/api/v1/'
2 | export const HOST_PORT = window.location.port === 80 ? '' : `:${window.location.port}`
3 | export const HOST_URL = `${window.location.protocol}//${window.location.hostname}${HOST_PORT}`
4 |
--------------------------------------------------------------------------------
/static/js/attributions.js:
--------------------------------------------------------------------------------
1 |
2 | const clipped = (e) => {
3 | var btn = e.trigger
4 | var txt = btn.innerHTML
5 |
6 | btn.classList.toggle('animated')
7 | btn.classList.toggle('bounce')
8 | btn.innerHTML = "Copied!"
9 |
10 | e.clearSelection()
11 | window.setTimeout(() => {
12 | btn.classList.remove('bounce')
13 | btn.classList.remove('animated')
14 | btn.innerHTML = txt
15 | }, 1500)
16 | }
17 |
18 | const attributions = (clipboard) => {
19 | clipboard.on('success', clipped)
20 | }
21 |
22 | export default attributions
23 |
--------------------------------------------------------------------------------
/static/js/form.js:
--------------------------------------------------------------------------------
1 | /* When a new search is initiated, reset the page counter */
2 | export const resetSearchOnSubmit = (form) => {
3 | form.elements["page"].value = 1
4 | }
5 |
6 | export const showFormElements = (form) => {
7 | showForm(form)
8 |
9 | // Do the same for the delete form
10 | var deleteForm = form.nextElementSibling
11 | if (deleteForm) {
12 | showForm(deleteForm)
13 | }
14 |
15 | }
16 |
17 | const showForm = (form) => {
18 |
19 | for (var el of form.querySelectorAll('input')) {
20 | el.style.display = 'inherit'
21 | }
22 | for (var el of form.querySelectorAll('textarea')) {
23 | el.style.display = 'inherit'
24 | }
25 | for (var el of form.querySelectorAll('label')) {
26 | el.style.display = 'inherit'
27 | }
28 | for (var el of form.querySelectorAll('.form-readonly')) {
29 | el.style.display = 'none'
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/static/js/index.js:
--------------------------------------------------------------------------------
1 | import Promise from 'promise-polyfill'
2 | if (!window.Promise) {
3 | window.Promise = Promise
4 | }
5 |
6 | import Clipboard from 'clipboard'
7 | import attributions from './attributions'
8 | import * as list from './list'
9 | import * as form from './form'
10 | import * as grid from './grid'
11 | import * as favorite from './favorite'
12 | import * as tags from './tags'
13 | import * as search from './search'
14 |
15 | const init = () => {
16 | var clipboardText = new Clipboard('.clipboard-sel-text')
17 | var clipboardHTML = new Clipboard('.clipboard-sel-html', {
18 | text: () => {
19 | const htmlBlock = document.querySelector('.attribution')
20 | return htmlBlock.innerHTML
21 | }
22 | })
23 | attributions(clipboardText)
24 | attributions(clipboardHTML)
25 | }
26 |
27 | document.addEventListener('DOMContentLoaded', () => {
28 | init()
29 | })
30 |
31 | window.openledger = {}
32 | window.openledger.list = list
33 | window.openledger.form = form
34 | window.openledger.grid = grid
35 | window.openledger.favorite = favorite
36 | window.openledger.tags = tags
37 | window.openledger.search = search
38 |
--------------------------------------------------------------------------------
/static/js/search.js:
--------------------------------------------------------------------------------
1 | document.addEventListener('DOMContentLoaded', () => {
2 | var workTypes = document.querySelectorAll('.work-type')
3 | for (let workType of workTypes) {
4 | let typeSelector = workType.querySelector('input[name="work_types"]')
5 | let subtype = workType.querySelectorAll('li input')
6 | // If typeSelectorPhotos is clicked, select all its children
7 | typeSelector.addEventListener('click', () => {
8 | for (let st of subtype) {
9 | st.checked = typeSelector.checked
10 | }
11 | })
12 |
13 | }
14 | var adv_search = document.getElementById ("toggle_advanced_search");
15 | if (adv_search) {
16 | adv_search.addEventListener ('click', function () {
17 | var filters = document.querySelector (".search-filters")
18 | if (filters.classList.contains ('hide')) {
19 | filters.classList.remove ("hide");
20 | this.innerText = 'Hide advanced filters';
21 | } else {
22 | filters.classList.add ("hide");
23 | this.innerText = 'Advanced search';
24 | }
25 |
26 | })
27 | }
28 | })
29 |
--------------------------------------------------------------------------------
/static/js/test/global.js:
--------------------------------------------------------------------------------
1 | // test/global.js
2 | import jsdom from 'jsdom';
3 |
4 | global.document = jsdom.jsdom('');
5 | global.window = document.defaultView;
6 | global.navigator = window.navigator;
7 |
--------------------------------------------------------------------------------
/static/js/test/test-utils.js:
--------------------------------------------------------------------------------
1 |
2 | var _attr = function(val){
3 | var out = val.substr(5);
4 | return out.split("-").map((part, i) => {
5 | return part.charAt(0).toUpperCase() + part.substr(1);
6 | }).join("");
7 | },
8 |
9 | _data = (el) => {
10 | let atts = el.attributes,
11 | len = atts.length,
12 | attr,
13 | dataDict = [],
14 | proxy = {},
15 | dataName
16 | for (let i=0; i < len; i++){
17 | attr = atts[i].nodeName;
18 | if (attr.indexOf("data-") === 0) {
19 | dataName = _arr(attr);
20 | if (dataDict.hasOwnProperty(dataName)) {
21 | dataDict[dataName] = atts[i].nodeValue
22 | Object.defineProperty(proxy, dataName, {
23 | get: () => dataDict[dataName],
24 | set: (val) => {
25 | dataDict[dataName] = val
26 | el.setAttribute(attr, val)
27 | }
28 | })
29 | }
30 | }
31 | }
32 | return proxy;
33 | };
34 |
35 | Object.defineProperty(global.window.Element.prototype, "dataset", {
36 | get: function() {
37 | return _data(this)
38 | }
39 | })
40 |
--------------------------------------------------------------------------------
/static/js/test/testFavorite.js:
--------------------------------------------------------------------------------
1 | import 'jsdom-global/register'
2 | import sinon from 'sinon'
3 | import { assert } from 'chai'
4 |
5 | import * as favorite from '../favorite'
6 |
7 | require( "./test-utils" )
8 |
9 | describe('setAsFavorite', () => {
10 | var form
11 | beforeEach(() => {
12 | form = document.createElement('form')
13 | form.innerHTML = ``
17 | })
18 | it('sets/removes the classes of the form in added-mode', () => {
19 | favorite.setAsFavorite(form)
20 | var button = form.querySelector('button')
21 | assert(button.classList.contains('success'))
22 | assert(!button.classList.contains('secondary'))
23 | })
24 | })
25 |
26 | describe('removeAsFavorite', () => {
27 | var form
28 | beforeEach(() => {
29 | form = document.createElement('form')
30 | form.innerHTML = ``
34 | })
35 |
36 | it('sets/removes the classes of the form in removed-mode', () => {
37 | favorite.removeAsFavorite(form)
38 | var button = form.querySelector('button')
39 | assert(!button.classList.contains('success'))
40 | assert(button.classList.contains('secondary'))
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/static/js/test/testForm.js:
--------------------------------------------------------------------------------
1 | import 'jsdom-global/register'
2 | import sinon from 'sinon'
3 | import { assert } from 'chai'
4 | var fs = require('fs')
5 |
6 | import * as forms from '../form'
7 |
8 | describe('showForm', () => {
9 | var form
10 |
11 | beforeEach(() => {
12 |
13 | form = document.createElement('form')
14 | form.innerHTML = ``
23 |
24 | }),
25 |
26 | it('shows any form field children when clicked', () => {
27 |
28 | assert('none' === form.querySelector('input').style.display)
29 | assert('none' === form.querySelector('textarea').style.display)
30 | forms.showFormElements(form)
31 | assert('inherit' === form.querySelector('input').style.display)
32 | assert('inherit' === form.querySelector('textarea').style.display)
33 | }),
34 |
35 | it('hides any element with the `readonly` class', () => {
36 | assert('none' != form.querySelector('.form-readonly').style.display)
37 | forms.showFormElements(form)
38 | assert('none' === form.querySelector('.form-readonly').style.display)
39 | })
40 | })
41 |
--------------------------------------------------------------------------------
/static/js/test/testList.js:
--------------------------------------------------------------------------------
1 | import 'jsdom-global/register'
2 | import sinon from 'sinon'
3 | import { assert } from 'chai'
4 |
5 | import * as list from '../list'
6 | import * as utils from '../utils'
7 |
8 | require( "./test-utils" )
9 |
10 |
11 | describe('checkStatus', () => {
12 | it('returns the response if a 200 status code is returned', () => {
13 | const response = {}
14 | response.status = 200
15 | assert(response === utils.checkStatus(response))
16 | }),
17 | it('raises an exception if a non 200 status code is returned', () => {
18 | const response = {}
19 | response.status = 404
20 | assert.throws(() => utils.checkStatus(response))
21 | })
22 | })
23 |
24 | describe('clearResponse', () => {
25 | it('removes any child nodes from the object', () => {
26 | var r = document.createElement('div')
27 | r.innerHTML = "children"
28 | utils.clearResponse(r)
29 | assert('' === r.innerHTML)
30 | }),
31 | it('make the object not visible', () => {
32 | var r = document.createElement('div')
33 | utils.clearResponse(r)
34 | assert('none' === r.style.display)
35 | })
36 | })
37 |
38 | describe('clearAutocomplete', () => {
39 | it('removes any child nodes from the object', () => {
40 | var r = document.createElement('div')
41 | r.innerHTML = "children"
42 | utils.clearAutocomplete(r)
43 | assert('' === r.innerHTML)
44 | })
45 | })
46 |
47 | describe('clearForm', () => {
48 | var form
49 | beforeEach(() => {
50 | form = document.createElement('form')
51 | form.innerHTML = `
52 |
53 |
54 | `
55 | })
56 | it('removes child elements from the autocomplete child', () => {
57 | var auto = form.querySelector('.autocomplete')
58 | auto.appendChild(document.createElement('li'))
59 | assert('' != auto.innerHTML)
60 | utils.clearForm(form)
61 | assert('' === auto.innerHTML)
62 | }),
63 | it('resets the form', () => {
64 | form.elements["title"].value = 'hello'
65 | utils.clearForm(form)
66 | assert('' === form.elements["title"].value)
67 | }),
68 | it('hides the form', () => {
69 | assert('none' != form.style.display)
70 | utils.clearForm(form)
71 | assert('none' === form.style.display)
72 | }),
73 | it('removes the animation', () => {
74 | form.classList.add('pulse')
75 | assert(1 === form.classList.length)
76 | utils.clearForm(form)
77 | assert(0 === form.classList.length)
78 | })
79 | })
80 |
--------------------------------------------------------------------------------
/static/js/utils.js:
--------------------------------------------------------------------------------
1 | export const detectIE = () => {
2 | var ua = window.navigator.userAgent
3 | var msie = ua.indexOf('MSIE ')
4 | if (msie > 0) {
5 | // IE 10 or older => return version number
6 | return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10)
7 | }
8 |
9 | var trident = ua.indexOf('Trident/')
10 | if (trident > 0) {
11 | // IE 11 => return version number
12 | var rv = ua.indexOf('rv:')
13 | return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10)
14 | }
15 |
16 | var edge = ua.indexOf('Edge/')
17 | if (edge > 0) {
18 | // Edge (IE 12+) => return version number
19 | return parseInt(ua.substring(edge + 5, ua.indexOf('.', edge)), 10)
20 | }
21 | // other browser
22 | return false
23 | }
24 |
25 | export const clearAutocomplete = (autocomplete) => {
26 | autocomplete.innerHTML = ''
27 | }
28 |
29 | export const clearResponse = (response) => {
30 | response.style.display = 'none'
31 | response.innerHTML = ''
32 | }
33 |
34 | export const checkStatus = (response) => {
35 | if (response.status >= 200 && response.status < 300) {
36 | return response
37 | }
38 | else {
39 | var error = new Error(response.statusText)
40 | error.response = response
41 | throw error
42 | }
43 | }
44 |
45 | export const clearForm = (form) => {
46 | var autocomplete = form.querySelector('.autocomplete')
47 | clearAutocomplete(autocomplete)
48 | form.reset()
49 | form.style.display = 'none'
50 | form.classList.remove('pulse')
51 | var input = form.querySelector('input[type=text]')
52 | input.dataset.sel = -1 // Reset the autocomplete pointer
53 |
54 | }
55 |
56 | export const showUpdateMessage = (msg) => {
57 | msg.style.display = 'block'
58 | msg.innerHTML = "Saving..."
59 | }
60 |
--------------------------------------------------------------------------------
/static/scss/_about.scss:
--------------------------------------------------------------------------------
1 | .about {
2 | table {
3 | th {
4 | text-align: left;
5 | }
6 | }
7 | .provider-total {
8 | font-weight: bold;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/static/scss/_base.scss:
--------------------------------------------------------------------------------
1 | /* Fonts */
2 | html, body {
3 | height: 100%;
4 | }
5 | a {
6 | color: $cyan;
7 | }
8 | a:hover {
9 | text-decoration: underline;
10 | }
11 |
12 | .page-wrap {
13 | min-height: 100%;
14 | /* equal to footer height */
15 | margin-bottom: -($footer-height);
16 | }
17 |
18 | .page-wrap:after {
19 | content: "";
20 | display: block;
21 | }
22 |
23 | footer, .page-wrap:after {
24 | min-height: $footer-height;
25 | }
26 |
27 | body, p, h1, h2, h3, h4, h5, h6 {
28 | font-family: "Source Sans Pro",sans-serif;
29 | }
30 |
31 | h5 {
32 | margin-top: 4rem;
33 | }
34 |
35 | .hero {
36 | background: linear-gradient(to bottom, rgba(255,255,255,1) 80%, rgba(233,233,233,1) 100%);
37 | border: none;
38 | border-bottom: 1px solid rgb(200,200,200);
39 | padding: 2rem 0 2rem 0;
40 | &.image-detail figure {
41 | margin: auto;
42 | max-width: 70vw;
43 | }
44 | }
45 |
46 | /* Faster animation than the default animate.css */
47 | @keyframes zoomOut {
48 | from {
49 | opacity: 1;
50 | }
51 |
52 | 20% {
53 | opacity: 0;
54 | -webkit-transform: scale3d(.3, .3, .3);
55 | transform: scale3d(.3, .3, .3);
56 | }
57 |
58 | to {
59 | opacity: 0;
60 | }
61 | }
62 |
63 | /* Floating Feedback bar */
64 | .feedback-bar {
65 | display: none;
66 | }
67 |
68 | @include for-size(tablet-portrait-up) {
69 |
70 | .feedback-bar {
71 | display: block;
72 | position: fixed;
73 | right: -20px;
74 | top: 50%;
75 | background: $cyan;
76 | transform: rotate(270deg);
77 | transform-origin: top;
78 | border-top: 5px solid $white;
79 | border-left: 5px solid $white;
80 | border-right: 5px solid $white;
81 | border-radius: 20px 20px 0 0;
82 | box-shadow: 0 0 10px 0 rgba(0,0,0,.8);
83 |
84 | h3 {
85 | padding: .5rem 1rem;
86 | color: $white;
87 | font-size: 25px;
88 |
89 | }
90 | }
91 | }
92 |
93 | /* Logo image next to title */
94 | .logo-black {
95 | width: 100px;
96 | height:100px;
97 | margin-top: 20px;
98 | }
99 |
--------------------------------------------------------------------------------
/static/scss/_detail.scss:
--------------------------------------------------------------------------------
1 |
2 | .metadata h4 {
3 | font-size: 1rem;
4 | font-weight: bold;
5 | line-height: 1rem;
6 | margin: 0;
7 | padding: 0;
8 | }
9 |
10 | .metadata ul {
11 | list-style-type: none;
12 | margin: 0;
13 | }
14 | .metadata li {
15 | line-height: 2rem;
16 | }
17 |
18 | .attribution-button {
19 | width: 200px;
20 | }
21 |
22 | a.license-link, a.license-link:hover {
23 | text-decoration: none;
24 | }
25 |
--------------------------------------------------------------------------------
/static/scss/_favorite.scss:
--------------------------------------------------------------------------------
1 | /* Favorite buttons */
2 | .add-to-favorite-container {
3 |
4 | button.tiny {
5 | font-size: 20px;
6 | padding: 3px;
7 | margin: 0 0 0 2px;
8 | }
9 | }
10 |
11 | .image-result {
12 | .add-to-favorite-container {
13 | display: inline-block;
14 | vertical-align: middle;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/static/scss/_footer.scss:
--------------------------------------------------------------------------------
1 | footer {
2 |
3 | box-shadow: 0 0 10px 0 rgba(0,0,0,.8);
4 | background-color: $dark-gray;
5 | color: $gray;
6 | font-size: 14px;
7 | line-height: 1.3em;
8 | width: 100%;
9 | padding: 25px 0;
10 | margin-top: 25px;
11 |
12 | ul, li {
13 | list-style-type: none;
14 | display: inline;
15 | margin: 0;
16 | }
17 | a {
18 | color: $gray;
19 | }
20 | .openledger-info, .license-info {
21 | font-size: 12px;
22 | line-height: 18px;
23 | a {
24 | text-decoration: underline;
25 | }
26 | .license-link {
27 | text-decoration: none;
28 | }
29 | }
30 | li {
31 | padding: 0 .5rem 0 0;
32 | &:not(:first-child) {
33 | padding: 0 .5rem;
34 | border-left: 1px solid $gray;
35 | }
36 | }
37 | .custom-logo {
38 | margin-bottom: 2rem;
39 | }
40 | .contact-info {
41 | h6 {
42 | font-size: 18px;
43 | font-weight: bold;
44 | line-height: 19.5px;
45 | a {
46 | color: $white;
47 | }
48 | }
49 | a.mail, a.tel {
50 | color: $cyan;
51 | }
52 | }
53 | @media screen and (max-width: 39.9375em) {
54 | .contact-info {
55 | display: none;
56 | }
57 | }
58 | .footer-main {
59 | img {
60 | max-width: 90%;
61 | @media screen and (max-width: 39.9375em) {
62 | display: none;
63 | }
64 | }
65 | ul {
66 | margin-top: 2rem;
67 | display: block;
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/static/scss/_forms.scss:
--------------------------------------------------------------------------------
1 | .button {
2 | background-color: $cyan;
3 | &:hover {
4 | background-color: darken($cyan, 10%);
5 | }
6 | &.success {
7 | background-color: $green;
8 | &:hover {
9 | background-color: lighten($green, 10%);
10 | }
11 | }
12 |
13 | }
14 |
15 | .list-detail {
16 | &> form {
17 | input {
18 | display: none;
19 | }
20 | textarea {
21 | display: none;
22 | }
23 | label {
24 | display: none;
25 | }
26 | }
27 | }
28 |
29 |
30 | .search-form {
31 | margin-top: 1rem;
32 | border: none;
33 | padding: 0;
34 | @include for-size(tablet-portrait-up) {
35 | padding: 0 20%;
36 | }
37 | ul, li {
38 | display: inline-block;
39 | margin: 0;
40 | }
41 | p {
42 | color: $dark-gray;
43 | margin: 1rem 0;
44 | }
45 | h4 {
46 | color: $gray;
47 | }
48 |
49 | input {
50 | margin: 0
51 | }
52 |
53 | .search-filters {
54 | @include for-size(phone-only) {
55 | /* In small viewports add top-margin */
56 | margin-top: 1rem;
57 | .column, label, li {
58 | font-size: .8rem;
59 | }
60 | li {
61 | display: block;
62 | }
63 | }
64 | .column {
65 | margin-bottom: 1rem;
66 | }
67 | label {
68 | display: inline-block;
69 | }
70 | .search-collections {
71 | ul {
72 | display: block;
73 | margin-left: 1rem;
74 | font-size: .875rem;
75 | }
76 | li {
77 | margin-left: .5rem;
78 | }
79 | }
80 | legend {
81 | text-transform: uppercase;
82 | font-size: smaller;
83 | margin: 0;
84 | padding: 0;
85 | }
86 | }
87 |
88 | .search-field-container {
89 |
90 | background: white;
91 | line-height: 1rem;
92 | height: 50px;
93 |
94 | input, i {
95 | border: 1px solid #cacaca;
96 | display: inline-block;
97 | height: 50px;
98 | float: left;
99 | }
100 | i {
101 | text-align: center;
102 | padding: 1rem;
103 | width: 50px;
104 | display: none;
105 | @include for-size(tablet-landscape-up) {
106 | display: inline-block;
107 | }
108 | }
109 | input[type=text] {
110 | width: 80%;
111 | font-size: 1rem;
112 | line-height: 1rem;
113 | box-shadow: none;
114 | margin: 0;
115 | @include for-size(tablet-landscape-up) {
116 | border-left: none;
117 | }
118 | }
119 | input[type=submit] {
120 | border-left: none;
121 | width: 50px;
122 | height: 50px;
123 | margin: 0;
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/static/scss/_licenses.scss:
--------------------------------------------------------------------------------
1 | .license-logo {
2 | max-width: 25px;
3 | max-height: 25px;
4 | }
5 |
--------------------------------------------------------------------------------
/static/scss/_lists.scss:
--------------------------------------------------------------------------------
1 | /* List buttons */
2 |
3 | /* List detail page */
4 | .list-detail {
5 | margin-top: 2rem;
6 |
7 | .list-description {
8 | font-size: 1.4rem;
9 | border-left: 1px solid rgb(200,200,200);
10 | margin: 2rem 0;
11 | padding: 1rem;
12 | }
13 |
14 | .list-public {
15 | margin: 2rem 0;
16 | font-style: italic;
17 | }
18 | .list-title {
19 | a, a:hover {
20 | text-decoration: none;
21 | }
22 | }
23 | }
24 |
25 | /* List listing page */
26 | .list-iterator {
27 | margin-top: 2rem;
28 |
29 | .list-iterable {
30 | border-bottom: 1px solid rgb(200, 200, 200);
31 | padding: 1rem 0;
32 |
33 | .list-description {
34 | text-size: smaller;
35 | }
36 | .list-dates {
37 | font-size: smaller;
38 | font-style: italic;
39 | }
40 | .list-title {
41 | a, a:hover {
42 | text-decoration: none !important;
43 | }
44 | }
45 | }
46 | }
47 |
48 |
49 | /* List creation features */
50 |
51 | .add-to-list-container {
52 |
53 | .add-to-list {
54 | position: absolute;
55 | width: 400px;
56 | display: none;
57 | z-index: 99;
58 |
59 | input {
60 | margin: 0;
61 | }
62 | }
63 | .add-to-list-response {
64 | display: none;
65 | position: absolute;
66 | background: white;
67 | box-shadow: 0px 10px 36px -9px rgba(0,0,0,0.58);
68 | padding: 20px;
69 | min-width: 400px;
70 | z-index: 1;
71 | }
72 |
73 | }
74 |
75 | /* Add-to-list from results page */
76 | .image-result {
77 |
78 | .add-to-list-container {
79 | display: inline-block;
80 | vertical-align: middle;
81 | }
82 | .add-to-list-container button.secondary {
83 | background: rgb(230,230,230);
84 | color: rgb(10,10,10);
85 | margin: 0;
86 | }
87 |
88 | .add-to-list-container button.secondary:hover {
89 | background: #3adb76;
90 | color: white;
91 | }
92 |
93 | .add-to-list-container {
94 | position: relative;
95 | }
96 |
97 | .add-to-list {
98 | position: absolute;
99 | left: -10px;
100 | padding: 10px;
101 | background: white;
102 | }
103 |
104 | form {
105 | display: inline-block;
106 | }
107 | }
108 |
109 | /* Autocomplete */
110 | .autocomplete {
111 | position: relative;
112 | width: 100%;
113 |
114 | li {
115 | list-style-type: none;
116 | border-bottom: 1px solid rgb(230,230,230);
117 | padding: .5rem;
118 | color: rgb(150,150,150);
119 | background-color: $white;
120 | box-shadow: 0px 10px 36px -9px rgba(0,0,0,0.58);
121 | &.hover {
122 | background-color: rgb(230,230,230);
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/static/scss/_nav.scss:
--------------------------------------------------------------------------------
1 | nav.top-bar {
2 | padding: .5rem 2rem;
3 | background-image: linear-gradient(90deg,#EE5B32,#FB7928,#EE5B32);
4 | color: $white;
5 |
6 | .top-bar-title img.cc-site-logo {
7 | width: 200px;
8 |
9 | @include for-size(tablet-portrait-up) {
10 | width: 240px;
11 | height: 57px;
12 | }
13 | }
14 | a {
15 | color: $white;
16 | }
17 | ul {
18 | margin: 1rem auto;
19 | list-style-type: none;
20 | background: none;
21 | display: inline-block;
22 | @include for-size(tablet-portrait-up) {
23 | margin: 0;
24 | display: block;
25 | text-align: right;
26 | }
27 | }
28 | li {
29 | font-weight: 700;
30 | display: inline-block;
31 | border-left: 1px solid $white;
32 | padding: 0 .8em;
33 | @include for-size(tablet-portrait-up) {
34 | font-size: 28px;
35 | }
36 | }
37 | ul.login {
38 | li {
39 | font-size: 1rem;
40 | border: none;
41 | &.username {
42 | border-right: 1px solid $white;
43 | }
44 | }
45 | }
46 | }
47 |
48 | header {
49 | box-shadow: 0 0 10px 0 rgba(0,0,0,.8);
50 | margin-bottom: 2rem;
51 | }
52 |
--------------------------------------------------------------------------------
/static/scss/_photoswipe_skin_custom.scss:
--------------------------------------------------------------------------------
1 | .data_container {
2 | visibility: hidden;
3 | }
4 | .data_container.fake {
5 | min-height: 10%;
6 | }
7 | .data_container * {
8 | margin-bottom: 0 !important;
9 | }
10 | .data_container.show {
11 | visibility: visible;
12 | width: 100%;
13 | min-height: 10%;
14 | max-height: 10%;
15 | height: 10% !important;
16 | overflow: hidden;
17 | background-color: rgba(255,255,255,0.8) !important;
18 | }
19 | .pswp--zoomed-in .pswp__caption {
20 | display: none;
21 | visibility: hidden !important;
22 | }
23 | .data_container {
24 | font-size: 0.8em;
25 | padding-left: 0.5rem;
26 | }
27 | .data_container .license img {
28 | width: 30px;
29 | margin-right: 2px;
30 | }
31 | .pswp__button--attribution, .pswp__button--favorite, .pswp__button--list {
32 | background-image: none !important;
33 | color: white;
34 | }
35 | .pswp__button.waiting {
36 | background-color: red;
37 | }
38 | .pswp__button.is_fav {
39 | background-color: yellow;
40 | }
41 | .pswp__button--attribution {
42 | background-color: #2199e8;
43 | }
44 | .pswp__button--list {
45 | background-color: #01a635;
46 | }
47 | .pswp__button.done {
48 | background-color: yellow;
49 | color: black;
50 | }
51 | .pswp__top-bar {
52 | opacity: 1 !important;
53 | }
54 |
55 | .pswp .add-to-list-container {
56 | .add-to-list, .add-to-list-response {
57 | right: 1%;
58 | top: 40px;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/static/scss/_result.scss:
--------------------------------------------------------------------------------
1 |
2 | .image-result {
3 | margin: 10px;
4 | width: 210px;
5 | &:hover {
6 | figcaption {
7 | display: block;
8 | }
9 | }
10 |
11 | button.tiny {
12 | font-size: 20px;
13 | padding: 3px;
14 | margin: 0 0 0 2px;
15 | }
16 | a, a:hover {
17 | text-decoration: none;
18 | }
19 | }
20 |
21 | figure {
22 | margin: 0;
23 | position: relative;
24 | }
25 |
26 | figcaption {
27 | padding: 10px 5px 5px 5px;
28 | background: rgba(255, 255, 255, 0.75);
29 | position: absolute;
30 | bottom: 0;
31 | left: 0;
32 | right: 0;
33 | a {
34 | color: $black;
35 | }
36 |
37 | .title {
38 | text-align: left;
39 | font-size: 0.8rem;
40 | line-height: 1.1rem;
41 | }
42 | display: none;
43 | }
44 |
45 | .figure-wrapper {
46 | max-height: 200px;
47 | overflow: hidden;
48 | }
49 | .figure-metadata {
50 | cursor: pointer;
51 | }
52 | .results {
53 |
54 | visibility: hidden;
55 | }
56 | .loading-spinner {
57 | position: absolute;
58 | top: 80%;
59 | left: calc(50% - 60px); /* Position at 50% minus half the width of the spinner */
60 | display: none;
61 | }
62 |
63 | .grid {
64 | margin: 0 auto;
65 | .grid-item {
66 | margin: 0 auto 10px auto;
67 | width: 210px;
68 | &:hover {
69 | figcaption {
70 | display: block;
71 | }
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/static/scss/_tags.scss:
--------------------------------------------------------------------------------
1 | /* Tag creation features */
2 |
3 | .add-to-tags-container {
4 |
5 | .add-tags {
6 | position: absolute;
7 | width: 400px;
8 | display: none;
9 | z-index: 99;
10 |
11 | input {
12 | margin: 0;
13 | }
14 | }
15 | .add-tags-response {
16 | display: none;
17 | position: absolute;
18 | background: white;
19 | box-shadow: 0px 10px 36px -9px rgba(0,0,0,0.58);
20 | padding: 20px;
21 | min-width: 400px;
22 | z-index: 1;
23 | }
24 |
25 | }
26 | /* Tag listing page */
27 | .tag-iterator {
28 | margin-top: 2rem;
29 |
30 | .tag-iterable {
31 | border-bottom: 1px solid rgb(200, 200, 200);
32 | padding: 1rem 0;
33 |
34 | .tag-description {
35 | text-size: smaller;
36 | }
37 | .tag-dates {
38 | font-size: smaller;
39 | font-style: italic;
40 | }
41 | }
42 | }
43 |
44 |
45 | .system-tags-container {
46 | h4 {
47 | margin-bottom: .5rem;
48 | }
49 | max-width: 70%;
50 | }
51 |
52 | .user-tags-header {
53 | margin-bottom: .5rem;
54 | a {
55 | font-size: 1rem;
56 | text-decoration: underline;
57 | text-transform: uppercase;
58 | }
59 | }
60 |
61 | .user-tags-container {
62 | max-width: 70%;
63 | .label {
64 | margin: 5px 5px 0 0;
65 | font-size: 1rem;
66 | &.fi-x:before {
67 | content: "\f217\00a0";
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/static/scss/app.scss:
--------------------------------------------------------------------------------
1 |
2 | /* Colors */
3 | $white: rgb(255, 255, 255);
4 | $black: rgb(20, 20, 20);
5 | $cyan: rgb(4, 155, 206);
6 | $orange-dark: rgb(236, 89, 47);
7 | $green: rgb(1, 166, 53);
8 | $dark-gray: rgb(43, 43, 43);
9 | $gray: rgb(153, 153, 153);
10 | $yellow: #FEEB32;
11 | $dark-yellow: #EFBE00;
12 |
13 | /* Other globals */
14 | $footer-height: 200px;
15 |
16 | @mixin for-size($range) {
17 | $phone-upper-boundary: 600px;
18 | $tablet-portrait-upper-boundary: 900px;
19 | $tablet-landscape-upper-boundary: 1200px;
20 | $desktop-upper-boundary: 1800px;
21 |
22 | @if $range == phone-only {
23 | @media (max-width: #{$phone-upper-boundary - 1}) { @content; }
24 | } @else if $range == tablet-portrait-up {
25 | @media (min-width: $phone-upper-boundary) { @content; }
26 | } @else if $range == tablet-landscape-up {
27 | @media (min-width: $tablet-landscape-upper-boundary) { @content; }
28 | } @else if $range == desktop-up {
29 | @media (min-width: $tablet-landscape-upper-boundary) { @content; }
30 | } @else if $range == big-desktop-up {
31 | @media (min-width: $desktop-upper-boundary) { @content; }
32 | }
33 | }
34 |
35 | @import 'base';
36 | @import 'about';
37 | @import 'detail';
38 | @import 'forms';
39 | @import 'licenses';
40 | @import 'lists';
41 | @import 'tags';
42 | @import 'favorite';
43 | @import 'result';
44 | @import 'nav';
45 | @import 'footer';
46 | @import 'photoswipe', "photoswipe_skin", "photoswipe_skin_custom"
47 |
--------------------------------------------------------------------------------
/util/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/util/__init__.py
--------------------------------------------------------------------------------
/util/list-es-snapshots.py:
--------------------------------------------------------------------------------
1 | from aws_requests_auth.aws_auth import AWSRequestsAuth
2 | import requests
3 | import os
4 | import datetime
5 | import json
6 |
7 | SNAPSHOT_DIR = 'ccsearch-snapshots'
8 |
9 | # List all ES snapshots for this domain
10 |
11 | if __name__ == "__main__":
12 | auth = AWSRequestsAuth(aws_access_key=os.environ['ES_AWS_ACCESS_KEY_ID'],
13 | aws_secret_access_key=os.environ['ES_AWS_SECRET_ACCESS_KEY'],
14 | aws_host=os.environ['ES_CLUSTER_DNS'],
15 | aws_region=os.environ['ES_REGION'],
16 | aws_service='es')
17 | auth.encode = lambda x: bytes(x.encode('utf-8'))
18 |
19 | print("Listing available snapshots for {}".format(os.environ['ES_CLUSTER_DNS']))
20 | resp = requests.get('https://{}/_snapshot/{}/_all'.format(os.environ['ES_CLUSTER_DNS'],
21 | SNAPSHOT_DIR),
22 | auth=auth)
23 | content = resp.json()
24 | print(json.dumps(content, indent=4))
25 |
--------------------------------------------------------------------------------
/util/make-es-snapshot.py:
--------------------------------------------------------------------------------
1 | from aws_requests_auth.aws_auth import AWSRequestsAuth
2 | import requests
3 | import os
4 | import datetime
5 |
6 | SNAPSHOT_DIR = 'ccsearch-snapshots'
7 |
8 | # Run this just one time to register the specified cluster for manual
9 | # backups. See the shared loader environment for env variables.
10 |
11 | if __name__ == "__main__":
12 | auth = AWSRequestsAuth(aws_access_key=os.environ['ES_AWS_ACCESS_KEY_ID'],
13 | aws_secret_access_key=os.environ['ES_AWS_SECRET_ACCESS_KEY'],
14 | aws_host=os.environ['ES_CLUSTER_DNS'],
15 | aws_region=os.environ['ES_REGION'],
16 | aws_service='es')
17 | auth.encode = lambda x: bytes(x.encode('utf-8'))
18 |
19 | cluster_name = os.environ['ES_CLUSTER_DNS'].split('.')[0]
20 | snapshot_name = cluster_name + '-' + datetime.datetime.now().strftime("%Y-%m-%d")
21 |
22 | resp = requests.put('https://{}/_snapshot/{}/{}'.format(
23 | os.environ['ES_CLUSTER_DNS'],
24 | SNAPSHOT_DIR,
25 | snapshot_name),
26 | auth=auth)
27 | print(resp.content)
28 |
--------------------------------------------------------------------------------
/util/register-es-snapshots.py:
--------------------------------------------------------------------------------
1 | from aws_requests_auth.aws_auth import AWSRequestsAuth
2 | import requests
3 | import os
4 |
5 | SNAPSHOT_DIR = 'ccsearch-snapshots'
6 |
7 | # Run this just one time to register the specified cluster for manual
8 | # backups. See the shared loader environment for env variables.
9 |
10 | if __name__ == "__main__":
11 | auth = AWSRequestsAuth(aws_access_key=os.environ['ES_AWS_ACCESS_KEY_ID'],
12 | aws_secret_access_key=os.environ['ES_AWS_SECRET_ACCESS_KEY'],
13 | aws_host=os.environ['ES_CLUSTER_DNS'],
14 | aws_region=os.environ['ES_REGION'],
15 | aws_service='es')
16 | auth.encode = lambda x: bytes(x.encode('utf-8'))
17 |
18 | data = bytes('{"type": "s3","settings": { ' + \
19 | '"bucket": "' + os.environ['ES_MANUAL_SNAPSHOT_S3_BUCKET'] + \
20 | '","region": "' + os.environ['ES_REGION'] + \
21 | '","role_arn": "' + os.environ['ES_IAM_MANUAL_SNAPSHOT_ROLE_ARN'] + \
22 | '"}}', encoding="utf-8")
23 |
24 | resp = requests.post('https://{}/_snapshot/{}'.format(os.environ['ES_CLUSTER_DNS'],
25 | SNAPSHOT_DIR),
26 | auth=auth,
27 | data=data)
28 | print(resp.content)
29 |
--------------------------------------------------------------------------------
/util/restore-es-snapshot.py:
--------------------------------------------------------------------------------
1 | from aws_requests_auth.aws_auth import AWSRequestsAuth
2 | import requests
3 | import sys
4 | import os
5 | import datetime
6 |
7 | SNAPSHOT_DIR = 'ccsearch-snapshots'
8 |
9 | # Run this just one time to register the specified cluster for manual
10 | # backups. See the shared loader environment for env variables.
11 |
12 | if __name__ == "__main__":
13 | snapshot_name = sys.argv[1]
14 | to_host = sys.argv[2]
15 |
16 | auth = AWSRequestsAuth(aws_access_key=os.environ['ES_AWS_ACCESS_KEY_ID'],
17 | aws_secret_access_key=os.environ['ES_AWS_SECRET_ACCESS_KEY'],
18 | aws_host=to_host,
19 | aws_region=os.environ['ES_REGION'],
20 | aws_service='es')
21 | auth.encode = lambda x: bytes(x.encode('utf-8'))
22 |
23 |
24 |
25 | print("Restoring snapshot {} to {}".format(snapshot_name, to_host))
26 |
27 | resp = requests.post('https://{}/_snapshot/{}/{}/_restore'.format(to_host,
28 | SNAPSHOT_DIR,
29 | snapshot_name,
30 | ),
31 | auth=auth)
32 | print(resp.content)
33 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/aws_requests_auth/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/util/scheduled-snapshots/aws_requests_auth/__init__.py
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # __
4 | # /__) _ _ _ _ _/ _
5 | # / ( (- (/ (/ (- _) / _)
6 | # /
7 |
8 | """
9 | Requests HTTP library
10 | ~~~~~~~~~~~~~~~~~~~~~
11 |
12 | Requests is an HTTP library, written in Python, for human beings. Basic GET
13 | usage:
14 |
15 | >>> import requests
16 | >>> r = requests.get('https://www.python.org')
17 | >>> r.status_code
18 | 200
19 | >>> 'Python is a programming language' in r.content
20 | True
21 |
22 | ... or POST:
23 |
24 | >>> payload = dict(key1='value1', key2='value2')
25 | >>> r = requests.post('http://httpbin.org/post', data=payload)
26 | >>> print(r.text)
27 | {
28 | ...
29 | "form": {
30 | "key2": "value2",
31 | "key1": "value1"
32 | },
33 | ...
34 | }
35 |
36 | The other HTTP methods are supported - see `requests.api`. Full documentation
37 | is at .
38 |
39 | :copyright: (c) 2016 by Kenneth Reitz.
40 | :license: Apache 2.0, see LICENSE for more details.
41 | """
42 |
43 | __title__ = 'requests'
44 | __version__ = '2.13.0'
45 | __build__ = 0x021300
46 | __author__ = 'Kenneth Reitz'
47 | __license__ = 'Apache 2.0'
48 | __copyright__ = 'Copyright 2016 Kenneth Reitz'
49 |
50 | # Attempt to enable urllib3's SNI support, if possible
51 | try:
52 | from .packages.urllib3.contrib import pyopenssl
53 | pyopenssl.inject_into_urllib3()
54 | except ImportError:
55 | pass
56 |
57 | import warnings
58 |
59 | # urllib3's DependencyWarnings should be silenced.
60 | from .packages.urllib3.exceptions import DependencyWarning
61 | warnings.simplefilter('ignore', DependencyWarning)
62 |
63 | from . import utils
64 | from .models import Request, Response, PreparedRequest
65 | from .api import request, get, head, post, patch, put, delete, options
66 | from .sessions import session, Session
67 | from .status_codes import codes
68 | from .exceptions import (
69 | RequestException, Timeout, URLRequired,
70 | TooManyRedirects, HTTPError, ConnectionError,
71 | FileModeWarning, ConnectTimeout, ReadTimeout
72 | )
73 |
74 | # Set default logging handler to avoid "No handler found" warnings.
75 | import logging
76 | try: # Python 2.7+
77 | from logging import NullHandler
78 | except ImportError:
79 | class NullHandler(logging.Handler):
80 | def emit(self, record):
81 | pass
82 |
83 | logging.getLogger(__name__).addHandler(NullHandler())
84 |
85 | # FileModeWarnings go off per the default.
86 | warnings.simplefilter('default', FileModeWarning, append=True)
87 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/_internal_utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | requests._internal_utils
5 | ~~~~~~~~~~~~~~
6 |
7 | Provides utility functions that are consumed internally by Requests
8 | which depend on extremely few external helpers (such as compat)
9 | """
10 |
11 | from .compat import is_py2, builtin_str, str
12 |
13 |
14 | def to_native_string(string, encoding='ascii'):
15 | """Given a string object, regardless of type, returns a representation of
16 | that string in the native string type, encoding and decoding where
17 | necessary. This assumes ASCII unless told otherwise.
18 | """
19 | if isinstance(string, builtin_str):
20 | out = string
21 | else:
22 | if is_py2:
23 | out = string.encode(encoding)
24 | else:
25 | out = string.decode(encoding)
26 |
27 | return out
28 |
29 |
30 | def unicode_is_ascii(u_string):
31 | """Determine if unicode string only contains ASCII characters.
32 |
33 | :param str u_string: unicode string to check. Must be unicode
34 | and not Python 2 `str`.
35 | :rtype: bool
36 | """
37 | assert isinstance(u_string, str)
38 | try:
39 | u_string.encode('ascii')
40 | return True
41 | except UnicodeEncodeError:
42 | return False
43 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/certs.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | requests.certs
6 | ~~~~~~~~~~~~~~
7 |
8 | This module returns the preferred default CA certificate bundle.
9 |
10 | If you are packaging Requests, e.g., for a Linux distribution or a managed
11 | environment, you can change the definition of where() to return a separately
12 | packaged CA bundle.
13 | """
14 | import os.path
15 |
16 | try:
17 | from certifi import where
18 | except ImportError:
19 | def where():
20 | """Return the preferred certificate bundle."""
21 | # vendored bundle inside Requests
22 | return os.path.join(os.path.dirname(__file__), 'cacert.pem')
23 |
24 | if __name__ == '__main__':
25 | print(where())
26 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/compat.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | requests.compat
5 | ~~~~~~~~~~~~~~~
6 |
7 | This module handles import compatibility issues between Python 2 and
8 | Python 3.
9 | """
10 |
11 | from .packages import chardet
12 |
13 | import sys
14 |
15 | # -------
16 | # Pythons
17 | # -------
18 |
19 | # Syntax sugar.
20 | _ver = sys.version_info
21 |
22 | #: Python 2.x?
23 | is_py2 = (_ver[0] == 2)
24 |
25 | #: Python 3.x?
26 | is_py3 = (_ver[0] == 3)
27 |
28 | try:
29 | import simplejson as json
30 | except (ImportError, SyntaxError):
31 | # simplejson does not support Python 3.2, it throws a SyntaxError
32 | # because of u'...' Unicode literals.
33 | import json
34 |
35 | # ---------
36 | # Specifics
37 | # ---------
38 |
39 | if is_py2:
40 | from urllib import quote, unquote, quote_plus, unquote_plus, urlencode, getproxies, proxy_bypass
41 | from urlparse import urlparse, urlunparse, urljoin, urlsplit, urldefrag
42 | from urllib2 import parse_http_list
43 | import cookielib
44 | from Cookie import Morsel
45 | from StringIO import StringIO
46 | from .packages.urllib3.packages.ordered_dict import OrderedDict
47 |
48 | builtin_str = str
49 | bytes = str
50 | str = unicode
51 | basestring = basestring
52 | numeric_types = (int, long, float)
53 | integer_types = (int, long)
54 |
55 | elif is_py3:
56 | from urllib.parse import urlparse, urlunparse, urljoin, urlsplit, urlencode, quote, unquote, quote_plus, unquote_plus, urldefrag
57 | from urllib.request import parse_http_list, getproxies, proxy_bypass
58 | from http import cookiejar as cookielib
59 | from http.cookies import Morsel
60 | from io import StringIO
61 | from collections import OrderedDict
62 |
63 | builtin_str = str
64 | str = str
65 | bytes = bytes
66 | basestring = (str, bytes)
67 | numeric_types = (int, float)
68 | integer_types = (int,)
69 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/hooks.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | requests.hooks
5 | ~~~~~~~~~~~~~~
6 |
7 | This module provides the capabilities for the Requests hooks system.
8 |
9 | Available hooks:
10 |
11 | ``response``:
12 | The response generated from a Request.
13 | """
14 | HOOKS = ['response']
15 |
16 |
17 | def default_hooks():
18 | return dict((event, []) for event in HOOKS)
19 |
20 | # TODO: response is the only one
21 |
22 |
23 | def dispatch_hook(key, hooks, hook_data, **kwargs):
24 | """Dispatches a hook dictionary on a given piece of data."""
25 | hooks = hooks or dict()
26 | hooks = hooks.get(key)
27 | if hooks:
28 | if hasattr(hooks, '__call__'):
29 | hooks = [hooks]
30 | for hook in hooks:
31 | _hook_data = hook(hook_data, **kwargs)
32 | if _hook_data is not None:
33 | hook_data = _hook_data
34 | return hook_data
35 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/__init__.py:
--------------------------------------------------------------------------------
1 | '''
2 | Debian and other distributions "unbundle" requests' vendored dependencies, and
3 | rewrite all imports to use the global versions of ``urllib3`` and ``chardet``.
4 | The problem with this is that not only requests itself imports those
5 | dependencies, but third-party code outside of the distros' control too.
6 |
7 | In reaction to these problems, the distro maintainers replaced
8 | ``requests.packages`` with a magical "stub module" that imports the correct
9 | modules. The implementations were varying in quality and all had severe
10 | problems. For example, a symlink (or hardlink) that links the correct modules
11 | into place introduces problems regarding object identity, since you now have
12 | two modules in `sys.modules` with the same API, but different identities::
13 |
14 | requests.packages.urllib3 is not urllib3
15 |
16 | With version ``2.5.2``, requests started to maintain its own stub, so that
17 | distro-specific breakage would be reduced to a minimum, even though the whole
18 | issue is not requests' fault in the first place. See
19 | https://github.com/kennethreitz/requests/pull/2375 for the corresponding pull
20 | request.
21 | '''
22 |
23 | from __future__ import absolute_import
24 | import sys
25 |
26 | try:
27 | from . import urllib3
28 | except ImportError:
29 | import urllib3
30 | sys.modules['%s.urllib3' % __name__] = urllib3
31 |
32 | try:
33 | from . import chardet
34 | except ImportError:
35 | import chardet
36 | sys.modules['%s.chardet' % __name__] = chardet
37 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/chardet/__init__.py:
--------------------------------------------------------------------------------
1 | ######################## BEGIN LICENSE BLOCK ########################
2 | # This library is free software; you can redistribute it and/or
3 | # modify it under the terms of the GNU Lesser General Public
4 | # License as published by the Free Software Foundation; either
5 | # version 2.1 of the License, or (at your option) any later version.
6 | #
7 | # This library is distributed in the hope that it will be useful,
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
10 | # Lesser General Public License for more details.
11 | #
12 | # You should have received a copy of the GNU Lesser General Public
13 | # License along with this library; if not, write to the Free Software
14 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
15 | # 02110-1301 USA
16 | ######################### END LICENSE BLOCK #########################
17 |
18 | __version__ = "2.3.0"
19 | from sys import version_info
20 |
21 |
22 | def detect(aBuf):
23 | if ((version_info < (3, 0) and isinstance(aBuf, unicode)) or
24 | (version_info >= (3, 0) and not isinstance(aBuf, bytes))):
25 | raise ValueError('Expected a bytes object, not a unicode object')
26 |
27 | from . import universaldetector
28 | u = universaldetector.UniversalDetector()
29 | u.reset()
30 | u.feed(aBuf)
31 | u.close()
32 | return u.result
33 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/chardet/big5prober.py:
--------------------------------------------------------------------------------
1 | ######################## BEGIN LICENSE BLOCK ########################
2 | # The Original Code is Mozilla Communicator client code.
3 | #
4 | # The Initial Developer of the Original Code is
5 | # Netscape Communications Corporation.
6 | # Portions created by the Initial Developer are Copyright (C) 1998
7 | # the Initial Developer. All Rights Reserved.
8 | #
9 | # Contributor(s):
10 | # Mark Pilgrim - port to Python
11 | #
12 | # This library is free software; you can redistribute it and/or
13 | # modify it under the terms of the GNU Lesser General Public
14 | # License as published by the Free Software Foundation; either
15 | # version 2.1 of the License, or (at your option) any later version.
16 | #
17 | # This library is distributed in the hope that it will be useful,
18 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 | # Lesser General Public License for more details.
21 | #
22 | # You should have received a copy of the GNU Lesser General Public
23 | # License along with this library; if not, write to the Free Software
24 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
25 | # 02110-1301 USA
26 | ######################### END LICENSE BLOCK #########################
27 |
28 | from .mbcharsetprober import MultiByteCharSetProber
29 | from .codingstatemachine import CodingStateMachine
30 | from .chardistribution import Big5DistributionAnalysis
31 | from .mbcssm import Big5SMModel
32 |
33 |
34 | class Big5Prober(MultiByteCharSetProber):
35 | def __init__(self):
36 | MultiByteCharSetProber.__init__(self)
37 | self._mCodingSM = CodingStateMachine(Big5SMModel)
38 | self._mDistributionAnalyzer = Big5DistributionAnalysis()
39 | self.reset()
40 |
41 | def get_charset_name(self):
42 | return "Big5"
43 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/chardet/chardetect.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | Script which takes one or more file paths and reports on their detected
4 | encodings
5 |
6 | Example::
7 |
8 | % chardetect somefile someotherfile
9 | somefile: windows-1252 with confidence 0.5
10 | someotherfile: ascii with confidence 1.0
11 |
12 | If no paths are provided, it takes its input from stdin.
13 |
14 | """
15 |
16 | from __future__ import absolute_import, print_function, unicode_literals
17 |
18 | import argparse
19 | import sys
20 | from io import open
21 |
22 | from chardet import __version__
23 | from chardet.universaldetector import UniversalDetector
24 |
25 |
26 | def description_of(lines, name='stdin'):
27 | """
28 | Return a string describing the probable encoding of a file or
29 | list of strings.
30 |
31 | :param lines: The lines to get the encoding of.
32 | :type lines: Iterable of bytes
33 | :param name: Name of file or collection of lines
34 | :type name: str
35 | """
36 | u = UniversalDetector()
37 | for line in lines:
38 | u.feed(line)
39 | u.close()
40 | result = u.result
41 | if result['encoding']:
42 | return '{0}: {1} with confidence {2}'.format(name, result['encoding'],
43 | result['confidence'])
44 | else:
45 | return '{0}: no result'.format(name)
46 |
47 |
48 | def main(argv=None):
49 | '''
50 | Handles command line arguments and gets things started.
51 |
52 | :param argv: List of arguments, as if specified on the command-line.
53 | If None, ``sys.argv[1:]`` is used instead.
54 | :type argv: list of str
55 | '''
56 | # Get command line arguments
57 | parser = argparse.ArgumentParser(
58 | description="Takes one or more file paths and reports their detected \
59 | encodings",
60 | formatter_class=argparse.ArgumentDefaultsHelpFormatter,
61 | conflict_handler='resolve')
62 | parser.add_argument('input',
63 | help='File whose encoding we would like to determine.',
64 | type=argparse.FileType('rb'), nargs='*',
65 | default=[sys.stdin])
66 | parser.add_argument('--version', action='version',
67 | version='%(prog)s {0}'.format(__version__))
68 | args = parser.parse_args(argv)
69 |
70 | for f in args.input:
71 | if f.isatty():
72 | print("You are running chardetect interactively. Press " +
73 | "CTRL-D twice at the start of a blank line to signal the " +
74 | "end of your input. If you want help, run chardetect " +
75 | "--help\n", file=sys.stderr)
76 | print(description_of(f, f.name))
77 |
78 |
79 | if __name__ == '__main__':
80 | main()
81 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/chardet/charsetprober.py:
--------------------------------------------------------------------------------
1 | ######################## BEGIN LICENSE BLOCK ########################
2 | # The Original Code is Mozilla Universal charset detector code.
3 | #
4 | # The Initial Developer of the Original Code is
5 | # Netscape Communications Corporation.
6 | # Portions created by the Initial Developer are Copyright (C) 2001
7 | # the Initial Developer. All Rights Reserved.
8 | #
9 | # Contributor(s):
10 | # Mark Pilgrim - port to Python
11 | # Shy Shalom - original C code
12 | #
13 | # This library is free software; you can redistribute it and/or
14 | # modify it under the terms of the GNU Lesser General Public
15 | # License as published by the Free Software Foundation; either
16 | # version 2.1 of the License, or (at your option) any later version.
17 | #
18 | # This library is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 | # Lesser General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Lesser General Public
24 | # License along with this library; if not, write to the Free Software
25 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
26 | # 02110-1301 USA
27 | ######################### END LICENSE BLOCK #########################
28 |
29 | from . import constants
30 | import re
31 |
32 |
33 | class CharSetProber:
34 | def __init__(self):
35 | pass
36 |
37 | def reset(self):
38 | self._mState = constants.eDetecting
39 |
40 | def get_charset_name(self):
41 | return None
42 |
43 | def feed(self, aBuf):
44 | pass
45 |
46 | def get_state(self):
47 | return self._mState
48 |
49 | def get_confidence(self):
50 | return 0.0
51 |
52 | def filter_high_bit_only(self, aBuf):
53 | aBuf = re.sub(b'([\x00-\x7F])+', b' ', aBuf)
54 | return aBuf
55 |
56 | def filter_without_english_letters(self, aBuf):
57 | aBuf = re.sub(b'([A-Za-z])+', b' ', aBuf)
58 | return aBuf
59 |
60 | def filter_with_english_letters(self, aBuf):
61 | # TODO
62 | return aBuf
63 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/chardet/codingstatemachine.py:
--------------------------------------------------------------------------------
1 | ######################## BEGIN LICENSE BLOCK ########################
2 | # The Original Code is mozilla.org code.
3 | #
4 | # The Initial Developer of the Original Code is
5 | # Netscape Communications Corporation.
6 | # Portions created by the Initial Developer are Copyright (C) 1998
7 | # the Initial Developer. All Rights Reserved.
8 | #
9 | # Contributor(s):
10 | # Mark Pilgrim - port to Python
11 | #
12 | # This library is free software; you can redistribute it and/or
13 | # modify it under the terms of the GNU Lesser General Public
14 | # License as published by the Free Software Foundation; either
15 | # version 2.1 of the License, or (at your option) any later version.
16 | #
17 | # This library is distributed in the hope that it will be useful,
18 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 | # Lesser General Public License for more details.
21 | #
22 | # You should have received a copy of the GNU Lesser General Public
23 | # License along with this library; if not, write to the Free Software
24 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
25 | # 02110-1301 USA
26 | ######################### END LICENSE BLOCK #########################
27 |
28 | from .constants import eStart
29 | from .compat import wrap_ord
30 |
31 |
32 | class CodingStateMachine:
33 | def __init__(self, sm):
34 | self._mModel = sm
35 | self._mCurrentBytePos = 0
36 | self._mCurrentCharLen = 0
37 | self.reset()
38 |
39 | def reset(self):
40 | self._mCurrentState = eStart
41 |
42 | def next_state(self, c):
43 | # for each byte we get its class
44 | # if it is first byte, we also get byte length
45 | # PY3K: aBuf is a byte stream, so c is an int, not a byte
46 | byteCls = self._mModel['classTable'][wrap_ord(c)]
47 | if self._mCurrentState == eStart:
48 | self._mCurrentBytePos = 0
49 | self._mCurrentCharLen = self._mModel['charLenTable'][byteCls]
50 | # from byte's class and stateTable, we get its next state
51 | curr_state = (self._mCurrentState * self._mModel['classFactor']
52 | + byteCls)
53 | self._mCurrentState = self._mModel['stateTable'][curr_state]
54 | self._mCurrentBytePos += 1
55 | return self._mCurrentState
56 |
57 | def get_current_charlen(self):
58 | return self._mCurrentCharLen
59 |
60 | def get_coding_state_machine(self):
61 | return self._mModel['name']
62 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/chardet/compat.py:
--------------------------------------------------------------------------------
1 | ######################## BEGIN LICENSE BLOCK ########################
2 | # Contributor(s):
3 | # Ian Cordasco - port to Python
4 | #
5 | # This library is free software; you can redistribute it and/or
6 | # modify it under the terms of the GNU Lesser General Public
7 | # License as published by the Free Software Foundation; either
8 | # version 2.1 of the License, or (at your option) any later version.
9 | #
10 | # This library is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 | # Lesser General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU Lesser General Public
16 | # License along with this library; if not, write to the Free Software
17 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
18 | # 02110-1301 USA
19 | ######################### END LICENSE BLOCK #########################
20 |
21 | import sys
22 |
23 |
24 | if sys.version_info < (3, 0):
25 | base_str = (str, unicode)
26 | else:
27 | base_str = (bytes, str)
28 |
29 |
30 | def wrap_ord(a):
31 | if sys.version_info < (3, 0) and isinstance(a, base_str):
32 | return ord(a)
33 | else:
34 | return a
35 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/chardet/constants.py:
--------------------------------------------------------------------------------
1 | ######################## BEGIN LICENSE BLOCK ########################
2 | # The Original Code is Mozilla Universal charset detector code.
3 | #
4 | # The Initial Developer of the Original Code is
5 | # Netscape Communications Corporation.
6 | # Portions created by the Initial Developer are Copyright (C) 2001
7 | # the Initial Developer. All Rights Reserved.
8 | #
9 | # Contributor(s):
10 | # Mark Pilgrim - port to Python
11 | # Shy Shalom - original C code
12 | #
13 | # This library is free software; you can redistribute it and/or
14 | # modify it under the terms of the GNU Lesser General Public
15 | # License as published by the Free Software Foundation; either
16 | # version 2.1 of the License, or (at your option) any later version.
17 | #
18 | # This library is distributed in the hope that it will be useful,
19 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 | # Lesser General Public License for more details.
22 | #
23 | # You should have received a copy of the GNU Lesser General Public
24 | # License along with this library; if not, write to the Free Software
25 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
26 | # 02110-1301 USA
27 | ######################### END LICENSE BLOCK #########################
28 |
29 | _debug = 0
30 |
31 | eDetecting = 0
32 | eFoundIt = 1
33 | eNotMe = 2
34 |
35 | eStart = 0
36 | eError = 1
37 | eItsMe = 2
38 |
39 | SHORTCUT_THRESHOLD = 0.95
40 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/chardet/cp949prober.py:
--------------------------------------------------------------------------------
1 | ######################## BEGIN LICENSE BLOCK ########################
2 | # The Original Code is mozilla.org code.
3 | #
4 | # The Initial Developer of the Original Code is
5 | # Netscape Communications Corporation.
6 | # Portions created by the Initial Developer are Copyright (C) 1998
7 | # the Initial Developer. All Rights Reserved.
8 | #
9 | # Contributor(s):
10 | # Mark Pilgrim - port to Python
11 | #
12 | # This library is free software; you can redistribute it and/or
13 | # modify it under the terms of the GNU Lesser General Public
14 | # License as published by the Free Software Foundation; either
15 | # version 2.1 of the License, or (at your option) any later version.
16 | #
17 | # This library is distributed in the hope that it will be useful,
18 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 | # Lesser General Public License for more details.
21 | #
22 | # You should have received a copy of the GNU Lesser General Public
23 | # License along with this library; if not, write to the Free Software
24 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
25 | # 02110-1301 USA
26 | ######################### END LICENSE BLOCK #########################
27 |
28 | from .mbcharsetprober import MultiByteCharSetProber
29 | from .codingstatemachine import CodingStateMachine
30 | from .chardistribution import EUCKRDistributionAnalysis
31 | from .mbcssm import CP949SMModel
32 |
33 |
34 | class CP949Prober(MultiByteCharSetProber):
35 | def __init__(self):
36 | MultiByteCharSetProber.__init__(self)
37 | self._mCodingSM = CodingStateMachine(CP949SMModel)
38 | # NOTE: CP949 is a superset of EUC-KR, so the distribution should be
39 | # not different.
40 | self._mDistributionAnalyzer = EUCKRDistributionAnalysis()
41 | self.reset()
42 |
43 | def get_charset_name(self):
44 | return "CP949"
45 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/chardet/euckrprober.py:
--------------------------------------------------------------------------------
1 | ######################## BEGIN LICENSE BLOCK ########################
2 | # The Original Code is mozilla.org code.
3 | #
4 | # The Initial Developer of the Original Code is
5 | # Netscape Communications Corporation.
6 | # Portions created by the Initial Developer are Copyright (C) 1998
7 | # the Initial Developer. All Rights Reserved.
8 | #
9 | # Contributor(s):
10 | # Mark Pilgrim - port to Python
11 | #
12 | # This library is free software; you can redistribute it and/or
13 | # modify it under the terms of the GNU Lesser General Public
14 | # License as published by the Free Software Foundation; either
15 | # version 2.1 of the License, or (at your option) any later version.
16 | #
17 | # This library is distributed in the hope that it will be useful,
18 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 | # Lesser General Public License for more details.
21 | #
22 | # You should have received a copy of the GNU Lesser General Public
23 | # License along with this library; if not, write to the Free Software
24 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
25 | # 02110-1301 USA
26 | ######################### END LICENSE BLOCK #########################
27 |
28 | from .mbcharsetprober import MultiByteCharSetProber
29 | from .codingstatemachine import CodingStateMachine
30 | from .chardistribution import EUCKRDistributionAnalysis
31 | from .mbcssm import EUCKRSMModel
32 |
33 |
34 | class EUCKRProber(MultiByteCharSetProber):
35 | def __init__(self):
36 | MultiByteCharSetProber.__init__(self)
37 | self._mCodingSM = CodingStateMachine(EUCKRSMModel)
38 | self._mDistributionAnalyzer = EUCKRDistributionAnalysis()
39 | self.reset()
40 |
41 | def get_charset_name(self):
42 | return "EUC-KR"
43 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/chardet/euctwprober.py:
--------------------------------------------------------------------------------
1 | ######################## BEGIN LICENSE BLOCK ########################
2 | # The Original Code is mozilla.org code.
3 | #
4 | # The Initial Developer of the Original Code is
5 | # Netscape Communications Corporation.
6 | # Portions created by the Initial Developer are Copyright (C) 1998
7 | # the Initial Developer. All Rights Reserved.
8 | #
9 | # Contributor(s):
10 | # Mark Pilgrim - port to Python
11 | #
12 | # This library is free software; you can redistribute it and/or
13 | # modify it under the terms of the GNU Lesser General Public
14 | # License as published by the Free Software Foundation; either
15 | # version 2.1 of the License, or (at your option) any later version.
16 | #
17 | # This library is distributed in the hope that it will be useful,
18 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 | # Lesser General Public License for more details.
21 | #
22 | # You should have received a copy of the GNU Lesser General Public
23 | # License along with this library; if not, write to the Free Software
24 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
25 | # 02110-1301 USA
26 | ######################### END LICENSE BLOCK #########################
27 |
28 | from .mbcharsetprober import MultiByteCharSetProber
29 | from .codingstatemachine import CodingStateMachine
30 | from .chardistribution import EUCTWDistributionAnalysis
31 | from .mbcssm import EUCTWSMModel
32 |
33 | class EUCTWProber(MultiByteCharSetProber):
34 | def __init__(self):
35 | MultiByteCharSetProber.__init__(self)
36 | self._mCodingSM = CodingStateMachine(EUCTWSMModel)
37 | self._mDistributionAnalyzer = EUCTWDistributionAnalysis()
38 | self.reset()
39 |
40 | def get_charset_name(self):
41 | return "EUC-TW"
42 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/chardet/gb2312prober.py:
--------------------------------------------------------------------------------
1 | ######################## BEGIN LICENSE BLOCK ########################
2 | # The Original Code is mozilla.org code.
3 | #
4 | # The Initial Developer of the Original Code is
5 | # Netscape Communications Corporation.
6 | # Portions created by the Initial Developer are Copyright (C) 1998
7 | # the Initial Developer. All Rights Reserved.
8 | #
9 | # Contributor(s):
10 | # Mark Pilgrim - port to Python
11 | #
12 | # This library is free software; you can redistribute it and/or
13 | # modify it under the terms of the GNU Lesser General Public
14 | # License as published by the Free Software Foundation; either
15 | # version 2.1 of the License, or (at your option) any later version.
16 | #
17 | # This library is distributed in the hope that it will be useful,
18 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 | # Lesser General Public License for more details.
21 | #
22 | # You should have received a copy of the GNU Lesser General Public
23 | # License along with this library; if not, write to the Free Software
24 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
25 | # 02110-1301 USA
26 | ######################### END LICENSE BLOCK #########################
27 |
28 | from .mbcharsetprober import MultiByteCharSetProber
29 | from .codingstatemachine import CodingStateMachine
30 | from .chardistribution import GB2312DistributionAnalysis
31 | from .mbcssm import GB2312SMModel
32 |
33 | class GB2312Prober(MultiByteCharSetProber):
34 | def __init__(self):
35 | MultiByteCharSetProber.__init__(self)
36 | self._mCodingSM = CodingStateMachine(GB2312SMModel)
37 | self._mDistributionAnalyzer = GB2312DistributionAnalysis()
38 | self.reset()
39 |
40 | def get_charset_name(self):
41 | return "GB2312"
42 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/chardet/mbcsgroupprober.py:
--------------------------------------------------------------------------------
1 | ######################## BEGIN LICENSE BLOCK ########################
2 | # The Original Code is Mozilla Universal charset detector code.
3 | #
4 | # The Initial Developer of the Original Code is
5 | # Netscape Communications Corporation.
6 | # Portions created by the Initial Developer are Copyright (C) 2001
7 | # the Initial Developer. All Rights Reserved.
8 | #
9 | # Contributor(s):
10 | # Mark Pilgrim - port to Python
11 | # Shy Shalom - original C code
12 | # Proofpoint, Inc.
13 | #
14 | # This library is free software; you can redistribute it and/or
15 | # modify it under the terms of the GNU Lesser General Public
16 | # License as published by the Free Software Foundation; either
17 | # version 2.1 of the License, or (at your option) any later version.
18 | #
19 | # This library is distributed in the hope that it will be useful,
20 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
22 | # Lesser General Public License for more details.
23 | #
24 | # You should have received a copy of the GNU Lesser General Public
25 | # License along with this library; if not, write to the Free Software
26 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
27 | # 02110-1301 USA
28 | ######################### END LICENSE BLOCK #########################
29 |
30 | from .charsetgroupprober import CharSetGroupProber
31 | from .utf8prober import UTF8Prober
32 | from .sjisprober import SJISProber
33 | from .eucjpprober import EUCJPProber
34 | from .gb2312prober import GB2312Prober
35 | from .euckrprober import EUCKRProber
36 | from .cp949prober import CP949Prober
37 | from .big5prober import Big5Prober
38 | from .euctwprober import EUCTWProber
39 |
40 |
41 | class MBCSGroupProber(CharSetGroupProber):
42 | def __init__(self):
43 | CharSetGroupProber.__init__(self)
44 | self._mProbers = [
45 | UTF8Prober(),
46 | SJISProber(),
47 | EUCJPProber(),
48 | GB2312Prober(),
49 | EUCKRProber(),
50 | CP949Prober(),
51 | Big5Prober(),
52 | EUCTWProber()
53 | ]
54 | self.reset()
55 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/chardet/utf8prober.py:
--------------------------------------------------------------------------------
1 | ######################## BEGIN LICENSE BLOCK ########################
2 | # The Original Code is mozilla.org code.
3 | #
4 | # The Initial Developer of the Original Code is
5 | # Netscape Communications Corporation.
6 | # Portions created by the Initial Developer are Copyright (C) 1998
7 | # the Initial Developer. All Rights Reserved.
8 | #
9 | # Contributor(s):
10 | # Mark Pilgrim - port to Python
11 | #
12 | # This library is free software; you can redistribute it and/or
13 | # modify it under the terms of the GNU Lesser General Public
14 | # License as published by the Free Software Foundation; either
15 | # version 2.1 of the License, or (at your option) any later version.
16 | #
17 | # This library is distributed in the hope that it will be useful,
18 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 | # Lesser General Public License for more details.
21 | #
22 | # You should have received a copy of the GNU Lesser General Public
23 | # License along with this library; if not, write to the Free Software
24 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
25 | # 02110-1301 USA
26 | ######################### END LICENSE BLOCK #########################
27 |
28 | from . import constants
29 | from .charsetprober import CharSetProber
30 | from .codingstatemachine import CodingStateMachine
31 | from .mbcssm import UTF8SMModel
32 |
33 | ONE_CHAR_PROB = 0.5
34 |
35 |
36 | class UTF8Prober(CharSetProber):
37 | def __init__(self):
38 | CharSetProber.__init__(self)
39 | self._mCodingSM = CodingStateMachine(UTF8SMModel)
40 | self.reset()
41 |
42 | def reset(self):
43 | CharSetProber.reset(self)
44 | self._mCodingSM.reset()
45 | self._mNumOfMBChar = 0
46 |
47 | def get_charset_name(self):
48 | return "utf-8"
49 |
50 | def feed(self, aBuf):
51 | for c in aBuf:
52 | codingState = self._mCodingSM.next_state(c)
53 | if codingState == constants.eError:
54 | self._mState = constants.eNotMe
55 | break
56 | elif codingState == constants.eItsMe:
57 | self._mState = constants.eFoundIt
58 | break
59 | elif codingState == constants.eStart:
60 | if self._mCodingSM.get_current_charlen() >= 2:
61 | self._mNumOfMBChar += 1
62 |
63 | if self.get_state() == constants.eDetecting:
64 | if self.get_confidence() > constants.SHORTCUT_THRESHOLD:
65 | self._mState = constants.eFoundIt
66 |
67 | return self.get_state()
68 |
69 | def get_confidence(self):
70 | unlike = 0.99
71 | if self._mNumOfMBChar < 6:
72 | for i in range(0, self._mNumOfMBChar):
73 | unlike = unlike * ONE_CHAR_PROB
74 | return 1.0 - unlike
75 | else:
76 | return unlike
77 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/idna/__init__.py:
--------------------------------------------------------------------------------
1 | from .core import *
2 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/idna/compat.py:
--------------------------------------------------------------------------------
1 | from .core import *
2 | from .codec import *
3 |
4 | def ToASCII(label):
5 | return encode(label)
6 |
7 | def ToUnicode(label):
8 | return decode(label)
9 |
10 | def nameprep(s):
11 | raise NotImplementedError("IDNA 2008 does not utilise nameprep protocol")
12 |
13 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/idna/intranges.py:
--------------------------------------------------------------------------------
1 | """
2 | Given a list of integers, made up of (hopefully) a small number of long runs
3 | of consecutive integers, compute a representation of the form
4 | ((start1, end1), (start2, end2) ...). Then answer the question "was x present
5 | in the original list?" in time O(log(# runs)).
6 | """
7 |
8 | import bisect
9 |
10 | def intranges_from_list(list_):
11 | """Represent a list of integers as a sequence of ranges:
12 | ((start_0, end_0), (start_1, end_1), ...), such that the original
13 | integers are exactly those x such that start_i <= x < end_i for some i.
14 | """
15 |
16 | sorted_list = sorted(list_)
17 | ranges = []
18 | last_write = -1
19 | for i in range(len(sorted_list)):
20 | if i+1 < len(sorted_list):
21 | if sorted_list[i] == sorted_list[i+1]-1:
22 | continue
23 | current_range = sorted_list[last_write+1:i+1]
24 | range_tuple = (current_range[0], current_range[-1] + 1)
25 | ranges.append(range_tuple)
26 | last_write = i
27 |
28 | return tuple(ranges)
29 |
30 |
31 | def intranges_contain(int_, ranges):
32 | """Determine if `int_` falls into one of the ranges in `ranges`."""
33 | tuple_ = (int_, int_)
34 | pos = bisect.bisect_left(ranges, tuple_)
35 | # we could be immediately ahead of a tuple (start, end)
36 | # with start < int_ <= end
37 | if pos > 0:
38 | left, right = ranges[pos-1]
39 | if left <= int_ < right:
40 | return True
41 | # or we could be immediately behind a tuple (int_, end)
42 | if pos < len(ranges):
43 | left, _ = ranges[pos]
44 | if left == int_:
45 | return True
46 | return False
47 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/urllib3/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | urllib3 - Thread-safe connection pooling and re-using.
3 | """
4 |
5 | from __future__ import absolute_import
6 | import warnings
7 |
8 | from .connectionpool import (
9 | HTTPConnectionPool,
10 | HTTPSConnectionPool,
11 | connection_from_url
12 | )
13 |
14 | from . import exceptions
15 | from .filepost import encode_multipart_formdata
16 | from .poolmanager import PoolManager, ProxyManager, proxy_from_url
17 | from .response import HTTPResponse
18 | from .util.request import make_headers
19 | from .util.url import get_host
20 | from .util.timeout import Timeout
21 | from .util.retry import Retry
22 |
23 |
24 | # Set default logging handler to avoid "No handler found" warnings.
25 | import logging
26 | try: # Python 2.7+
27 | from logging import NullHandler
28 | except ImportError:
29 | class NullHandler(logging.Handler):
30 | def emit(self, record):
31 | pass
32 |
33 | __author__ = 'Andrey Petrov (andrey.petrov@shazow.net)'
34 | __license__ = 'MIT'
35 | __version__ = '1.20'
36 |
37 | __all__ = (
38 | 'HTTPConnectionPool',
39 | 'HTTPSConnectionPool',
40 | 'PoolManager',
41 | 'ProxyManager',
42 | 'HTTPResponse',
43 | 'Retry',
44 | 'Timeout',
45 | 'add_stderr_logger',
46 | 'connection_from_url',
47 | 'disable_warnings',
48 | 'encode_multipart_formdata',
49 | 'get_host',
50 | 'make_headers',
51 | 'proxy_from_url',
52 | )
53 |
54 | logging.getLogger(__name__).addHandler(NullHandler())
55 |
56 |
57 | def add_stderr_logger(level=logging.DEBUG):
58 | """
59 | Helper for quickly adding a StreamHandler to the logger. Useful for
60 | debugging.
61 |
62 | Returns the handler after adding it.
63 | """
64 | # This method needs to be in this __init__.py to get the __name__ correct
65 | # even if urllib3 is vendored within another package.
66 | logger = logging.getLogger(__name__)
67 | handler = logging.StreamHandler()
68 | handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s'))
69 | logger.addHandler(handler)
70 | logger.setLevel(level)
71 | logger.debug('Added a stderr logging handler to logger: %s', __name__)
72 | return handler
73 |
74 |
75 | # ... Clean up.
76 | del NullHandler
77 |
78 |
79 | # All warning filters *must* be appended unless you're really certain that they
80 | # shouldn't be: otherwise, it's very hard for users to use most Python
81 | # mechanisms to silence them.
82 | # SecurityWarning's always go off by default.
83 | warnings.simplefilter('always', exceptions.SecurityWarning, append=True)
84 | # SubjectAltNameWarning's should go off once per host
85 | warnings.simplefilter('default', exceptions.SubjectAltNameWarning, append=True)
86 | # InsecurePlatformWarning's don't vary between requests, so we keep it default.
87 | warnings.simplefilter('default', exceptions.InsecurePlatformWarning,
88 | append=True)
89 | # SNIMissingWarnings should go off only once.
90 | warnings.simplefilter('default', exceptions.SNIMissingWarning, append=True)
91 |
92 |
93 | def disable_warnings(category=exceptions.HTTPWarning):
94 | """
95 | Helper for quickly disabling all urllib3 warnings.
96 | """
97 | warnings.simplefilter('ignore', category)
98 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/urllib3/contrib/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/util/scheduled-snapshots/requests/packages/urllib3/contrib/__init__.py
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/urllib3/filepost.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 | import codecs
3 |
4 | from uuid import uuid4
5 | from io import BytesIO
6 |
7 | from .packages import six
8 | from .packages.six import b
9 | from .fields import RequestField
10 |
11 | writer = codecs.lookup('utf-8')[3]
12 |
13 |
14 | def choose_boundary():
15 | """
16 | Our embarrassingly-simple replacement for mimetools.choose_boundary.
17 | """
18 | return uuid4().hex
19 |
20 |
21 | def iter_field_objects(fields):
22 | """
23 | Iterate over fields.
24 |
25 | Supports list of (k, v) tuples and dicts, and lists of
26 | :class:`~urllib3.fields.RequestField`.
27 |
28 | """
29 | if isinstance(fields, dict):
30 | i = six.iteritems(fields)
31 | else:
32 | i = iter(fields)
33 |
34 | for field in i:
35 | if isinstance(field, RequestField):
36 | yield field
37 | else:
38 | yield RequestField.from_tuples(*field)
39 |
40 |
41 | def iter_fields(fields):
42 | """
43 | .. deprecated:: 1.6
44 |
45 | Iterate over fields.
46 |
47 | The addition of :class:`~urllib3.fields.RequestField` makes this function
48 | obsolete. Instead, use :func:`iter_field_objects`, which returns
49 | :class:`~urllib3.fields.RequestField` objects.
50 |
51 | Supports list of (k, v) tuples and dicts.
52 | """
53 | if isinstance(fields, dict):
54 | return ((k, v) for k, v in six.iteritems(fields))
55 |
56 | return ((k, v) for k, v in fields)
57 |
58 |
59 | def encode_multipart_formdata(fields, boundary=None):
60 | """
61 | Encode a dictionary of ``fields`` using the multipart/form-data MIME format.
62 |
63 | :param fields:
64 | Dictionary of fields or list of (key, :class:`~urllib3.fields.RequestField`).
65 |
66 | :param boundary:
67 | If not specified, then a random boundary will be generated using
68 | :func:`mimetools.choose_boundary`.
69 | """
70 | body = BytesIO()
71 | if boundary is None:
72 | boundary = choose_boundary()
73 |
74 | for field in iter_field_objects(fields):
75 | body.write(b('--%s\r\n' % (boundary)))
76 |
77 | writer(body).write(field.render_headers())
78 | data = field.data
79 |
80 | if isinstance(data, int):
81 | data = str(data) # Backwards compatibility
82 |
83 | if isinstance(data, six.text_type):
84 | writer(body).write(data)
85 | else:
86 | body.write(data)
87 |
88 | body.write(b'\r\n')
89 |
90 | body.write(b('--%s--\r\n' % (boundary)))
91 |
92 | content_type = str('multipart/form-data; boundary=%s' % boundary)
93 |
94 | return body.getvalue(), content_type
95 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/urllib3/packages/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 |
3 | from . import ssl_match_hostname
4 |
5 | __all__ = ('ssl_match_hostname', )
6 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/urllib3/packages/backports/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cc-archive/open-ledger/c2c7fa9d49475481e690738a0d96eec90d1fdd33/util/scheduled-snapshots/requests/packages/urllib3/packages/backports/__init__.py
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/urllib3/packages/backports/makefile.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | backports.makefile
4 | ~~~~~~~~~~~~~~~~~~
5 |
6 | Backports the Python 3 ``socket.makefile`` method for use with anything that
7 | wants to create a "fake" socket object.
8 | """
9 | import io
10 |
11 | from socket import SocketIO
12 |
13 |
14 | def backport_makefile(self, mode="r", buffering=None, encoding=None,
15 | errors=None, newline=None):
16 | """
17 | Backport of ``socket.makefile`` from Python 3.5.
18 | """
19 | if not set(mode) <= set(["r", "w", "b"]):
20 | raise ValueError(
21 | "invalid mode %r (only r, w, b allowed)" % (mode,)
22 | )
23 | writing = "w" in mode
24 | reading = "r" in mode or not writing
25 | assert reading or writing
26 | binary = "b" in mode
27 | rawmode = ""
28 | if reading:
29 | rawmode += "r"
30 | if writing:
31 | rawmode += "w"
32 | raw = SocketIO(self, rawmode)
33 | self._makefile_refs += 1
34 | if buffering is None:
35 | buffering = -1
36 | if buffering < 0:
37 | buffering = io.DEFAULT_BUFFER_SIZE
38 | if buffering == 0:
39 | if not binary:
40 | raise ValueError("unbuffered streams must be binary")
41 | return raw
42 | if reading and writing:
43 | buffer = io.BufferedRWPair(raw, raw, buffering)
44 | elif reading:
45 | buffer = io.BufferedReader(raw, buffering)
46 | else:
47 | assert writing
48 | buffer = io.BufferedWriter(raw, buffering)
49 | if binary:
50 | return buffer
51 | text = io.TextIOWrapper(buffer, encoding, errors, newline)
52 | text.mode = mode
53 | return text
54 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/urllib3/packages/ssl_match_hostname/__init__.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | try:
4 | # Our match_hostname function is the same as 3.5's, so we only want to
5 | # import the match_hostname function if it's at least that good.
6 | if sys.version_info < (3, 5):
7 | raise ImportError("Fallback to vendored code")
8 |
9 | from ssl import CertificateError, match_hostname
10 | except ImportError:
11 | try:
12 | # Backport of the function from a pypi module
13 | from backports.ssl_match_hostname import CertificateError, match_hostname
14 | except ImportError:
15 | # Our vendored copy
16 | from ._implementation import CertificateError, match_hostname
17 |
18 | # Not needed, but documenting what we provide.
19 | __all__ = ('CertificateError', 'match_hostname')
20 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/urllib3/util/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 | # For backwards compatibility, provide imports that used to be here.
3 | from .connection import is_connection_dropped
4 | from .request import make_headers
5 | from .response import is_fp_closed
6 | from .ssl_ import (
7 | SSLContext,
8 | HAS_SNI,
9 | IS_PYOPENSSL,
10 | assert_fingerprint,
11 | resolve_cert_reqs,
12 | resolve_ssl_version,
13 | ssl_wrap_socket,
14 | )
15 | from .timeout import (
16 | current_time,
17 | Timeout,
18 | )
19 |
20 | from .retry import Retry
21 | from .url import (
22 | get_host,
23 | parse_url,
24 | split_first,
25 | Url,
26 | )
27 | from .wait import (
28 | wait_for_read,
29 | wait_for_write
30 | )
31 |
32 | __all__ = (
33 | 'HAS_SNI',
34 | 'IS_PYOPENSSL',
35 | 'SSLContext',
36 | 'Retry',
37 | 'Timeout',
38 | 'Url',
39 | 'assert_fingerprint',
40 | 'current_time',
41 | 'is_connection_dropped',
42 | 'is_fp_closed',
43 | 'get_host',
44 | 'parse_url',
45 | 'make_headers',
46 | 'resolve_cert_reqs',
47 | 'resolve_ssl_version',
48 | 'split_first',
49 | 'ssl_wrap_socket',
50 | 'wait_for_read',
51 | 'wait_for_write'
52 | )
53 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/urllib3/util/response.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 | from ..packages.six.moves import http_client as httplib
3 |
4 | from ..exceptions import HeaderParsingError
5 |
6 |
7 | def is_fp_closed(obj):
8 | """
9 | Checks whether a given file-like object is closed.
10 |
11 | :param obj:
12 | The file-like object to check.
13 | """
14 |
15 | try:
16 | # Check `isclosed()` first, in case Python3 doesn't set `closed`.
17 | # GH Issue #928
18 | return obj.isclosed()
19 | except AttributeError:
20 | pass
21 |
22 | try:
23 | # Check via the official file-like-object way.
24 | return obj.closed
25 | except AttributeError:
26 | pass
27 |
28 | try:
29 | # Check if the object is a container for another file-like object that
30 | # gets released on exhaustion (e.g. HTTPResponse).
31 | return obj.fp is None
32 | except AttributeError:
33 | pass
34 |
35 | raise ValueError("Unable to determine whether fp is closed.")
36 |
37 |
38 | def assert_header_parsing(headers):
39 | """
40 | Asserts whether all headers have been successfully parsed.
41 | Extracts encountered errors from the result of parsing headers.
42 |
43 | Only works on Python 3.
44 |
45 | :param headers: Headers to verify.
46 | :type headers: `httplib.HTTPMessage`.
47 |
48 | :raises urllib3.exceptions.HeaderParsingError:
49 | If parsing errors are found.
50 | """
51 |
52 | # This will fail silently if we pass in the wrong kind of parameter.
53 | # To make debugging easier add an explicit check.
54 | if not isinstance(headers, httplib.HTTPMessage):
55 | raise TypeError('expected httplib.Message, got {0}.'.format(
56 | type(headers)))
57 |
58 | defects = getattr(headers, 'defects', None)
59 | get_payload = getattr(headers, 'get_payload', None)
60 |
61 | unparsed_data = None
62 | if get_payload: # Platform-specific: Python 3.
63 | unparsed_data = get_payload()
64 |
65 | if defects or unparsed_data:
66 | raise HeaderParsingError(defects=defects, unparsed_data=unparsed_data)
67 |
68 |
69 | def is_response_to_head(response):
70 | """
71 | Checks whether the request of a response has been a HEAD-request.
72 | Handles the quirks of AppEngine.
73 |
74 | :param conn:
75 | :type conn: :class:`httplib.HTTPResponse`
76 | """
77 | # FIXME: Can we do this somehow without accessing private httplib _method?
78 | method = response._method
79 | if isinstance(method, int): # Platform-specific: Appengine
80 | return method == 3
81 | return method.upper() == 'HEAD'
82 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/requests/packages/urllib3/util/wait.py:
--------------------------------------------------------------------------------
1 | from .selectors import (
2 | HAS_SELECT,
3 | DefaultSelector,
4 | EVENT_READ,
5 | EVENT_WRITE
6 | )
7 |
8 |
9 | def _wait_for_io_events(socks, events, timeout=None):
10 | """ Waits for IO events to be available from a list of sockets
11 | or optionally a single socket if passed in. Returns a list of
12 | sockets that can be interacted with immediately. """
13 | if not HAS_SELECT:
14 | raise ValueError('Platform does not have a selector')
15 | if not isinstance(socks, list):
16 | # Probably just a single socket.
17 | if hasattr(socks, "fileno"):
18 | socks = [socks]
19 | # Otherwise it might be a non-list iterable.
20 | else:
21 | socks = list(socks)
22 | with DefaultSelector() as selector:
23 | for sock in socks:
24 | selector.register(sock, events)
25 | return [key[0].fileobj for key in
26 | selector.select(timeout) if key[1] & events]
27 |
28 |
29 | def wait_for_read(socks, timeout=None):
30 | """ Waits for reading to be available from a list of sockets
31 | or optionally a single socket if passed in. Returns a list of
32 | sockets that can be read from immediately. """
33 | return _wait_for_io_events(socks, EVENT_READ, timeout)
34 |
35 |
36 | def wait_for_write(socks, timeout=None):
37 | """ Waits for writing to be available from a list of sockets
38 | or optionally a single socket if passed in. Returns a list of
39 | sockets that can be written to immediately. """
40 | return _wait_for_io_events(socks, EVENT_WRITE, timeout)
41 |
--------------------------------------------------------------------------------
/util/scheduled-snapshots/setup.cfg:
--------------------------------------------------------------------------------
1 | [install]
2 | prefix=
3 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 |
3 | var PROD = (process.env.NODE_ENV === 'production')
4 |
5 | var root = "/static"
6 |
7 | module.exports = [{
8 | context: __dirname + root + "/js",
9 | entry: ['whatwg-fetch', "./index"],
10 | output: {
11 | path: __dirname + root + "/js/build",
12 | filename: 'openledger.js',
13 | vendor: ['photoswipe', 'photoswipe/src/js/ui/photoswipe-ui-default.js']
14 | },
15 | module: {
16 | loaders: [{
17 | test: /.js/,
18 | loaders: ['babel-loader?cacheDirectory']
19 | }, {
20 | test: /\.json$/,
21 | loader: 'json'
22 | }
23 | ]
24 | },
25 | plugins: PROD ? [
26 | new webpack.optimize.UglifyJsPlugin({
27 | compress: {
28 | warnings: false,
29 | screw_ie8: true
30 | },
31 | comments: false
32 | }),
33 | new webpack.DefinePlugin({
34 | "process.env": {
35 | NODE_ENV: JSON.stringify("production")
36 | }
37 | })]
38 | : [new webpack.DefinePlugin({
39 | "process.env": {
40 | NODE_ENV: JSON.stringify("develop")
41 | }
42 | })]
43 | }
44 | ];
45 | module.exports[0].plugins.push (new webpack.ProvidePlugin({PhotoSwipe: 'photoswipe', PhotoSwipeUI_Default: './photoswipe-ui-default'}))
46 |
47 |
--------------------------------------------------------------------------------