├── .gitignore ├── .travis.yml ├── CHANGES.txt ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── annotran.egg-info ├── PKG-INFO ├── SOURCES.txt ├── dependency_links.txt ├── entry_points.txt ├── not-zip-safe ├── requires.txt └── top_level.txt ├── annotran ├── __init__.py ├── accounts │ ├── __init__.py │ └── views.py ├── admin.py ├── api │ ├── __init__.py │ └── search │ │ ├── __init__.py │ │ ├── core.py │ │ └── query.py ├── app.py ├── assets.yaml ├── client.py ├── conftest.py ├── groups │ ├── __init__.py │ └── views.py ├── help │ ├── __init__.py │ └── views.py ├── languages │ ├── __init__.py │ ├── models.py │ ├── schemas.py │ ├── test │ │ ├── __init__.py │ │ ├── models_test.py │ │ └── views_test.py │ └── views.py ├── mailer.py ├── pages │ ├── __init__.py │ ├── models.py │ ├── test │ │ ├── __init__.py │ │ ├── models_test.py │ │ └── views_test.py │ └── views.py ├── reports │ ├── __init__.py │ ├── models.py │ ├── test │ │ ├── __init__.py │ │ ├── models_test.py │ │ └── views_test.py │ └── views.py ├── resources.py ├── session.py ├── static │ ├── images │ │ ├── home.png │ │ ├── home.xcf │ │ └── stars.png │ └── scripts │ │ ├── annotator │ │ ├── guest.coffee │ │ ├── host.coffee │ │ ├── main.js │ │ ├── plugin │ │ │ ├── cssmodify.coffee │ │ │ ├── sentenceselection.coffee │ │ │ ├── substitution.coffee │ │ │ └── toolbar.coffee │ │ └── sidebar.coffee │ │ ├── app-controller.coffee │ │ ├── apps.js │ │ ├── directive │ │ ├── annotation.js │ │ ├── language-list.js │ │ ├── language-service.js │ │ ├── publish-annotation-btn.js │ │ ├── share-dialog.coffee │ │ ├── sidebar-tutorial.js │ │ ├── signin-control.js │ │ ├── thread.coffee │ │ ├── top-bar.js │ │ └── user-list.js │ │ ├── events.js │ │ ├── groups.js │ │ ├── languages.js │ │ ├── pages.js │ │ ├── reports.js │ │ ├── session.js │ │ ├── translations.js │ │ ├── vendor │ │ ├── annotator.js │ │ ├── css │ │ │ └── annotator.css │ │ ├── google.translate.lang.list.js │ │ ├── img │ │ │ ├── annotator-glyph-sprite.png │ │ │ └── annotator-icon-sprite.png │ │ └── plugins │ │ │ └── tags.coffee │ │ ├── votes.js │ │ └── widget-controller.coffee ├── styles │ └── annotran.css ├── templates │ ├── 5xx.html.jinja2 │ ├── accounts │ │ └── profile.html.jinja2 │ ├── admin │ │ ├── index.html.jinja2 │ │ ├── reports.html.jinja2 │ │ └── translation.html.jinja2 │ ├── annotran_help.html.jinja2 │ ├── app.html.jinja2 │ ├── client │ │ ├── annotation.html │ │ ├── language_list.html │ │ ├── publish_annotation_btn.html │ │ ├── share_dialog.html │ │ ├── sidebar_tutorial.html │ │ ├── signin_control.html │ │ ├── top_bar.html │ │ └── user_list.html │ ├── community-guidelines.html.jinja2 │ ├── groups │ │ ├── about-groups.html.jinja2 │ │ └── share.html.jinja2 │ ├── home.html.jinja2 │ ├── includes │ │ └── logo-header.html.jinja2 │ ├── layouts │ │ ├── admin.html.jinja2 │ │ └── base.html.jinja2 │ ├── notfound.html.jinja2 │ ├── privacy-policy.html.jinja2 │ └── terms-of-service.html.jinja2 ├── test │ ├── __init__.py │ └── admin_test.py ├── tests.py ├── translations │ ├── __init__.py │ ├── models.py │ ├── test │ │ ├── __init__.py │ │ ├── models_test.py │ │ └── views_test.py │ └── views.py ├── util │ ├── __init__.py │ └── util.py ├── views.py └── votes │ ├── __init__.py │ ├── models.py │ ├── test │ ├── __init__.py │ ├── models_test.py │ └── views_test.py │ └── views.py ├── conf ├── development.ini ├── production.ini └── test.ini ├── docs ├── QuickStart.odt ├── QuickStartGuide.pdf └── images │ ├── Adder.png │ ├── Blanksidebar.png │ ├── Bookmarklet.png │ ├── Dropdownlanguage.png │ ├── Rateandreport.png │ ├── Selecteditmode.png │ ├── Sidebarsignedin.png │ ├── Substitution.png │ ├── Userstranslations.png │ ├── Viewmodeclicktochange.png │ └── Writetranslation.png ├── package.json ├── requirements.txt ├── scripts └── postcss-filter.js ├── setup.cfg ├── setup.py └── setup.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # IPython Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # dotenv 81 | .env 82 | 83 | # virtualenv 84 | venv/ 85 | ENV/ 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | ### JetBrains template 93 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 94 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 95 | 96 | # User-specific stuff: 97 | .idea/workspace.xml 98 | .idea/tasks.xml 99 | .idea/dictionaries 100 | .idea/vcs.xml 101 | .idea/jsLibraryMappings.xml 102 | 103 | # Sensitive or high-churn files: 104 | .idea/dataSources.ids 105 | .idea/dataSources.xml 106 | .idea/dataSources.local.xml 107 | .idea/sqlDataSources.xml 108 | .idea/dynamic.xml 109 | .idea/uiDesigner.xml 110 | 111 | # Gradle: 112 | .idea/gradle.xml 113 | .idea/libraries 114 | 115 | # Mongo Explorer plugin: 116 | .idea/mongoSettings.xml 117 | 118 | ## File-based project format: 119 | *.iws 120 | 121 | ## Plugin-specific files: 122 | 123 | # IntelliJ 124 | /out/ 125 | 126 | # mpeltonen/sbt-idea plugin 127 | .idea_modules/ 128 | 129 | # JIRA plugin 130 | atlassian-ide-plugin.xml 131 | 132 | # Crashlytics plugin (for Android Studio and IntelliJ) 133 | com_crashlytics_export_strings.xml 134 | crashlytics.properties 135 | crashlytics-build.properties 136 | fabric.properties 137 | ### VirtualEnv template 138 | # Virtualenv 139 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 140 | .Python 141 | [Bb]in 142 | [Ii]nclude 143 | [Ll]ib 144 | [Ll]ib64 145 | [Ll]ocal 146 | pyvenv.cfg 147 | .venv 148 | pip-selfcheck.json 149 | 150 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # use Travis container build infrastructure 2 | 3 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | 0.0 2 | --- 3 | 4 | - Initial version 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Birkbeck College, University of London 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include annotran *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml 2 | include annotran/assets.yaml 3 | include Makefile AUTHORS CHANGES LICENSE NOTICE 4 | include Dockerfile 5 | include gunicorn.conf.py 6 | include package.json 7 | include requirements.txt 8 | graft conf 9 | graft docs 10 | prune docs/_build 11 | graft annotran/migrations 12 | graft annotran/static 13 | prune annotran/static/.webassets-cache 14 | graft annotran/templates 15 | graft scripts 16 | global-exclude __pycache__ 17 | global-exclude *.py[co] 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | default: deps 3 | 4 | deps: annotran.egg-info/.uptodate node_modules/.uptodate 5 | 6 | annotran.egg-info/.uptodate: setup.py requirements.txt 7 | pip install --use-wheel -e .[dev,testing,YAML] 8 | touch $@ 9 | 10 | node_modules/.uptodate: package.json 11 | npm install 12 | touch $@ 13 | 14 | clean: 15 | find . -type f -name "*.py[co]" -delete 16 | find . -type d -name "__pycache__" -delete 17 | rm -rf annotran/static/webassets-external 18 | rm -f annotran/static/scripts/vendor/*.min.js 19 | rm -f annotran/static/scripts/account.*js 20 | rm -f annotran/static/scripts/app.*js 21 | rm -f annotran/static/scripts/config.*js 22 | rm -f annotran/static/scripts/hypothesis.*js 23 | rm -f annotran/static/styles/*.css 24 | rm -f .coverage 25 | rm -f node_modules/.uptodate .eggs/.uptodate 26 | 27 | dev: deps 28 | @gunicorn --reload --paste conf/development.ini 29 | 30 | test: backend-test 31 | 32 | #backend-test: deps 33 | # @python setup.py test 34 | 35 | backend-test: 36 | py.test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Code Health](https://landscape.io/github/birkbeckOLH/annotran/master/landscape.svg?style=flat)](https://landscape.io/github/birkbeckOLH/annotran/master) [![Latest Version](https://img.shields.io/badge/python-2.7-blue.svg)]() [![License](http://img.shields.io/:license-mit-blue.svg)](https://github.com/birkbeckOLH/annotran/blob/master/LICENSE) 2 | 3 | #annotran 4 | 5 | ## About 6 | 7 | Annotran is an extension to the hypothesis annotation framework that allows users to translate web pages themselves and to view other users' translations. It is developed at the [Open Library of Humanities](https://about.openlibhums.org) and was funded by the Andrew W. Mellon Foundation. A [public alpha version is now available](https://annotran.openlibhums.org) to use (but with no guarantee that the server will always be up or that translations will not be deleted). 8 | 9 | ## Quirks 10 | 11 | Annotran does not work well on extremely dynamic pages. If the web page that you are translating changes substantially, then it is likely that your translation will break. 12 | 13 | ## Development 14 | 15 | This project is built as an extension to the hypothesis annotation framework: https://hypothes.is/. If you would like to join us in development efforts, or wish to run your own server, you can set up your development environment in the following way. Please note these instructions for Ubuntu 14.04. 16 | 17 | Download the source code: 18 | ``` 19 | git clone https://github.com/birkbeckOLH/annotran.git 20 | ``` 21 | Download the hypothes.is version 0.8.14: 22 | ``` 23 | git clone https://github.com/hypothesis/h.git 24 | cd h 25 | git reset --hard v0.8.14 26 | ``` 27 | Install `h` by refering to its documentation on how to install it. Search for this document in project files: ../h/docs/hacking/install.rst. In addition to the Docker containers there, you will also need nsqd. The full set of container installs are: 28 | 29 | ``` 30 | docker run -d --name postgres -p 5432:5432 postgres 31 | docker run -d --name elasticsearch -p 9200:9200 -p 9300:9300 nickstenning/elasticsearch-icu 32 | docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 --hostname rabbit rabbitmq:3-management 33 | docker run -d --name redis -p 6379:6379 redis 34 | docker run -d --name nsqd -p 4150:4150 -p 4151:4151 nsqio/nsq /nsqd 35 | ``` 36 | 37 | If you want to be able to easily monitor the emails that annotran/hypothes.is sends in your development environment, then you may wish to use the following command: 38 | 39 | ``` 40 | docker exec nsqd nsq_tail --topic email --nsqd-tcp-address localhost:4150 41 | ``` 42 | 43 | This can be important as account sign-up confirmation links are sent by email. 44 | 45 | Create a Python virtual environment. Refer to the documentation on how to create virtual environments: http://docs.python-guide.org/en/latest/dev/virtualenvs/ 46 | 47 | Run the following commands to install hypothes.is into your virtual environment: 48 | ``` 49 | cd .. 50 | cd annotran 51 | sudo apt-get install -y --no-install-recommends ruby-compass build-essential git libevent-dev libffi-dev libfontconfig libpq-dev python-dev python-pip python-virtualenv nodejs npm 52 | pip install -r requirements.txt 53 | pip install setuptools --upgrade 54 | sudo ln -s /usr/bin/nodejs /usr/bin/node 55 | make dev 56 | ``` 57 | 58 | ###Steps performed to extend and overwrite hypothes.is 59 | 60 | 1. Boot system. Since the annotran package has been installed into the same Python environment as the hypothes.is application, it is possibile to start the hypothes.is application from annotran. To do so, place a version of h's pyramid configuration inside annotran. 61 | 62 | 2. When extending Pyramid application (see documentation on how to do that: http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/extending.html), it is necessary to override views, routes and static assets (http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/assets.html#assets-chapter). To extend hypothes.is UI code, there are following steps performed: 63 | - There is assets.yaml file in annotran that is a copy of the same file from hypothes.is. Paths for assets that are overwritten in annotran are appropriately updated within this file. 64 | - Assets are overwritten by invoking config.override_asset(..) method. 65 | - Javascript is overwritten using Angular dependency injection. h/static/scripts/app.coffee is required from the main module within the annotran. 66 | 67 | 3. In app.py we replace a set of hypothesis functions using Python monkey patching. The replacement functions are in replacements.py. 68 | 69 | 4. To override Angular directives, add the directive file in static/scripts/directive and then edit apps.js to add an app decorator that selects the override directive. 70 | 71 | ###Understanding the different components and languages used by hypothes.is and overridden by annotran 72 | Hypothesis uses multiple technologies: 73 | 74 | * Pyramid for URL routing and setting handling 75 | * The Python files (.py) in the project are part of the Pyramid framework's handling 76 | * SQLAlchemy is used by the python files to read from and write to a PostgresSQL database and can be accessed by the python components 77 | * An elasticsearch instance is used to store annotations themselves and can be accessed by the python components 78 | * Annotator.js provides the core annotation functions 79 | * A set of coffeescript and javascript files (.coffee and .js) extend Annotator.js and provide the hypothes.is sidebar and plugins to the document we are annotating 80 | * Some javascript files make calls to the Pyramid framework's server-side python scripts (see above) and load database values in the client (see session.js for example) 81 | * An event bubbling framework exists within the javascript files that can be fired and extended 82 | 83 | ##How to contribute 84 | 85 | Tasks under development are available to investigate under the issues. You can also join in the discussion over there. 86 | 87 | -------------------------------------------------------------------------------- /annotran.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | CHANGES.txt 2 | LICENSE 3 | MANIFEST.in 4 | Makefile 5 | package.json 6 | production.ini 7 | requirements.txt 8 | setup.cfg 9 | setup.py 10 | setup.txt 11 | annotran/__init__.py 12 | annotran/admin.py 13 | annotran/app.py 14 | annotran/assets.yaml 15 | annotran/client.py 16 | annotran/conftest.py 17 | annotran/resources.py 18 | annotran/session.py 19 | annotran/tests.py 20 | annotran/views.py 21 | annotran.egg-info/.uptodate 22 | annotran.egg-info/PKG-INFO 23 | annotran.egg-info/SOURCES.txt 24 | annotran.egg-info/dependency_links.txt 25 | annotran.egg-info/not-zip-safe 26 | annotran.egg-info/requires.txt 27 | annotran.egg-info/top_level.txt 28 | annotran/accounts/__init__.py 29 | annotran/accounts/views.py 30 | annotran/api/__init__.py 31 | annotran/api/search/__init__.py 32 | annotran/api/search/core.py 33 | annotran/api/search/query.py 34 | annotran/groups/__init__.py 35 | annotran/groups/views.py 36 | annotran/help/__init__.py 37 | annotran/help/views.py 38 | annotran/languages/__init__.py 39 | annotran/languages/models.py 40 | annotran/languages/schemas.py 41 | annotran/languages/views.py 42 | annotran/pages/__init__.py 43 | annotran/pages/models.py 44 | annotran/pages/views.py 45 | annotran/reports/__init__.py 46 | annotran/reports/models.py 47 | annotran/reports/views.py 48 | annotran/static/images/home.png 49 | annotran/static/images/home.xcf 50 | annotran/static/images/stars.png 51 | annotran/static/scripts/app-controller.coffee 52 | annotran/static/scripts/apps.js 53 | annotran/static/scripts/events.js 54 | annotran/static/scripts/groups.js 55 | annotran/static/scripts/languages.js 56 | annotran/static/scripts/pages.js 57 | annotran/static/scripts/reports.js 58 | annotran/static/scripts/session.js 59 | annotran/static/scripts/votes.js 60 | annotran/static/scripts/widget-controller.coffee 61 | annotran/static/scripts/annotator/guest.coffee 62 | annotran/static/scripts/annotator/host.coffee 63 | annotran/static/scripts/annotator/main.js 64 | annotran/static/scripts/annotator/sidebar.coffee 65 | annotran/static/scripts/annotator/plugin/cssmodify.coffee 66 | annotran/static/scripts/annotator/plugin/sentenceselection.coffee 67 | annotran/static/scripts/annotator/plugin/substitution.coffee 68 | annotran/static/scripts/annotator/plugin/toolbar.coffee 69 | annotran/static/scripts/directive/annotation.js 70 | annotran/static/scripts/directive/language-list.js 71 | annotran/static/scripts/directive/language-service.js 72 | annotran/static/scripts/directive/share-dialog.coffee 73 | annotran/static/scripts/directive/signin-control.js 74 | annotran/static/scripts/directive/thread.coffee 75 | annotran/static/scripts/directive/top-bar.js 76 | annotran/static/scripts/directive/user-list.js 77 | annotran/static/scripts/vendor/annotator.js 78 | annotran/static/scripts/vendor/google.translate.lang.list.js 79 | annotran/static/scripts/vendor/css/annotator.css 80 | annotran/static/scripts/vendor/img/annotator-glyph-sprite.png 81 | annotran/static/scripts/vendor/img/annotator-icon-sprite.png 82 | annotran/static/scripts/vendor/plugins/tags.coffee 83 | annotran/styles/annotran.css 84 | annotran/templates/5xx.html.jinja2 85 | annotran/templates/annotran_help.html.jinja2 86 | annotran/templates/app.html.jinja2 87 | annotran/templates/home.html.jinja2 88 | annotran/templates/notfound.html.jinja2 89 | annotran/templates/accounts/profile.html.jinja2 90 | annotran/templates/admin/index.html.jinja2 91 | annotran/templates/admin/reports.html.jinja2 92 | annotran/templates/admin/translation.html.jinja2 93 | annotran/templates/client/annotation.html 94 | annotran/templates/client/language_list.html 95 | annotran/templates/client/share_dialog.html 96 | annotran/templates/client/sidebar_tutorial.html 97 | annotran/templates/client/signin_control.html 98 | annotran/templates/client/top_bar.html 99 | annotran/templates/client/user_list.html 100 | annotran/templates/groups/about-groups.html.jinja2 101 | annotran/templates/groups/share.html.jinja2 102 | annotran/templates/includes/logo-header.html.jinja2 103 | annotran/templates/layouts/admin.html.jinja2 104 | annotran/util/__init__.py 105 | annotran/util/util.py 106 | annotran/votes/__init__.py 107 | annotran/votes/models.py 108 | annotran/votes/views.py 109 | conf/development.ini 110 | conf/test.ini 111 | scripts/postcss-filter.js -------------------------------------------------------------------------------- /annotran.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /annotran.egg-info/entry_points.txt: -------------------------------------------------------------------------------- 1 | [paste.app_factory] 2 | main = annotran.app:main 3 | [console_scripts] 4 | initialize_annotran_db = annotran.scripts.initializedb:main 5 | -------------------------------------------------------------------------------- /annotran.egg-info/not-zip-safe: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /annotran.egg-info/requires.txt: -------------------------------------------------------------------------------- 1 | PyJWT>=1.0.0,<2.0.0 2 | SQLAlchemy>=0.8.0 3 | alembic>=0.7.0 4 | annotator>=0.14.2,<0.15 5 | blinker>=1.3,<1.4 6 | cryptacular>=1.4,<1.5 7 | cryptography>=0.7 8 | deform>=0.9,<1.0 9 | deform-jinja2>=0.5,<0.6 10 | elasticsearch>=1.1.0,<2.0.0 11 | gevent>=1.0.2,<1.1.0 12 | gnsq>=0.3.0,<0.4.0 13 | gunicorn>=19.2,<20 14 | itsdangerous>=0.24 15 | jsonpointer==1.0 16 | jsonschema>=2.5.1,<2.6 17 | pyramid>=1.5,<1.6 18 | psycogreen>=1.0 19 | psycopg2>=2.6.1 20 | pyramid_mailer>=0.13 21 | pyramid_tm>=0.7 22 | python-dateutil>=2.1 23 | python-slugify>=1.1.3,<1.2.0 24 | python-statsd>=1.7.0,<1.8.0 25 | webassets>=0.10,<0.11 26 | pyramid_webassets>=0.9,<1.0 27 | pyramid-jinja2>=2.3.3 28 | raven>=5.10.2,<5.11.0 29 | requests>=2.7.0 30 | unicodecsv>=0.14.1,<0.15 31 | ws4py>=0.3,<0.4 32 | zope.sqlalchemy>=0.7.6,<0.8.0 33 | repoze.sendmail<4.2 34 | 35 | [YAML] 36 | PyYAML 37 | 38 | [testing] 39 | mock>=1.3.0 40 | pytest>=2.5 41 | pytest-cov 42 | factory-boy 43 | 44 | [dev] 45 | pyramid_debugtoolbar>=2.1 46 | prospector[with_pyroma] 47 | pep257 48 | sphinxcontrib-httpdomain 49 | PyYAML -------------------------------------------------------------------------------- /annotran.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | annotran 2 | -------------------------------------------------------------------------------- /annotran/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def includeme(config): 4 | config.include('.views') 5 | -------------------------------------------------------------------------------- /annotran/accounts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/annotran/accounts/__init__.py -------------------------------------------------------------------------------- /annotran/accounts/views.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2013-2014 Hypothes.is Project and contributors 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | """ 24 | 25 | import annotran 26 | import annotran.views 27 | import deform 28 | 29 | from h import i18n 30 | from pyramid.view import view_config 31 | from h.accounts import schemas 32 | 33 | _ = i18n.TranslationString 34 | 35 | 36 | def register_controller_init_patch(self, request): 37 | tos_link = ('' + 38 | _('Terms of Service') + 39 | '') 40 | cg_link = ('' + 41 | _('Community Guidelines') + 42 | '') 43 | privacy_link = ('' + 44 | _('Privacy Policy') + 45 | '') 46 | form_footer = _( 47 | 'You are agreeing to be bound by our {tos_link}, ' 48 | '{cg_link} and {privacy_link}.').format(tos_link=tos_link, cg_link=cg_link, privacy_link=privacy_link) 49 | 50 | self.request = request 51 | self.schema = schemas.RegisterSchema().bind(request=self.request) 52 | self.form = deform.Form(self.schema, 53 | buttons=(_('Sign up'),), 54 | footer=form_footer) 55 | 56 | 57 | class ProfileController(object): 58 | def __init__(self): 59 | pass 60 | 61 | @staticmethod 62 | @view_config(request_method='GET') 63 | def profile_get(controller_instance): 64 | """ 65 | Shows the user's profile 66 | :param controller_instance: an instance of the ProfileController 67 | :return: a context dictionary for the profile template 68 | """ 69 | return {'email': controller_instance.request.authenticated_user.email, 70 | 'email_form': controller_instance.forms['email'].render(), 71 | 'password_form': controller_instance.forms['password'].render(), 72 | 'support_address': annotran.views.Shared.support_address} 73 | 74 | 75 | def auth_controller_init_patch(self, request): 76 | """ 77 | Replace the constructor of the h's h.account.views AuthController class - in order to skip the stream loading that 78 | is not used in the annotran 79 | :param request: the current request 80 | :return: None 81 | """ 82 | form_footer = '{text}'.format( 83 | href=request.route_path('forgot_password'), 84 | text=_('Forgot your password?')) 85 | self.request = request 86 | self.schema = schemas.LoginSchema().bind(request=self.request) 87 | self.form = deform.Form(self.schema, 88 | buttons=(_('Sign in'),), 89 | footer=form_footer) 90 | self.login_redirect = self.request.route_url('index') 91 | self.logout_redirect = self.request.route_url('index') 92 | -------------------------------------------------------------------------------- /annotran/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/annotran/api/__init__.py -------------------------------------------------------------------------------- /annotran/api/search/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/annotran/api/search/__init__.py -------------------------------------------------------------------------------- /annotran/api/search/core.py: -------------------------------------------------------------------------------- 1 | from h.api.search import query 2 | from h.api import nipsa 3 | 4 | 5 | def delete(request, params, private=True): 6 | """ 7 | Delete with the given params and return the matching annotations. 8 | 9 | :param request: the request object 10 | :type request: pyramid.request.Request 11 | 12 | :param params: the search parameters 13 | :type params: dict-like 14 | 15 | :param private: whether or not to include private annotations in the search 16 | results 17 | :type private: bool 18 | 19 | :returns: A dict with keys: 20 | "rows" (the list of matching annotations, as dicts) 21 | "total" (the number of matching annotations, an int) 22 | "replies": (the list of all replies to the annotations in "rows", if 23 | separate_replies=True was passed) 24 | :rtype: dict 25 | """ 26 | def make_builder(): 27 | builder_output = query.Builder() 28 | builder_output.append_filter(query.AuthFilter(request, private=private)) 29 | builder_output.append_filter(query.UriFilter()) 30 | builder_output.append_filter(lambda _: nipsa.nipsa_filter(request.authenticated_userid)) 31 | builder_output.append_filter(query.GroupFilter()) 32 | builder_output.append_matcher(query.AnyMatcher()) 33 | builder_output.append_matcher(query.TagsMatcher()) 34 | 35 | return builder_output 36 | 37 | builder = make_builder() 38 | 39 | es = request.es 40 | 41 | # N.B.: this function _does_ take a sort argument because hypothesis's version of the function is monkey-patched 42 | # to point to the version in query.py while still retaining the rest of the class 43 | es.conn.delete_by_query(index=es.index, doc_type=es.t.annotation, body=builder.build(params, sort=False)) 44 | -------------------------------------------------------------------------------- /annotran/api/search/query.py: -------------------------------------------------------------------------------- 1 | from h.api.search import query as hq 2 | 3 | 4 | def build(self, params, sort=True): 5 | """ 6 | Get the resulting query object from this query builder. 7 | :param self: an instance of a query object 8 | :param params: a set of parameters to pass to a query 9 | :param sort: whether or not to include sort and other keys that pertain only to a search query 10 | :return: 11 | """ 12 | params = params.copy() 13 | 14 | p_from = hq.extract_offset(params) 15 | p_size = hq.extract_limit(params) 16 | p_sort = hq.extract_sort(params) 17 | 18 | filters = [f(params) for f in self.filters] 19 | matchers = [m(params) for m in self.matchers] 20 | filters = [f for f in filters if f is not None] 21 | matchers = [m for m in matchers if m is not None] 22 | 23 | # Remaining parameters are added as straightforward key-value matchers 24 | for key, value in params.items(): 25 | matchers.append({"match": {key: value}}) 26 | 27 | query = {"match_all": {}} 28 | 29 | if matchers: 30 | query = {"bool": {"must": matchers}} 31 | 32 | if filters: 33 | query = { 34 | "filtered": { 35 | "filter": {"and": filters}, 36 | "query": query, 37 | } 38 | } 39 | 40 | ret = { 41 | "query": query, 42 | } 43 | 44 | if sort: 45 | ret["from"] = p_from 46 | ret["sort"] = p_sort 47 | ret["size"] = p_size 48 | 49 | return ret 50 | -------------------------------------------------------------------------------- /annotran/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2013-2014 Hypothes.is Project and contributors 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | """ 24 | 25 | # -*- coding: utf-8 -*- 26 | 27 | import os 28 | 29 | import pytest 30 | from h import conftest 31 | from h.config import normalize_database_url 32 | from pyramid.paster import get_appsettings 33 | 34 | 35 | class DummyFeature(object): 36 | """ 37 | A dummy feature flag looker-upper. 38 | 39 | Because we're probably testing all feature-flagged functionality, this 40 | feature client defaults every flag to *True*, which is the exact opposite 41 | of what happens outside of testing. 42 | """ 43 | 44 | def __init__(self): 45 | self.flags = {} 46 | 47 | 48 | class DummySession(object): 49 | """ 50 | A dummy database session. 51 | """ 52 | 53 | def __init__(self): 54 | self.added = [] 55 | self.deleted = [] 56 | self.flushed = False 57 | 58 | 59 | @pytest.fixture(scope='session', autouse=True) 60 | def settings(): 61 | """ 62 | Default app settings (conf/test.ini). 63 | :return: a list of loaded settings 64 | """ 65 | loaded_settings = get_appsettings('conf/test.ini') 66 | 67 | if 'TEST_DATABASE_URL' in os.environ: 68 | loaded_settings['sqlalchemy.url'] = normalize_database_url(os.environ['TEST_DATABASE_URL']) 69 | 70 | return loaded_settings 71 | 72 | 73 | DummySession.settings = settings 74 | 75 | setup_database = conftest.setup_database 76 | database_session = conftest.database_session 77 | config = conftest.config 78 | deform = conftest.deform 79 | authn_policy = conftest.authn_policy 80 | mailer = conftest.mailer 81 | notify = conftest.notify 82 | routes_mapper = conftest.routes_mapper 83 | _make_session = conftest._make_session 84 | -------------------------------------------------------------------------------- /annotran/groups/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/annotran/groups/__init__.py -------------------------------------------------------------------------------- /annotran/groups/views.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2013-2014 Hypothes.is Project and contributors 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | """ 24 | # monkey patching of hypothesis methods 25 | 26 | # annotran's version of h.groups.views._read_group 27 | import collections 28 | 29 | import h 30 | from h import presenters 31 | from h.api import search 32 | from h.api import uri 33 | from pyramid import renderers 34 | import h.models 35 | 36 | 37 | def read_group(request, group, language=None, search_url=None, user=None, render=True): 38 | """ 39 | Return the rendered "Share this group" page. 40 | 41 | This is the page that's shown when a user who is already a member of a group visits the group's URL. 42 | :param request: a request object 43 | :param group: a group object 44 | :param language: a language object to search or None 45 | :param search_url: the URL to search or None 46 | :param user: the user object to search or None 47 | :param render: whether or not to return a context object 48 | :return: if render, a context object for a template. If render is false, a list of annotations 49 | """ 50 | 51 | if group is None: 52 | public_group_id = "__world__" 53 | slug = "Public" 54 | else: 55 | public_group_id = group.pubid 56 | slug = group.slug 57 | 58 | url = request.route_url('group_read', pubid=public_group_id, slug=slug) 59 | 60 | parameters = {"group": public_group_id, "limit": 1000} 61 | 62 | if language: 63 | parameters['language'] = language.pubid 64 | 65 | if search_url: 66 | parameters['uri'] = search_url 67 | 68 | if user: 69 | parameters['user'] = user 70 | 71 | result = search.search(request, 72 | private=False, 73 | params=parameters) 74 | 75 | annotations = [presenters.AnnotationHTMLPresenter(h.models.Annotation(a)) 76 | for a in result['rows']] 77 | 78 | if render: 79 | # Group the annotations by URI. 80 | # Create a dict mapping the (normalized) URIs of the annotated documents 81 | # to the most recent annotation of each document. 82 | annotations_by_uri = collections.OrderedDict() 83 | for annotation in annotations: 84 | normalized_uri = uri.normalize(annotation.uri) 85 | if normalized_uri not in annotations_by_uri: 86 | annotations_by_uri[normalized_uri] = annotation 87 | if len(annotations_by_uri) >= 25: 88 | break 89 | 90 | document_links = [annotation.document_link 91 | for annotation in annotations_by_uri.values()] 92 | 93 | template_data = { 94 | 'group': group, 'group_url': url, 'document_links': document_links} 95 | 96 | return renderers.render_to_response( 97 | renderer_name='h:templates/groups/share.html.jinja2', 98 | value=template_data, request=request) 99 | else: 100 | return annotations 101 | -------------------------------------------------------------------------------- /annotran/help/__init__.py: -------------------------------------------------------------------------------- 1 | def includeme(config): 2 | config.include('.views') 3 | -------------------------------------------------------------------------------- /annotran/help/views.py: -------------------------------------------------------------------------------- 1 | from pyramid.view import view_config 2 | 3 | 4 | @view_config(renderer='annotran:templates/annotran_help.html.jinja2', route_name='annotran_help') 5 | @view_config(renderer='annotran:templates/annotran_help.html.jinja2', route_name='annotran_onboarding') 6 | def help_page(request): 7 | """ 8 | Render the help page for annotran 9 | :param request: the current request 10 | :return: a context for the template 11 | """ 12 | current_route = request.matched_route.name 13 | return { 14 | 'embed_js_url': request.route_path('embed'), 15 | 'is_help': current_route == 'annotran_help', 16 | 'is_onboarding': current_route == 'annotran_onboarding', 17 | } 18 | 19 | 20 | def includeme(config): 21 | """ 22 | Pyramid's includeme setup 23 | :param config: the configuration to which to add routes 24 | :return: None 25 | """ 26 | config.add_route('annotran_help', '/docs/annotran_help') 27 | config.add_route('annotran_onboarding', '/annotran_welcome') 28 | 29 | config.scan(__name__) 30 | -------------------------------------------------------------------------------- /annotran/languages/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def includeme(config): 5 | config.include('.views') 6 | -------------------------------------------------------------------------------- /annotran/languages/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Copyright (c) 2013-2014 Hypothes.is Project and contributors 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 21 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | """ 25 | 26 | # -*- coding: utf-8 -*- 27 | 28 | import datetime 29 | import h.pubid 30 | import sqlalchemy as sa 31 | from h.db import Base 32 | from sqlalchemy.orm import exc 33 | 34 | LANGUAGE_NAME_MIN_LENGTH = 4 35 | LANGUAGE_NAME_MAX_LENGTH = 25 36 | 37 | 38 | class Language(Base): 39 | """ 40 | Represents a language in the database 41 | """ 42 | __tablename__ = 'language' 43 | 44 | id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) 45 | # We don't expose the integer PK to the world, so we generate a short 46 | # random string to use as the publicly visible ID. 47 | pubid = sa.Column(sa.Text(), 48 | default=h.pubid.generate, 49 | unique=True, 50 | nullable=False) 51 | name = sa.Column(sa.UnicodeText(), nullable=False, unique=True) 52 | created = sa.Column(sa.DateTime, 53 | default=datetime.datetime.utcnow, 54 | server_default=sa.func.now(), 55 | nullable=False) 56 | 57 | def __init__(self, name): 58 | """ 59 | Initialize a language 60 | :param name: the name of the language 61 | """ 62 | self.name = name 63 | 64 | @sa.orm.validates('name') 65 | def validate_name(self, key, name): 66 | """ 67 | Validate that the language name is of an acceptable length 68 | :param key: the uniquely identifying key (not used) 69 | :param name: the name of the language 70 | :return: either the name or a ValueError exception 71 | """ 72 | if not LANGUAGE_NAME_MIN_LENGTH <= len(name) <= LANGUAGE_NAME_MAX_LENGTH: 73 | raise ValueError('name must be between {min} and {max} characters ' 74 | 'long'.format(min=LANGUAGE_NAME_MIN_LENGTH, 75 | max=LANGUAGE_NAME_MAX_LENGTH)) 76 | return name 77 | 78 | @classmethod 79 | def get_by_public_language_id(cls, public_language_id): 80 | """ 81 | Get a language with the given public language ID 82 | :param public_language_id: the public language ID 83 | :return: either a language or None 84 | """ 85 | if public_language_id: 86 | return cls.query.filter(cls.pubid == public_language_id).first() 87 | else: 88 | return None 89 | 90 | @classmethod 91 | def get_by_id(cls, id_): 92 | """ 93 | Get a language by its ID 94 | :param id_: the ID of the language 95 | :return: a language or None 96 | """ 97 | try: 98 | return cls.query.filter( 99 | cls.id == id_).one() 100 | except exc.NoResultFound: 101 | return None 102 | 103 | @classmethod 104 | def get_by_name(cls, name): 105 | """ 106 | Get a language by name 107 | :param name: the name of the language 108 | :return: a language or None 109 | """ 110 | try: 111 | return cls.query.filter( 112 | cls.name == name).one() 113 | except exc.NoResultFound: 114 | return None 115 | 116 | def __repr__(self): 117 | """ 118 | The internal representation of the language 119 | :return: the name of the language 120 | """ 121 | return '{0}'.format(self.name) 122 | -------------------------------------------------------------------------------- /annotran/languages/schemas.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Copyright (c) 2013-2014 Hypothes.is Project and contributors 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 21 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | """ 25 | 26 | # -*- coding: utf-8 -*- 27 | 28 | import colander 29 | import deform 30 | import deform.widget 31 | from annotran.languages.models import LANGUAGE_NAME_MAX_LENGTH 32 | from annotran.languages.models import LANGUAGE_NAME_MIN_LENGTH 33 | from h import i18n 34 | from h.accounts.schemas import CSRFSchema 35 | 36 | _ = i18n.TranslationString 37 | 38 | 39 | class LanguageSchema(CSRFSchema): 40 | """The schema for the create-a-new-language form.""" 41 | 42 | name = colander.SchemaNode( 43 | colander.String(), 44 | title=_("What do you want to call the language?"), 45 | validator=colander.Length( 46 | min=LANGUAGE_NAME_MIN_LENGTH, 47 | max=LANGUAGE_NAME_MAX_LENGTH), 48 | widget=deform.widget.TextInputWidget( 49 | autofocus=True, 50 | css_class="language-form__name-input js-language-name-input", 51 | disable_autocomplete=True, 52 | label_css_class="language-form__name-label", 53 | max_length=LANGUAGE_NAME_MAX_LENGTH, 54 | placeholder=_("Language Name"))) 55 | -------------------------------------------------------------------------------- /annotran/languages/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/annotran/languages/test/__init__.py -------------------------------------------------------------------------------- /annotran/languages/test/models_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | from h import db 5 | from annotran.languages import models as lang_models 6 | 7 | 8 | def test_init(): 9 | name = "abc_language" 10 | l = lang_models.Language(name=name) 11 | db.Session.add(l) 12 | db.Session.flush() 13 | assert l.id 14 | assert l.name == name 15 | 16 | 17 | def test_with_short_name(): 18 | """Should raise ValueError if name shorter than 4 characters.""" 19 | with pytest.raises(ValueError): 20 | lang_models.Language(name="abc") 21 | 22 | 23 | def test_with_long_name(): 24 | """Should raise ValueError if name longer than 25 characters.""" 25 | with pytest.raises(ValueError): 26 | lang_models.Language(name="abcdefghijklmnopqrstuvwxyz") 27 | 28 | 29 | def test_repr(): 30 | name = "abc_language" 31 | 32 | l = lang_models.Language(name=name) 33 | db.Session.add(l) 34 | db.Session.flush() 35 | 36 | assert repr(l) == name 37 | 38 | def test_get_by_id_when_id_does_exist(): 39 | l = lang_models.Language(name="abc_language") 40 | 41 | db.Session.add(l) 42 | db.Session.flush() 43 | 44 | assert lang_models.Language.get_by_id(l.id) == l 45 | 46 | 47 | def test_get_by_id_when_id_does_not_exist(): 48 | l = lang_models.Language(name="abc_language") 49 | 50 | db.Session.add(l) 51 | db.Session.flush() 52 | 53 | assert lang_models.Language.get_by_id(23) is None 54 | 55 | def test_get_by_name(): 56 | name="abc_language" 57 | 58 | l = lang_models.Language(name=name) 59 | db.Session.add(l) 60 | db.Session.flush() 61 | 62 | assert lang_models.Language.get_by_name(name) == l 63 | 64 | def test_get_by_public_language_id(): 65 | l = lang_models.Language(name="abc_language") 66 | 67 | db.Session.add(l) 68 | db.Session.flush() 69 | 70 | assert lang_models.Language.get_by_public_language_id(l.pubid) == l 71 | -------------------------------------------------------------------------------- /annotran/languages/test/views_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import mock 3 | import pytest 4 | from pyramid import httpexceptions 5 | 6 | from annotran.languages import views 7 | 8 | _SENTINEL = object() 9 | 10 | 11 | def _mock_request(feature=None, settings=None, params=None, 12 | authenticated_userid=_SENTINEL, route_url=None, **kwargs): 13 | """Return a mock Pyramid request object.""" 14 | params = params or {"foo": "bar"} 15 | if authenticated_userid is _SENTINEL: 16 | authenticated_userid = "acct:fred@annotran.com" 17 | return mock.Mock( 18 | feature=feature or (lambda feature: True), 19 | registry=mock.Mock(settings=settings or {}), 20 | params=params, POST=params, 21 | authenticated_userid=authenticated_userid, 22 | route_url=route_url or mock.Mock(return_value="test-read-url"), 23 | **kwargs) 24 | 25 | 26 | def _matchdict(): 27 | """Return a matchdict like the one the translation_read route receives.""" 28 | return {"pubid": mock.sentinel.pubid, "slug": mock.sentinel.slug} 29 | 30 | 31 | # The fixtures required to mock create()'s dependencies when a language does not exist in a db. 32 | create_fixtures = pytest.mark.usefixtures('session_model') 33 | 34 | @create_fixtures 35 | def test_create_adds_language_to_db(): 36 | """ 37 | This should add the new language to the database session, whih is added only if it does not exit in a db. 38 | After successfully creating a new language it should redirect 39 | """ 40 | request = _mock_request(matchdict={'public_group_id': '12345', 'language': 'zrrtgy'}) 41 | result = views.add_language(request) 42 | request.db.add.assert_called_once() 43 | assert isinstance(result, httpexceptions.HTTPRedirection) 44 | 45 | 46 | # The fixtures required to mock create()'s dependencies for an existing language. 47 | create_fixtures = pytest.mark.usefixtures('LanguageSchema', 'Language', 48 | 'session_model') 49 | @create_fixtures 50 | def test_create_redirects_to_translation_read_page(Language): 51 | """ 52 | After successfully fetching a mock Language object it 53 | should not add that one into db but it should redirect. 54 | """ 55 | language = mock.Mock(public_group_id='12345', language='language') 56 | Language.return_value = language 57 | request = _mock_request(matchdict={'public_group_id': '12345', 'language': 'test_language'}) 58 | result = views.add_language(request) 59 | assert not request.db.add.called 60 | assert isinstance(result, httpexceptions.HTTPRedirection) 61 | 62 | @create_fixtures 63 | def test_create_with_non_ascii_name(): 64 | request = _mock_request(matchdict={'public_group_id': 'abc', 'language': u"☆ ßüper Gröup ☆"}) 65 | views.add_language(request) 66 | 67 | 68 | @pytest.fixture 69 | def LanguageSchema(request): 70 | patcher = mock.patch('annotran.languages.schemas.LanguageSchema', autospec=True) 71 | request.addfinalizer(patcher.stop) 72 | return patcher.start() 73 | 74 | @pytest.fixture 75 | def Language(request): 76 | patcher = mock.patch('annotran.languages.models.Language', autospec=True) 77 | request.addfinalizer(patcher.stop) 78 | return patcher.start() 79 | 80 | @pytest.fixture 81 | def session_model(request): 82 | patcher = mock.patch('annotran.session.model') 83 | request.addfinalizer(patcher.stop) 84 | return patcher.start() 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /annotran/languages/views.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Copyright (c) 2013-2014 Hypothes.is Project and contributors 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 21 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | """ 25 | 26 | # -*- coding: utf-8 -*- 27 | from pyramid import httpexceptions as exc 28 | from pyramid.view import view_config 29 | from h import i18n 30 | 31 | import models 32 | 33 | _ = i18n.TranslationString 34 | 35 | 36 | @view_config(route_name='language_add', 37 | request_method='POST') 38 | def add_language(request): 39 | """ 40 | This view adds a language 41 | :param request: a request object 42 | :return: None 43 | """ 44 | if request.authenticated_userid is None: 45 | raise exc.HTTPNotFound() 46 | 47 | name = request.matchdict["language"] 48 | public_group_id = request.matchdict["public_group_id"] 49 | 50 | language = models.Language.get_by_name(name) 51 | 52 | if not language: 53 | language = models.Language(name=name) 54 | request.db.add(language) 55 | # We need to flush the db session here so that language.id will be generated. 56 | request.db.flush() 57 | url = request.route_url('translation_read', public_language_id=language.pubid, public_group_id=public_group_id) 58 | return exc.HTTPSeeOther(url) 59 | 60 | def includeme(config): 61 | """ 62 | Pyramid's router configuration 63 | :param config: the config to which to commit the routes 64 | :return: None 65 | """ 66 | config.add_route('language_add', 'languages/{language}/{public_group_id}/addLanguage') 67 | config.scan(__name__) 68 | -------------------------------------------------------------------------------- /annotran/mailer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """A module for sending emails asynchronously. 3 | 4 | Most code that sends emails should do so through this module. 5 | 6 | User code should call the send() function to cause an email to be sent 7 | asynchronously. 8 | 9 | """ 10 | 11 | import pyramid_mailer 12 | import pyramid_mailer.message 13 | 14 | 15 | def send(request, recipients, subject, body): 16 | """Cause an email to be sent asynchronously. 17 | 18 | :param request: the request object for the request that wants to send the 19 | email 20 | :type request: pyramid.request.Request 21 | 22 | :param recipients: the list of email addresses to send the email to 23 | :type recipients: list of unicode strings 24 | 25 | :param subject: the subject of the email 26 | :type subject: unicode 27 | 28 | :param body: the body of the email 29 | :type body: unicode 30 | 31 | """ 32 | 33 | email = pyramid_mailer.message.Message(subject=subject, recipients=recipients, body=body) 34 | 35 | pyramid_mailer.get_mailer(request).send_immediately(email) 36 | -------------------------------------------------------------------------------- /annotran/pages/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def includeme(config): 5 | config.include('.views') 6 | -------------------------------------------------------------------------------- /annotran/pages/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2013-2014 Hypothes.is Project and contributors 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | """ 24 | 25 | # -*- coding: utf-8 -*- 26 | 27 | import datetime 28 | 29 | import sqlalchemy as sa 30 | from h.db import Base 31 | from sqlalchemy.orm import exc 32 | 33 | 34 | class Page(Base): 35 | """ 36 | Represents a page in the database 37 | """ 38 | __tablename__ = 'page' 39 | 40 | id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) 41 | uri = sa.Column(sa.Text(), 42 | unique=True, 43 | nullable=False) 44 | created = sa.Column(sa.DateTime, 45 | default=datetime.datetime.utcnow, 46 | server_default=sa.func.now(), 47 | nullable=False) 48 | 49 | def __init__(self, uri): 50 | """ 51 | Initialize a page 52 | :param uri: the URI of the page 53 | """ 54 | self.uri = uri 55 | 56 | @classmethod 57 | def get_by_uri(cls, uri): 58 | """ 59 | Get the page with the given URI 60 | :param uri: the URI to fetch 61 | :return: a page or None 62 | """ 63 | return cls.query.filter(cls.uri == uri).first() 64 | 65 | @classmethod 66 | def get_by_id(cls, id_): 67 | """ 68 | Get the page by a specified ID 69 | :param id_: the ID 70 | :return: a page or None 71 | """ 72 | try: 73 | return cls.query.filter( 74 | cls.id == id_).one() 75 | except exc.NoResultFound: 76 | return None 77 | -------------------------------------------------------------------------------- /annotran/pages/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/annotran/pages/test/__init__.py -------------------------------------------------------------------------------- /annotran/pages/test/models_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from h import db 4 | from annotran.pages import models as pages_models 5 | 6 | def test_init(): 7 | uri="http://www.annotran_test.com/" 8 | 9 | p = pages_models.Page(uri=uri) 10 | db.Session.add(p) 11 | db.Session.flush() 12 | 13 | assert p.id 14 | assert p.uri == uri 15 | 16 | def test_get_by_uri(): 17 | uri="http://www.annotran_test.com/" 18 | 19 | p = pages_models.Page(uri=uri) 20 | db.Session.add(p) 21 | db.Session.flush() 22 | 23 | assert pages_models.Page.get_by_uri(uri) == p 24 | 25 | 26 | def test_get_by_id(): 27 | uri="http://www.annotran_test.com/" 28 | 29 | p = pages_models.Page(uri=uri) 30 | db.Session.add(p) 31 | db.Session.flush() 32 | 33 | assert pages_models.Page.get_by_id(p.id) == p -------------------------------------------------------------------------------- /annotran/pages/test/views_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import mock 3 | import pytest 4 | from pyramid import httpexceptions 5 | 6 | from annotran.pages import views 7 | 8 | 9 | _SENTINEL = object() 10 | 11 | 12 | def _mock_request(feature=None, settings=None, params=None, 13 | authenticated_userid=_SENTINEL, route_url=None, **kwargs): 14 | """Return a mock Pyramid request object.""" 15 | params = params or {"foo": "bar"} 16 | if authenticated_userid is _SENTINEL: 17 | authenticated_userid = "acct:fred@annotran.com" 18 | return mock.Mock( 19 | feature=feature or (lambda feature: True), 20 | registry=mock.Mock(settings=settings or {}), 21 | params=params, POST=params, 22 | authenticated_userid=authenticated_userid, 23 | route_url=route_url or mock.Mock(return_value="test-read-url"), 24 | **kwargs) 25 | 26 | # The fixtures required to mock create()'s dependencies when a page does not exist in a db. 27 | create_fixtures = pytest.mark.usefixtures('Language', 28 | 'session_model') 29 | 30 | @create_fixtures 31 | def test_create_adds_page_to_db(Language): 32 | """ 33 | This should add the new page to the database session, which is added only if it does not exit in a db. 34 | After successfully creating a new page it should redirect. 35 | """ 36 | language = mock.Mock() 37 | Language.return_value = language 38 | 39 | request = _mock_request(matchdict={'language_name': 'test', 'public_group_id': 12345, 40 | 'page_url': 'http://www.annotran_testing.com'}) 41 | result = views.add_page(request) 42 | 43 | request.db.add.assert_called_once() 44 | assert isinstance(result, httpexceptions.HTTPRedirection) 45 | 46 | # The fixtures required to mock create()'s dependencies for an existing page. 47 | create_fixtures = pytest.mark.usefixtures('Page', 'Language', 48 | 'session_model') 49 | 50 | @create_fixtures 51 | def test_create_redirects_to_page_read_page(Page, Language): 52 | """ 53 | After successfully fetching mock Page and Language objects it 54 | should not add that page into db but it should redirect. 55 | """ 56 | language = mock.Mock() 57 | Language.return_value = language 58 | page = mock.Mock(id=1) 59 | Page.return_value = page 60 | 61 | request = _mock_request(matchdict={'language_name': 'test', 'public_group_id': 12345, 62 | 'page_url': 'http://www.annotran_testing.com'}) 63 | result = views.add_page(request) 64 | assert not request.db.add.called 65 | assert isinstance(result, httpexceptions.HTTPRedirection) 66 | 67 | @pytest.fixture 68 | def Page(request): 69 | patcher = mock.patch('annotran.pages.models.Page', autospec=True) 70 | request.addfinalizer(patcher.stop) 71 | return patcher.start() 72 | 73 | @pytest.fixture 74 | def Language(request): 75 | patcher = mock.patch('annotran.languages.models.Language', autospec=True) 76 | request.addfinalizer(patcher.stop) 77 | return patcher.start() 78 | 79 | @pytest.fixture 80 | def session_model(request): 81 | patcher = mock.patch('annotran.session.model') 82 | request.addfinalizer(patcher.stop) 83 | return patcher.start() 84 | -------------------------------------------------------------------------------- /annotran/pages/views.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2013-2014 Hypothes.is Project and contributors 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | """ 24 | 25 | # -*- coding: utf-8 -*- 26 | import urllib 27 | 28 | import annotran 29 | import annotran.languages.models 30 | import annotran.pages.models 31 | from h import i18n 32 | from pyramid import httpexceptions as exc 33 | from pyramid.view import view_config 34 | 35 | _ = i18n.TranslationString 36 | 37 | 38 | @view_config(route_name='page_add', 39 | request_method='POST') 40 | def add_page(request): 41 | """ 42 | Add a page to the database 43 | :param request: a request object 44 | :return: a redirect to the translation_read URL 45 | """ 46 | if request.authenticated_userid is None: 47 | raise exc.HTTPNotFound() 48 | 49 | name = request.matchdict["language_name"] 50 | page_id = urllib.unquote(urllib.unquote(request.matchdict["page_url"])) 51 | public_group_id = request.matchdict["public_group_id"] 52 | 53 | language = annotran.languages.models.Language.get_by_name(name) 54 | page = annotran.pages.models.Page.get_by_uri(page_id) 55 | 56 | if not page: 57 | page = annotran.pages.models.Page(uri=page_id) 58 | request.db.add(page) 59 | request.db.flush() 60 | 61 | url = request.route_url('translation_read', public_language_id=language.pubid, public_group_id=public_group_id) 62 | return exc.HTTPSeeOther(url) 63 | 64 | 65 | def includeme(config): 66 | """ 67 | Pyramid's router configuration 68 | :param config: the config object to which to append routes 69 | :return: None 70 | """ 71 | config.add_route('page_add', 'pages/{language_name}/{page_url}/{public_group_id}/addPage') 72 | config.scan(__name__) 73 | -------------------------------------------------------------------------------- /annotran/reports/__init__.py: -------------------------------------------------------------------------------- 1 | def includeme(config): 2 | config.include('.views') 3 | -------------------------------------------------------------------------------- /annotran/reports/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import annotran.languages.models 5 | import annotran.pages.models 6 | import annotran.translations.models 7 | import h.accounts.models 8 | import h.groups.models 9 | import sqlalchemy as sa 10 | from h.db import Base 11 | from sqlalchemy.orm import exc 12 | from sqlalchemy import ForeignKeyConstraint 13 | 14 | 15 | class Report(Base): 16 | """ 17 | Represents an abuse report in the database 18 | """ 19 | __tablename__ = 'report' 20 | 21 | id = sa.Column(sa.Integer, autoincrement=True, primary_key=True) 22 | 23 | 24 | page_id = sa.Column(sa.Integer) 25 | language_id = sa.Column(sa.Integer) 26 | group_id = sa.Column(sa.Integer) 27 | 28 | sa.ForeignKeyConstraint([page_id, language_id, group_id], 29 | [annotran.translations.models.Translation.page_id, 30 | annotran.translations.models.Translation.language_id, 31 | annotran.translations.models.Translation.group_id]) 32 | 33 | author_id = sa.Column(sa.Integer, sa.ForeignKey(h.accounts.models.User.id)) 34 | author = sa.orm.relationship('User', backref='author_report', foreign_keys=[author_id]) 35 | 36 | reporter_id = sa.Column(sa.Integer, sa.ForeignKey(h.accounts.models.User.id)) 37 | Reporter = sa.orm.relationship('User', backref='reporter_report', foreign_keys=[reporter_id]) 38 | 39 | def __init__(self, translation, author, reporter): 40 | """ 41 | Initialize a report 42 | :param translation: the translation (page, group, language) to which the report corresponds 43 | :param author: the translation author to which the report corresponds 44 | :param reporter: the reporting user to which the report corresponds 45 | """ 46 | if translation and author and reporter: 47 | self.page_id = translation.page_id 48 | self.language_id = translation.language_id 49 | self.group_id = translation.group_id 50 | self.author_id = author.id 51 | self.reporter_id = reporter.id 52 | 53 | @classmethod 54 | def get_report(cls, translation, author, reporter): 55 | """ 56 | Get a report by page, language, group, author, and reporter 57 | :param translation: the translation (page, group, language) to which the report corresponds 58 | :param author: the author to query 59 | :param reporter: the reporting user to query 60 | :return: 61 | """ 62 | if translation and author and reporter: 63 | try: 64 | return cls.query.filter( 65 | cls.page_id == translation.page_id, 66 | cls.language_id == translation.language_id, 67 | cls.group_id == translation.group_id, 68 | cls.author_id == author.id, 69 | cls.reporter_id == reporter.id).one() 70 | except exc.NoResultFound: 71 | return None 72 | return None 73 | 74 | @classmethod 75 | def get_by_id(cls, id_): 76 | """ 77 | Get a report with a specific ID 78 | :param id_: the ID to query 79 | :return: a report or None 80 | """ 81 | try: 82 | return cls.query.filter( 83 | cls.id == id_).one() 84 | except exc.NoResultFound: 85 | return None 86 | 87 | @classmethod 88 | def get_all(cls): 89 | """ 90 | Get all reports 91 | :return: a list of all reports or an empty list 92 | """ 93 | return cls.query.all() 94 | -------------------------------------------------------------------------------- /annotran/reports/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/annotran/reports/test/__init__.py -------------------------------------------------------------------------------- /annotran/reports/views.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | import annotran 4 | import annotran.translations.models 5 | import annotran.mailer 6 | import annotran.pages.models 7 | import annotran.languages.models 8 | import annotran.reports.models 9 | import annotran.views 10 | import h 11 | import h.groups.models 12 | import h.models 13 | from pyramid import httpexceptions as exc 14 | from pyramid.view import view_config 15 | 16 | 17 | @view_config(route_name='report_add', request_method='POST', renderer='annotran:templates/home.html.jinja2') 18 | def add_report(request): 19 | """ 20 | Add an abuse report to the database 21 | :param request: a request object 22 | :return: a redirect to the abuse reports page 23 | """ 24 | if request.authenticated_userid is None: 25 | raise exc.HTTPNotFound() 26 | 27 | reporter = request.authenticated_user 28 | if reporter is None: 29 | raise exc.HTTPNotFound() 30 | 31 | public_language_id = request.matchdict["public_language_id"] 32 | page_url = urllib.unquote(urllib.unquote(request.matchdict["page_uri"])) 33 | public_group_id = request.matchdict['public_group_id'] 34 | user_id = request.matchdict['user_id'] 35 | 36 | page = annotran.pages.models.Page.get_by_uri(page_url) 37 | author = h.models.User.get_by_username(user_id) 38 | reporter = h.models.User.get_by_username(request.authenticated_user.username) 39 | group = h.groups.models.Group.get_by_pubid(public_group_id) 40 | language = annotran.languages.models.Language.get_by_public_language_id(public_language_id) 41 | 42 | if page is None or author is None or reporter is None or group is None or language is None: 43 | raise exc.HTTPNotFound() 44 | 45 | translation = annotran.translations.models.Translation.get_translation(page, language, group) 46 | 47 | if translation is None: 48 | raise exc.HTTPNotFound() 49 | 50 | report = annotran.reports.models.Report.get_report(translation, author, reporter) 51 | 52 | # if already in a database, it means it was reported previously 53 | if report: 54 | return exc.HTTPBadRequest() 55 | 56 | report = annotran.reports.models.Report(translation, author, reporter) 57 | request.db.add(report) 58 | request.db.flush() 59 | 60 | reports = request.route_url('admin_reports') 61 | 62 | body_text = u'Hello,\n\nA new abuse report has been filed. ' \ 63 | u'Please see {0}.\n\nAnnotran'.format(reports) 64 | 65 | annotran.mailer.send(request, subject=u'A new abuse report has been filed', 66 | recipients=[annotran.views.Shared.support_address], 67 | body=body_text) 68 | 69 | return {} 70 | 71 | 72 | def includeme(config): 73 | """ 74 | Pyramid's router configuration 75 | :param config: the configuration object to which to push our routes 76 | :return: None 77 | """ 78 | config.add_route('report_add', 'reports/{user_id}/{public_group_id}/{public_language_id}/{page_uri}/addReport') 79 | config.scan(__name__) 80 | -------------------------------------------------------------------------------- /annotran/resources.py: -------------------------------------------------------------------------------- 1 | from pyramid.security import Allow 2 | 3 | 4 | __acl__ = [ 5 | (Allow, 'group:__admin__', 'admin_index'), 6 | (Allow, 'group:__staff__', 'admin_index'), 7 | (Allow, 'group:__admin__', 'admin_features'), 8 | (Allow, 'group:__admin__', 'admin_nipsa'), 9 | (Allow, 'group:__admin__', 'admin_admins'), 10 | (Allow, 'group:__admin__', 'admin_reports'), 11 | (Allow, 'group:__staff__', 'admin_reports'), 12 | (Allow, 'group:__admin__', 'admin_staff'), 13 | (Allow, 'group:__admin__', 'admin_users'), 14 | (Allow, 'group:__staff__', 'admin_users'), 15 | (Allow, 'group:__admin__', 'admin_badge'), 16 | (Allow, 'group:__admin__', 'admin_groups'), 17 | (Allow, 'group:__staff__', 'admin_groups'), 18 | (Allow, 'group:__staff__', 'admin_view_translation'), 19 | (Allow, 'group:__admin__', 'admin_view_translation'), 20 | (Allow, 'group:__staff__', 'admin_delete_translation'), 21 | (Allow, 'group:__admin__', 'admin_delete_translation'), 22 | (Allow, 'group:__staff__', 'admin_delete_block_translation'), 23 | (Allow, 'group:__admin__', 'admin_delete_block_translation'), 24 | (Allow, 'group:__staff__', 'admin_delete_report'), 25 | (Allow, 'group:__admin__', 'admin_delete_report'), 26 | (Allow, 'group:__staff__', 'admin_delete_block_report'), 27 | (Allow, 'group:__admin__', 'admin_delete_block_report'), 28 | ] 29 | -------------------------------------------------------------------------------- /annotran/static/images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/annotran/static/images/home.png -------------------------------------------------------------------------------- /annotran/static/images/home.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/annotran/static/images/home.xcf -------------------------------------------------------------------------------- /annotran/static/images/stars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/annotran/static/images/stars.png -------------------------------------------------------------------------------- /annotran/static/scripts/annotator/guest.coffee: -------------------------------------------------------------------------------- 1 | Guest = require('../../../../../h/h/static/scripts/annotator/guest.coffee'); 2 | extend = require('extend') 3 | Annotator = require('annotator') 4 | $ = Annotator.$ 5 | 6 | class GuestExt extends Guest 7 | 8 | constructor: (element, options) -> 9 | super 10 | 11 | 12 | html: extend {}, Annotator::html, 13 | adder: ''' 14 |
15 | 16 |
17 | ''' 18 | 19 | anchor: (annotation) -> 20 | # disable anchoring pip display in sidebar 21 | null 22 | 23 | _connectAnnotationSync: (crossframe) => 24 | super 25 | 26 | crossframe.on 'sentenceSelectionOn', () => 27 | Annotator = require('annotator') 28 | Annotator._instances[0].plugins.SentenceSelection.toggleOperationOn() 29 | 30 | crossframe.on 'sentenceSelectionOff', () => 31 | Annotator = require('annotator') 32 | Annotator._instances[0].plugins.SentenceSelection.toggleOperationOff() 33 | 34 | crossframe.on 'hideAdder', () => 35 | Annotator = require('annotator') 36 | Annotator._instances[0].plugins.CSSModify.hideAdder() 37 | 38 | crossframe.on 'moveToNextSentence', () => 39 | Annotator = require('annotator') 40 | Annotator._instances[0].plugins.SentenceSelection.moveToNextSentence() 41 | 42 | crossframe.on 'resetDOM', () => 43 | Annotator = require('annotator') 44 | Annotator._instances[0].plugins.Substitution.clearDOM() 45 | 46 | crossframe.on 'updateAnnotationList', (annotations) => 47 | Annotator._instances[0].loadedAnnotations = annotations 48 | 49 | crossframe.on 'pushAnnotation', (annot) => 50 | Annotator._instances[0].loadedAnnotations.push annot 51 | 52 | crossframe.on 'stashAnnotations', (annotations) => 53 | Annotator._instances[0].loadedAnnotations = annotations 54 | 55 | crossframe.on 'passAnnotations', (annotations) => 56 | Annotator = require('annotator') 57 | Annotator._instances[0].plugins.Substitution.clearDOM() 58 | Annotator._instances[0].plugins.CSSModify.hideAdder() 59 | 60 | if annotations.length > 0 61 | Annotator._instances[0].plugins.Substitution.multipleSubstitution(annotations) 62 | 63 | onAdderClick: (event) -> 64 | event.preventDefault?() 65 | event.stopPropagation?() 66 | @adder.hide() 67 | switch $(event.target).data('action') 68 | when 'comment' 69 | this.createAnnotation() 70 | Annotator.Util.getGlobal().getSelection().removeAllRanges() 71 | 72 | 73 | module.exports = GuestExt -------------------------------------------------------------------------------- /annotran/static/scripts/annotator/host.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Copyright (c) 2013-2014 Hypothes.is Project and contributors 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | ### 24 | 25 | Annotator = require('annotator') 26 | $ = Annotator.$ 27 | 28 | Guest = require('./guest') 29 | 30 | 31 | module.exports = class Host extends Guest 32 | constructor: (element, options) -> 33 | if options.firstRun 34 | options.app += (if '?' in options.app then '&' else '?') + 'firstrun' 35 | 36 | # Create the iframe 37 | app = $('') 38 | .attr('name', 'hyp_sidebar_frame') 39 | # enable media in annotations to be shown fullscreen 40 | .attr('allowfullscreen', '') 41 | .attr('seamless', '') 42 | .attr('src', options.app + "?url=" + encodeURIComponent(window.location.href)) 43 | 44 | @frame = $('
') 45 | .css('display', 'none') 46 | .addClass('annotator-frame annotator-outer') 47 | .appendTo(element) 48 | 49 | super 50 | 51 | app.appendTo(@frame) 52 | 53 | this.on 'panelReady', => 54 | # Initialize tool state. 55 | if options.showHighlights == undefined 56 | # Highlights are on by default. 57 | options.showHighlights = true 58 | this.setVisibleHighlights(options.showHighlights) 59 | 60 | this.show() 61 | 62 | # Show the UI 63 | @frame.css('display', '') 64 | 65 | destroy: -> 66 | @frame.remove() 67 | super 68 | -------------------------------------------------------------------------------- /annotran/static/scripts/annotator/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2013-2014 Hypothes.is Project and contributors 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | */ 24 | 25 | var extend = require('extend'); 26 | var Annotator = require('annotator'); 27 | 28 | // Polyfills 29 | var g = Annotator.Util.getGlobal(); 30 | if (g.wgxpath) { 31 | g.wgxpath.install(); 32 | } 33 | 34 | // Applications 35 | Annotator.Guest = require('./guest'); 36 | Annotator.Host = require('../../../../../h/h/static/scripts/annotator/host'); 37 | Annotator.Sidebar = require('./sidebar'); 38 | Annotator.PdfSidebar = require('../../../../../h/h/static/scripts/annotator/pdf-sidebar'); 39 | 40 | // UI plugins 41 | Annotator.Plugin.BucketBar = require('../../../../../h/h/static/scripts/annotator/plugin/bucket-bar'); 42 | Annotator.Plugin.Toolbar = require('./plugin/toolbar'); 43 | Annotator.Plugin.CSSModify = require('./plugin/cssmodify'); 44 | 45 | // Document type plugins 46 | Annotator.Plugin.PDF = require('../../../../../h/h/static/scripts/annotator/plugin/pdf'); 47 | require('../../../../../h/h/static/scripts/vendor/annotator.document'); // Does not export the plugin :( 48 | 49 | // Selection plugins 50 | Annotator.Plugin.TextSelection = require('../../../../../h/h/static/scripts/annotator/plugin/textselection'); 51 | Annotator.Plugin.SentenceSelection = require('./plugin/sentenceselection'); 52 | Annotator.Plugin.Substitution = require('./plugin/substitution'); 53 | 54 | // Cross-frame communication 55 | Annotator.Plugin.CrossFrame = require('../../../../../h/h/static/scripts/annotator/plugin/cross-frame'); 56 | Annotator.Plugin.CrossFrame.AnnotationSync = require('../../../../../h/h/static/scripts/annotation-sync'); 57 | Annotator.Plugin.CrossFrame.Bridge = require('../../../../../h/h/static/scripts/bridge'); 58 | Annotator.Plugin.CrossFrame.Discovery = require('../../../../../h/h/static/scripts/discovery'); 59 | 60 | var docs = 'https://h.readthedocs.org/en/latest/hacking/customized-embedding.html'; 61 | var options = { 62 | app: jQuery('link[type="application/annotator+html"]').attr('href') 63 | }; 64 | 65 | if (window.hasOwnProperty('hypothesisConfig')) { 66 | if (typeof window.hypothesisConfig === 'function') { 67 | extend(options, window.hypothesisConfig()); 68 | } else { 69 | throw new TypeError('hypothesisConfig must be a function, see: ' + docs); 70 | } 71 | } 72 | 73 | Annotator.noConflict().$.noConflict(true)(function() { 74 | 'use strict'; 75 | var Klass = window.PDFViewerApplication ? 76 | Annotator.PdfSidebar : 77 | Annotator.Sidebar; 78 | if (options.hasOwnProperty('constructor')) { 79 | Klass = options.constructor; 80 | delete options.constructor; 81 | } 82 | window.annotator = new Klass(document.body, options); 83 | }); 84 | -------------------------------------------------------------------------------- /annotran/static/scripts/annotator/plugin/cssmodify.coffee: -------------------------------------------------------------------------------- 1 | Annotator = require('annotator') 2 | $ = Annotator.$ 3 | 4 | # This plugin modifies the host page's CSS according to crossframe calls 5 | module.exports = class CSSModify extends Annotator.Plugin 6 | 7 | pluginInit: -> 8 | # Register the event handlers required for creating a selection 9 | super 10 | null 11 | 12 | hideAdder: () -> 13 | $(".annotator-adder").css({"visibility": "hidden"}) 14 | 15 | showAdder: () -> 16 | $(".annotator-adder").css({"visibility": "visible"}) -------------------------------------------------------------------------------- /annotran/static/scripts/annotator/plugin/toolbar.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Copyright (c) 2013-2014 Hypothes.is Project and contributors 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | ### 24 | 25 | Toolbar = require('../../../../../../h/h/static/scripts/annotator/plugin/toolbar.coffee'); 26 | Annotator = require('annotator') 27 | $ = Annotator.$ 28 | 29 | makeButton = (item) -> 30 | anchor = $('') 31 | .attr('href', '') 32 | .attr('title', item.title) 33 | .attr('name', item.name) 34 | .on(item.on) 35 | .addClass('annotator-frame-button') 36 | .addClass(item.class) 37 | button = $('
  • ').append(anchor) 38 | return button[0] 39 | 40 | class ToolbarExt extends Toolbar 41 | pluginInit: -> 42 | @annotator.toolbar = @toolbar = $(@html) 43 | if @options.container? 44 | $(@options.container).append @toolbar 45 | else 46 | $(@element).append @toolbar 47 | 48 | items = [ 49 | "title": "Toggle Sidebar" 50 | "class": "annotator-frame-button--sidebar_toggle h-icon-chevron-left" 51 | "name": "sidebar-toggle" 52 | "on": 53 | "click": (event) => 54 | event.preventDefault() 55 | event.stopPropagation() 56 | collapsed = @annotator.frame.hasClass('annotator-collapsed') 57 | if collapsed 58 | @annotator.show() 59 | else 60 | @annotator.hide() 61 | ] 62 | @buttons = $(makeButton(item) for item in items) 63 | 64 | list = $('') 65 | @buttons.appendTo(list) 66 | @toolbar.append(list) 67 | 68 | # Remove focus from the anchors when clicked, this removes the focus 69 | # styles intended only for keyboard navigation. IE/FF apply the focus 70 | # psuedo-class to a clicked element. 71 | @toolbar.on('mouseup', 'a', (event) -> $(event.target).blur()) 72 | 73 | module.exports = ToolbarExt -------------------------------------------------------------------------------- /annotran/static/scripts/app-controller.coffee: -------------------------------------------------------------------------------- 1 | 2 | appcontroller = require('../../../../h/h/static/scripts/app-controller.coffee'); 3 | persona = require('../../../../h/h/static/scripts/filter/persona.js') 4 | 5 | class AppControllerExt extends appcontroller 6 | this.$inject = [ 7 | '$controller', '$document', '$location', '$rootScope', '$route', '$scope', 8 | '$window', 'annotationUI', 'auth', 'drafts', 'features', 'groups', 'languages' 9 | 'identity', 'session' 10 | ] 11 | constructor: ( 12 | $controller, $document, $location, $rootScope, $route, $scope, 13 | $window, annotationUI, auth, drafts, features, groups, languages, 14 | identity, session 15 | ) -> 16 | 17 | super($controller, $document, $location, $rootScope, $route, $scope, 18 | $window, annotationUI, auth, drafts, features, groups, 19 | identity, session) 20 | 21 | $scope.$root.sentencebysentence = "off" 22 | $scope.$root.userListvisible = true 23 | $scope.$root.editOnly = false 24 | $scope.$root.userAnnotations = [] 25 | $scope.$root.allPageAnnotations = [] 26 | $scope.$root.pageUri = window.location.href 27 | $scope.$root.pageUri = decodeURIComponent($scope.$root.pageUri); 28 | $scope.$root.pageUri = encodeURIComponent(encodeURIComponent(getParameterByName("url", $scope.$root.pageUri))); 29 | 30 | 31 | $scope.$root.updateUserList = (direction) -> 32 | # clear the array 33 | $scope.$root.list_of_users = [] 34 | 35 | keys = Object.keys($scope.$root.users_no_scores) 36 | if (keys.length != 0) 37 | for i in [0 .. (keys.length-1)] 38 | delete $scope.$root.users_no_scores[keys[i]] 39 | 40 | keys = Object.keys($scope.$root.users_with_scores) 41 | if (keys.length != 0) 42 | for i in [0 .. (keys.length-1)] 43 | delete $scope.$root.users_with_scores[keys[i]] 44 | 45 | # get all users using annot list 46 | getUsers() 47 | 48 | dupeCheck = [] 49 | 50 | if $scope.$root.allPageAnnotations.length > 0 51 | if (session.state.votes != undefined && session.state.votes.length != 0) 52 | for i in [0 .. (session.state.votes.length-1)] 53 | if (groups.focused().id == session.state.votes[i].group_id && 54 | languages.focused().id == session.state.votes[i].language_id) 55 | author = setUserWithScore(session.state.votes[i].author_id) 56 | if (!author) 57 | continue 58 | score = session.state.votes[i].avg_score 59 | auth_obj = {} 60 | auth_obj["score"] = score 61 | auth_obj["author"] = author 62 | $scope.$root.list_of_users.push auth_obj 63 | 64 | #push unvoted users 65 | keys = Object.keys($scope.$root.users_no_scores) 66 | if (keys.length != 0) 67 | for i in [0 .. (keys.length-1)] 68 | auth_obj = {} 69 | auth_obj["score"] = 0 70 | auth_obj["author"] = $scope.$root.users_no_scores[keys[i]] 71 | $scope.$root.list_of_users.push auth_obj 72 | 73 | if (direction == 1) #reverse the order; 74 | $scope.$root.list_of_users = $scope.$root.list_of_users.reverse() 75 | 76 | return $scope.$root.list_of_users 77 | 78 | $scope.$root.users_no_scores = {} #all users on the page, for all groups and langauges 79 | getUsers = () -> 80 | for entry in $scope.$root.allPageAnnotations 81 | parsed = persona.parseAccountID(entry.user) 82 | if (groups.focused().id == entry.group && languages.focused().id == entry.language) 83 | if Object.keys($scope.$root.users_no_scores).length != 0 && $scope.$root.users_no_scores[parsed.username] != undefined 84 | continue 85 | else 86 | $scope.$root.users_no_scores[parsed.username] = parsed 87 | 88 | $scope.$root.users_with_scores = {} 89 | setUserWithScore = (authorusername) -> 90 | user_info = $scope.$root.users_no_scores[authorusername] 91 | $scope.$root.users_with_scores[authorusername] = user_info 92 | delete $scope.$root.users_no_scores[authorusername] 93 | return user_info 94 | 95 | $scope.$root.allVotes = {} 96 | getAuthorTotalScore = (authorusername) -> 97 | author_score = 0.0 98 | if (session.state.votes != undefined && session.state.votes.length != 0) 99 | if Object.keys($scope.$root.allVotes).length == 0 100 | for i in [0 .. (session.state.votes.length-1)] 101 | $scope.$root.allVotes[session.state.votes[i].author_id] = session.state.votes[i] 102 | if session.state.votes[i].author_id == author.username 103 | author_score = session.state.votes[i].avg_score 104 | else 105 | auth_obj = $scope.$root.allVotes[author.username] 106 | if (auth_obj) 107 | author_score = auth_obj.avg_score 108 | return author_score 109 | 110 | ` 111 | function getParameterByName(name, url) { 112 | if (!url) url = pageid; 113 | name = name.replace(/[\[\]]/g, "\\$&"); 114 | var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), 115 | results = regex.exec(url); 116 | if (!results) return null; 117 | if (!results[2]) return ''; 118 | return results[2]; 119 | //return decodeURIComponent(results[2].replace(/\+/g, " ")); 120 | } 121 | ` 122 | 123 | module.exports = AppControllerExt -------------------------------------------------------------------------------- /annotran/static/scripts/apps.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | 4 | // this assumes that h is stored in the same root directory as annotran 5 | require('../../../../h/h/static/scripts/app.coffee'); 6 | 7 | require('./directive/language-list.js'); 8 | require('./events.js'); 9 | var app = angular.module("h"); 10 | 11 | app.controller('AppController', require('./app-controller')) 12 | .controller('WidgetController', require('./widget-controller')) 13 | .directive('languageList', require('./directive/language-list').directive) 14 | .directive('userList', require('./directive/user-list').directive) 15 | .directive('annotation', require('./directive/annotation').directive) 16 | .directive('topBar', require('./directive/top-bar').directive) 17 | .directive('thread', require('./directive/thread')) 18 | .directive('shareDialog', require('./directive/share-dialog')) 19 | .directive('signinControl', require('./directive/signin-control')) 20 | .directive('sidebarTutorial', require('./directive/sidebar-tutorial').directive) 21 | .directive('publishAnnotationBtn', require('./directive/publish-annotation-btn').directive) 22 | .directive('stars', function() { 23 | return { 24 | restrict: 'E', 25 | link: function($scope, elem, attr) { 26 | function starify() { 27 | var val = parseFloat($scope.starscore); 28 | var size = Math.max(0, (Math.min(5, val))) * 16; 29 | var span = angular.element('').css('width', size+'px'); 30 | angular.element(elem).html(span[0].outerHTML); 31 | } 32 | starify(); 33 | }, 34 | scope: { 35 | starscore: '=' 36 | }, 37 | }; 38 | }) 39 | 40 | .service('languages', require('./languages')) 41 | .service('pages', require('./pages')) 42 | .service('translations', require('./translations')) 43 | .service('groups', require('./groups')) 44 | .service('session', require('./session')) 45 | .service('votes', require('./votes')) 46 | .service('reports', require('./reports')) 47 | 48 | app.factory('langListFactory', require('./directive/language-service').factory) 49 | 50 | 51 | app.controller("languageController", ['$scope', 'langListFactory', 52 | function ($scope, langListFactory) { 53 | $scope.languages = langListFactory.getLanguages(); 54 | $scope.language = null; 55 | }]); 56 | 57 | 58 | 59 | 60 | 61 | 62 | // these decorators override hypothes.is's decorators 63 | 64 | app.decorator( 65 | "annotationDirective", 66 | function annotationDirectiveDecorator( $delegate ) { 67 | return( [ $delegate[1] ] ); 68 | } 69 | ); 70 | 71 | app.decorator( 72 | "topBarDirective", 73 | function topBarDirectiveDecorator( $delegate ) { 74 | return( [ $delegate[1] ] ); 75 | } 76 | ); 77 | 78 | app.decorator( 79 | "sidebarTutorialDirective", 80 | function sidebarTutorialDirectiveDecorator( $delegate ) { 81 | return( [ $delegate[1] ] ); 82 | } 83 | ); 84 | 85 | app.decorator( 86 | "threadDirective", 87 | function threadDirectiveDecorator( $delegate ) { 88 | return( [ $delegate[1] ] ); 89 | } 90 | ); 91 | 92 | app.decorator( 93 | "shareDialogDirective", 94 | function shareDialogDirectiveDecorator( $delegate ) { 95 | return( [ $delegate[1] ] ); 96 | } 97 | ); 98 | 99 | app.decorator( 100 | "signinControlDirective", 101 | function signinControlDirectiveDecorator( $delegate ) { 102 | return( [ $delegate[1] ] ); 103 | } 104 | ); 105 | 106 | app.decorator( 107 | "publishAnnotationBtnDirective", 108 | function publishAnnotationBtnDirectiveDecorator( $delegate ) { 109 | return( [ $delegate[1] ] ); 110 | } 111 | ); 112 | 113 | /* 114 | app.controller('AppController', ['$scope', '$controller', function ( $controller, $document, $location, $rootScope, $route, $scope, 115 | $window, annotationUI, auth, drafts, features, groups, identity, session) { 116 | // Initialize the super class and extend it. 117 | angular.extend(this, $controller('AppController', { $controller: $controller, $document: $document, $location: $location, $rootScope: $rootScope, $route: $route, $scope: $scope, 118 | $window: $window, annotationUI: annotationUI, auth: auth, drafts: drafts,features: features, groups: groups, 119 | identity: identity, session: session})); 120 | console.log($scope.shareDialog); 121 | }]); 122 | */ -------------------------------------------------------------------------------- /annotran/static/scripts/directive/language-list.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var events = require('../../../../../h/h/static/scripts/events.js'); 4 | var eventsa = require('../events'); 5 | var Annotator = require('annotator'); 6 | var $ = Annotator.$; 7 | 8 | var mouseUpPrevent = true; 9 | var clickPrevent = true; 10 | 11 | // @ngInject 12 | function LanguageListController($scope, $window, languages, groups, pages) { 13 | $scope.addLanguage = function (language) { 14 | clickPrevent = false; 15 | mouseUpPrevent = false; 16 | 17 | // this will fire when the user selects the top entry in the list ("Add a new translation") 18 | if(language == null){ return; } 19 | 20 | if (languages.hasLanguageAlready(language.name) == false) { 21 | var message = 'Are you sure you want to add new translations for the language "' + 22 | language.name + '"?'; 23 | if ($window.confirm(message)) { 24 | languages.addLanguage(language.name); 25 | } 26 | } else { 27 | var message = 'Language "' + 28 | language.name + '" has already been added.'; 29 | alert(message); 30 | languages.focusByName(language.name); 31 | } 32 | $("#zeroIndex").prop('selected', true); 33 | }; 34 | 35 | //This method is used for a list of existing languages - those that are already created for that page. 36 | $scope.focusLanguage = function (languageId) { 37 | $scope.$root.setMode('view'); 38 | $scope.$root.selectedUser = undefined; 39 | languages.focus(languageId); 40 | }; 41 | 42 | 43 | $scope.showUserList = function () { 44 | $scope.$root.userListvisible = true; 45 | $scope.$root.updateUserList(0); 46 | } 47 | 48 | $scope.handleSubmenuMU = function (event) { 49 | if (mouseUpPrevent == true) { 50 | event.stopPropagation(); 51 | event.preventDefault(); 52 | } 53 | else{ 54 | mouseUpPrevent = true; 55 | } 56 | }; 57 | 58 | $scope.handleSubmenuClick = function (event) { 59 | if (clickPrevent == true) { 60 | event.stopPropagation(); 61 | event.preventDefault(); 62 | } 63 | else{ 64 | clickPrevent = true; 65 | } 66 | }; 67 | 68 | } 69 | 70 | /** 71 | * @ngdoc directive 72 | * @name languageList 73 | * @restrict AE 74 | * @description Displays a list of available languages 75 | */ 76 | // @ngInject 77 | function languageList($window, languages, settings) { 78 | return { 79 | controller: LanguageListController, 80 | link: function ($scope, elem, attrs) { 81 | $scope.languages = languages; 82 | }, 83 | restrict: 'E', 84 | scope: { 85 | auth: '=', 86 | userList: '=', 87 | showUserList: '=' 88 | }, 89 | templateUrl: 'language_list.html' 90 | }; 91 | }; 92 | 93 | module.exports = { 94 | directive: languageList, 95 | Controller: LanguageListController 96 | }; 97 | -------------------------------------------------------------------------------- /annotran/static/scripts/directive/publish-annotation-btn.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var eventsa = require('../events'); 4 | 5 | /** 6 | * @ngdoc directive 7 | * @name publishAnnotationBtn 8 | * @description Displays a combined privacy/selection post button to post 9 | * a new annotation 10 | */ 11 | // @ngInject 12 | function publishAnnotationBtnController(crossframe, $rootScope, $scope) { 13 | 14 | this.showDropdown = false; 15 | this.privateLabel = 'Only Me'; 16 | 17 | $rootScope.$on('moving_to_sentence', function () { 18 | if($rootScope.sentencebysentence == "on") { 19 | $scope.vm.onCancel(); 20 | } 21 | }); 22 | 23 | $rootScope.$on('close_open_annotations', function () { 24 | $scope.vm.onCancel(); 25 | }); 26 | 27 | this.publishDestination = function () { 28 | return this.isShared ? this.group.name : this.privateLabel; 29 | } 30 | 31 | this.moveToNext = function() { 32 | crossframe.call("moveToNextSentence"); 33 | } 34 | 35 | this.groupType = function () { 36 | return this.group.public ? 'public' : 'group'; 37 | } 38 | 39 | this.setPrivacy = function (level) { 40 | this.onSetPrivacy({level: level}); 41 | } 42 | } 43 | 44 | /** 45 | * @ngdoc directive 46 | * @name languageList 47 | * @restrict AE 48 | * @description Displays a list of available languages 49 | */ 50 | // @ngInject 51 | function publishAnnotationBtn($window, languages, settings) { 52 | return { 53 | bindToController: true, 54 | controller: publishAnnotationBtnController, 55 | controllerAs: 'vm', 56 | restrict: 'E', 57 | scope: { 58 | group: '=', 59 | canPost: '=', 60 | isShared: '=', 61 | onCancel: '&', 62 | onSave: '&', 63 | onSetPrivacy: '&' 64 | }, 65 | templateUrl: 'publish_annotation_btn.html' 66 | }; 67 | }; 68 | 69 | module.exports = { 70 | directive: publishAnnotationBtn, 71 | Controller: publishAnnotationBtnController 72 | }; 73 | -------------------------------------------------------------------------------- /annotran/static/scripts/directive/share-dialog.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Copyright (c) 2013-2014 Hypothes.is Project and contributors 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 19 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | 23 | This is the override of the directive from h - for the purpose of "via annotran" link usage. 24 | ### 25 | 26 | 27 | 28 | ###* 29 | # @ngdoc directive 30 | # @name share-dialog 31 | # @restrict A 32 | # @description This dialog generates a via link to the page h is currently 33 | # loaded on. 34 | ### 35 | module.exports = ['crossframe', (crossframe) -> 36 | link: (scope, elem, attrs, ctrl) -> 37 | scope.viaPageLink = '' 38 | 39 | # Watch scope.shareDialog.visible: when it changes to true, focus input 40 | # and selection. 41 | scope.$watch (-> scope.shareDialog?.visible), (visible) -> 42 | if visible 43 | scope.$evalAsync(-> elem.find('#via').focus().select()) 44 | 45 | scope.$watchCollection (-> crossframe.frames), (frames) -> 46 | if not frames.length 47 | return 48 | # Check to see if we are on a via page. If so, we just return the URI. 49 | re = /https:\/\/via\.annotran\.??/ 50 | if re.test(frames[0].uri) 51 | scope.viaPageLink = frames[0].uri 52 | else 53 | scope.viaPageLink = 'https://via.annotran.??/' + frames[0].uri 54 | 55 | restrict: 'E' 56 | templateUrl: 'share_dialog.html' 57 | ] 58 | -------------------------------------------------------------------------------- /annotran/static/scripts/directive/sidebar-tutorial.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2013-2014 Hypothes.is Project and contributors 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | ''' 24 | **/ 25 | 26 | /** 27 | * This is the override of the sidebar-tutorial directive from h. We do not use session preferences, 28 | * but rather hide the entire tutorial once user clicks on "Close" button. The tutorial is always shown on the first load. -!> 29 | **/ 30 | 31 | 'use strict'; 32 | 33 | // @ngInject 34 | function SidebarTutorialController() { 35 | var vm = this; 36 | 37 | var showSidebarTutorial = true; 38 | 39 | vm.showSidebarTutorial = function () { 40 | return showSidebarTutorial; 41 | }; 42 | 43 | vm.dismiss = function () { 44 | showSidebarTutorial = false; 45 | }; 46 | } 47 | 48 | /** 49 | * @ngdoc directive 50 | * @name sidebarTutorial 51 | * @description Displays a short tutorial in the sidebar. 52 | */ 53 | // @ngInject 54 | module.exports = { 55 | directive: function () { 56 | return { 57 | bindToController: true, 58 | controller: SidebarTutorialController, 59 | controllerAs: 'vm', 60 | restrict: 'E', 61 | scope: { 62 | auth: '=', 63 | }, 64 | templateUrl: 'sidebar_tutorial.html' 65 | }; 66 | }, 67 | Controller: SidebarTutorialController 68 | }; 69 | -------------------------------------------------------------------------------- /annotran/static/scripts/directive/signin-control.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function () { 4 | return { 5 | bindToController: true, 6 | controllerAs: 'vm', 7 | //@ngInject 8 | controller: function (settings) { 9 | this.serviceUrl = settings.serviceUrl; 10 | this.support_address = settings.supportAddress; 11 | }, 12 | restrict: 'E', 13 | scope: { 14 | /** 15 | * An object representing the current authentication status. 16 | */ 17 | auth: '=', 18 | /** 19 | * Called when the user clicks on the "Sign in" text. 20 | */ 21 | onLogin: '&', 22 | /** 23 | * Called when the user clicks on the "Sign out" text. 24 | */ 25 | onLogout: '&', 26 | /** 27 | * Whether or not to use the new design for the control. 28 | * 29 | * FIXME: should be removed when the old design is deprecated. 30 | */ 31 | newStyle: '=', 32 | }, 33 | templateUrl: 'signin_control.html', 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /annotran/static/scripts/directive/top-bar.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | directive: function () { 5 | return { 6 | restrict: 'E', 7 | priority: 0, 8 | scope: { 9 | auth: '=', 10 | isSidebar: '=', 11 | onLogin: '&', 12 | onLogout: '&', 13 | searchController: '=', 14 | accountDialog: '=', 15 | shareDialog: '=', 16 | sortBy: '=', 17 | sortOptions: '=', 18 | userList: '=', 19 | onChangeSortBy: '&', 20 | showUserList: '&' 21 | }, 22 | templateUrl: 'top_bar.html', 23 | }; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /annotran/static/scripts/directive/user-list.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var persona = require('../../../../../h/h/static/scripts/filter/persona.js'); 3 | var events = require('../../../../../h/h/static/scripts/events.js'); 4 | var eventsa = require('../events'); 5 | 6 | // @ngInject 7 | function Controller($scope, $window, session, settings, languages, votes, reports, crossframe) { 8 | 9 | this.serviceUrl = settings.serviceUrl; 10 | $scope.$root.sentenceMode = "on"; 11 | $scope.$root.mode = "view"; 12 | 13 | if ($scope.model == null) { 14 | $scope.model = {}; 15 | } 16 | 17 | $scope.hideUserList = function () { 18 | $scope.$root.userListvisible = false; 19 | }; 20 | 21 | $scope.setUserForReset = function () { 22 | this.$root.selectedUser = undefined; 23 | this.$root.editOnly = false; 24 | 25 | $scope.$root.$broadcast(eventsa.LANGUAGE_FOCUSED, languages.focused().id); 26 | 27 | }; 28 | 29 | $scope.$root.setMode = function(modeValue) { 30 | $scope.$root.mode = modeValue; 31 | $scope.setUserForReset(); 32 | if ($scope.$root.mode == 'view') { 33 | if ($scope.$root.sentenceMode == "on") { 34 | crossframe.call("sentenceSelectionOff"); 35 | } 36 | crossframe.call("hideAdder"); 37 | $scope.$root.$broadcast(eventsa.CLOSE_OPEN_ANNOTATIONS); 38 | } 39 | }; 40 | 41 | $scope.setUserForEdit = function () { 42 | this.$root.selectedUser = "self"; 43 | this.$root.editOnly = true; 44 | 45 | if (languages.focused().id == undefined) { 46 | var message = 'Select language.'; 47 | $window.confirm(message); 48 | } else { 49 | $scope.$root.$broadcast(eventsa.LANGUAGE_FOCUSED, languages.focused().id); 50 | } 51 | }; 52 | 53 | $scope.setUser = function (id) { 54 | var selectedUser = "acct:" + id.username + "@" + id.provider; 55 | 56 | if (selectedUser == this.$root.currentUser) { 57 | this.$root.selectedUser = "self"; 58 | this.$root.editOnly = true; 59 | } else { 60 | this.$root.selectedUser = id; 61 | this.$root.editOnly = false; 62 | } 63 | $scope.$root.$broadcast(eventsa.LANGUAGE_FOCUSED, languages.focused().id); 64 | 65 | }; 66 | 67 | $scope.enableSentence = function () { 68 | $scope.$root.sentenceMode = "on"; 69 | $scope.setUserForReset(); 70 | this.$root.sentencebysentence = $scope.$root.sentenceMode; 71 | 72 | crossframe.call("sentenceSelectionOn"); 73 | }; 74 | 75 | $scope.disableSentence = function () { 76 | $scope.$root.sentenceMode = "off"; 77 | this.$root.sentencebysentence = $scope.$root.sentenceMode; 78 | 79 | crossframe.call("sentenceSelectionOff"); 80 | $scope.$root.$broadcast(eventsa.CLOSE_OPEN_ANNOTATIONS); 81 | }; 82 | 83 | $scope.vote = function(author) { 84 | if (author != this.$root.currentUser) { 85 | $scope.author = author; 86 | } 87 | }; 88 | 89 | $scope.addVote = function(author, score) { 90 | $scope.author=author; 91 | 92 | var voteRet = votes.addVote(author.username, languages.focused().id, score); 93 | 94 | //set scope.author to an empty dictionary in order to hide the box once the user has voted. 95 | $scope.author={}; 96 | 97 | return voteRet; 98 | }; 99 | 100 | $scope.addReport = function(author) { 101 | $scope.author=author; 102 | 103 | var message = 'Are you sure you want to report ' + author.username + '\'s translations as abusive?'; 104 | if ($window.confirm(message)) { 105 | var reportRet = reports.addReport(author.username, languages.focused().id); 106 | // set scope.author to an empty dictionary in order to hide the box once the user has voted. 107 | $scope.author={}; 108 | return reportRet; 109 | } 110 | }; 111 | 112 | $scope.showVote = function(author, score) { 113 | 114 | votes.showVote(author, score); 115 | 116 | }; 117 | 118 | $scope.voteAuthor = function(user) { 119 | return function(score) { 120 | if ($scope.author == undefined || user == undefined) 121 | return false; 122 | if ($scope.author == user) 123 | return true; 124 | else 125 | return false; 126 | } 127 | }; 128 | 129 | $scope.reverseUserList = function(direction) { 130 | $scope.$root.direction = direction; 131 | $scope.$root.updateUserList($scope.$root.direction); 132 | }; 133 | 134 | $scope.userList = function () { 135 | return $scope.$root.updateUserList(0) 136 | }; 137 | 138 | // for some reason we have to use an array here as NG repeat won't handle it properly otherwise 139 | $scope.list_of_users = $scope.$root.list_of_users; 140 | $scope.$root.updateUserList(0); 141 | 142 | } 143 | 144 | // @ngInject 145 | function userList (languages) { 146 | return { 147 | controller: Controller, 148 | bindToController: true, 149 | controllerAs: 'vm', 150 | restrict: 'E', 151 | link: function ($scope, elem, attrs) { 152 | $scope.languages = languages; 153 | }, 154 | scope: { 155 | auth: '=', 156 | session: '=', 157 | onClose: '&', 158 | showUserList: '=', 159 | languages: '&', 160 | }, 161 | templateUrl: 'user_list.html' 162 | }; 163 | } 164 | 165 | module.exports = { 166 | directive: userList, 167 | Controller: Controller 168 | }; 169 | -------------------------------------------------------------------------------- /annotran/static/scripts/events.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2013-2014 Hypothes.is Project and contributors 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 21 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | ''' 25 | **/ 26 | 27 | 28 | /** 29 | * This module extends the set of global events that are dispatched 30 | * on $rootScope and defined in the same module in h 31 | */ 32 | module.exports = { 33 | /** Broadcast when a language is added */ 34 | LANGUAGE_ADDED: 'languageAdded', 35 | /** Broadcast when a page is added */ 36 | PAGE_ADDED: 'pageAdded', 37 | /** Broadcast when the session is reloaded */ 38 | SESSION_RELOADED: 'SessionReloaded', 39 | /** Broadcast when the currently selected language changes */ 40 | LANGUAGE_FOCUSED: 'languageFocused', 41 | /** Broadcast when the list of groups changes */ 42 | LANGUAGES_CHANGED: 'languagesChanged', 43 | /** Broadcast when the list of users is loaded*/ 44 | USERS_LOADED: 'usersLoaded', 45 | /** Broadcast when the currently selected user changes*/ 46 | USERS_FOCUSED: 'userFocused', 47 | /** Broadcast when the user deleted an annotation*/ 48 | USER_DELETED_ANNOTATION: 'user_deleted_annotation', 49 | /** Broadcast when moving to another sentence in sentence-by-sentence mode*/ 50 | MOVING_TO_SENTENCE: 'moving_to_sentence', 51 | /** Broadcast when we want to close all active annotations*/ 52 | CLOSE_OPEN_ANNOTATIONS: 'close_open_annotations', 53 | /** Broadcast when the rootscope lists are updated*/ 54 | ROOTSCOPE_LISTS_UPDATED: 'rootscope_lists_updated', 55 | }; -------------------------------------------------------------------------------- /annotran/static/scripts/groups.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2013-2014 Hypothes.is Project and contributors 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 21 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | ''' 25 | **/ 26 | 27 | /** 28 | * @ngdoc service 29 | * @name groups 30 | * 31 | * @description Provides access to the list of groups that the user is currently 32 | * a member of and the currently selected group in the UI. 33 | * 34 | * The list of groups is initialized from the session state 35 | * and can then later be updated using the add() and remove() 36 | * methods. 37 | */ 38 | 'use strict'; 39 | 40 | var STORAGE_KEY = 'hypothesis.groups.focus'; 41 | 42 | // this assumes that h is stored in the same root directory as annotran 43 | var events = require('../../../../h/h/static/scripts/events.js'); 44 | 45 | // @ngInject 46 | function groups(localStorage, session, settings, $rootScope, $http) { 47 | // The currently focused group. This is the group that's shown as selected in 48 | // the groups dropdown, the annotations displayed are filtered to only ones 49 | // that belong to this group, and any new annotations that the user creates 50 | // will be created in this group. 51 | var focusedGroup; 52 | var eventBroadcasted = false; 53 | 54 | function all() { 55 | return session.state.groups || []; 56 | }; 57 | 58 | // Return the full object for the group with the given id. 59 | function get(id) { 60 | var gs = all(); 61 | for (var i = 0, max = gs.length; i < max; i++) { 62 | if (gs[i].id === id) { 63 | //$rootScope.$broadcast(events.GROUP_FOCUSED, gs[i].id); 64 | return gs[i]; 65 | } 66 | } 67 | }; 68 | 69 | /** Leave the group with the given ID. 70 | * Returns a promise which resolves when the action completes. 71 | */ 72 | function leave(id) { 73 | var response = $http({ 74 | method: 'POST', 75 | url: settings.serviceUrl + 'groups/' + id + '/leave', 76 | }); 77 | 78 | // the groups list will be updated in response to a session state 79 | // change notification from the server. We could improve the UX here 80 | // by optimistically updating the session state 81 | 82 | return response; 83 | }; 84 | 85 | 86 | /** Return the currently focused group. If no group is explicitly focused we 87 | * will check localStorage to see if we have persisted a focused group from 88 | * a previous session. Lastly, we fall back to the first group available. 89 | */ 90 | function focused() { 91 | if (focusedGroup) { 92 | return focusedGroup; 93 | } 94 | 95 | var fromStorage = get(localStorage.getItem(STORAGE_KEY)); 96 | 97 | if (fromStorage) { 98 | focusedGroup = fromStorage; 99 | 100 | return focusedGroup.id; 101 | } 102 | 103 | return all()[0].id; 104 | } 105 | 106 | /** Set the group with the passed id as the currently focused group. */ 107 | function focus(id) { 108 | var g = get(id); 109 | if (g) { 110 | focusedGroup = g; 111 | localStorage.setItem(STORAGE_KEY, g.id); 112 | $rootScope.$broadcast(events.GROUP_FOCUSED, g.id); 113 | } 114 | } 115 | 116 | // reset the focused group if the user leaves it 117 | $rootScope.$on(events.GROUPS_CHANGED, function () { 118 | $rootScope.setMode('view'); 119 | if (focusedGroup) { 120 | focusedGroup = get(focusedGroup.id); 121 | $rootScope.$broadcast(events.GROUP_FOCUSED, focusedGroup); 122 | if (!focusedGroup) { 123 | var focusResult = focused(); 124 | $rootScope.$broadcast(events.GROUP_FOCUSED, focusResult); 125 | } 126 | } 127 | else { 128 | var focusResult = focused(); 129 | $rootScope.$broadcast(events.GROUP_FOCUSED, focusResult); 130 | } 131 | }); 132 | 133 | return { 134 | all: all, 135 | get: get, 136 | 137 | leave: leave, 138 | 139 | focused: focused, 140 | focus: focus, 141 | }; 142 | } 143 | 144 | module.exports = groups; 145 | -------------------------------------------------------------------------------- /annotran/static/scripts/pages.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @ngdoc service 4 | * @name pages 5 | * 6 | * @description Adds a page for which at least one language has been added previously. 7 | * 8 | */ 9 | 'use strict'; 10 | 11 | var eventsa = require('./events'); 12 | 13 | // @ngInject 14 | function pages(settings, session, $rootScope, $http, translations) { 15 | 16 | function addPage(languageName) { 17 | var response = $http({ 18 | method: 'POST', 19 | url: settings.serviceUrl + 'pages/' + languageName + '/' + $rootScope.pageUri + '/' + $rootScope.groupPubid + '/addPage', 20 | }).then(function successCallback(response) { 21 | $rootScope.$broadcast(eventsa.PAGE_ADDED, languageName); 22 | });; 23 | return response; 24 | }; 25 | 26 | $rootScope.$on(eventsa.LANGUAGE_ADDED, function (event, languageName) { 27 | addPage(languageName); 28 | session.reload(languageName); 29 | }); 30 | 31 | return { 32 | addPage: addPage, 33 | }; 34 | }; 35 | 36 | module.exports = pages; -------------------------------------------------------------------------------- /annotran/static/scripts/reports.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @ngdoc service 4 | * @name languages 5 | * 6 | * @description Provides access to the list of languages for available translations. 7 | * 8 | * The list of languages is initialized from the session state 9 | * and can then later be updated using the add() method. 10 | */ 11 | 'use strict'; 12 | 13 | var eventsa = require('./events'); 14 | 15 | 16 | // @ngInject 17 | function reports(settings, session, $rootScope, $http, $window) { 18 | 19 | 20 | function addReport(userId, languageId) { 21 | 22 | var pageId = $rootScope.pageUri; 23 | var groupPubid = $rootScope.groupPubid; 24 | 25 | var response = $http({ 26 | method: 'POST', 27 | url: settings.serviceUrl + '/reports/' + userId + '/' + groupPubid + '/' + languageId + '/' + pageId + '/' + 'addReport' 28 | }).then(function successCallback(response) { 29 | var message = 'You reported ' + 30 | userId + '\'s translations as abusive.'; 31 | alert(message); 32 | }, function errorCallback(response) { 33 | var message = 'You already reported this ' + 34 | userId + '\'s translations as abusive.'; 35 | alert(message); 36 | }); 37 | 38 | session.reload(""); 39 | 40 | return response; 41 | } 42 | 43 | 44 | return { 45 | addReport: addReport 46 | }; 47 | } 48 | 49 | module.exports = reports; -------------------------------------------------------------------------------- /annotran/static/scripts/translations.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @ngdoc service 4 | * @name translations 5 | * 6 | * @description Adds a unique translation record for a page and selected group and language. 7 | * 8 | */ 9 | 'use strict'; 10 | 11 | var eventsa = require('./events'); 12 | 13 | // @ngInject 14 | function translations(settings, session, $rootScope, $http) { 15 | 16 | function addTranslation(languageName) { 17 | var response = $http({ 18 | method: 'POST', 19 | url: settings.serviceUrl + 'translations/' + languageName + '/' + $rootScope.groupPubid + '/' + $rootScope.pageUri + '/addTranslation', 20 | }); 21 | return response; 22 | }; 23 | 24 | $rootScope.$on(eventsa.PAGE_ADDED, function (event, languageName) { 25 | addTranslation(languageName); 26 | session.reload(languageName); 27 | }); 28 | 29 | return { 30 | addTranslation: addTranslation, 31 | }; 32 | }; 33 | 34 | module.exports = translations; -------------------------------------------------------------------------------- /annotran/static/scripts/vendor/google.translate.lang.list.js: -------------------------------------------------------------------------------- 1 | (function(){function d(b){var a=document.getElementsByTagName("head")[0];a||(a=document.body.parentNode.appendChild(document.createElement("head")));a.appendChild(b)}function _loadJs(b){var a=document.createElement("script");a.type="text/javascript";a.charset="UTF-8";a.src=b;d(a)}function _loadCss(b){var a=document.createElement("link");a.type="text/css";a.rel="stylesheet";a.charset="UTF-8";a.href=b;d(a)}function _isNS(b){b=b.split(".");for(var a=window,c=0;c 2 | 3 | 4 | 5 | 6 | 7 | Server Error 8 | 58 | 59 | 60 |
    61 | 62 |

    Uh-oh, something went wrong!

    63 |

    We’re very sorry, our application wasn’t able to load this page. The 64 | team has been notified and we’ll fix it shortly.

    65 |

    If the problem persists or you'd like more information please contact {{ support_address }}.

    66 |
    67 | 68 | 69 | -------------------------------------------------------------------------------- /annotran/templates/accounts/profile.html.jinja2: -------------------------------------------------------------------------------- 1 | {% extends "h:templates/layouts/profile.html.jinja2" %} 2 | 3 | {% set page_route = 'profile' %} 4 | {% set page_title = 'Profile' %} 5 | 6 | {% block title %} 7 | Profile | Annotran 8 | {% endblock %} 9 | 10 | {% block page_content %} 11 |
    12 |

    13 | {% trans %}Change your email address{% endtrans %} 14 |

    15 | 16 | {% trans %}Your current email address is:{% endtrans %} 17 | {{ email }}. 18 | 19 | {{ email_form | safe }} 20 | 21 |

    22 | {% trans %}Change your password{% endtrans %} 23 |

    24 | {{ password_form | safe }} 25 | 26 |

    27 | {% trans %}Delete account{% endtrans %} 28 |

    29 | 30 | {% trans %} 31 | If you would like to delete your account, please email us at 32 | {{support_address}} from your 33 | registered email address, and we'll take it from there. 34 | {% endtrans %} 35 | 36 |
    37 | {% endblock page_content %} 38 | -------------------------------------------------------------------------------- /annotran/templates/admin/index.html.jinja2: -------------------------------------------------------------------------------- 1 | {% extends "h:templates/layouts/admin.html.jinja2" %} 2 | 3 | {% set page_id = 'index' %} 4 | {% set page_title = 'Administration pages' %} 5 | 6 | {% block content %} 7 |

    8 | This is the annotran administration console. 9 |

    10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /annotran/templates/admin/reports.html.jinja2: -------------------------------------------------------------------------------- 1 | {% extends "h:templates/layouts/admin.html.jinja2" %} 2 | 3 | {% set page_id = 'reports' %} 4 | {% set page_title = 'Abuse Reports' %} 5 | 6 | {% block content %} 7 |

    8 | The following translations have been reported as abusive. Please review these and decide what action to take. 9 |

    10 |

    11 | N.B. your changes will take effect immediately when you 12 | save. 13 |

    14 |
    15 |
    16 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% for translation in reports %} 31 | 32 | 35 | 38 | 41 | 44 | 45 | {% endfor %} 46 | 47 |
    PageUserReporting userAction
    33 | {{ translation.url }} 34 | 36 | {{ translation.author }} 37 | 39 | {{ translation.reporter }} 40 | 42 | View report 43 |
    48 |
    49 |
    50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /annotran/templates/admin/translation.html.jinja2: -------------------------------------------------------------------------------- 1 | {% extends "h:templates/layouts/admin.html.jinja2" %} 2 | 3 | {% set page_id = 'reports' %} 4 | {% set page_title = 'View a translation' %} 5 | 6 | {% block content %} 7 | 8 | 9 |

    Translation

    10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% for annotation in annotations %} 19 | 20 | 21 | 22 | 23 | {% endfor %} 24 |
    Original textUser translation
    {{ original[loop.index0] }}{{ annotation }}
    25 | 26 |

    Options

    27 | 33 | 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /annotran/templates/app.html.jinja2: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | Annotran 35 | {# the tag is required by Angular JS when HTML 5 history is 36 | enabled. We shouldn't really need this because a given instance of the app 37 | only ever displays a single route. 38 | #} 39 | 40 | {% for url in app_css_urls %} 41 | 42 | {% endfor %} 43 | {% for attrs in meta_attrs -%} 44 | 45 | {% endfor -%} 46 | {% if link_tags %} 47 | {% for link in link_tags %} 48 | 50 | {% endfor %} 51 | {% endif %} 52 | 53 | 54 | 65 | 66 | 67 | 74 | 75 |
    76 | 78 | 79 | 80 | 81 |
    82 |
    83 | 84 | 85 | {% for template in angular_templates -%} 86 | 89 | {% endfor -%} 90 | 91 | 92 | 95 | 96 | 97 | {% for url in app_js_urls %} 98 | 99 | {% endfor %} 100 | 101 | 102 | {% if ga_tracking_id %} 103 | 104 | 105 | 113 | 114 | {% endif %} 115 | 116 | 117 | -------------------------------------------------------------------------------- /annotran/templates/client/language_list.html: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | 29 |
    33 | 45 | 46 | 81 |
    82 | -------------------------------------------------------------------------------- /annotran/templates/client/publish_annotation_btn.html: -------------------------------------------------------------------------------- 1 |
    2 | 9 | 10 |
    11 | 37 |
    38 |
    39 | 45 | -------------------------------------------------------------------------------- /annotran/templates/client/share_dialog.html: -------------------------------------------------------------------------------- 1 | 29 | 30 |
    31 | 35 |
    36 | 39 |
    40 |

    Share the link below to show anyone these translations and invite them to contribute their own.

    41 |

    46 | 63 |
    64 |
    65 |
    66 | -------------------------------------------------------------------------------- /annotran/templates/client/sidebar_tutorial.html: -------------------------------------------------------------------------------- 1 |
    2 | 4 |

    How to get started

    5 | 33 |
    34 | -------------------------------------------------------------------------------- /annotran/templates/client/signin_control.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 |
    13 | 18 | 20 | 21 | 41 |
    42 | 43 | 44 | 45 | 46 | Sign in 47 | 48 |
    52 | 53 | {{vm.auth.username}}/{{vm.auth.provider}} 57 | 58 | 72 |
    73 | -------------------------------------------------------------------------------- /annotran/templates/client/top_bar.html: -------------------------------------------------------------------------------- 1 | 26 | 27 | 29 | 30 | 32 |
    33 | 34 |
    35 | 42 | 43 |
    44 | 49 | 50 |
    51 | 56 |
    57 | 58 | 59 |
    60 | 67 | 68 | 74 | 75 | 80 | 81 |
    82 |
    83 | -------------------------------------------------------------------------------- /annotran/templates/client/user_list.html: -------------------------------------------------------------------------------- 1 |
    5 |
    6 | 29 |
    30 |
    31 |

    Before you can create or view a translation, you must select or add a language above.

    32 |
    33 |
    34 |

    35 | 36 | Sign in to write your own translation. 37 | 38 |

    39 |
    40 |

    41 |

    42 |
    43 |

    44 | There are no available translations. 45 |

    46 |

    47 | 48 | View the original document. 49 |

    50 |
    51 |
    52 |

    53 | Select a user below to view a translation of this page: 54 |

    55 |
    56 |

    57 | Sort by score: descending 58 |

    59 |

    60 | Sort by score: ascending 61 |

    62 | 91 |
    92 |
    93 |
    94 |
    95 |
    96 | -------------------------------------------------------------------------------- /annotran/templates/community-guidelines.html.jinja2: -------------------------------------------------------------------------------- 1 | 26 | 27 | {% extends "../../../h/h/templates/layouts/base.html.jinja2" %} 28 | 29 | {% block title %}Annotran | Community Guidelines.{% endblock %} 30 | 31 | {% block styles %} 32 | {% assets "help_page_css" %} 33 | 34 | {% endassets %} 35 | 44 | {% endblock %} 45 | 46 | 47 | 48 | {% block content %} 49 |
    50 |
    51 | {% include "includes/logo-header.html.jinja2" %} 52 |
    53 |
    54 |
    55 |

    Annotran Community Guidelines

    56 |
    57 |
    58 |

    Annotran Community Guidelines

    59 |

    Annotran is an open platform for translating web resources. We believe translations can improve conversations on the web and we hope communities thrive by sharing knowledge and collaborating across linguistic boundaries.

    60 |

    We have a set of guidelines for participating in public translations through annotran. By Public, we mean translations that are published to the Annotran Public channel.

    61 |
    62 |
    63 |

    Things you should do:

    64 |
      65 |
    • Be civil. When there’s disagreement, be respectful.
    • 66 |
    • Make sure your translations are constructive, informative and well-intentioned.
    • 67 |
    68 |
    69 |
    70 |

    Things you shouldn’t do:

    71 |
      72 |
    • Engage in illegal activity.
    • 73 |
    • Circumvent copyright or post copyrighted works in translations.
    • 74 |
    • Post translations of works that are under copyright without an open license.
    • 75 |
    • Engage in personal attacks, trolling or harassment against other users.
    • 76 |
    • Engage in marketing or advertising of your own offerings or others’.
    • 77 |
    • Post explicit content.
    • 78 |
    • Clutter the public channel with test messages.
    • 79 |
    80 |
    81 |
    82 |

    Use of the Annotran service is a privilege, not a right. We reserve the right to limit, suspend or terminate users who violate these guidelines.

    83 |
    84 |
    85 |
    86 | {% endblock %}} -------------------------------------------------------------------------------- /annotran/templates/groups/about-groups.html.jinja2: -------------------------------------------------------------------------------- 1 | 26 | 27 |

    Groups let you translate content on the web privately.

    28 | -------------------------------------------------------------------------------- /annotran/templates/groups/share.html.jinja2: -------------------------------------------------------------------------------- 1 | 26 | 27 | {% extends "h:templates/layouts/base.html.jinja2" %} 28 | 29 | {% block page_title %}{{ group.name }}{% endblock page_title %} 30 | 31 | {% block styles %} 32 | {% assets "site_css" %} 33 | 34 | {% endassets %} 35 | {% endblock %} 36 | 37 | {% block content %} 38 |
    39 |
    40 | 41 |
    {{ group.name }}
    42 | {% if document_links %} 43 | 46 | View recent group translations 47 | {% else %} 48 |

    This group has not shared any translations yet.

    49 | {% endif %} 50 |
    51 | {% if document_links %} 52 |
    53 |
      54 |

      Group documents:

      55 | {% for document_link in document_links %} 56 |
    • {{ document_link }}
    • 57 | {% endfor %} 58 |
    59 |
    60 | {% endif %} 61 | 90 |
    91 | {% endblock content %} 92 | 93 | {% block scripts %} 94 | {% assets "site_js" %} 95 | 96 | {% endassets %} 97 | {% endblock scripts %} 98 | -------------------------------------------------------------------------------- /annotran/templates/home.html.jinja2: -------------------------------------------------------------------------------- 1 | {% set rss_feed_url = base_url + "feed/" %} 2 | 3 | {% extends "h:templates/layouts/base.html.jinja2" %} 4 | 5 | {% block title %}Annotran | Translate by annotating.{% endblock %} 6 | 7 | {% block meta %} 8 | {{ super() }} 9 | 10 | 12 | 13 | {% endblock %} 14 | 15 | {% block styles %} 16 | {% assets "front_page_css" %} 17 | 18 | {% endassets %} 19 | {% endblock %} 20 | 21 | {% block content %} 22 | 35 | 62 | 63 |
    64 |
    65 |
    66 |
    67 |
    68 |
    69 |

    Translate with anyone, anywhere

    70 |

    71 | Annotran uses the annotation technology of hypothes.is to allow anyone to translate a web page. 72 |

    73 |

    Annotran is developed by the Open Library of Humanities 74 | at Birkbeck with generous support from the Andrew W. Mellon Foundation. 75 |

    76 | 77 |

    Please note: Annotran is currently in a public alpha phase. We give no guarantees that your translations will not be deleted.

    78 | 79 |

    80 | Use 81 | 82 | bookmarklet 83 | 84 | to add Annotran launch feature to your browser. 85 |

    86 | 87 | 88 | 117 | 118 | 119 |
    120 |
    121 |
    122 |
    123 |
    124 |
    125 | 126 | {% endblock %} 127 | 128 | {% block scripts %} 129 | {% assets "front_page_js" %} 130 | 131 | {% endassets %} 132 | {% endblock %} -------------------------------------------------------------------------------- /annotran/templates/includes/logo-header.html.jinja2: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | annotran 4 |
    5 |
    6 | -------------------------------------------------------------------------------- /annotran/templates/layouts/admin.html.jinja2: -------------------------------------------------------------------------------- 1 | {%- set nav_pages = [ 2 | ('index', 'admin_index', 'Home'), 3 | ('reports', 'admin_reports', 'Reports'), 4 | ('admins', 'admin_admins', 'Administrators'), 5 | ('staff', 'admin_staff', 'Staff'), 6 | ('users', 'admin_users', 'Users'), 7 | ('groups', 'admin_groups', 'Groups'), 8 | ] -%} 9 | 10 | {%- set page_id = page_id|default('home') -%} 11 | {%- set page_title = page_title|default('Administration pages') -%} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Annotran: {{ page_title }} 22 | 23 | 24 | {% block styles %} 25 | {% assets "admin_css" %}{% endassets %} 26 | {% endblock %} 27 | 28 | 29 | 55 | 56 |
    57 |
    58 |
    59 | {% include "h:templates/includes/flashbar.html.jinja2" %} 60 |

    {{ page_title }}

    61 | {% block content %}{% endblock %} 62 |
    63 |
    64 |
    65 | 66 | {% block scripts %} 67 | {% assets "admin_js" %}{% endassets %} 68 | {% endblock %} 69 | 70 | 71 | -------------------------------------------------------------------------------- /annotran/templates/layouts/base.html.jinja2: -------------------------------------------------------------------------------- 1 | 26 | 27 | {% extends "../../../../h/h/templates/layouts/base.html.jinja2" %} 28 | 29 | 30 | {%- block title -%} 31 | {%- if self.page_title %}{{self.page_title()}} | {% endif -%} 32 | Annotran 33 | {%- endblock -%} -------------------------------------------------------------------------------- /annotran/templates/notfound.html.jinja2: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | 29 | {% extends "layouts/base.html.jinja2" %} 30 | 31 | {% block title %}Page Not Found{% endblock %} 32 | 33 | {% block styles %} 34 | {% assets "app_css" %} 35 | 36 | {% endassets %} 37 | {% endblock %} 38 | 39 | {% block content %} 40 |
    41 |

    There’s nothing here!

    42 |

    Either this page doesn’t exist, or you don’t have the permissions required for viewing it.

    43 |
    44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /annotran/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/annotran/test/__init__.py -------------------------------------------------------------------------------- /annotran/tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pyramid import testing 4 | 5 | 6 | class ViewTests(unittest.TestCase): 7 | def setUp(self): 8 | self.config = testing.setUp() 9 | 10 | def tearDown(self): 11 | testing.tearDown() 12 | 13 | def test_my_view(self): 14 | from .views import my_view 15 | request = testing.DummyRequest() 16 | info = my_view(request) 17 | self.assertEqual(info['project'], 'annotran') 18 | -------------------------------------------------------------------------------- /annotran/translations/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def includeme(config): 5 | config.include('.views') 6 | -------------------------------------------------------------------------------- /annotran/translations/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import annotran 5 | import annotran.languages.models as lang_models 6 | import annotran.pages.models 7 | import h 8 | import h.accounts.models 9 | import h.groups.models 10 | import sqlalchemy as sa 11 | from h.db import Base 12 | from sqlalchemy.orm import exc 13 | from sqlalchemy import and_ 14 | 15 | 16 | class Translation(Base): 17 | """ 18 | Represents a translation in the database 19 | """ 20 | __tablename__ = 'translation' 21 | 22 | page_id = sa.Column(sa.Integer, sa.ForeignKey(annotran.pages.models.Page.id), primary_key=True) 23 | page = sa.orm.relationship('Page', backref='page_translation') 24 | 25 | language_id = sa.Column(sa.Integer, sa.ForeignKey(annotran.languages.models.Language.id), primary_key=True) 26 | language = sa.orm.relationship('Language', backref='language_translation') 27 | 28 | group_id = sa.Column(sa.Integer, sa.ForeignKey(h.groups.models.Group.id), primary_key=True) 29 | group = sa.orm.relationship('Group', backref='group_translation') 30 | 31 | def __init__(self, page, language, group): 32 | """ 33 | Initializes a translation 34 | :param page: the page 35 | :param language: the language 36 | :param group: the group 37 | """ 38 | if group and page and language: 39 | self.page_id = page.id 40 | self.language_id = language.id 41 | self.group_id = group.id 42 | 43 | @classmethod 44 | def get_translation(cls, page, language, group): 45 | """ 46 | Retrieve a translation by page, language and group 47 | :param page: the page ID 48 | :param language: the language ID 49 | :param group: the group ID 50 | :return: a translation or None 51 | """ 52 | try: 53 | if group is None: 54 | return cls.query.filter( 55 | cls.page_id == page.id, 56 | cls.language_id == language.id, 57 | cls.group_id == -1).one() 58 | else: 59 | return cls.query.filter( 60 | cls.page_id == page.id, 61 | cls.language_id == language.id, 62 | cls.group_id == group.id).one() 63 | except exc.NoResultFound: 64 | return None 65 | 66 | @classmethod 67 | def get_public_translations(cls, page): 68 | """ 69 | Get a list of public translations for a page 70 | :param page: the page to query 71 | :return: a list of languages or an empty list 72 | """ 73 | try: 74 | return lang_models.Language.query.\ 75 | join(cls, and_(cls.page_id == page.id, 76 | cls.group_id == -1, 77 | lang_models.Language.id == cls.language_id)).\ 78 | with_entities(lang_models.Language.id, 79 | lang_models.Language.name, 80 | lang_models.Language.pubid, 81 | cls.group_id).all() 82 | except exc.NoResultFound: 83 | return None 84 | 85 | @classmethod 86 | def get_page_translations(cls, page): 87 | """ 88 | Get a list of all translations for a page 89 | :param page: the page to query 90 | :return: a list of languages or an empty list 91 | """ 92 | try: 93 | return lang_models.Language.query.\ 94 | join(cls, and_(cls.page_id == page.id, 95 | lang_models.Language.id == cls.language_id)).\ 96 | with_entities(lang_models.Language.id, 97 | lang_models.Language.name, 98 | lang_models.Language.pubid, 99 | cls.group_id).all() 100 | except exc.NoResultFound: 101 | return None 102 | 103 | ''' 104 | We currently do not support deletion of added translation. 105 | This means once language is added for a page and a group, it will be there for all 106 | authorised users even without any translations on the page. 107 | The only way to remove such "empty" translations is for admin to directly remove them from the database. 108 | ''' 109 | @classmethod 110 | def delete_translation(cls, page, language, group): 111 | """ 112 | Delete all translation for a page, language and group 113 | :param page: the page ID 114 | :param language: the language ID 115 | :param group: the group ID 116 | :return: the result of the delete method or None 117 | """ 118 | return None 119 | 120 | @classmethod 121 | def get_by_composite_id(cls, page_id, language_id, group_id): 122 | """ 123 | Get a translation by a composite ID 124 | :param page_id, language_id, group_id: IDs to query 125 | :return: a translation or None 126 | """ 127 | try: 128 | return cls.query.filter( 129 | cls.page_id == page_id, 130 | cls.language_id == language_id, 131 | cls.group_id == group_id).one() 132 | except exc.NoResultFound: 133 | return None 134 | -------------------------------------------------------------------------------- /annotran/translations/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/annotran/translations/test/__init__.py -------------------------------------------------------------------------------- /annotran/translations/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import urllib 3 | 4 | from pyramid import httpexceptions as exc 5 | from pyramid.view import view_config 6 | from h import i18n 7 | from annotran.util import util 8 | 9 | import h 10 | import annotran 11 | import h.groups.models 12 | import annotran.pages.models 13 | import annotran.groups.views 14 | 15 | from annotran.languages.models import Language as lang_models 16 | from annotran.translations.models import Translation as tran_models 17 | 18 | _ = i18n.TranslationString 19 | 20 | 21 | @view_config(route_name='add_translation', 22 | request_method='POST') 23 | def add_translation(request): 24 | """ 25 | This view adds a translation 26 | :param request: a request object 27 | :return: a redirect to the translation_read method 28 | 29 | """ 30 | if request.authenticated_userid is None: 31 | raise exc.HTTPNotFound() 32 | 33 | name = request.matchdict["language"] 34 | public_group_id = request.matchdict["public_group_id"] 35 | page_url = urllib.unquote(urllib.unquote(request.matchdict["page_url"])) 36 | page = annotran.pages.models.Page.get_by_uri(page_url) 37 | 38 | group = h.groups.models.Group.get_by_pubid(public_group_id) 39 | language = annotran.languages.models.Language.get_by_name(name) 40 | 41 | translation = None 42 | if page and language and group: 43 | translation = annotran.translations.models.Translation.get_by_composite_id(page.id, language.id, group.id) 44 | else: 45 | return {} 46 | 47 | if translation is None: 48 | translation = annotran.translations.models.Translation(page=page, language=language, group=group) 49 | request.db.add(translation) 50 | request.db.flush() 51 | 52 | url = request.route_url('translation_read', public_language_id=language.pubid, public_group_id=public_group_id) 53 | return exc.HTTPSeeOther(url) 54 | 55 | 56 | @view_config(route_name='translation_read', request_method='GET') 57 | def read(request): 58 | """ 59 | Read the list of languages available in a group 60 | :param request: the request object 61 | :return: a list of languages in a group 62 | """ 63 | public_language_id = request.matchdict["public_language_id"] 64 | language = annotran.languages.models.Language.get_by_public_language_id(public_language_id) 65 | public_group_id = request.matchdict["public_group_id"] 66 | group = h.groups.models.Group.get_by_pubid(public_group_id) 67 | 68 | if group.id == -1: 69 | # this is the public group 70 | return annotran.groups.views.read_group(request, group, language=language) 71 | if not request.authenticated_userid: 72 | return None 73 | else: 74 | if group in request.authenticated_user.groups: 75 | return annotran.groups.views.read_group(request, group, language=language) 76 | else: 77 | return None 78 | 79 | 80 | def includeme(config): 81 | """ 82 | Pyramid's router configuration 83 | :param config: the config to which to commit the routes 84 | :return: None 85 | """ 86 | config.add_route('add_translation', 'translations/{language}/{public_group_id}/{page_url}/addTranslation') 87 | config.add_route('translation_read', '/languages/{public_language_id}/{public_group_id}') 88 | config.scan(__name__) -------------------------------------------------------------------------------- /annotran/util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/annotran/util/__init__.py -------------------------------------------------------------------------------- /annotran/util/util.py: -------------------------------------------------------------------------------- 1 | import re 2 | import urllib 3 | 4 | 5 | def strip_logout(url): 6 | """ 7 | Removes the in-built hypothes.is-appended &__formid__=logout from a URL 8 | :param url: the URL 9 | :return: a clean URL 10 | """ 11 | try: 12 | url = re.sub('&__formid__=logout$', '', url).split("#")[0] 13 | except IndexError: 14 | url = '' 15 | return url 16 | 17 | 18 | def get_url_from_request(request): 19 | """ 20 | Extracts a URL from a request and removes the in-built hypothes.is-appended &__formid__=login 21 | :param request: the request 22 | :return: a clean URL 23 | """ 24 | try: 25 | url = urllib.unquote( 26 | urllib.unquote(request.url.split('?')[1].replace('url=', '')).split('?')[1].replace('url=', '')) 27 | url = re.sub('&__formid__=login$', '', url).split("#")[0] 28 | except IndexError: 29 | url = '' 30 | return url 31 | -------------------------------------------------------------------------------- /annotran/views.py: -------------------------------------------------------------------------------- 1 | from h import i18n 2 | from pyramid.view import (view_config) 3 | 4 | _ = i18n.TranslationStringFactory(__package__) 5 | 6 | 7 | @view_config(route_name='termsofservice', request_method='GET', 8 | renderer='annotran:templates/terms-of-service.html.jinja2') 9 | def terms_of_service(context, request): 10 | """Display the terms of service.""" 11 | return {'support_address': Shared.support_address} 12 | 13 | 14 | @view_config(route_name='communityguidelines', request_method='GET', 15 | renderer='annotran:templates/community-guidelines.html.jinja2') 16 | def community_guidelines(context, request): 17 | """Display the terms of service.""" 18 | return {'support_address': Shared.support_address} 19 | 20 | 21 | @view_config(route_name='privacypolicy', request_method='GET', 22 | renderer='annotran:templates/privacy-policy.html.jinja2') 23 | def privacy_policy(context, request): 24 | """Display the terms of service.""" 25 | return {'support_address': Shared.support_address} 26 | 27 | 28 | def includeme(config): 29 | """ 30 | Pyramid includeme setup method to add routes 31 | :param config: the configuration supplied by pyramid 32 | :return: None 33 | """ 34 | config.add_route('termsofservice', '/terms-of-service') 35 | config.add_route('communityguidelines', '/community-guidelines') 36 | config.add_route('privacypolicy', '/privacy-policy') 37 | config.scan(__name__) 38 | 39 | 40 | class Shared(object): 41 | def __init__(self): 42 | pass 43 | 44 | support_address = "" 45 | -------------------------------------------------------------------------------- /annotran/votes/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def includeme(config): 5 | config.include('.views') 6 | -------------------------------------------------------------------------------- /annotran/votes/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/annotran/votes/test/__init__.py -------------------------------------------------------------------------------- /annotran/votes/views.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | import annotran 4 | import annotran.languages.models 5 | import annotran.pages.models 6 | import h 7 | import h.groups.models 8 | import h.models 9 | import models 10 | from h import i18n 11 | from pyramid import httpexceptions as exc 12 | from pyramid.view import view_config 13 | 14 | _ = i18n.TranslationString 15 | 16 | 17 | @view_config(route_name='vote_add', request_method='POST') 18 | def add_vote(request): 19 | """ 20 | Add a vote to the database 21 | :param request: the current request object 22 | :return: a redirect to language read 23 | """ 24 | voter = request.authenticated_user 25 | if voter is None: 26 | raise exc.HTTPNotFound() 27 | 28 | page_uri = urllib.unquote(urllib.unquote(request.matchdict["page_uri"])) 29 | public_group_id = request.matchdict['public_group_id'] 30 | public_language_id = request.matchdict["public_language_id"] 31 | score = request.matchdict["score"] 32 | username = request.matchdict['username'] 33 | 34 | author = h.models.User.get_by_username(username) 35 | 36 | group = h.groups.models.Group.get_by_pubid(public_group_id) 37 | 38 | page = annotran.pages.models.Page.get_by_uri(page_uri) 39 | 40 | voter = h.models.User.get_by_username(request.authenticated_user.username) 41 | 42 | language = annotran.languages.models.Language.get_by_public_language_id(public_language_id) 43 | 44 | if language is None or page is None or \ 45 | group is None or author is None: 46 | raise exc.HTTPNotFound() 47 | 48 | vote = models.Vote.get_vote(page, language, group, author, voter) 49 | 50 | # storing last selected value only 51 | if vote: 52 | request.db.delete(vote) 53 | request.db.flush() 54 | vote = models.Vote(score, page, language, group, author, voter) 55 | request.db.add(vote) 56 | request.db.flush() 57 | 58 | url = request.route_url('translation_read', public_language_id=language.pubid, public_group_id=public_group_id) 59 | return exc.HTTPSeeOther(url) 60 | 61 | 62 | @view_config(route_name='vote_delete', request_method='POST', renderer='annotran:templates/home.html.jinja2') 63 | def delete_vote(request): 64 | """ 65 | Delete a vote from the database 66 | :param request: the current request object 67 | :return: a redirect to language read 68 | """ 69 | if request.authenticated_user is None: 70 | raise exc.HTTPNotFound() 71 | 72 | public_language_id = request.matchdict["public_language_id"] 73 | public_group_id = request.matchdict['public_group_id'] 74 | page_uri = urllib.unquote(urllib.unquote(request.matchdict['page_uri'])) 75 | 76 | page = annotran.pages.models.Page.get_by_uri(page_uri) 77 | language = annotran.languages.models.Language.get_by_public_language_id(public_language_id) 78 | 79 | # only authenticated used can delete translations and consequently their scores 80 | user = h.models.User.get_by_username(request.authenticated_user.username) 81 | group = h.groups.models.Group.get_by_pubid(public_group_id) 82 | 83 | if language is None or page is None \ 84 | or group is None or user is None: 85 | raise exc.HTTPNotFound() 86 | 87 | models.Vote.delete_votes(page, language, group, user) 88 | request.db.flush() 89 | 90 | return {} 91 | 92 | 93 | def includeme(config): 94 | """ 95 | Pyramid's router configuration 96 | :param config: the configuration object to which to add our routes 97 | :return: None 98 | """ 99 | config.add_route('vote_add', 'votes/{username}/{public_group_id}/{public_language_id}/{page_uri}/{score}/addVote') 100 | config.add_route('vote_delete', 'votes/{public_group_id}/{public_language_id}/{page_uri}/deleteVote') 101 | config.scan(__name__) 102 | -------------------------------------------------------------------------------- /conf/development.ini: -------------------------------------------------------------------------------- 1 | ### 2 | # app configuration 3 | # http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/environment.html 4 | ### 5 | 6 | [pipeline:main] 7 | pipeline: annotran 8 | 9 | [app:annotran] 10 | use = egg:annotran 11 | 12 | h.db.should_create_all: True 13 | h.search.autoconfig: True 14 | 15 | annotran.app.support_address: martin@martineve.com 16 | 17 | es.host: http://localhost:9200 18 | 19 | mail.default_sender: "Annotation Daemon" 20 | mail.host = localhost 21 | mail.port = 25 22 | 23 | pyramid.debug_all: True 24 | pyramid.reload_templates: True 25 | pyramid.includes: 26 | pyramid_debugtoolbar 27 | pyramid_mailer 28 | pyramid_tm 29 | h.debug 30 | h.session 31 | 32 | h.client_id: nosuchid 33 | h.client_secret: nosuchsecret 34 | 35 | secret_key: notverysecretafterall 36 | 37 | sqlalchemy.url: postgresql://postgres@localhost/postgres 38 | 39 | #sqlalchemy.url = sqlite:///%(here)s/annotran.sqlite 40 | 41 | webassets.base_dir: h:static 42 | webassets.base_url: assets 43 | webassets.cache: False 44 | webassets.debug: True 45 | webassets.manifest: False 46 | webassets.static_view: True 47 | 48 | # By default, the toolbar only appears for clients from IP addresses 49 | # '127.0.0.1' and '::1'. 50 | # debugtoolbar.hosts = 127.0.0.1 ::1 51 | 52 | ;http://docs.pylonsproject.org/projects/pyramid-debugtoolbar/en/latest/#settings 53 | debugtoolbar.show_on_exc_only: True 54 | 55 | ### 56 | # wsgi server configuration 57 | ### 58 | 59 | [server:main] 60 | use: egg:gunicorn 61 | host: localhost 62 | port: 5000 63 | graceful_timeout: 0 64 | timeout: 0 65 | errorlog: - 66 | reload: True 67 | 68 | ### 69 | # logging configuration 70 | # http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/logging.html 71 | ### 72 | 73 | [loggers] 74 | keys = root, gunicorn.error, annotran 75 | 76 | [handlers] 77 | keys = console 78 | 79 | [formatters] 80 | keys = generic 81 | 82 | [logger_root] 83 | level = INFO 84 | handlers = console 85 | 86 | [logger_gunicorn.error] 87 | level = INFO 88 | handlers = 89 | qualname = gunicorn.error 90 | 91 | [logger_annotran] 92 | level = INFO 93 | handlers = 94 | qualname = annotran 95 | 96 | [logger_sqlalchemy] 97 | level = INFO 98 | handlers = 99 | qualname = sqlalchemy.engine 100 | # "level = INFO" logs SQL queries. 101 | # "level = DEBUG" logs SQL queries and results. 102 | # "level = WARN" logs neither. (Recommended for production systems.) 103 | 104 | [handler_console] 105 | class = StreamHandler 106 | args = () 107 | formatter = generic 108 | 109 | [formatter_generic] 110 | format = %(asctime)s [%(process)d] [%(name)s:%(levelname)s] %(message)s 111 | 112 | -------------------------------------------------------------------------------- /conf/production.ini: -------------------------------------------------------------------------------- 1 | ### 2 | # app configuration 3 | # http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/environment.html 4 | ### 5 | 6 | [app:main] 7 | pyramid.includes: 8 | pyramid_jinja2 9 | pyramid_mailer 10 | pyramid_tm 11 | h.session 12 | 13 | use = egg:annotran 14 | 15 | pyramid.reload_templates = false 16 | pyramid.debug_authorization = false 17 | pyramid.debug_notfound = false 18 | pyramid.debug_routematch = false 19 | pyramid.default_locale_name = en 20 | 21 | h.db.should_create_all: True 22 | h.search.autoconfig: True 23 | 24 | annotran.app.support_address: martin.eve@openlibhums.org 25 | 26 | es.host: http://localhost:9200 27 | 28 | mail.default_sender: "Annotation Daemon" 29 | 30 | h.client_id: nosuchid 31 | h.client_secret: nosuchsecret 32 | 33 | secret_key: notverysecretafterall 34 | 35 | sqlalchemy.url: postgresql://postgres@localhost/postgres 36 | 37 | #sqlalchemy.url = sqlite:///%(here)s/annotran.sqlite 38 | 39 | webassets.base_dir: h:static 40 | webassets.base_url: assets 41 | webassets.cache: False 42 | webassets.debug: True 43 | webassets.manifest: False 44 | webassets.static_view: True 45 | 46 | [server:main] 47 | use: egg:gunicorn 48 | host = 127.0.0.1 49 | port = 5000 50 | 51 | ### 52 | # logging configuration 53 | # http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/logging.html 54 | ### 55 | 56 | [loggers] 57 | keys = root, annotran, sqlalchemy 58 | 59 | [handlers] 60 | keys = console 61 | 62 | [formatters] 63 | keys = generic 64 | 65 | [logger_root] 66 | level = WARN 67 | handlers = console 68 | 69 | [logger_annotran] 70 | level = WARN 71 | handlers = 72 | qualname = annotran 73 | 74 | [logger_sqlalchemy] 75 | level = WARN 76 | handlers = 77 | qualname = sqlalchemy.engine 78 | # "level = INFO" logs SQL queries. 79 | # "level = DEBUG" logs SQL queries and results. 80 | # "level = WARN" logs neither. (Recommended for production systems.) 81 | 82 | [handler_console] 83 | class = StreamHandler 84 | args = (sys.stderr,) 85 | level = NOTSET 86 | formatter = generic 87 | 88 | [formatter_generic] 89 | format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s 90 | -------------------------------------------------------------------------------- /conf/test.ini: -------------------------------------------------------------------------------- 1 | ;Copyright (c) 2013-2014 Hypothes.is Project and contributors 2 | 3 | ;Redistribution and use in source and binary forms, with or without 4 | ;modification, are permitted provided that the following conditions are met: 5 | 6 | ;1. Redistributions of source code must retain the above copyright notice, this 7 | ; list of conditions and the following disclaimer. 8 | ;2. Redistributions in binary form must reproduce the above copyright notice, 9 | ; this list of conditions and the following disclaimer in the documentation 10 | ; and/or other materials provided with the distribution. 11 | 12 | ;THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 13 | ;ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 14 | ;WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 15 | ;DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 16 | ;ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 17 | ;(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 18 | ;LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 19 | ;ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 20 | ;(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 21 | ;SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | ; 23 | # annotran's version of test.ini 24 | 25 | [app:main] 26 | use: egg:annotran 27 | 28 | sqlalchemy.url: postgresql://postgres@localhost/annotrantest 29 | 30 | webassets.base_dir: annotran:static 31 | webassets.base_url: assets 32 | webassets.coffee_no_bare: True 33 | webassets.cache: False 34 | webassets.debug: True 35 | webassets.manifest: False 36 | webassets.static_view: True 37 | 38 | 39 | [loggers] 40 | keys = root, annotran 41 | 42 | 43 | [handlers] 44 | keys = console 45 | 46 | 47 | [formatters] 48 | keys = generic 49 | 50 | 51 | [logger_root] 52 | handlers = console 53 | 54 | 55 | [logger_annotran] 56 | level = INFO 57 | handlers = 58 | qualname = annotran 59 | 60 | 61 | [handler_console] 62 | class = StreamHandler 63 | args = () 64 | formatter = generic 65 | 66 | 67 | [formatter_generic] 68 | format = %(asctime)s [%(process)d] [%(name)s:%(levelname)s] %(message)s 69 | -------------------------------------------------------------------------------- /docs/QuickStart.odt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/docs/QuickStart.odt -------------------------------------------------------------------------------- /docs/QuickStartGuide.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/docs/QuickStartGuide.pdf -------------------------------------------------------------------------------- /docs/images/Adder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/docs/images/Adder.png -------------------------------------------------------------------------------- /docs/images/Blanksidebar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/docs/images/Blanksidebar.png -------------------------------------------------------------------------------- /docs/images/Bookmarklet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/docs/images/Bookmarklet.png -------------------------------------------------------------------------------- /docs/images/Dropdownlanguage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/docs/images/Dropdownlanguage.png -------------------------------------------------------------------------------- /docs/images/Rateandreport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/docs/images/Rateandreport.png -------------------------------------------------------------------------------- /docs/images/Selecteditmode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/docs/images/Selecteditmode.png -------------------------------------------------------------------------------- /docs/images/Sidebarsignedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/docs/images/Sidebarsignedin.png -------------------------------------------------------------------------------- /docs/images/Substitution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/docs/images/Substitution.png -------------------------------------------------------------------------------- /docs/images/Userstranslations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/docs/images/Userstranslations.png -------------------------------------------------------------------------------- /docs/images/Viewmodeclicktochange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/docs/images/Viewmodeclicktochange.png -------------------------------------------------------------------------------- /docs/images/Writetranslation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openlibhums/annotran/42678afaee6d4b57cfaddb402bc6f15b37fdd027/docs/images/Writetranslation.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "h", 3 | "private": true, 4 | "version": "0.0.0", 5 | "description": "The Internet, peer reviewed.", 6 | "dependencies": { 7 | "angular": "1.4.7", 8 | "angular-animate": "1.4.7", 9 | "angular-jwt": "0.0.9", 10 | "angular-mocks": "1.4.7", 11 | "angular-resource": "1.4.7", 12 | "angular-route": "1.4.7", 13 | "angular-sanitize": "1.4.7", 14 | "angular-toastr": "^1.5.0", 15 | "angulartics": "0.17.2", 16 | "autofill-event": "0.0.1", 17 | "autoprefixer": "^6.0.3", 18 | "babelify": "^6.1.3", 19 | "bootstrap": "3.3.5", 20 | "browserify": "^9.0.3", 21 | "browserify-ngannotate": "^1.0.1", 22 | "browserify-shim": "^3.8.3", 23 | "clean-css": "3.3.9", 24 | "coffee-script": "1.7.1", 25 | "coffeeify": "^1.0.0", 26 | "core-js": "^1.2.5", 27 | "diff-match-patch": "^1.0.0", 28 | "document-base-uri": "^1.0.0", 29 | "dom-anchor-fragment": "^1.0.1", 30 | "dom-anchor-text-position": "^2.0.0", 31 | "dom-anchor-text-quote": "^2.0.0", 32 | "dom-seek": "^1.0.1", 33 | "es6-promise": "^3.0.2", 34 | "escape-html": "^1.0.3", 35 | "extend": "^2.0.0", 36 | "hammerjs": "^2.0.4", 37 | "inherits": "^2.0.1", 38 | "is-equal-shallow": "^0.1.3", 39 | "jquery": "1.11.1", 40 | "js-polyfills": "^0.1.11", 41 | "ng-tags-input": "2.2.0", 42 | "node-uuid": "^1.4.3", 43 | "page": "^1.6.4", 44 | "postcss": "^5.0.6", 45 | "raf": "^3.1.0", 46 | "raven-js": "^2.0.2", 47 | "retry": "^0.8.0", 48 | "scroll-into-view": "^1.3.1", 49 | "showdown": "^1.2.1", 50 | "tiny-emitter": "^1.0.1", 51 | "uglify-js": "^2.4.14", 52 | "unorm": "^1.3.3" 53 | }, 54 | "devDependencies": { 55 | "chai": "^3.2.0", 56 | "jscs": "^1.13.1", 57 | "karma": "^0.13.10", 58 | "karma-browserify": "^3.0.3", 59 | "karma-chai": "^0.1.0", 60 | "karma-cli": "0.0.4", 61 | "karma-mocha": "^0.1.4", 62 | "karma-ng-html2js-preprocessor": "^0.1.0", 63 | "karma-phantomjs-launcher": "^0.1.4", 64 | "karma-sinon": "^1.0.4", 65 | "mocha": "^1.20.1", 66 | "phantom-ownpropertynames": "^1.0.0", 67 | "phantomjs": "^1.9.7", 68 | "proxyquire": "^1.6.0", 69 | "proxyquire-universal": "^1.0.8", 70 | "proxyquireify": "^3.0.0", 71 | "sinon": "1.16.1", 72 | "whatwg-fetch": "^0.10.1" 73 | }, 74 | "engines": { 75 | "node": "0.10.x" 76 | }, 77 | "scripts": { 78 | "test": "echo \"Error: no test specified\" && exit 1" 79 | }, 80 | "repository": { 81 | "type": "git", 82 | "url": "https://github.com/hypothesis/h.git" 83 | }, 84 | "license": "Simplified BSD License", 85 | "bugs": { 86 | "url": "https://github.com/hypothesis/h/issues" 87 | }, 88 | "homepage": "https://github.com/hypothesis/h", 89 | "browserify": { 90 | "transform": [ 91 | "browserify-ngannotate", 92 | "browserify-shim" 93 | ] 94 | }, 95 | "browser": { 96 | "annotator": "../h/h/static/scripts/vendor/annotator.js", 97 | "angular": "./node_modules/angular/angular.js", 98 | "hammerjs": "./node_modules/hammerjs/hammer.js", 99 | "jquery": "./node_modules/jquery/dist/jquery.js" 100 | }, 101 | "browserify-shim": { 102 | "annotator": { 103 | "exports": "Annotator", 104 | "depends": [ 105 | "jquery" 106 | ] 107 | }, 108 | "angular": { 109 | "exports": "global:angular", 110 | "depends": [ 111 | "jquery" 112 | ] 113 | }, 114 | "hammerjs": "Hammer", 115 | "jquery": "$" 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | -e .[YAML] 3 | certifi 4 | cffi 5 | gunicorn 6 | pyramid_redis_sessions 7 | requests[security] 8 | wsaccel 9 | h 10 | waitress -------------------------------------------------------------------------------- /scripts/postcss-filter.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // post-process CSS using PostCSS 4 | // (https://github.com/postcss/postcss) 5 | // 6 | // This adds vendor prefixes using autoprefixer 7 | // https://github.com/postcss/autoprefixer 8 | 9 | require('es6-promise').polyfill(); 10 | 11 | var autoprefixer = require('autoprefixer'); 12 | var postcss = require('postcss'); 13 | 14 | var inputCss = ''; 15 | 16 | process.stdin.on('data', function (chunk) { 17 | inputCss += chunk; 18 | }); 19 | 20 | process.stdin.on('end', function () { 21 | postcss([autoprefixer]) 22 | .process(inputCss) 23 | .then(function (result) { 24 | console.log(result.css); 25 | }); 26 | }); 27 | 28 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [easy_install] 2 | zip_ok = false 3 | 4 | [pytest] 5 | norecursedirs = env lib node_modules *.egg 6 | 7 | [pep257] 8 | ignore = D202 9 | explain = true 10 | 11 | [yapf] 12 | based_on_style = pep8 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright (c) 2013-2014 Hypothes.is Project and contributors 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | ''' 24 | 25 | # -*- coding: utf-8 -*- 26 | from __future__ import print_function 27 | 28 | import sys 29 | import os 30 | 31 | from setuptools import setup, find_packages 32 | from setuptools.command.sdist import sdist as _sdist 33 | from setuptools.command.test import test as TestCommand 34 | 35 | from setuptools import setup, find_packages 36 | 37 | here = os.path.abspath(os.path.dirname(__file__)) 38 | with open(os.path.join(here, 'README.md')) as f: 39 | README = f.read() 40 | with open(os.path.join(here, 'CHANGES.txt')) as f: 41 | CHANGES = f.read() 42 | 43 | 44 | class PyTest(TestCommand): 45 | user_options = [ 46 | ('cov', None, 'measure coverage') 47 | ] 48 | 49 | def initialize_options(self): 50 | TestCommand.initialize_options(self) 51 | self.cov = None 52 | 53 | def finalize_options(self): 54 | TestCommand.finalize_options(self) 55 | self.test_args = ['annotran'] 56 | self.test_suite = True 57 | if self.cov: 58 | self.test_args += ['--cov', 'annotran', 59 | '--cov-config', '.coveragerc'] 60 | 61 | def run_tests(self): 62 | import pytest 63 | errno = pytest.main(self.test_args) 64 | sys.exit(errno) 65 | 66 | INSTALL_REQUIRES = [ 67 | 'PyJWT>=1.0.0,<2.0.0', 68 | 'SQLAlchemy>=0.8.0', 69 | 'alembic>=0.7.0', 70 | 'annotator>=0.14.2,<0.15', 71 | 'blinker>=1.3,<1.4', 72 | 'cryptacular>=1.4,<1.5', 73 | 'cryptography>=0.7', 74 | 'deform>=0.9,<1.0', 75 | 'deform-jinja2>=0.5,<0.6', 76 | 'elasticsearch>=1.1.0,<2.0.0', 77 | 'gevent>=1.0.2,<1.1.0', 78 | 'gnsq>=0.3.0,<0.4.0', 79 | 'gunicorn>=19.2,<20', 80 | 'itsdangerous>=0.24', 81 | 'jsonpointer==1.0', 82 | 'jsonschema>=2.5.1,<2.6', 83 | 'pyramid>=1.5,<1.6', 84 | 'psycogreen>=1.0', 85 | 'psycopg2>=2.6.1', 86 | 'pyramid_mailer>=0.13', 87 | 'pyramid_tm>=0.7', 88 | 'python-dateutil>=2.1', 89 | 'python-slugify>=1.1.3,<1.2.0', 90 | 'python-statsd>=1.7.0,<1.8.0', 91 | 'webassets>=0.10,<0.11', 92 | 'pyramid_webassets>=0.9,<1.0', 93 | 'pyramid-jinja2>=2.3.3', 94 | 'raven>=5.10.2,<5.11.0', 95 | 'requests>=2.7.0', 96 | 'unicodecsv>=0.14.1,<0.15', 97 | 'ws4py>=0.3,<0.4', 98 | 'zope.sqlalchemy>=0.7.6,<0.8.0', 99 | 100 | # Version pin for known bug 101 | # https://github.com/repoze/repoze.sendmail/issues/31 102 | 'repoze.sendmail<4.2', 103 | ] 104 | 105 | DEV_EXTRAS = ['pyramid_debugtoolbar>=2.1', 'prospector[with_pyroma]', 'pep257', 106 | 'sphinxcontrib-httpdomain'] 107 | TESTING_EXTRAS = ['mock>=1.3.0', 'pytest>=2.5', 'pytest-cov', 'factory-boy'] 108 | YAML_EXTRAS = ['PyYAML'] 109 | 110 | setup(name='annotran', 111 | version='0.0', 112 | description='annotran', 113 | long_description=README + '\n\n' + CHANGES, 114 | classifiers=[ 115 | "Programming Language :: Python", 116 | "Framework :: Pyramid", 117 | "Topic :: Internet :: WWW/HTTP", 118 | "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", 119 | ], 120 | author='', 121 | author_email='', 122 | url='', 123 | keywords='web wsgi bfg pylons pyramid', 124 | packages=find_packages(exclude=['*.test']), 125 | include_package_data=True, 126 | zip_safe=False, 127 | install_requires=INSTALL_REQUIRES, 128 | extras_require={ 129 | 'dev': DEV_EXTRAS + YAML_EXTRAS, 130 | 'testing': TESTING_EXTRAS, 131 | 'YAML': YAML_EXTRAS, 132 | }, 133 | tests_require=DEV_EXTRAS + TESTING_EXTRAS, 134 | cmdclass={'test': PyTest}, 135 | entry_points="""\ 136 | [paste.app_factory] 137 | main = annotran.app:main 138 | [console_scripts] 139 | initialize_annotran_db = annotran.scripts.initializedb:main 140 | """, 141 | ) 142 | -------------------------------------------------------------------------------- /setup.txt: -------------------------------------------------------------------------------- 1 | 1)create requirements.txt and reference the location of h 2 | 2) workon your ve from terminal 3 | 3) pip install -r requirements.txt 4 | --------------------------------------------------------------------------------