├── .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 | 15 | 18 | 19 | {% for provider in providers %} 20 | 21 | 24 | 27 | 28 | {% endfor %} 29 |
13 | Source 14 | 16 | Number of works 17 |
22 | {{ provider['display_name'] }} 23 | 25 | {{ provider['hits']}} 26 |
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 |
6 | 7 | 17 | 18 |
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 |
8 | 9 |
10 |
11 | 12 | 13 | 21 | 22 |
23 |
24 | 25 |

{{ image.title|truncate(25) }}{% if image.creator %}, 26 | {{ image.creator|truncate(20) }} 27 | {% endif %} 28 |

29 |
30 | 31 | 45 | 46 |
47 |
48 | 49 |
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 | 13 | 14 |
16 | 17 |
18 | 19 |
20 |
21 |
22 | {% endmacro -%} 23 | -------------------------------------------------------------------------------- /imageledger/jinja2/includes/metadata.html: -------------------------------------------------------------------------------- 1 |
2 |

"" by

3 |

4 | Licensed under 5 |

6 |

7 | Original source via 8 |

9 |
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 |
3 | 4 |
5 |
6 |
7 | CC 8 |
9 | Search 10 |
11 | 12 | {{ form.search }} 13 | {{ form.page }} 14 | 15 |
16 | 17 |
18 |
19 |

20 | Use this prototype to find images that you can use and remix across several open archives. 21 | Give us your feedback to help us design a front door to the commons. 22 |

23 | 24 |
25 |
26 |
27 |

Filters

28 |
29 |
30 |
31 | 32 | Find images I can... 33 | 34 | {{ form.licenses }} 35 |
36 |
37 | 38 | Search within: 39 | 40 | {{ form.search_fields }} 41 |
42 |
43 | 44 | Results per page: 45 | 46 | {{ form.per_page }} 47 |
48 |
49 |
50 |
51 | 52 | Search from these collections: 53 | 54 | {% for type in form.work_types %} 55 |
56 | {{ type }} 57 |
    58 | {% for prov in work_types[type.data.value]|sort %} 59 | {% for provider in form.providers if provider.data.value == prov %} 60 |
  • 61 | {{ provider }} 62 |
  • 63 | {% endfor %} 64 | {% endfor %} 65 |
66 |
67 | {% endfor %} 68 |
69 |
70 |
71 |
72 | {%if request.GET.get('advanced') == None and request.GET|length > 0 %} 73 | Advanced Search. 74 | {% endif %} 75 |
76 | -------------------------------------------------------------------------------- /imageledger/jinja2/includes/tags.html: -------------------------------------------------------------------------------- 1 | {% macro add(image, request, size) -%} 2 |
3 | 13 | 14 |
15 | 16 |
17 | 18 |
19 |
20 |
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 |
10 | {{ csrf_input }} 11 |

12 |
13 | {{ object.title }} 14 | 15 |
16 | 17 | {{ form.title }} 18 |

19 | 20 |
21 |
22 | {{ object.description or "No description provided"}} 23 |
24 | 25 | {{ form.description }} 26 | {{ form.description.errors }} 27 |
28 | 29 |
30 | List is visible to the public? 31 | {{ form.is_public }} 32 | 33 | {{ "Yes" if object.is_public else "No"}} 34 | 35 |
36 | 37 | 38 | 39 |
40 | 41 |
42 | {{ csrf_input }} 43 | 44 |
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 |
51 | 52 | 53 |
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 |

32 | {{ lst.title }} 33 | 34 |

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 |
46 | 47 | 48 |
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 |
6 | 7 |

Your account

8 |

9 | You have 10 | {{ counts.lists }} list{{ counts.lists|pluralize}}, 11 | {{ counts.tags }} tag{{ counts.tags|pluralize}}, and 12 | {{ counts.favorites }} favorite{{ counts.favorites|pluralize }}. 13 |

14 | 15 |
16 |

17 | If you'd like to delete your account and all of your personal information: 18 |

19 | 20 |
21 | {{ csrf_input }} 22 | 24 |
25 |

26 | Deleting your account cannot be undone. All Lists, Favorites, and user-created Tags will be 27 | destructively deleted. 28 |

29 |
30 |
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 |
7 | 8 | 9 |

My Tags

10 | {% if object_list %} 11 |

12 | Tags you've added to images: 13 |

14 | 15 | {% for tag in object_list %} 16 |
17 |

{{ tag.name }}

18 |

19 | Created on {{ tag.created_on.strftime('%a, %d %b %Y') }}, last updated {{ tag.updated_on.strftime('%d %b %Y')}} 20 |

21 |
22 | {% for ut in tag.user_tags.all() %} 23 | {% set detail_url = url('detail', ut.image.identifier) %} 24 | {% call image_result.show(detail_url, ut.image, request) %} 25 | {% endcall %} 26 | {% endfor %} 27 |
28 |
29 | 30 | {% endfor %} 31 | {% else %} 32 |

33 | You have no tags at the moment. Search for images and tag them for later reference. 34 |

35 | {% endif %} 36 |
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 = `
14 | 15 | 16 |
` 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 = `
31 | 32 | 33 |
` 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 = `
15 | 16 | 17 |
18 |
19 | event 20 |
21 |
22 |
` 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 | --------------------------------------------------------------------------------