├── .env.example ├── .flake8 ├── .flaskenv ├── .github ├── FUNDING.yml └── workflows │ └── run_tests.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Procfile ├── README.md ├── app ├── ReadabiliPy │ ├── .gitignore │ ├── .pylintrc │ ├── .travis.yml │ ├── LICENSE │ ├── README.md │ ├── __init__.py │ ├── benchmarks │ │ ├── Dockerfile │ │ └── README.md │ ├── extract_article.py │ ├── javascript │ │ ├── ExtractArticle.js │ │ └── Readability.js │ ├── package.json │ ├── readabilipy │ │ ├── __init__.py │ │ ├── extractors │ │ │ ├── __init__.py │ │ │ ├── extract_date.py │ │ │ ├── extract_element.py │ │ │ └── extract_title.py │ │ ├── simple_json.py │ │ ├── simple_tree.py │ │ └── simplifiers │ │ │ ├── __init__.py │ │ │ ├── html.py │ │ │ └── text.py │ ├── requirements-dev.txt │ └── requirements.txt ├── __init__.py ├── api │ ├── __init__.py │ ├── articles.py │ ├── auth.py │ ├── errors.py │ ├── helpers │ │ ├── add_article_methods.py │ │ ├── add_highlight_methods.py │ │ ├── article_query_maker.py │ │ ├── highlight_query_maker.py │ │ ├── query_maker.py │ │ └── update_tags.py │ ├── highlights.py │ ├── tags.py │ ├── tasks.py │ ├── tokens.py │ └── users.py ├── auth │ ├── __init__.py │ ├── email.py │ ├── forms.py │ └── routes.py ├── cli.py ├── content │ ├── __init__.py │ └── routes.py ├── dotcom │ ├── __init__.py │ └── routes.py ├── email.py ├── errors │ ├── __init__.py │ └── handlers.py ├── export.py ├── helpers │ ├── delete_user.py │ ├── ebooks.py │ ├── export_helpers.py │ ├── on_demand │ │ ├── absolute_urls.py │ │ ├── create_records.py │ │ ├── dedupe.py │ │ ├── delete_all.sql │ │ ├── format_article.py │ │ ├── highlight_prompts.py │ │ ├── lazy_images.py │ │ ├── match_tags.py │ │ └── protect_images.py │ ├── pdf.py │ ├── pulltext.py │ ├── review.py │ └── user_content.py ├── main │ ├── __init__.py │ ├── forms.py │ └── routes.py ├── models.py ├── robots-dev.txt ├── robots-prod.txt ├── service-worker.js ├── settings │ ├── __init__.py │ ├── email.py │ ├── forms.py │ └── routes.py ├── static │ ├── app.js │ ├── articles.js │ ├── btn_google_signin_dark_normal_web.png │ ├── btn_google_signin_dark_pressed_web.png │ ├── css │ │ ├── _articles.scss │ │ ├── _buttons.scss │ │ ├── _dark.scss │ │ ├── _dashboard.scss │ │ ├── _filters.scss │ │ ├── _highlights.scss │ │ ├── _landing.scss │ │ ├── _legal.scss │ │ ├── _main.scss │ │ ├── _nav.scss │ │ ├── _page_layout.scss │ │ ├── _pagination.scss │ │ ├── _review.scss │ │ ├── _settings.scss │ │ ├── _sidebar.scss │ │ ├── _sidebar_bookmarks.scss │ │ ├── _style_old.scss │ │ ├── _variables.scss │ │ ├── alerts.scss │ │ ├── dotcom │ │ │ ├── styles.css │ │ │ ├── styles.css.map │ │ │ └── styles.scss │ │ ├── fa │ │ │ ├── scss │ │ │ │ ├── _animated.scss │ │ │ │ ├── _bordered-pulled.scss │ │ │ │ ├── _core.scss │ │ │ │ ├── _fixed-width.scss │ │ │ │ ├── _icons.scss │ │ │ │ ├── _larger.scss │ │ │ │ ├── _list.scss │ │ │ │ ├── _mixins.scss │ │ │ │ ├── _rotated-flipped.scss │ │ │ │ ├── _screen-reader.scss │ │ │ │ ├── _shims.scss │ │ │ │ ├── _stacked.scss │ │ │ │ ├── _variables.scss │ │ │ │ ├── brands.scss │ │ │ │ ├── fontawesome.scss │ │ │ │ ├── regular.scss │ │ │ │ ├── solid.scss │ │ │ │ └── v4-shims.scss │ │ │ └── webfonts │ │ │ │ ├── fa-brands-400.eot │ │ │ │ ├── fa-brands-400.svg │ │ │ │ ├── fa-brands-400.ttf │ │ │ │ ├── fa-brands-400.woff │ │ │ │ ├── fa-brands-400.woff2 │ │ │ │ ├── fa-regular-400.eot │ │ │ │ ├── fa-regular-400.svg │ │ │ │ ├── fa-regular-400.ttf │ │ │ │ ├── fa-regular-400.woff │ │ │ │ ├── fa-regular-400.woff2 │ │ │ │ ├── fa-solid-900.eot │ │ │ │ ├── fa-solid-900.svg │ │ │ │ ├── fa-solid-900.ttf │ │ │ │ ├── fa-solid-900.woff │ │ │ │ └── fa-solid-900.woff2 │ │ ├── style.css │ │ ├── style.css.map │ │ └── style.scss │ ├── fileinput.js │ ├── font-awesome │ │ ├── HELP-US-OUT.txt │ │ ├── css │ │ │ ├── font-awesome.css │ │ │ └── font-awesome.min.css │ │ ├── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.svg │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ └── fontawesome-webfont.woff2 │ │ ├── less │ │ │ ├── animated.less │ │ │ ├── bordered-pulled.less │ │ │ ├── core.less │ │ │ ├── fixed-width.less │ │ │ ├── font-awesome.less │ │ │ ├── icons.less │ │ │ ├── larger.less │ │ │ ├── list.less │ │ │ ├── mixins.less │ │ │ ├── path.less │ │ │ ├── rotated-flipped.less │ │ │ ├── screen-reader.less │ │ │ ├── stacked.less │ │ │ └── variables.less │ │ └── scss │ │ │ ├── _animated.scss │ │ │ ├── _bordered-pulled.scss │ │ │ ├── _core.scss │ │ │ ├── _fixed-width.scss │ │ │ ├── _icons.scss │ │ │ ├── _larger.scss │ │ │ ├── _list.scss │ │ │ ├── _mixins.scss │ │ │ ├── _path.scss │ │ │ ├── _rotated-flipped.scss │ │ │ ├── _screen-reader.scss │ │ │ ├── _stacked.scss │ │ │ ├── _variables.scss │ │ │ └── font-awesome.scss │ ├── highlight.js │ ├── highlighter.png │ ├── images │ │ ├── apple-touch-icon.png │ │ ├── assets │ │ │ ├── icon_1125x2436.png │ │ │ ├── icon_1136x640.png │ │ │ ├── icon_1242x2208.png │ │ │ ├── icon_1242x2688.png │ │ │ ├── icon_1334x750.png │ │ │ ├── icon_1536x2048.png │ │ │ ├── icon_1668x2224.png │ │ │ ├── icon_1668x2388.png │ │ │ ├── icon_1792x828.png │ │ │ ├── icon_180x180.png │ │ │ ├── icon_2048x1536.png │ │ │ ├── icon_2048x2732.png │ │ │ ├── icon_2208x1242.png │ │ │ ├── icon_2224x1668.png │ │ │ ├── icon_2388x1668.png │ │ │ ├── icon_2436x1125.png │ │ │ ├── icon_2688x1242.png │ │ │ ├── icon_2732x2048.png │ │ │ ├── icon_640x1136.png │ │ │ ├── icon_750x1334.png │ │ │ └── icon_828x1792.png │ │ ├── darkModeOff.png │ │ ├── darkModeOn.png │ │ ├── dotcom │ │ │ ├── content_in_a_single_place.png │ │ │ ├── easy-to-read.png │ │ │ ├── highlights.png │ │ │ ├── icons │ │ │ │ ├── automate.png │ │ │ │ ├── automate.webp │ │ │ │ ├── highlighter.png │ │ │ │ ├── highlighter.webp │ │ │ │ ├── personalize.png │ │ │ │ ├── personalize.webp │ │ │ │ ├── save.png │ │ │ │ └── save.webp │ │ │ ├── inapp │ │ │ │ ├── customize.png │ │ │ │ ├── customize.webp │ │ │ │ ├── extension.png │ │ │ │ ├── extension.webp │ │ │ │ ├── metadata.png │ │ │ │ ├── metadata.webp │ │ │ │ ├── notes.png │ │ │ │ └── notes.webp │ │ │ ├── lurnby-favicon.png │ │ │ ├── lurnby-taking-notes.jpg │ │ │ ├── lurnby.png │ │ │ ├── lurnbyhalf.png │ │ │ ├── lurnbyquarter.png │ │ │ └── topics.png │ │ ├── error.svg │ │ ├── icons │ │ │ ├── check.svg │ │ │ ├── more.svg │ │ │ ├── nav_left.svg │ │ │ ├── nav_right.svg │ │ │ ├── open.svg │ │ │ └── x.svg │ │ ├── lurnby-logo │ │ │ ├── apple-touch-icon.png │ │ │ ├── lurnby.png │ │ │ ├── lurnby_logo.png │ │ │ ├── lurnby_logo.psd │ │ │ ├── lurnby_logo@0,25x.png │ │ │ ├── lurnby_logo@0,5x.png │ │ │ └── lurnby_logo@0,75x.png │ │ ├── lurnby.png │ │ ├── lurnbyDesktop.png │ │ ├── lurnbyMobileScreens.png │ │ ├── rr-19.png │ │ ├── rr-192.png │ │ ├── rr-48.png │ │ ├── rr-64.png │ │ ├── rr-96.png │ │ ├── rr-icon │ │ │ ├── rr-19.png │ │ │ ├── rr-192.png │ │ │ ├── rr-48.png │ │ │ ├── rr-64.png │ │ │ ├── rr-96.png │ │ │ ├── rr_w_sign.png │ │ │ ├── rr_w_sign.webp │ │ │ ├── rr_w_sign_cropped.png │ │ │ ├── rr_w_sign_cropped.webp │ │ │ ├── rr_w_sign_hello.png │ │ │ ├── rr_w_sign_hello.webp │ │ │ ├── rr_w_sign_hello_cropped.png │ │ │ ├── rr_w_sign_hello_cropped.webp │ │ │ ├── rr_w_sign_welcome.png │ │ │ ├── rr_w_sign_welcome.webp │ │ │ ├── rr_w_sign_welcome_cropped.png │ │ │ ├── rr_w_sign_welcome_cropped.webp │ │ │ ├── rrbetterface-32.png │ │ │ ├── rrbetterface.png │ │ │ ├── rrfeedback-30.png │ │ │ ├── rrfeedback-40.png │ │ │ ├── rrfeedback-40hr.png │ │ │ └── rrfeedback.png │ │ ├── rrfeedback-30.png │ │ ├── rrfeedback-40.png │ │ ├── rrwlurnbyshirt.psd │ │ ├── screenshots │ │ │ ├── addarticle.png │ │ │ ├── highlights.png │ │ │ ├── homepage.png │ │ │ ├── readingdark.png │ │ │ ├── readinglight.png │ │ │ └── review.png │ │ ├── smart-cat.gif │ │ ├── splashscreens │ │ │ ├── ipad_splash.png │ │ │ ├── ipadpro1_splash.png │ │ │ ├── ipadpro2_splash.png │ │ │ ├── ipadpro3_splash.png │ │ │ ├── iphone5_splash.png │ │ │ ├── iphone6_splash.png │ │ │ ├── iphoneplus_splash.png │ │ │ ├── iphonex_splash.png │ │ │ ├── iphonexr_splash.png │ │ │ └── iphonexsmax_splash.png │ │ └── success.svg │ ├── js │ │ ├── accept_tos.js │ │ ├── autocomplete.js │ │ ├── bookmarks.js │ │ ├── copyToClipboard.js │ │ ├── rangy-core.js │ │ ├── rangy │ │ │ ├── LICENSE │ │ │ └── README.md │ │ └── settings.js │ ├── lineheightmax.svg │ ├── lineheightmid.svg │ ├── lineheightmin.svg │ ├── lurnby.webmanifest │ ├── offline.html │ ├── robots │ │ ├── robots-dev.txt │ │ └── robots-prod.txt │ ├── rr-100.png │ ├── rr-selecttext.js │ ├── rrbetterface-32.png │ ├── rrbetterface2.png │ ├── service-worker.js │ ├── sidebar.js │ ├── spinning-circles.svg │ ├── tag.js │ ├── texthighlighter │ │ └── TextHighlighter.js │ ├── topic.js │ └── updatehighlight.js ├── tasks.py └── templates │ ├── _all_articles.html │ ├── _article.html │ ├── _articles_with_filter.html │ ├── _ios_splash.html │ ├── _overview.html │ ├── add_article_modal.html │ ├── article_card.html │ ├── articles.html │ ├── articles_all.html │ ├── articles_filter.html │ ├── articles_new.html │ ├── auth │ ├── email │ │ ├── reset_password.html │ │ ├── reset_password.txt │ │ ├── verify_email.html │ │ └── verify_email.txt │ ├── login.html │ ├── register.html │ ├── reset_password.html │ └── reset_password_request.html │ ├── base.html │ ├── dashboard │ ├── app_dashboard.html │ ├── suggestion_dash.html │ └── user_dash.html │ ├── dotcom │ ├── base.html │ ├── default.html │ ├── find-out-more-about-lurnby.html │ ├── how-lurnby-works.html │ ├── how-much-lurnby-costs.html │ ├── how-to-start-using-lurnby.html │ ├── index.html │ ├── landing.html │ ├── tutorials-and-demos.html │ └── who-is-lurnby-for.html │ ├── email │ ├── content │ │ ├── recent_highlights.html │ │ └── recent_highlights.txt │ ├── email_base.html │ ├── export_highlights.html │ └── export_highlights.txt │ ├── errors │ ├── 404.html │ └── 500.html │ ├── experiments │ └── uploads.html │ ├── filter_highlights.html │ ├── filter_highlights2.html │ ├── highlight.html │ ├── highlights.html │ ├── highlights2.html │ ├── legal │ ├── _ipp.html │ ├── _privacy.html │ ├── _tos.html │ ├── _tos_modal.html │ ├── accept_tos_modal.html │ ├── auth_tos_accept.html │ ├── ipp.html │ ├── privacy.html │ └── tos.html │ ├── resources.html │ ├── review │ ├── filter_review.html │ └── review.html │ ├── settings.html │ ├── settings │ ├── email │ │ ├── delete_verify.html │ │ ├── delete_verify.txt │ │ ├── verify_email.html │ │ └── verify_email.txt │ ├── settings_account.html │ ├── settings_account_email.html │ ├── settings_base.html │ ├── settings_communication.html │ ├── settings_content.html │ ├── settings_delete_confirm.html │ ├── settings_delete_verify.html │ └── settings_password.html │ ├── tags │ └── tags.html │ ├── text.html │ ├── topics.html │ ├── topics_all.html │ └── viewarticle.html ├── boot.sh ├── config.py ├── coverage └── lcov.info ├── data.py ├── dockerRun.md ├── dotcom ├── __init__.py ├── default.html ├── find-out-more-about-lurnby.html ├── how-lurnby-works.html ├── how-much-lurnby-costs.html ├── how-to-start-using-lurnby.html ├── index.html ├── ssg.py ├── static │ ├── styles.css │ ├── styles.css.map │ └── styles.scss ├── templates │ ├── base.html │ ├── default.html │ ├── find-out-more-about-lurnby.html │ ├── how-lurnby-works.html │ ├── how-much-lurnby-costs.html │ ├── how-to-start-using-lurnby.html │ ├── index.html │ ├── tutorials-and-demos.html │ └── who-is-lurnby-for.html ├── tutorials-and-demos.html └── who-is-lurnby-for.html ├── install.md ├── learnbetter.py ├── mac-install-notes.md ├── migrations ├── README ├── alembic.ini ├── env.py ├── script.py.mako └── versions │ ├── 0801537eefe1_added_unique_to_comms.py │ ├── 0c05d3df71bd_added_reflections_to_article.py │ ├── 12f7ab99d9d2_added_messages_table.py │ ├── 202c385e6984_added_uuid_to_highlight.py │ ├── 26b62ee75c23_added_uuid_to_tag.py │ ├── 27e6fca13770_added_review_count_to_user_model_for_.py │ ├── 35127e9854c7_added_prompt_to_highlight.py │ ├── 3edaed8395c0_added_last_used_to_topic_model.py │ ├── 4796cfdbe050_added_processing_field.py │ ├── 50e2f2ff2dbd_adding_index_on_article_title_lowercase.py │ ├── 5c5bf645c104_added_tos_to_user_model.py │ ├── 5e2af35aa067_added_article_count_and_highlight_count_.py │ ├── 755e6e328fcf_added_comms_model.py │ ├── 8287a55072a2_added_source_to_highlight.py │ ├── 83c88503ad2b_added_event_class.py │ ├── 894a4c974216_begin_here_fucked_up_too_many_things_.py │ ├── a544d948cd1b_added_date_read_date_and_date_read_time.py │ ├── ac6229daac86_added_deleted_attribute.py │ ├── ae7646aac3f6_added_backred.py │ ├── b1bd350dc073_changed_comms_to_be_1_1.py │ ├── cdddbf0edeb6_added_do_not_review_bool_to_highlight_.py │ ├── e5f879371002_added_unique_constraints_to_uuid_cols.py │ └── eb9ea8694722_added_untagged_to_highlight_model.py ├── package-lock.json ├── package.json ├── reqtreebackup.txt ├── requirements.txt ├── runtime.txt ├── tests ├── __init__.py ├── api │ ├── __init__.py │ ├── test_articles.py │ ├── test_highlights.py │ ├── test_tags.py │ ├── test_tasks.py │ └── test_users.py ├── helpers │ ├── test_ebooks.py │ ├── test_export_helpers.py │ └── test_pdf.py ├── mocks │ ├── mock.epub │ ├── mock.pdf │ ├── mock_html_req.txt │ ├── mock_redis.py │ ├── mocks.py │ └── test.pdf ├── models │ ├── test_comms_model.py │ ├── test_events_model.py │ ├── test_highlight_model.py │ ├── test_tag_model.py │ └── test_user_model.py ├── tasks │ ├── test_create_recall_text.py │ └── test_delete_user.py └── test_email.py └── wsgi.py /.env.example: -------------------------------------------------------------------------------- 1 | # For google oauth and login 2 | GOOGLE_CLIENT_ID= 3 | GOOGLE_CLIENT_SECRET= 4 | 5 | # For app emails including auth, system emails, logging and debugging 6 | MAIL_PASSWORD= 7 | MAIL_DEFAULT_SENDER= 8 | 9 | # For storing images 10 | AWS_ACCESS_KEY_ID= 11 | AWS_SECRET_ACCESS_KEY= 12 | AWS_BUCKET= 13 | 14 | # For securing your sessions 15 | SECRET_KEY = 16 | 17 | # Database 18 | DATABASE_URL= 19 | 20 | # For generating recent highlights emails 21 | SERVER_NAME=http://localhost:3000 -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .git,__pycache__,.github,.aws-sam,.vscode,aws-dist,frontend,.pytest_cache,venv,./app/ReadabiliPy,./migrations 3 | max-line-length = 119 4 | ignore = E266 5 | per-file-ignores = \ 6 | app/api/helpers/article_query_maker.py:E712, \ 7 | app/main/routes.py:E712, \ 8 | app/helpers/user_content.py:E712 \ 9 | app/api/__init__.py:E402,F401 10 | 11 | -------------------------------------------------------------------------------- /.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=learnbetter.py 2 | FLASK_RUN_CERT=./certs/cert.pem 3 | FLASK_RUN_KEY=./certs/key.pem 4 | FLASK_DEBUG=1 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: Lurnby 4 | -------------------------------------------------------------------------------- /.github/workflows/run_tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Run Tests 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | workflow_dispatch: 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | python: 18 | runs-on: ubuntu-20.04 19 | steps: 20 | - uses: actions/checkout@v3 21 | 22 | - name: Set up Python 3.9 23 | uses: actions/setup-python@v3 24 | with: 25 | python-version: "3.9" 26 | 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 31 | 32 | - name: Install npm dependencies 33 | run: | 34 | cd app/ReadabiliPy 35 | npm i 36 | cd ../.. 37 | 38 | - name: Lint with flake8 39 | run: | 40 | # stop the build if there are Python syntax errors or undefined names 41 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 42 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 43 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 44 | 45 | - name: Test with pytest 46 | run: | 47 | python -m pytest 48 | 49 | - name: Upload coverage reports to Codecov 50 | uses: codecov/codecov-action@v3 51 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9.9 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y postgresql-server-dev-all \ 5 | gcc \ 6 | python3-dev \ 7 | musl-dev \ 8 | nodejs \ 9 | npm \ 10 | mupdf-tools pdftk 11 | 12 | RUN curl -fsSL https://deb.nodesource.com/setup_current.x | bash - && \ 13 | apt-get install -y nodejs 14 | 15 | RUN useradd lurnby 16 | 17 | WORKDIR /home/lurnby 18 | 19 | COPY requirements.txt requirements.txt 20 | RUN python3 -m venv venv 21 | RUN venv/bin/pip install --upgrade pip 22 | RUN venv/bin/pip install -r requirements.txt 23 | RUN venv/bin/pip install gunicorn 24 | 25 | COPY app app 26 | COPY migrations migrations 27 | COPY learnbetter.py config.py boot.sh data.py package.json package-lock.json ./ 28 | RUN chmod +x boot.sh 29 | 30 | ENV FLASK_APP learnbetter.py 31 | 32 | RUN chown -R lurnby:lurnby ./ 33 | USER lurnby 34 | 35 | EXPOSE 5000 36 | ENTRYPOINT ["./boot.sh"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Rostislav Roznoshchik 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: flask db upgrade; gunicorn learnbetter:app npm start 2 | worker: rq worker -u $REDIS_URL lurnby-tasks -------------------------------------------------------------------------------- /app/ReadabiliPy/.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: enabled 2 | dist: xenial 3 | language: python 4 | python: 5 | - 3.6 6 | - 3.7 7 | 8 | before_install: 9 | # Install node.js following instructions from 10 | # https://nodejs.org/en/download/package-manager/ 11 | - sudo apt install curl 12 | - curl -sL https://deb.nodesource.com/setup_11.x | sudo bash - 13 | - sudo apt install nodejs 14 | 15 | install: 16 | # Install node.js dependencies 17 | - npm install 18 | # Install python dependencies 19 | - pip install -r requirements-dev.txt 20 | 21 | script: 22 | # Run all pytest unit tests 23 | - python -m pytest -v tests --cov readabilipy --cov-report term-missing --benchmark-disable 24 | # Run pyflakes for error detection 25 | - pyflakes *.py readabilipy tests 26 | # Check PEP8 compliance (ignoring long lines) 27 | - pycodestyle --statistics --ignore=E501 --count *.py readabilipy tests 28 | # Run pylint for stricter error checking 29 | - pylint readabilipy tests 30 | 31 | after_success: 32 | # Upload results to coveralls.io 33 | - coveralls 34 | -------------------------------------------------------------------------------- /app/ReadabiliPy/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 The Alan Turing Institute 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 | -------------------------------------------------------------------------------- /app/ReadabiliPy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/ReadabiliPy/__init__.py -------------------------------------------------------------------------------- /app/ReadabiliPy/benchmarks/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | 3 | # Install requirements 4 | RUN apt-get update 5 | RUN apt-get -y install curl 6 | RUN curl -sL https://deb.nodesource.com/setup_11.x | bash - 7 | RUN apt install nodejs 8 | RUN npm install 9 | RUN pip install --upgrade pip 10 | RUN apt-get install -y git 11 | 12 | # Clone ReadabiliPy and install python packages 13 | RUN git clone https://github.com/alan-turing-institute/ReadabiliPy 14 | WORKDIR "/ReadabiliPy" 15 | RUN git pull 16 | RUN pip install -r requirements-dev.txt 17 | 18 | # Run the benchmarks with Pytest 19 | CMD pytest --benchmark-only 20 | -------------------------------------------------------------------------------- /app/ReadabiliPy/javascript/ExtractArticle.js: -------------------------------------------------------------------------------- 1 | 2 | var path = require("path"); 3 | var fs = require("fs"); 4 | 5 | var url = require("url"); 6 | 7 | const JSDOM = require('jsdom').JSDOM 8 | 9 | 10 | // We want to load Readability, which isn't set up as commonjs libraries, 11 | // and so we need to do some hocus-pocus with 'vm' to import them on a separate scope 12 | // (identical) scope context. 13 | var vm = require("vm"); 14 | var readabilityPath = path.join(__dirname, "Readability.js"); 15 | 16 | var scopeContext = {}; 17 | // We generally expect dump() and console.{whatever} to work, so make these available 18 | // in the scope we're using: 19 | scopeContext.dump = console.log; 20 | scopeContext.console = console; 21 | scopeContext.URL = url.URL; 22 | scopeContext.JSDOM = JSDOM; 23 | 24 | // Actually load files. NB: if either of the files has parse errors, 25 | // node is dumb and shows you a syntax error *at this callsite* . Don't try to find 26 | // a syntax error on this line, there isn't one. Go look in the file it's loading instead. 27 | vm.runInNewContext(fs.readFileSync(readabilityPath), scopeContext, readabilityPath); 28 | 29 | var Readability = scopeContext.Readability; 30 | 31 | var argv = require('minimist')(process.argv.slice(2)); 32 | 33 | function readFile(filePath) { 34 | return fs.readFileSync(filePath, {encoding: "utf-8"}).trim(); 35 | } 36 | function writeFile(data, filePath) { 37 | return fs.writeFileSync(filePath, data, {encoding: "utf-8"}); 38 | } 39 | 40 | var inFilePath = argv['i']; 41 | var outFilePath; 42 | 43 | if (typeof(argv['o']) !== 'undefined') { 44 | outFilePath = argv['o']; 45 | } else { 46 | outFilePath = inFilePath + ".simple.json"; 47 | } 48 | var html = readFile(inFilePath); 49 | 50 | var doc = new scopeContext.JSDOM(html).window.document; 51 | var article = new scopeContext.Readability(doc).parse(); 52 | 53 | writeFile(JSON.stringify(article), outFilePath); -------------------------------------------------------------------------------- /app/ReadabiliPy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ReadabiliPy", 3 | "version": "0.1.0", 4 | "description": "An augmented Python wrapper for the Mozilla standalone Readability.js package.", 5 | "main": "ExtractArticle.js", 6 | "scripts": {}, 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/alan-turing-institute/ReadabiliPy" 10 | }, 11 | "author": "", 12 | "license": "Apache-2.0", 13 | "bugs": { 14 | "url": "https://github.com/alan-turing-institute/ReadabiliPy/issues" 15 | }, 16 | "engines": { 17 | "node": ">=11.0.0" 18 | }, 19 | "homepage": "https://github.com/alan-turing-institute/ReadabiliPy", 20 | "devDependencies": {}, 21 | "dependencies": { 22 | "jsdom": ">=12.2.0", 23 | "minimist": "^1.2.5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/ReadabiliPy/readabilipy/__init__.py: -------------------------------------------------------------------------------- 1 | from .simple_json import simple_json_from_html_string 2 | from .simple_tree import simple_tree_from_html_string 3 | 4 | __all__ = [ 5 | "simple_json_from_html_string", 6 | "simple_tree_from_html_string", 7 | ] 8 | -------------------------------------------------------------------------------- /app/ReadabiliPy/readabilipy/extractors/__init__.py: -------------------------------------------------------------------------------- 1 | from .extract_date import extract_date, ensure_iso_date_format 2 | from .extract_title import extract_title 3 | 4 | __all__ = [ 5 | "extract_date", 6 | "extract_title", 7 | "ensure_iso_date_format", 8 | ] 9 | -------------------------------------------------------------------------------- /app/ReadabiliPy/readabilipy/simplifiers/__init__.py: -------------------------------------------------------------------------------- 1 | from .text import ( 2 | normalise_text, 3 | normalise_unicode, 4 | normalise_whitespace, 5 | strip_control_characters, 6 | strip_html_whitespace, 7 | ) 8 | 9 | __all__ = [ 10 | "normalise_text", 11 | "normalise_unicode", 12 | "normalise_whitespace", 13 | "strip_control_characters", 14 | "strip_html_whitespace", 15 | ] 16 | -------------------------------------------------------------------------------- /app/ReadabiliPy/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4>=4.7.1 2 | datetime 3 | html5lib 4 | lxml 5 | pycodestyle 6 | pyflakes 7 | pylint 8 | pytest 9 | pytest-benchmark 10 | pytest-cov 11 | python-coveralls 12 | regex 13 | -------------------------------------------------------------------------------- /app/ReadabiliPy/requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4>=4.7.1 2 | datetime 3 | html5lib 4 | lxml 5 | regex 6 | -------------------------------------------------------------------------------- /app/api/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint("api", __name__) 4 | 5 | from app.api import tokens, errors, users, articles, tasks, highlights, tags 6 | -------------------------------------------------------------------------------- /app/api/auth.py: -------------------------------------------------------------------------------- 1 | from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth 2 | from app.models import User 3 | from app.api.errors import error_response 4 | 5 | basic_auth = HTTPBasicAuth() 6 | token_auth = HTTPTokenAuth() 7 | 8 | 9 | @basic_auth.verify_password 10 | def verify_password(username, password): 11 | if username and password: 12 | user = User.query.filter_by(username=username).first() 13 | if not user: 14 | user = User.query.filter_by(email=username).first() 15 | if user and user.check_password(password): 16 | return user 17 | 18 | 19 | @basic_auth.error_handler 20 | def basic_auth_error(status): 21 | return error_response(status) 22 | 23 | 24 | @token_auth.verify_token 25 | def verify_token(token): 26 | return User.check_token(token) if token else None 27 | 28 | 29 | @token_auth.error_handler 30 | def token_auth_error(status): 31 | return error_response(status) 32 | -------------------------------------------------------------------------------- /app/api/errors.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | from werkzeug.http import HTTP_STATUS_CODES 3 | 4 | 5 | def error_response(status_code, message=None): 6 | payload = {"error": HTTP_STATUS_CODES.get(status_code, "Unknown error")} 7 | if message: 8 | payload["message"] = message 9 | response = jsonify(payload) 10 | response.status_code = status_code 11 | return response 12 | 13 | 14 | def bad_request(message): 15 | return error_response(400, message) 16 | 17 | 18 | class LurnbyValueError(ValueError): 19 | def __init__(self, *args: object) -> None: 20 | super().__init__(*args) 21 | -------------------------------------------------------------------------------- /app/api/helpers/query_maker.py: -------------------------------------------------------------------------------- 1 | def apply_pagination(query, page="1", per_page="15"): 2 | """applies pagination 3 | 4 | Args: 5 | query (flask_sqlalchemy.query.Query): base query object 6 | page (str): an int string for which page of results e.g "1" 7 | per_page (str): "all" or int string e.g "15" or "30" 8 | Returns: 9 | query (flask_sqlalchemy.query.Query): updated query object 10 | """ 11 | # prepare to paginate results 12 | result_count = query.count() 13 | if per_page == "all": 14 | per_page = result_count 15 | else: 16 | per_page = int(per_page) 17 | 18 | query = query.paginate(page=int(page), per_page=per_page, error_out=False) 19 | 20 | return query 21 | -------------------------------------------------------------------------------- /app/api/helpers/update_tags.py: -------------------------------------------------------------------------------- 1 | from app import db 2 | from app.models import Tag 3 | 4 | 5 | def update_tags(tags, resource): 6 | """updates an articles or highlights tags and returns the updated resource 7 | fully replaces the tags on the resource to match the tags list that is passed in. 8 | 9 | Args: 10 | tags (string[]): list of tag names 11 | resource (article or highlight object): resource 12 | 13 | Returns: 14 | article: updated with tags 15 | """ 16 | resource_tags = resource.tag_list 17 | for tag_name in resource_tags: 18 | if tag_name not in tags: 19 | tag = Tag.query.filter_by(name=tag_name).first() 20 | if tag: 21 | resource.remove_tag(tag) 22 | 23 | for tag_name in tags: 24 | if tag_name not in resource_tags: 25 | tag = Tag.query.filter_by(name=tag_name, user_id=resource.user_id).first() 26 | if not tag: 27 | tag = Tag(name=tag_name, user_id=resource.user_id) 28 | db.session.add(tag) 29 | resource.add_tag(tag) 30 | 31 | return resource 32 | -------------------------------------------------------------------------------- /app/api/tasks.py: -------------------------------------------------------------------------------- 1 | from flask import current_app, request, url_for, jsonify 2 | from app import CustomLogger 3 | from app.models import Task 4 | from app.api import bp 5 | from app.api.auth import token_auth 6 | from app.api.errors import bad_request, error_response 7 | import time 8 | 9 | logger = CustomLogger("API") 10 | 11 | 12 | @bp.route("/tasks/", methods=["GET"]) 13 | @token_auth.login_required 14 | def get_task_status(task_id): 15 | try: 16 | task = Task.query.filter_by(id=task_id).first() 17 | if task.user_id != token_auth.current_user().id: 18 | return error_response(404, "resource not found") 19 | 20 | article_id = request.args.get("article_id", None) 21 | location = ( 22 | url_for("api.get_article", article_uuid=article_id) if article_id else None 23 | ) 24 | 25 | try: 26 | current_app.redis.ping() 27 | except Exception: 28 | logger.error("No Redis Instance") 29 | task.complete = True 30 | 31 | for _ in range(10): 32 | time.sleep(1) 33 | if task.complete: 34 | response = jsonify( 35 | processing=False, progress=100, task_id=task_id, location=location 36 | ) 37 | response.status_code = 200 38 | return response 39 | 40 | response = jsonify( 41 | processing=True, progress=task.get_progress(), task_id=task_id 42 | ) 43 | response.status_code = 200 44 | return response 45 | 46 | except Exception as e: 47 | if hasattr(e, "msg"): 48 | return bad_request(e.msg) 49 | else: 50 | logger.error(e) 51 | return bad_request("Something went wrong.") 52 | -------------------------------------------------------------------------------- /app/api/tokens.py: -------------------------------------------------------------------------------- 1 | from app import db 2 | from app.api import bp 3 | from app.api.auth import basic_auth, token_auth 4 | from app.api.errors import bad_request 5 | from app.models import User, Comms 6 | 7 | from flask import jsonify, request, url_for 8 | 9 | 10 | @bp.route("/tokens", methods=["POST", "GET"]) 11 | @basic_auth.login_required 12 | def get_token(): 13 | token = basic_auth.current_user().get_token() 14 | db.session.commit() 15 | return jsonify({"token": token, "id": basic_auth.current_user().id}) 16 | 17 | 18 | @bp.route("/tokens/goog", methods=["POST"]) 19 | def google_login(): 20 | data = request.get_json() or {} 21 | if "goog_id" not in data or "email" not in data or "first_name" not in data: 22 | return bad_request( 23 | "must include goog_id, \ 24 | email, and first_name fields" 25 | ) 26 | 27 | user = User.query.filter_by(email=data["email"]).first() 28 | if user: 29 | token = user.get_token() 30 | db.session.commit() 31 | response = jsonify({"token": token, "id": user.id}) 32 | return response 33 | 34 | user = User( 35 | goog_id=data["goog_id"], email=data["email"], firstname=data["first_name"] 36 | ) 37 | token = user.get_token() 38 | db.session.add(user) 39 | db.session.commit() 40 | comms = Comms(user_id=user.id) 41 | db.session.add(comms) 42 | db.session.commit() 43 | response = jsonify({"token": token, "id": user.id}) 44 | response.status_code = 201 45 | response.headers["location"] = url_for("api.get_user_tags", id=user.id) 46 | return response 47 | 48 | 49 | @bp.route("/tokens", methods=["DELETE"]) 50 | @token_auth.login_required 51 | def revoke_token(): 52 | token_auth.current_user().revoke_token() 53 | db.session.commit() 54 | return "", 204 55 | -------------------------------------------------------------------------------- /app/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint("auth", __name__) 4 | 5 | from app.auth import routes, email, forms # noqa : E402, F401 6 | -------------------------------------------------------------------------------- /app/auth/email.py: -------------------------------------------------------------------------------- 1 | from app.email import send_email 2 | 3 | from flask import render_template, current_app 4 | 5 | 6 | def send_password_reset_email(user): 7 | token = user.get_reset_password_token() 8 | print(f"sending email - [Lurnby] Reset Your Password for user: {user.id}") 9 | send_email( 10 | "Lurnby - Reset Your Password", 11 | sender=current_app.config["ADMINS"][0], 12 | recipients=[user.email], 13 | text_body=render_template( 14 | "auth/email/reset_password.txt", user=user, token=token 15 | ), 16 | html_body=render_template( 17 | "auth/email/reset_password.html", user=user, token=token 18 | ), 19 | ) 20 | -------------------------------------------------------------------------------- /app/cli.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from app import db 4 | from app.models import User 5 | from app.helpers.user_content import get_recent_highlights 6 | from app.helpers.delete_user import check_for_delete 7 | 8 | 9 | def register(app): 10 | @app.cli.command() 11 | def scheduled(): 12 | """Run scheduled job.""" 13 | print("Running Scheduled Job...") 14 | print(datetime.utcnow()) 15 | username = User.query.first().username 16 | print(User.query.first().username) 17 | User.query.first().username = "chiepa" 18 | db.session.commit 19 | print(User.query.first().username) 20 | User.query.first().username = username 21 | db.session.commit 22 | # time.sleep(5) 23 | print("Done!") 24 | print(datetime.utcnow()) 25 | 26 | @app.cli.command() 27 | def recent_highlights(): 28 | """get highlights.""" 29 | get_recent_highlights() 30 | 31 | @app.cli.command() 32 | def delete_from_az(): 33 | """delete from amazon""" 34 | check_for_delete() 35 | -------------------------------------------------------------------------------- /app/content/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint("content", __name__) 4 | 5 | from app.content import routes # noqa : E402, F401 6 | -------------------------------------------------------------------------------- /app/content/routes.py: -------------------------------------------------------------------------------- 1 | from flask import flash, render_template, request 2 | 3 | from flask_login import current_user 4 | 5 | import json 6 | from app.content import bp 7 | from app import db 8 | from app.models import Event 9 | 10 | 11 | @bp.route("/terms-of-service", methods=["GET"]) 12 | def terms(): 13 | return render_template("legal/tos.html") 14 | 15 | 16 | @bp.route("/privacy-policy", methods=["GET"]) 17 | def privacy(): 18 | return render_template("legal/privacy.html") 19 | 20 | 21 | @bp.route("/intellectual-property-policy", methods=["GET"]) 22 | def ipp(): 23 | return render_template("legal/ipp.html") 24 | 25 | 26 | @bp.route("/legal/accept_terms", methods=["GET", "POST"]) 27 | def accept_terms(): 28 | 29 | if request.method == "POST": 30 | data = json.loads(request.data) 31 | action = data["action"] 32 | 33 | if action == "accept_terms": 34 | current_user.tos = True 35 | flash( 36 | "Thank you for accepting the terms and continuing to use Lurnby", 37 | "success", 38 | ) 39 | ev = Event.add("tos accepted") 40 | if ev: 41 | db.session.add(ev) 42 | db.session.commit() 43 | 44 | return json.dumps({"accepted": True}) 45 | 46 | else: 47 | flash( 48 | "Please accept our updated terms to continue using Lurnby or delete your account below", 49 | "error", 50 | ) 51 | 52 | return json.dumps({"html": render_template("legal/accept_tos_modal.html")}), 200 53 | -------------------------------------------------------------------------------- /app/dotcom/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint("dotcom", __name__) 4 | 5 | from app.dotcom import routes # noqa : E402, F401 6 | -------------------------------------------------------------------------------- /app/email.py: -------------------------------------------------------------------------------- 1 | from flask_mail import Message 2 | from flask import current_app 3 | 4 | from threading import Thread 5 | from app import mail 6 | 7 | 8 | def send_async_email(app, msg): 9 | with app.app_context(): 10 | mail.send(msg) 11 | 12 | 13 | def send_email( 14 | subject, 15 | sender, 16 | recipients, 17 | text_body, 18 | html_body, 19 | attachments=None, 20 | sync=False, 21 | extra_headers=None, 22 | ): 23 | msg = Message( 24 | subject, sender=sender, recipients=recipients, extra_headers=extra_headers 25 | ) 26 | msg.body = text_body 27 | msg.html = html_body 28 | if attachments: 29 | for attachment in attachments: 30 | msg.attach(*attachment) 31 | if sync: 32 | mail.send(msg) 33 | else: 34 | Thread( 35 | target=send_async_email, args=(current_app._get_current_object(), msg) 36 | ).start() 37 | -------------------------------------------------------------------------------- /app/errors/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint("errors", __name__) 4 | 5 | from app.errors import handlers # noqa : E402, F401 6 | -------------------------------------------------------------------------------- /app/errors/handlers.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, request 2 | from app import db 3 | 4 | from app.errors import bp 5 | from app.api.errors import error_response as api_error_response 6 | 7 | 8 | def wants_json_response(): 9 | json = request.accept_mimetypes["application/json"] 10 | html = request.accept_mimetypes["text/html"] 11 | return json >= html 12 | 13 | 14 | @bp.app_errorhandler(404) 15 | def not_found_error(error): 16 | if wants_json_response(): 17 | return api_error_response(404) 18 | return render_template("errors/404.html"), 404 19 | 20 | 21 | @bp.app_errorhandler(500) 22 | def internal_error(error): 23 | db.session.rollback() 24 | 25 | if wants_json_response(): 26 | return api_error_response(500) 27 | return render_template("errors/500.html"), 500 28 | -------------------------------------------------------------------------------- /app/helpers/on_demand/absolute_urls.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | from app import db, CustomLogger 3 | 4 | logger = CustomLogger("Helpers") 5 | 6 | 7 | def set_absolute_urls(*articles): 8 | for a in articles: 9 | if a.source_url: 10 | soup = BeautifulSoup(a.content, "html5lib") 11 | images = soup.find_all("img") 12 | for img in images: 13 | try: 14 | if "http" not in img["src"]: 15 | img["src"] = f'{a.source_url}{img["src"]}' 16 | except Exception: 17 | logger.error( 18 | f"set_absolute_urls - error with article: {a.uuid}\n img: \n{img}" 19 | ) 20 | links = soup.find_all("a") 21 | for link in links: 22 | try: 23 | if "http" not in link["href"]: 24 | link["href"] = f'{a.source_url}{link["href"]}' 25 | except Exception: 26 | logger.error( 27 | f"set_absolute_urls error with article: {a.uuid}\n url: \n{link}" 28 | ) 29 | 30 | a.content = str(soup.prettify()) 31 | db.session.commit() 32 | 33 | 34 | def fix_article_note_links(a): 35 | if a.notes and a.notes != "": 36 | soup = BeautifulSoup(a.notes, "html5lib") 37 | links = soup.find_all("a") 38 | for link in links: 39 | try: 40 | if "/article/" not in link["href"]: 41 | link["href"] = f'/article/{link["href"]}' 42 | except Exception: 43 | logger.error( 44 | f"fix_article_note_links - error with article: {a.uuid}\n url: \n{link}" 45 | ) 46 | a.notes = str(soup.prettify()) 47 | db.session.commit() 48 | -------------------------------------------------------------------------------- /app/helpers/on_demand/dedupe.py: -------------------------------------------------------------------------------- 1 | # Finding duplicate articles 2 | 3 | # this will print out articles that have 4 | # the same titles. for each match, it will print 2x -> 1,5 & 5,1 5 | from app.models import Article 6 | 7 | 8 | u = None 9 | 10 | 11 | def dedupe(n): 12 | a = d[n] 13 | for key, value in d.items(): 14 | if a == value and key != n: 15 | print(f"{key} <-> {n}") 16 | 17 | 18 | articles = u.articles.all() 19 | d = {} 20 | for a in articles: 21 | d[a.id] = a.title 22 | 23 | for k, v in d.items(): 24 | dedupe(k) 25 | 26 | # get article, check title, 27 | # check highlight amount, see if it's already archived. 28 | a = Article.query.filter_by(id=173).first() 29 | a.title 30 | a.highlights.count() 31 | a.archived 32 | -------------------------------------------------------------------------------- /app/helpers/on_demand/delete_all.sql: -------------------------------------------------------------------------------- 1 | delete from article; 2 | delete from highlight; 3 | delete from tag; 4 | delete from topic; 5 | delete from highlights_topics; 6 | delete from user; 7 | delete from tags_highlights; 8 | delete from tags_topics; 9 | delete from tags_articles; 10 | delete from task; 11 | delete from notification; -------------------------------------------------------------------------------- /app/helpers/on_demand/format_article.py: -------------------------------------------------------------------------------- 1 | # set empty content 2 | content = [] 3 | # get content and split it at the pre tag 4 | with open("content.html", "r") as x: 5 | text = x.read() 6 | content = text.split("") 7 | # create empty string 8 | html = "" 9 | for block in content: 10 | # each block is a pre element 11 | html += "
" # open div 12 | block = block.replace("
", "")  # remove old pre
13 |     paragraphs = block.split("\n")  # split block on each new line for paragrapgs
14 |     new_para = ""  # empty para string
15 |     for p in paragraphs:
16 |         new_para += f"

{p}

" # add p tags 17 | block = new_para.split("

") # split at empty paragraphs 18 | for b in block: 19 | html += f"
{b}
" # wrap sections in divs 20 | html += "
" 21 | 22 | content = html.split("
") # split at empty divs 23 | 24 | with open("newfile.html", "w") as f: 25 | for block in content: 26 | f.write(block) # write to file 27 | -------------------------------------------------------------------------------- /app/helpers/on_demand/highlight_prompts.py: -------------------------------------------------------------------------------- 1 | from app import db 2 | from app.models import Highlight, Topic 3 | 4 | 5 | def createHighlightPrompts(): 6 | highlights = Highlight.query.all() 7 | for highlight in highlights: 8 | flashcard = Topic.query.filter_by(title="flashcard").first() 9 | flashcards = Topic.query.filter_by(title="flashcards").first() 10 | 11 | if flashcard and highlight.is_added_topic(flashcard): 12 | highlight.prompt = highlight.text 13 | highlight.text = highlight.note 14 | continue 15 | elif flashcards and highlight.is_added_topic(flashcards): 16 | highlight.prompt = highlight.text 17 | highlight.text = highlight.note 18 | continue 19 | else: 20 | highlight.user.launch_task( 21 | "create_recall_text", "Creating highlight recall text", highlight.id 22 | ) 23 | 24 | # create_recall_text(highlight.id) 25 | 26 | db.session.commit() 27 | -------------------------------------------------------------------------------- /app/helpers/on_demand/lazy_images.py: -------------------------------------------------------------------------------- 1 | from app import db 2 | from bs4 import BeautifulSoup 3 | 4 | 5 | def set_images_lazy(*articles): 6 | for a in articles: 7 | soup = BeautifulSoup(a.content, "html5lib") 8 | images = soup.find_all("img") 9 | for img in images: 10 | img["loading"] = "lazy" 11 | a.content = str(soup.prettify()) 12 | db.session.commit() 13 | -------------------------------------------------------------------------------- /app/helpers/on_demand/match_tags.py: -------------------------------------------------------------------------------- 1 | from app import db 2 | from app.models import Article, Tag 3 | 4 | 5 | def match_tags(): 6 | articles = Article.query.all() 7 | for a in articles: 8 | tags = a.tags.all() 9 | for t in tags: 10 | if t.user_id != a.user_id: 11 | a.remove_tag(t) 12 | tag = Tag.query.filter_by(name=t.name, user_id=a.user_id).first() 13 | if not tag: 14 | tag = Tag(name=t.name, user_id=a.user_id) 15 | db.session.add(tag) 16 | a.add_tag(tag) 17 | db.session.commit() 18 | -------------------------------------------------------------------------------- /app/helpers/on_demand/protect_images.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | from app import db 3 | from app.models import Article 4 | 5 | 6 | def protect_images(): 7 | articles = Article.query.all() 8 | for a in articles: 9 | soup = BeautifulSoup(a.content, "html.parser") 10 | images = soup.find_all("img") 11 | for img in images: 12 | try: 13 | if "/download/" in img["src"]: 14 | u = img["src"].replace("/download/", f"/download/{a.user_id}/") 15 | img["src"] = u 16 | except Exception: 17 | pass 18 | a.content = str(soup.prettify()) 19 | db.session.commit() 20 | -------------------------------------------------------------------------------- /app/helpers/pulltext.py: -------------------------------------------------------------------------------- 1 | # from readabilipy import simple_json_from_html_string 2 | # import requests 3 | 4 | # this uses a readability port to clean up the text 5 | # This version of readabilipy is updated with a later version of mozilla code 6 | # it returns the cleaned up html or text. 7 | 8 | import requests 9 | from app.ReadabiliPy.readabilipy.simple_json import simple_json_from_html_string 10 | 11 | 12 | def pull_text(url): 13 | 14 | headers = {"User-Agent": "Mozilla/5.0"} 15 | response = requests.get(url, headers=headers) 16 | response.encoding = "utf-8" 17 | article = simple_json_from_html_string( 18 | response.text, content_digests=False, node_indexes=False, use_readability=True 19 | ) 20 | return article 21 | -------------------------------------------------------------------------------- /app/main/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint("main", __name__) 4 | 5 | from app.main import routes # noqa E402, F401 6 | -------------------------------------------------------------------------------- /app/main/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from flask_wtf.file import FileField, FileAllowed 3 | from wtforms import StringField, SubmitField, TextAreaField, DecimalField, BooleanField 4 | from wtforms.fields.html5 import URLField 5 | from wtforms.validators import DataRequired, URL, ValidationError, Email, Optional 6 | from app.models import Topic 7 | 8 | 9 | class ContentForm(FlaskForm): 10 | url = URLField("URL", validators=[URL(), Optional()]) 11 | epub = FileField( 12 | "Choose an epub file", validators=[FileAllowed(["epub"], "Epub only.")] 13 | ) 14 | 15 | title = StringField("Title") 16 | source = StringField("Source") 17 | text = TextAreaField("Copy and paste text") 18 | submit = SubmitField("Get Content") 19 | 20 | 21 | class SuggestionForm(FlaskForm): 22 | title = StringField("Title", validators=[DataRequired()]) 23 | url = URLField("URL", validators=[URL()]) 24 | summary = TextAreaField("Summary") 25 | submit = SubmitField("Add") 26 | 27 | 28 | class AddTopicForm(FlaskForm): 29 | title = StringField("Topic title ...", validators=[DataRequired()]) 30 | 31 | def validate_title(self, title): 32 | title = Topic.query.filter_by(title=title.data.lower()).first() 33 | if title is not None: 34 | raise ValidationError("Topic with this name already exists.") 35 | 36 | 37 | class AddHighlightForm(FlaskForm): 38 | text = TextAreaField("Highlight") 39 | note = TextAreaField("Add a note or description") 40 | prompt = TextAreaField("Add a prompt for reviewing") 41 | position = DecimalField(places=3, rounding=None, use_locale=False) 42 | do_not_review = BooleanField() 43 | 44 | 45 | class AddApprovedSenderForm(FlaskForm): 46 | email = StringField("Email", validators=[DataRequired(), Email()]) 47 | submit = SubmitField("Approve email") 48 | -------------------------------------------------------------------------------- /app/robots-dev.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /app/robots-prod.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /app -------------------------------------------------------------------------------- /app/service-worker.js: -------------------------------------------------------------------------------- 1 | const CACHE_NAME = 'static-cache-v1.1'; 2 | 3 | const FILES_TO_CACHE = [ 4 | '/static/offline.html', 5 | ]; 6 | 7 | self.addEventListener('install', (evt) => { 8 | console.log('[ServiceWorker] Install'); 9 | evt.waitUntil( 10 | caches.open(CACHE_NAME).then((cache) => { 11 | console.log('[ServiceWorker] Pre-caching offline page'); 12 | return cache.addAll(FILES_TO_CACHE); 13 | }) 14 | ); 15 | 16 | self.skipWaiting(); 17 | }); 18 | 19 | 20 | self.addEventListener('activate', (evt) => { 21 | console.log('[ServiceWorker] Activate'); 22 | evt.waitUntil( 23 | caches.keys().then((keyList) => { 24 | return Promise.all(keyList.map((key) => { 25 | if (key !== CACHE_NAME) { 26 | console.log('[ServiceWorker] Removing old cache', key); 27 | return caches.delete(key); 28 | } 29 | })); 30 | }) 31 | ); 32 | self.clients.claim(); 33 | }); 34 | 35 | 36 | // self.addEventListener('fetch', function(event) { 37 | // event.respondWith(fetch(event.request)); 38 | // }); 39 | 40 | 41 | self.addEventListener('fetch', (evt) => { 42 | if (evt.request.mode !== 'navigate') { 43 | return; 44 | } 45 | evt.respondWith(fetch(evt.request).catch(() => { 46 | return caches.open(CACHE_NAME).then((cache) => { 47 | return cache.match('/static/offline.html'); 48 | }); 49 | }) 50 | ); 51 | }); -------------------------------------------------------------------------------- /app/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint("settings", __name__) 4 | 5 | from app.settings import routes # noqa : E402, F401 6 | -------------------------------------------------------------------------------- /app/settings/email.py: -------------------------------------------------------------------------------- 1 | from app.email import send_email 2 | 3 | from flask import render_template, current_app 4 | 5 | 6 | def send_email_verification(user, email): 7 | token = user.get_reset_password_token() 8 | print(f"sending email - [Lurnby] Verify your email for user: {user.id}") 9 | 10 | send_email( 11 | "Lurnby - Verify your email", 12 | sender=current_app.config["ADMINS"][0], 13 | recipients=[email], 14 | text_body=render_template( 15 | "settings/email/verify_email.txt", user=user, token=token, email=email 16 | ), 17 | html_body=render_template( 18 | "settings/email/verify_email.html", user=user, token=token, email=email 19 | ), 20 | ) 21 | 22 | 23 | def send_delete_verification(user): 24 | token = user.get_delete_account_token() 25 | print(f"sending email - [Lurnby] Confirm account deletion for user: {user.id}") 26 | send_email( 27 | "Lurnby - Confirm account deletion", 28 | sender=current_app.config["ADMINS"][0], 29 | recipients=[user.email], 30 | text_body=render_template( 31 | "settings/email/delete_verify.txt", user=user, token=token 32 | ), 33 | html_body=render_template( 34 | "settings/email/delete_verify.html", user=user, token=token 35 | ), 36 | ) 37 | -------------------------------------------------------------------------------- /app/settings/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import ( 3 | StringField, 4 | SubmitField, 5 | BooleanField, 6 | SelectField, 7 | ) 8 | from wtforms.validators import DataRequired, Email 9 | 10 | 11 | class AddApprovedSenderForm(FlaskForm): 12 | email = StringField("Email", validators=[DataRequired(), Email()]) 13 | submit = SubmitField("Approve email") 14 | 15 | 16 | class DeleteAccountForm(FlaskForm): 17 | export = SelectField( 18 | "Export Type", 19 | choices=[ 20 | ("none", "Don't Export"), 21 | ("txt", "Export as TXT"), 22 | ("json", "Export as JSON"), 23 | ], 24 | ) 25 | submit = SubmitField("Delete my account") 26 | 27 | 28 | class CommunicationForm(FlaskForm): 29 | educational = BooleanField("Educational") 30 | informational = BooleanField("Informational") 31 | promotions = BooleanField("Promotions") 32 | highlights = BooleanField("Highlights") 33 | reminders = BooleanField("Review") 34 | submit = SubmitField("Update my preferences") 35 | -------------------------------------------------------------------------------- /app/static/app.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | /* Progressive Web App Register Service Worker */ 4 | if ('serviceWorker' in navigator) { 5 | navigator.serviceWorker 6 | // .register('{{url_for("static",filename="service-worker.js")}}') 7 | .register('/service-worker.js') 8 | .then(function(registration) { 9 | console.log('Service Worker Registered!'); 10 | return registration; 11 | }) 12 | .catch(function(err) { 13 | console.error('Unable to register service worker.', err); 14 | }); 15 | } -------------------------------------------------------------------------------- /app/static/btn_google_signin_dark_normal_web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/btn_google_signin_dark_normal_web.png -------------------------------------------------------------------------------- /app/static/btn_google_signin_dark_pressed_web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/btn_google_signin_dark_pressed_web.png -------------------------------------------------------------------------------- /app/static/css/_buttons.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .main-button.add-article{ 4 | min-width: 80px; 5 | margin-left: 0px; 6 | margin-right: 8px; 7 | margin-bottom: 8px; 8 | } 9 | 10 | 11 | .main-button { 12 | color: $dark; 13 | background: #F9F7F4; 14 | border-radius:0; 15 | margin: 0 0 0 16px; 16 | padding: 8px 16px; 17 | border: $dark-grey 1px solid; 18 | 19 | &.add-highlight { 20 | background-color: $highlight-yellow; 21 | } 22 | 23 | } 24 | 25 | .main-button:hover{ 26 | color: white; 27 | background: $dark; 28 | 29 | } 30 | 31 | .cancel, .edit{ 32 | background:transparent; 33 | margin-right: 0; 34 | //text-decoration: underline; 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /app/static/css/_dashboard.scss: -------------------------------------------------------------------------------- 1 | .dash_nav { 2 | margin-top: 40px; 3 | ul{ 4 | display: flex; 5 | list-style-type: none; 6 | li { 7 | margin-right: 24px; 8 | text-decoration: underline; 9 | } 10 | 11 | } 12 | 13 | } 14 | 15 | .dash_form { 16 | input, textarea { 17 | padding-left: 8px; 18 | width: 500px; 19 | border:0; 20 | margin: 4px 16px; 21 | } 22 | 23 | 24 | textarea { 25 | border: 0; 26 | min-height: 200px; 27 | } 28 | } 29 | 30 | .dashboard_content { 31 | padding: 16px; 32 | display: flex; 33 | flex-wrap: wrap; 34 | } 35 | 36 | .dashboard_item { 37 | width: 312px; 38 | background: white; 39 | margin: 16px; 40 | padding: 24px; 41 | } 42 | 43 | .dashboard_item.half { 44 | width: 48%; 45 | } 46 | 47 | @media(max-width:576px){ 48 | .dashboard_item.half { 49 | width: 90%; 50 | margin-right: 16px; 51 | } 52 | 53 | .dash_form { 54 | input, textarea { 55 | width: 90%; 56 | margin-right: 16px; 57 | 58 | } 59 | 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /app/static/css/_nav.scss: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////// 2 | // Haven't added all nav code // 3 | // Think it's in bootstrap. // 4 | //////////////////////////////////////// 5 | 6 | ////////////////////// 7 | // Feedback // 8 | ////////////////////// 9 | 10 | .feedback{ 11 | img{ 12 | image-rendering: crisp-edges; 13 | -ms-interpolation-mode: nearest-neighbor; 14 | } 15 | img:hover { 16 | cursor:url("data:image/svg+xml;utf8,❤️") 16 0,auto; 17 | animation: rotate 4s infinite alternate; 18 | } 19 | } 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/static/css/_review.scss: -------------------------------------------------------------------------------- 1 | .review-item { 2 | max-width: 640px; 3 | margin: 16px 0 32px 0; 4 | 5 | .highlight-content{ 6 | margin-bottom: 48px; 7 | } 8 | .review-actions{ 9 | display: flex; 10 | .main-button { 11 | margin-left: 0; 12 | margin-right: 8px; 13 | border: none; 14 | 15 | &.view_highlight:hover{ 16 | svg{ 17 | fill: #FFF; 18 | } 19 | } 20 | } 21 | 22 | .review-outcome{ 23 | margin-left: auto; 24 | 25 | .remember{ 26 | margin-right: 0px; 27 | } 28 | 29 | button{ 30 | width: auto; 31 | } 32 | 33 | @media(min-width: 376px){ 34 | button{ 35 | width: 88px; 36 | } 37 | } 38 | 39 | } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /app/static/css/_sidebar.scss: -------------------------------------------------------------------------------- 1 | 2 | .light-control-mode { 3 | 4 | .main-button { 5 | background-color: white; 6 | } 7 | 8 | .main-button:hover { 9 | background-color: $dark; 10 | color: white; 11 | } 12 | } 13 | 14 | .dark-control-mode { 15 | input { 16 | background: $dark; 17 | color: white; 18 | border: black; 19 | } 20 | 21 | .main-button { 22 | background-color: $dark; 23 | color: white; 24 | } 25 | 26 | .main-button:hover { 27 | background-color: white; 28 | color: $dark; 29 | } 30 | 31 | .bookmark-location { 32 | color: white; 33 | } 34 | 35 | .clear-button{ 36 | background: $dark; 37 | color: white; 38 | } 39 | } -------------------------------------------------------------------------------- /app/static/css/_sidebar_bookmarks.scss: -------------------------------------------------------------------------------- 1 | // This is for the bookmarking modal while reading the article. 2 | // perhaps it's extraneous, but for now I'll work like this until 3 | // I can find a better way to cleanup all of my css code. 4 | 5 | #bookmarks { 6 | display: block; 7 | height: 150px; 8 | overflow-y: scroll; 9 | } 10 | 11 | .bookmark-section { 12 | margin-bottom: 32px; 13 | display: flex; 14 | flex-wrap: auto; 15 | 16 | input { 17 | padding: 8px; 18 | padding-left: 12px; 19 | } 20 | @media(max-width: 374px){ 21 | input { 22 | max-width:75%; 23 | } 24 | } 25 | } 26 | 27 | .bookmark-location { 28 | border: none; 29 | background-color:transparent; 30 | text-decoration: underline; 31 | padding: 8px; 32 | } 33 | 34 | .bookmark-location:hover { 35 | background-color: $dark; 36 | color: white; 37 | } 38 | 39 | .clear-button { 40 | border: none; 41 | border-radius: 24px; 42 | padding: 1px 4px; 43 | background-color: transparent; 44 | color: red; 45 | font-size: 12px; 46 | } 47 | 48 | .clear-button:hover { 49 | background-color: red; 50 | color: white; 51 | 52 | } 53 | -------------------------------------------------------------------------------- /app/static/css/_variables.scss: -------------------------------------------------------------------------------- 1 | // colors 2 | $highlight-yellow: #FFF79F; 3 | $light-pink: #FEF7F6; 4 | $salmon: #fab1a0; 5 | $red: #db4928; 6 | 7 | $main-grey: #F9F7F4; 8 | $footer-grey: #dbdbdb; 9 | $bg-grey:#F2F1EE; 10 | $dark: #424242; 11 | $dark-grey: #9c9894; 12 | $logo-grey: #cbc1bf; 13 | 14 | $green: #B0C6BB; 15 | 16 | 17 | // screen sizes 18 | $mobile-max:575px; //mobile 19 | $sm:576px; // vertical tab / horizontal phone 20 | $md:768px; // vertical tab 21 | 22 | $tablet-max: 991px; 23 | $lg: 992px; //small desktops like chromebooks 24 | $xl: 1200px; 25 | $xxl: 1400px; 26 | -------------------------------------------------------------------------------- /app/static/css/dotcom/styles.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["styles.scss"],"names":[],"mappings":"AACQ;AAER;EACI;;;AAKJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EAEI;;;AAGJ;EACI;;;AAGJ;EACI;EACA;;;AAIH;EACG;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EAEI;EACA;EAEA;EACA;EACA;;;AAKJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;;;AAIJ;EACI;EAEA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;;;AAIJ;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGJ;EACI;;;AAEJ;AAcA;EACI;IACI;;;EAGJ;IACI;IACA;;;EAEJ;IACI;IACA;IACA;IACA;;;EAGJ;IACI;IACA;;;EAGJ;IACI;IAEA;;;EAIJ;IACI;;;EAKR;IACI;AAA0B;IAC1B;IACA;IACA;IACA;;;EAGA;IACA;;;EAGD;IACI;IACA;IACA;;;AASP;EACI;EACA;EACA;;AAEA;EACI;EACA;;;AAUR;EACI;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA","file":"styles.css"} -------------------------------------------------------------------------------- /app/static/css/fa/scss/_animated.scss: -------------------------------------------------------------------------------- 1 | // Animated Icons 2 | // -------------------------- 3 | 4 | .#{$fa-css-prefix}-spin { 5 | animation: fa-spin 2s infinite linear; 6 | } 7 | 8 | .#{$fa-css-prefix}-pulse { 9 | animation: fa-spin 1s infinite steps(8); 10 | } 11 | 12 | @keyframes fa-spin { 13 | 0% { 14 | transform: rotate(0deg); 15 | } 16 | 17 | 100% { 18 | transform: rotate(360deg); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/static/css/fa/scss/_bordered-pulled.scss: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-border { 5 | border: solid .08em $fa-border-color; 6 | border-radius: .1em; 7 | padding: .2em .25em .15em; 8 | } 9 | 10 | .#{$fa-css-prefix}-pull-left { float: left; } 11 | .#{$fa-css-prefix}-pull-right { float: right; } 12 | 13 | .#{$fa-css-prefix}, 14 | .fas, 15 | .far, 16 | .fal, 17 | .fab { 18 | &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } 19 | &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } 20 | } 21 | -------------------------------------------------------------------------------- /app/static/css/fa/scss/_core.scss: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}, 5 | .fas, 6 | .far, 7 | .fal, 8 | .fad, 9 | .fab { 10 | -moz-osx-font-smoothing: grayscale; 11 | -webkit-font-smoothing: antialiased; 12 | display: inline-block; 13 | font-style: normal; 14 | font-variant: normal; 15 | text-rendering: auto; 16 | line-height: 1; 17 | } 18 | 19 | %fa-icon { 20 | @include fa-icon; 21 | } 22 | -------------------------------------------------------------------------------- /app/static/css/fa/scss/_fixed-width.scss: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .#{$fa-css-prefix}-fw { 4 | text-align: center; 5 | width: $fa-fw-width; 6 | } 7 | -------------------------------------------------------------------------------- /app/static/css/fa/scss/_larger.scss: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | // makes the font 33% larger relative to the icon container 5 | .#{$fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -.0667em; 9 | } 10 | 11 | .#{$fa-css-prefix}-xs { 12 | font-size: .75em; 13 | } 14 | 15 | .#{$fa-css-prefix}-sm { 16 | font-size: .875em; 17 | } 18 | 19 | @for $i from 1 through 10 { 20 | .#{$fa-css-prefix}-#{$i}x { 21 | font-size: $i * 1em; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/static/css/fa/scss/_list.scss: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-ul { 5 | list-style-type: none; 6 | margin-left: $fa-li-width * 5/4; 7 | padding-left: 0; 8 | 9 | > li { position: relative; } 10 | } 11 | 12 | .#{$fa-css-prefix}-li { 13 | left: -$fa-li-width; 14 | position: absolute; 15 | text-align: center; 16 | width: $fa-li-width; 17 | line-height: inherit; 18 | } 19 | -------------------------------------------------------------------------------- /app/static/css/fa/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | @mixin fa-icon { 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | display: inline-block; 8 | font-style: normal; 9 | font-variant: normal; 10 | font-weight: normal; 11 | line-height: 1; 12 | } 13 | 14 | @mixin fa-icon-rotate($degrees, $rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; 16 | transform: rotate($degrees); 17 | } 18 | 19 | @mixin fa-icon-flip($horiz, $vert, $rotation) { 20 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; 21 | transform: scale($horiz, $vert); 22 | } 23 | 24 | 25 | // Only display content to screen readers. A la Bootstrap 4. 26 | // 27 | // See: http://a11yproject.com/posts/how-to-hide-content/ 28 | 29 | @mixin sr-only { 30 | border: 0; 31 | clip: rect(0, 0, 0, 0); 32 | height: 1px; 33 | margin: -1px; 34 | overflow: hidden; 35 | padding: 0; 36 | position: absolute; 37 | width: 1px; 38 | } 39 | 40 | // Use in conjunction with .sr-only to only display content when it's focused. 41 | // 42 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 43 | // 44 | // Credit: HTML5 Boilerplate 45 | 46 | @mixin sr-only-focusable { 47 | &:active, 48 | &:focus { 49 | clip: auto; 50 | height: auto; 51 | margin: 0; 52 | overflow: visible; 53 | position: static; 54 | width: auto; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/static/css/fa/scss/_rotated-flipped.scss: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } 5 | .#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } 6 | .#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } 7 | 8 | .#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } 9 | .#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } 10 | .#{$fa-css-prefix}-flip-both, .#{$fa-css-prefix}-flip-horizontal.#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(-1, -1, 2); } 11 | 12 | // Hook for IE8-9 13 | // ------------------------- 14 | 15 | :root { 16 | .#{$fa-css-prefix}-rotate-90, 17 | .#{$fa-css-prefix}-rotate-180, 18 | .#{$fa-css-prefix}-rotate-270, 19 | .#{$fa-css-prefix}-flip-horizontal, 20 | .#{$fa-css-prefix}-flip-vertical, 21 | .#{$fa-css-prefix}-flip-both { 22 | filter: none; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/static/css/fa/scss/_screen-reader.scss: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { @include sr-only; } 5 | .sr-only-focusable { @include sr-only-focusable; } 6 | -------------------------------------------------------------------------------- /app/static/css/fa/scss/_stacked.scss: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-stack { 5 | display: inline-block; 6 | height: 2em; 7 | line-height: 2em; 8 | position: relative; 9 | vertical-align: middle; 10 | width: ($fa-fw-width*2); 11 | } 12 | 13 | .#{$fa-css-prefix}-stack-1x, 14 | .#{$fa-css-prefix}-stack-2x { 15 | left: 0; 16 | position: absolute; 17 | text-align: center; 18 | width: 100%; 19 | } 20 | 21 | .#{$fa-css-prefix}-stack-1x { 22 | line-height: inherit; 23 | } 24 | 25 | .#{$fa-css-prefix}-stack-2x { 26 | font-size: 2em; 27 | } 28 | 29 | .#{$fa-css-prefix}-inverse { 30 | color: $fa-inverse; 31 | } 32 | -------------------------------------------------------------------------------- /app/static/css/fa/scss/brands.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @import 'variables'; 6 | 7 | @font-face { 8 | font-family: 'Font Awesome 5 Brands'; 9 | font-style: normal; 10 | font-weight: 400; 11 | font-display: $fa-font-display; 12 | src: url('#{$fa-font-path}/fa-brands-400.eot'); 13 | src: url('#{$fa-font-path}/fa-brands-400.eot?#iefix') format('embedded-opentype'), 14 | url('#{$fa-font-path}/fa-brands-400.woff2') format('woff2'), 15 | url('#{$fa-font-path}/fa-brands-400.woff') format('woff'), 16 | url('#{$fa-font-path}/fa-brands-400.ttf') format('truetype'), 17 | url('#{$fa-font-path}/fa-brands-400.svg#fontawesome') format('svg'); 18 | } 19 | 20 | .fab { 21 | font-family: 'Font Awesome 5 Brands'; 22 | font-weight: 400; 23 | } 24 | -------------------------------------------------------------------------------- /app/static/css/fa/scss/fontawesome.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @import 'variables'; 6 | @import 'mixins'; 7 | @import 'core'; 8 | @import 'larger'; 9 | @import 'fixed-width'; 10 | @import 'list'; 11 | @import 'bordered-pulled'; 12 | @import 'animated'; 13 | @import 'rotated-flipped'; 14 | @import 'stacked'; 15 | @import 'icons'; 16 | @import 'screen-reader'; 17 | -------------------------------------------------------------------------------- /app/static/css/fa/scss/regular.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @import 'variables'; 6 | 7 | @font-face { 8 | font-family: 'Font Awesome 5 Free'; 9 | font-style: normal; 10 | font-weight: 400; 11 | font-display: $fa-font-display; 12 | src: url('#{$fa-font-path}/fa-regular-400.eot'); 13 | src: url('#{$fa-font-path}/fa-regular-400.eot?#iefix') format('embedded-opentype'), 14 | url('#{$fa-font-path}/fa-regular-400.woff2') format('woff2'), 15 | url('#{$fa-font-path}/fa-regular-400.woff') format('woff'), 16 | url('#{$fa-font-path}/fa-regular-400.ttf') format('truetype'), 17 | url('#{$fa-font-path}/fa-regular-400.svg#fontawesome') format('svg'); 18 | } 19 | 20 | .far { 21 | font-family: 'Font Awesome 5 Free'; 22 | font-weight: 400; 23 | } 24 | -------------------------------------------------------------------------------- /app/static/css/fa/scss/solid.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @import 'variables'; 6 | 7 | @font-face { 8 | font-family: 'Font Awesome 5 Free'; 9 | font-style: normal; 10 | font-weight: 900; 11 | font-display: $fa-font-display; 12 | src: url('#{$fa-font-path}/fa-solid-900.eot'); 13 | src: url('#{$fa-font-path}/fa-solid-900.eot?#iefix') format('embedded-opentype'), 14 | url('#{$fa-font-path}/fa-solid-900.woff2') format('woff2'), 15 | url('#{$fa-font-path}/fa-solid-900.woff') format('woff'), 16 | url('#{$fa-font-path}/fa-solid-900.ttf') format('truetype'), 17 | url('#{$fa-font-path}/fa-solid-900.svg#fontawesome') format('svg'); 18 | } 19 | 20 | .fa, 21 | .fas { 22 | font-family: 'Font Awesome 5 Free'; 23 | font-weight: 900; 24 | } 25 | -------------------------------------------------------------------------------- /app/static/css/fa/scss/v4-shims.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @import 'variables'; 6 | @import 'shims'; 7 | -------------------------------------------------------------------------------- /app/static/css/fa/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/css/fa/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /app/static/css/fa/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/css/fa/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /app/static/css/fa/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/css/fa/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /app/static/css/fa/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/css/fa/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /app/static/css/fa/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/css/fa/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /app/static/css/fa/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/css/fa/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /app/static/css/fa/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/css/fa/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /app/static/css/fa/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/css/fa/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /app/static/css/fa/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/css/fa/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /app/static/css/fa/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/css/fa/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /app/static/css/fa/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/css/fa/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /app/static/css/fa/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/css/fa/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /app/static/css/style.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | @import "page_layout"; 4 | @import "buttons"; 5 | @import "legal"; 6 | @import "settings"; 7 | @import "style_old"; 8 | @import "sidebar"; 9 | @import "sidebar_bookmarks"; 10 | @import "dashboard"; 11 | @import "nav"; 12 | @import "main"; 13 | @import "articles"; 14 | @import "highlights"; 15 | @import "filters"; 16 | @import "pagination"; 17 | @import "alerts"; 18 | @import "review"; 19 | @import "landing"; 20 | @import "dark"; -------------------------------------------------------------------------------- /app/static/font-awesome/HELP-US-OUT.txt: -------------------------------------------------------------------------------- 1 | I hope you love Font Awesome. If you've found it useful, please do me a favor and check out my latest project, 2 | Fort Awesome (https://fortawesome.com). It makes it easy to put the perfect icons on your website. Choose from our awesome, 3 | comprehensive icon sets or copy and paste your own. 4 | 5 | Please. Check it out. 6 | 7 | -Dave Gandy 8 | -------------------------------------------------------------------------------- /app/static/font-awesome/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/font-awesome/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /app/static/font-awesome/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/font-awesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /app/static/font-awesome/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/font-awesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /app/static/font-awesome/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/font-awesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /app/static/font-awesome/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/font-awesome/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /app/static/font-awesome/less/animated.less: -------------------------------------------------------------------------------- 1 | // Animated Icons 2 | // -------------------------- 3 | 4 | .@{fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .@{fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/static/font-awesome/less/bordered-pulled.less: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em @fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .@{fa-css-prefix}-pull-left { float: left; } 11 | .@{fa-css-prefix}-pull-right { float: right; } 12 | 13 | .@{fa-css-prefix} { 14 | &.@{fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.@{fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .@{fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /app/static/font-awesome/less/core.less: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /app/static/font-awesome/less/fixed-width.less: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .@{fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /app/static/font-awesome/less/font-awesome.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables.less"; 7 | @import "mixins.less"; 8 | @import "path.less"; 9 | @import "core.less"; 10 | @import "larger.less"; 11 | @import "fixed-width.less"; 12 | @import "list.less"; 13 | @import "bordered-pulled.less"; 14 | @import "animated.less"; 15 | @import "rotated-flipped.less"; 16 | @import "stacked.less"; 17 | @import "icons.less"; 18 | @import "screen-reader.less"; 19 | -------------------------------------------------------------------------------- /app/static/font-awesome/less/larger.less: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .@{fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .@{fa-css-prefix}-2x { font-size: 2em; } 11 | .@{fa-css-prefix}-3x { font-size: 3em; } 12 | .@{fa-css-prefix}-4x { font-size: 4em; } 13 | .@{fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /app/static/font-awesome/less/list.less: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: @fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .@{fa-css-prefix}-li { 11 | position: absolute; 12 | left: -@fa-li-width; 13 | width: @fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.@{fa-css-prefix}-lg { 17 | left: (-@fa-li-width + (4em / 14)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/static/font-awesome/less/mixins.less: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | .fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | .fa-icon-rotate(@degrees, @rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})"; 16 | -webkit-transform: rotate(@degrees); 17 | -ms-transform: rotate(@degrees); 18 | transform: rotate(@degrees); 19 | } 20 | 21 | .fa-icon-flip(@horiz, @vert, @rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)"; 23 | -webkit-transform: scale(@horiz, @vert); 24 | -ms-transform: scale(@horiz, @vert); 25 | transform: scale(@horiz, @vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | .sr-only() { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | .sr-only-focusable() { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/static/font-awesome/less/path.less: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}'); 7 | src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'), 8 | url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'), 9 | url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), 10 | url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'), 11 | url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /app/static/font-awesome/less/rotated-flipped.less: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } 5 | .@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } 6 | .@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } 7 | 8 | .@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } 9 | .@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .@{fa-css-prefix}-rotate-90, 15 | :root .@{fa-css-prefix}-rotate-180, 16 | :root .@{fa-css-prefix}-rotate-270, 17 | :root .@{fa-css-prefix}-flip-horizontal, 18 | :root .@{fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /app/static/font-awesome/less/screen-reader.less: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { .sr-only(); } 5 | .sr-only-focusable { .sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /app/static/font-awesome/less/stacked.less: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .@{fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .@{fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .@{fa-css-prefix}-inverse { color: @fa-inverse; } 21 | -------------------------------------------------------------------------------- /app/static/font-awesome/scss/_animated.scss: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .#{$fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .#{$fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/static/font-awesome/scss/_bordered-pulled.scss: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em $fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .#{$fa-css-prefix}-pull-left { float: left; } 11 | .#{$fa-css-prefix}-pull-right { float: right; } 12 | 13 | .#{$fa-css-prefix} { 14 | &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .#{$fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /app/static/font-awesome/scss/_core.scss: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /app/static/font-awesome/scss/_fixed-width.scss: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .#{$fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /app/static/font-awesome/scss/_larger.scss: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .#{$fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .#{$fa-css-prefix}-2x { font-size: 2em; } 11 | .#{$fa-css-prefix}-3x { font-size: 3em; } 12 | .#{$fa-css-prefix}-4x { font-size: 4em; } 13 | .#{$fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /app/static/font-awesome/scss/_list.scss: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: $fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .#{$fa-css-prefix}-li { 11 | position: absolute; 12 | left: -$fa-li-width; 13 | width: $fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.#{$fa-css-prefix}-lg { 17 | left: -$fa-li-width + (4em / 14); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/static/font-awesome/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | @mixin fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | @mixin fa-icon-rotate($degrees, $rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; 16 | -webkit-transform: rotate($degrees); 17 | -ms-transform: rotate($degrees); 18 | transform: rotate($degrees); 19 | } 20 | 21 | @mixin fa-icon-flip($horiz, $vert, $rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; 23 | -webkit-transform: scale($horiz, $vert); 24 | -ms-transform: scale($horiz, $vert); 25 | transform: scale($horiz, $vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | @mixin sr-only { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | @mixin sr-only-focusable { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/static/font-awesome/scss/_path.scss: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); 7 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), 8 | url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), 9 | url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), 10 | url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), 11 | url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /app/static/font-awesome/scss/_rotated-flipped.scss: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } 5 | .#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } 6 | .#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } 7 | 8 | .#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } 9 | .#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .#{$fa-css-prefix}-rotate-90, 15 | :root .#{$fa-css-prefix}-rotate-180, 16 | :root .#{$fa-css-prefix}-rotate-270, 17 | :root .#{$fa-css-prefix}-flip-horizontal, 18 | :root .#{$fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /app/static/font-awesome/scss/_screen-reader.scss: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { @include sr-only(); } 5 | .sr-only-focusable { @include sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /app/static/font-awesome/scss/_stacked.scss: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .#{$fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .#{$fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .#{$fa-css-prefix}-inverse { color: $fa-inverse; } 21 | -------------------------------------------------------------------------------- /app/static/font-awesome/scss/font-awesome.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables"; 7 | @import "mixins"; 8 | @import "path"; 9 | @import "core"; 10 | @import "larger"; 11 | @import "fixed-width"; 12 | @import "list"; 13 | @import "bordered-pulled"; 14 | @import "animated"; 15 | @import "rotated-flipped"; 16 | @import "stacked"; 17 | @import "icons"; 18 | @import "screen-reader"; 19 | -------------------------------------------------------------------------------- /app/static/highlighter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/highlighter.png -------------------------------------------------------------------------------- /app/static/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/apple-touch-icon.png -------------------------------------------------------------------------------- /app/static/images/assets/icon_1125x2436.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/assets/icon_1125x2436.png -------------------------------------------------------------------------------- /app/static/images/assets/icon_1136x640.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/assets/icon_1136x640.png -------------------------------------------------------------------------------- /app/static/images/assets/icon_1242x2208.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/assets/icon_1242x2208.png -------------------------------------------------------------------------------- /app/static/images/assets/icon_1242x2688.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/assets/icon_1242x2688.png -------------------------------------------------------------------------------- /app/static/images/assets/icon_1334x750.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/assets/icon_1334x750.png -------------------------------------------------------------------------------- /app/static/images/assets/icon_1536x2048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/assets/icon_1536x2048.png -------------------------------------------------------------------------------- /app/static/images/assets/icon_1668x2224.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/assets/icon_1668x2224.png -------------------------------------------------------------------------------- /app/static/images/assets/icon_1668x2388.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/assets/icon_1668x2388.png -------------------------------------------------------------------------------- /app/static/images/assets/icon_1792x828.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/assets/icon_1792x828.png -------------------------------------------------------------------------------- /app/static/images/assets/icon_180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/assets/icon_180x180.png -------------------------------------------------------------------------------- /app/static/images/assets/icon_2048x1536.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/assets/icon_2048x1536.png -------------------------------------------------------------------------------- /app/static/images/assets/icon_2048x2732.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/assets/icon_2048x2732.png -------------------------------------------------------------------------------- /app/static/images/assets/icon_2208x1242.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/assets/icon_2208x1242.png -------------------------------------------------------------------------------- /app/static/images/assets/icon_2224x1668.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/assets/icon_2224x1668.png -------------------------------------------------------------------------------- /app/static/images/assets/icon_2388x1668.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/assets/icon_2388x1668.png -------------------------------------------------------------------------------- /app/static/images/assets/icon_2436x1125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/assets/icon_2436x1125.png -------------------------------------------------------------------------------- /app/static/images/assets/icon_2688x1242.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/assets/icon_2688x1242.png -------------------------------------------------------------------------------- /app/static/images/assets/icon_2732x2048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/assets/icon_2732x2048.png -------------------------------------------------------------------------------- /app/static/images/assets/icon_640x1136.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/assets/icon_640x1136.png -------------------------------------------------------------------------------- /app/static/images/assets/icon_750x1334.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/assets/icon_750x1334.png -------------------------------------------------------------------------------- /app/static/images/assets/icon_828x1792.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/assets/icon_828x1792.png -------------------------------------------------------------------------------- /app/static/images/darkModeOff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/darkModeOff.png -------------------------------------------------------------------------------- /app/static/images/darkModeOn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/darkModeOn.png -------------------------------------------------------------------------------- /app/static/images/dotcom/content_in_a_single_place.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/content_in_a_single_place.png -------------------------------------------------------------------------------- /app/static/images/dotcom/easy-to-read.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/easy-to-read.png -------------------------------------------------------------------------------- /app/static/images/dotcom/highlights.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/highlights.png -------------------------------------------------------------------------------- /app/static/images/dotcom/icons/automate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/icons/automate.png -------------------------------------------------------------------------------- /app/static/images/dotcom/icons/automate.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/icons/automate.webp -------------------------------------------------------------------------------- /app/static/images/dotcom/icons/highlighter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/icons/highlighter.png -------------------------------------------------------------------------------- /app/static/images/dotcom/icons/highlighter.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/icons/highlighter.webp -------------------------------------------------------------------------------- /app/static/images/dotcom/icons/personalize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/icons/personalize.png -------------------------------------------------------------------------------- /app/static/images/dotcom/icons/personalize.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/icons/personalize.webp -------------------------------------------------------------------------------- /app/static/images/dotcom/icons/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/icons/save.png -------------------------------------------------------------------------------- /app/static/images/dotcom/icons/save.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/icons/save.webp -------------------------------------------------------------------------------- /app/static/images/dotcom/inapp/customize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/inapp/customize.png -------------------------------------------------------------------------------- /app/static/images/dotcom/inapp/customize.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/inapp/customize.webp -------------------------------------------------------------------------------- /app/static/images/dotcom/inapp/extension.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/inapp/extension.png -------------------------------------------------------------------------------- /app/static/images/dotcom/inapp/extension.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/inapp/extension.webp -------------------------------------------------------------------------------- /app/static/images/dotcom/inapp/metadata.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/inapp/metadata.png -------------------------------------------------------------------------------- /app/static/images/dotcom/inapp/metadata.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/inapp/metadata.webp -------------------------------------------------------------------------------- /app/static/images/dotcom/inapp/notes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/inapp/notes.png -------------------------------------------------------------------------------- /app/static/images/dotcom/inapp/notes.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/inapp/notes.webp -------------------------------------------------------------------------------- /app/static/images/dotcom/lurnby-favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/lurnby-favicon.png -------------------------------------------------------------------------------- /app/static/images/dotcom/lurnby-taking-notes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/lurnby-taking-notes.jpg -------------------------------------------------------------------------------- /app/static/images/dotcom/lurnby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/lurnby.png -------------------------------------------------------------------------------- /app/static/images/dotcom/lurnbyhalf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/lurnbyhalf.png -------------------------------------------------------------------------------- /app/static/images/dotcom/lurnbyquarter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/lurnbyquarter.png -------------------------------------------------------------------------------- /app/static/images/dotcom/topics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/dotcom/topics.png -------------------------------------------------------------------------------- /app/static/images/error.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/static/images/icons/check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/static/images/icons/more.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/static/images/icons/nav_left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/static/images/icons/nav_right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/static/images/icons/open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/static/images/icons/x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/static/images/lurnby-logo/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/lurnby-logo/apple-touch-icon.png -------------------------------------------------------------------------------- /app/static/images/lurnby-logo/lurnby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/lurnby-logo/lurnby.png -------------------------------------------------------------------------------- /app/static/images/lurnby-logo/lurnby_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/lurnby-logo/lurnby_logo.png -------------------------------------------------------------------------------- /app/static/images/lurnby-logo/lurnby_logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/lurnby-logo/lurnby_logo.psd -------------------------------------------------------------------------------- /app/static/images/lurnby-logo/lurnby_logo@0,25x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/lurnby-logo/lurnby_logo@0,25x.png -------------------------------------------------------------------------------- /app/static/images/lurnby-logo/lurnby_logo@0,5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/lurnby-logo/lurnby_logo@0,5x.png -------------------------------------------------------------------------------- /app/static/images/lurnby-logo/lurnby_logo@0,75x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/lurnby-logo/lurnby_logo@0,75x.png -------------------------------------------------------------------------------- /app/static/images/lurnby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/lurnby.png -------------------------------------------------------------------------------- /app/static/images/lurnbyDesktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/lurnbyDesktop.png -------------------------------------------------------------------------------- /app/static/images/lurnbyMobileScreens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/lurnbyMobileScreens.png -------------------------------------------------------------------------------- /app/static/images/rr-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-19.png -------------------------------------------------------------------------------- /app/static/images/rr-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-192.png -------------------------------------------------------------------------------- /app/static/images/rr-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-48.png -------------------------------------------------------------------------------- /app/static/images/rr-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-64.png -------------------------------------------------------------------------------- /app/static/images/rr-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-96.png -------------------------------------------------------------------------------- /app/static/images/rr-icon/rr-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rr-19.png -------------------------------------------------------------------------------- /app/static/images/rr-icon/rr-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rr-192.png -------------------------------------------------------------------------------- /app/static/images/rr-icon/rr-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rr-48.png -------------------------------------------------------------------------------- /app/static/images/rr-icon/rr-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rr-64.png -------------------------------------------------------------------------------- /app/static/images/rr-icon/rr-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rr-96.png -------------------------------------------------------------------------------- /app/static/images/rr-icon/rr_w_sign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rr_w_sign.png -------------------------------------------------------------------------------- /app/static/images/rr-icon/rr_w_sign.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rr_w_sign.webp -------------------------------------------------------------------------------- /app/static/images/rr-icon/rr_w_sign_cropped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rr_w_sign_cropped.png -------------------------------------------------------------------------------- /app/static/images/rr-icon/rr_w_sign_cropped.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rr_w_sign_cropped.webp -------------------------------------------------------------------------------- /app/static/images/rr-icon/rr_w_sign_hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rr_w_sign_hello.png -------------------------------------------------------------------------------- /app/static/images/rr-icon/rr_w_sign_hello.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rr_w_sign_hello.webp -------------------------------------------------------------------------------- /app/static/images/rr-icon/rr_w_sign_hello_cropped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rr_w_sign_hello_cropped.png -------------------------------------------------------------------------------- /app/static/images/rr-icon/rr_w_sign_hello_cropped.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rr_w_sign_hello_cropped.webp -------------------------------------------------------------------------------- /app/static/images/rr-icon/rr_w_sign_welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rr_w_sign_welcome.png -------------------------------------------------------------------------------- /app/static/images/rr-icon/rr_w_sign_welcome.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rr_w_sign_welcome.webp -------------------------------------------------------------------------------- /app/static/images/rr-icon/rr_w_sign_welcome_cropped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rr_w_sign_welcome_cropped.png -------------------------------------------------------------------------------- /app/static/images/rr-icon/rr_w_sign_welcome_cropped.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rr_w_sign_welcome_cropped.webp -------------------------------------------------------------------------------- /app/static/images/rr-icon/rrbetterface-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rrbetterface-32.png -------------------------------------------------------------------------------- /app/static/images/rr-icon/rrbetterface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rrbetterface.png -------------------------------------------------------------------------------- /app/static/images/rr-icon/rrfeedback-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rrfeedback-30.png -------------------------------------------------------------------------------- /app/static/images/rr-icon/rrfeedback-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rrfeedback-40.png -------------------------------------------------------------------------------- /app/static/images/rr-icon/rrfeedback-40hr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rrfeedback-40hr.png -------------------------------------------------------------------------------- /app/static/images/rr-icon/rrfeedback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rr-icon/rrfeedback.png -------------------------------------------------------------------------------- /app/static/images/rrfeedback-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rrfeedback-30.png -------------------------------------------------------------------------------- /app/static/images/rrfeedback-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rrfeedback-40.png -------------------------------------------------------------------------------- /app/static/images/rrwlurnbyshirt.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/rrwlurnbyshirt.psd -------------------------------------------------------------------------------- /app/static/images/screenshots/addarticle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/screenshots/addarticle.png -------------------------------------------------------------------------------- /app/static/images/screenshots/highlights.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/screenshots/highlights.png -------------------------------------------------------------------------------- /app/static/images/screenshots/homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/screenshots/homepage.png -------------------------------------------------------------------------------- /app/static/images/screenshots/readingdark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/screenshots/readingdark.png -------------------------------------------------------------------------------- /app/static/images/screenshots/readinglight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/screenshots/readinglight.png -------------------------------------------------------------------------------- /app/static/images/screenshots/review.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/screenshots/review.png -------------------------------------------------------------------------------- /app/static/images/smart-cat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/smart-cat.gif -------------------------------------------------------------------------------- /app/static/images/splashscreens/ipad_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/splashscreens/ipad_splash.png -------------------------------------------------------------------------------- /app/static/images/splashscreens/ipadpro1_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/splashscreens/ipadpro1_splash.png -------------------------------------------------------------------------------- /app/static/images/splashscreens/ipadpro2_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/splashscreens/ipadpro2_splash.png -------------------------------------------------------------------------------- /app/static/images/splashscreens/ipadpro3_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/splashscreens/ipadpro3_splash.png -------------------------------------------------------------------------------- /app/static/images/splashscreens/iphone5_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/splashscreens/iphone5_splash.png -------------------------------------------------------------------------------- /app/static/images/splashscreens/iphone6_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/splashscreens/iphone6_splash.png -------------------------------------------------------------------------------- /app/static/images/splashscreens/iphoneplus_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/splashscreens/iphoneplus_splash.png -------------------------------------------------------------------------------- /app/static/images/splashscreens/iphonex_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/splashscreens/iphonex_splash.png -------------------------------------------------------------------------------- /app/static/images/splashscreens/iphonexr_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/splashscreens/iphonexr_splash.png -------------------------------------------------------------------------------- /app/static/images/splashscreens/iphonexsmax_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/images/splashscreens/iphonexsmax_splash.png -------------------------------------------------------------------------------- /app/static/images/success.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/static/js/copyToClipboard.js: -------------------------------------------------------------------------------- 1 | const copyInputToClipboard = (id) => { 2 | navigator.clipboard.writeText(document.getElementById(id).value); 3 | } 4 | 5 | const copyHrefToClipboard = (id) => { 6 | navigator.clipboard.writeText(document.getElementById(id).href); 7 | } -------------------------------------------------------------------------------- /app/static/js/rangy/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Tim Down 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. -------------------------------------------------------------------------------- /app/static/js/rangy/README.md: -------------------------------------------------------------------------------- 1 | Rangy 2 | ===== 3 | 4 | A cross-browser JavaScript range and selection library. 5 | 6 | The current version is version 1.3.0. 7 | 8 | The latest source code and releases are on [GitHub](../../releases). 9 | 10 | ## Bower 11 | 12 | There is now an official Rangy package for Bower with Rangy 1.2 and 1.3 versions, called `rangy`. 13 | 14 | ## AMD 15 | 16 | Rangy 1.3 has AMD support. 17 | 18 | ## NPM 19 | 20 | There is an official Rangy module on NPM called [`rangy`](https://www.npmjs.org/package/rangy). 21 | 22 | ## Documentation 23 | 24 | Documentation is in [the GitHub wiki](https://github.com/timdown/rangy/wiki). 25 | -------------------------------------------------------------------------------- /app/static/js/settings.js: -------------------------------------------------------------------------------- 1 | var menu = byId('settings_menu') 2 | 3 | var move = (menu.scrollWidth - menu.clientWidth) / 2 4 | 5 | var left = byClass('nav_left')[0] 6 | var right = byClass('nav_right')[0] 7 | 8 | left.addEventListener('click', () => {menu.scrollLeft -= move} ) 9 | right.addEventListener('click', () => {menu.scrollLeft += move} ) 10 | 11 | function button_viz(){ 12 | // if (menu.scrollLeft == 0) { 13 | // left.style.display = "none" 14 | // right.style.display = "block" 15 | // } 16 | // else if(menu.scrollLeft == menu.scrollWidth - menu.clientWidth){ 17 | // left.style.display = "block" 18 | // right.style.display = "none" 19 | // } 20 | // else { 21 | // left.style.display = "block" 22 | // right.style.display = "block" 23 | // } 24 | 25 | if (menu.scrollLeft == 0) { 26 | left.style.opacity = 0 27 | right.style.opacity = 100 28 | } 29 | else if(menu.scrollLeft == menu.scrollWidth - menu.clientWidth){ 30 | left.style.opacity = 100 31 | right.style.opacity = 0 32 | } 33 | else { 34 | left.style.opacity = 100 35 | right.style.opacity = 100 36 | } 37 | 38 | 39 | 40 | } 41 | 42 | menu.addEventListener('scroll', () => {button_viz()}) -------------------------------------------------------------------------------- /app/static/lineheightmax.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/static/lineheightmid.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/static/lineheightmin.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/static/lurnby.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Lurnby - Read Better", 3 | "short_name": "Lurnby", 4 | "theme_color": "#FFF79F", 5 | "background_color": "#cbc1bf", 6 | "icons": [ 7 | { 8 | "src": "/static/images/rr-19.png", 9 | "type": "image/png", 10 | "sizes": "19x19" 11 | }, 12 | { 13 | "src": "/static/images/rr-48.png", 14 | "type": "image/png", 15 | "sizes": "48x48" 16 | }, 17 | { 18 | "src": "/static/images/rr-64.png", 19 | "type": "image/png", 20 | "sizes": "64x64" 21 | }, 22 | { 23 | "src": "/static/images/rr-96.png", 24 | "type": "image/png", 25 | "sizes": "96x96" 26 | }, 27 | { 28 | "src": "/static/images/rr-192.png", 29 | "type": "image/png", 30 | "sizes": "192x192" 31 | } 32 | 33 | ], 34 | "start_url": "/app", 35 | "display": "standalone", 36 | "orientation": "portrait" 37 | } -------------------------------------------------------------------------------- /app/static/offline.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 38 | 39 | 40 |
41 |
42 |
43 | 45 |

Lurnby doesn't work offline yet. We're working on it!

46 |

Ever tried, ever failed, no matter,
try again, fail again, fail better.
Samuel Beckett

47 |
48 |
49 |
50 | 51 | -------------------------------------------------------------------------------- /app/static/robots/robots-dev.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /app/static/robots/robots-prod.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /app -------------------------------------------------------------------------------- /app/static/rr-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/rr-100.png -------------------------------------------------------------------------------- /app/static/rr-selecttext.js: -------------------------------------------------------------------------------- 1 | var selectedText; 2 | function textActions() { 3 | var selectedObj = window.getSelection(); 4 | 5 | selectedText = selectedObj.getRangeAt(0); 6 | 7 | if (selectedText != "") { 8 | 9 | $('#infoDiv').css('display', 'block'); 10 | $('#infoDiv').css('position', 'absolute'); 11 | $('#infoDiv').css('left', event.pageX); 12 | $('#infoDiv').css('top', event.pageY); 13 | } 14 | 15 | } 16 | 17 | 18 | 19 | document.addEventListener("mouseup", textActions); 20 | document.addEventListener("keyup", textActions); 21 | document.addEventListener("click", function(){ 22 | if( $('#infodiv').css('display') == 'block') ){ 23 | $('#infodiv').css('display', 'none') 24 | } 25 | }) 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | $('#addcomment').click(function(){ 34 | $('#infoDiv').css('display', 'none'); 35 | var text = '

Add following text to comment function:

' + selectedText; 36 | bootbox.alert(text); 37 | 38 | }); 39 | 40 | $('#addtolist').click(function(){ 41 | $('#infoDiv').css('display', 'none'); 42 | var text = '

Add following text to some list that will be searchable

' + selectedText; 43 | bootbox.alert(text); 44 | }); 45 | 46 | 47 | 48 | 49 | document.addEventListener("mouseup", textActions); 50 | document.addEventListener("keyup", textActions); 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/static/rrbetterface-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/rrbetterface-32.png -------------------------------------------------------------------------------- /app/static/rrbetterface2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/app/static/rrbetterface2.png -------------------------------------------------------------------------------- /app/static/service-worker.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('install', (evt) => { 2 | console.log('[ServiceWorker] Install'); 3 | evt.waitUntil( 4 | caches.open(CACHE_NAME).then((cache) => { 5 | console.log('[ServiceWorker] Pre-caching offline page'); 6 | return cache.addAll(FILES_TO_CACHE); 7 | }) 8 | ); 9 | 10 | self.skipWaiting(); 11 | }); 12 | 13 | 14 | self.addEventListener('activate', (evt) => { 15 | console.log('[ServiceWorker] Activate'); 16 | evt.waitUntil( 17 | caches.keys().then((keyList) => { 18 | return Promise.all(keyList.map((key) => { 19 | if (key !== CACHE_NAME) { 20 | console.log('[ServiceWorker] Removing old cache', key); 21 | return caches.delete(key); 22 | } 23 | })); 24 | }) 25 | ); 26 | self.clients.claim(); 27 | }); 28 | 29 | 30 | self.addEventListener('fetch', function(event) { 31 | event.respondWith(fetch(event.request)); 32 | }); 33 | 34 | 35 | self.addEventListener('fetch', (evt) => { 36 | if (evt.request.mode !== 'navigate') { 37 | return; 38 | } 39 | evt.respondWith(fetch(evt.request).catch(() => { 40 | return caches.open(CACHE_NAME).then((cache) => { 41 | return cache.match('/static/offline.html'); 42 | }); 43 | }) 44 | ); 45 | }); -------------------------------------------------------------------------------- /app/templates/_all_articles.html: -------------------------------------------------------------------------------- 1 | {% for article in articles %} 2 | {% include '_article.html' %} 3 | {% endfor %} 4 | -------------------------------------------------------------------------------- /app/templates/_article.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% if article.title and article.title|length > 88 %} 4 |

{{'%.88s' % article.title}} ...

5 | {% else %} 6 |

{{ article.title}}

7 | {% endif %} 8 | 9 |
10 |
11 |

{{article['read_time']}}

12 |
13 | {% if article.unread %} 14 |

New

15 | {% elif article.done %} 16 |

Finished reading

17 | {% else %} 18 |

In progress

19 | {% endif %} 20 |
21 | 22 |
23 |
24 |
Progress:
25 |

{{article.progress|int}}%

26 |
27 | 28 |
29 |
30 |
31 |
Last opened:
32 |

{{ article.date_read.strftime("%d %b, %Y") }}

33 |
34 |
35 |
36 | 37 | Start reading 38 |
39 |
-------------------------------------------------------------------------------- /app/templates/article_card.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | {% if article.title|length > 88 %} 7 |

{{'%.88s' % article.title}} ...

8 | {% else %} 9 |

{{ article.title}}

10 | {% endif %} 11 |
12 | {% if article.unread == True %} 13 |

New

14 | {% endif %} 15 |
16 |
17 | 18 | 19 |
20 |
21 |
Read:
22 |

{{article.progress|round|int }}%

23 |
24 |
25 |
Highlights:
26 |

{{article.highlights.count()}}

27 |
28 |
29 |
Tags:
30 |

{{article.tags.count()}}

31 |
32 |
33 |
34 |
35 | 36 | details 37 | Start reading 38 | 39 |
40 |
41 | -------------------------------------------------------------------------------- /app/templates/auth/email/reset_password.html: -------------------------------------------------------------------------------- 1 | {% if user.firstname %} 2 |

Dear {{ user.firstname }},

3 | {% elif user.username%} 4 |

Dear {{ user.username }},

5 | {% else %} 6 |

Hello,

7 | {% endif %} 8 | 9 |

To reset your password click here.

10 |

Alternatively, you can paste the following link in your browser's address bar:

11 |

{{ url_for('auth.reset_password', token=token, _external=True) }}

12 |

If you have not requested a password reset simply ignore this message.

13 |

Sincerely,

14 |

The Lurnby team

15 | -------------------------------------------------------------------------------- /app/templates/auth/email/reset_password.txt: -------------------------------------------------------------------------------- 1 | 2 | {% if user.firstname %} 3 | Dear {{ user.firstname }}, 4 | {% elif user.username %} 5 | Dear {{ user.username }}, 6 | {% else %} 7 | Hello, 8 | {% endif %} 9 | 10 | To reset your password click on the following link: 11 | 12 | {{ url_for('auth.reset_password', token=token, _external=True) }} 13 | 14 | If you have not requested a password reset simply ignore this message. 15 | 16 | Sincerely, 17 | The Lurnby team -------------------------------------------------------------------------------- /app/templates/auth/email/verify_email.html: -------------------------------------------------------------------------------- 1 | There is no email verification yet while registering 2 | 3 | -------------------------------------------------------------------------------- /app/templates/auth/email/verify_email.txt: -------------------------------------------------------------------------------- 1 | 2 | {% if user.firstname %} 3 | Dear {{ user.firstname }}, 4 | {% elif %} 5 | Dear {{ user.username }}, 6 | {% else %} 7 | Hello, 8 | {% endif %} 9 | 10 | To verify your email click on the following link: 11 | 12 | {{ url_for('auth.verify_email', token=token, _external=True) }} 13 | 14 | If you have not requested an email change simply ignore this message. 15 | 16 | Sincerely, 17 | The Lurnby team -------------------------------------------------------------------------------- /app/templates/auth/reset_password.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block style %} 4 | 5 | {% endblock %} 6 | 7 | {% block JS %} 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
12 |
13 |

Reset your password

14 | {{ form.hidden_tag() }} 15 |
16 | 17 |
18 | {{ form.password(class="form-control", placeholder="Password", required=true, autofocus=true)}} 19 | {{ form.password.label}} 20 |
21 | {% for error in form.password.errors %} 22 | 25 | {% endfor %} 26 | 27 |
28 | {{ form.repeat_password(class="form-control", placeholder="Repeat Password", required=true, autofocus=true)}} 29 | {{ form.repeat_password.label}} 30 |
31 | {% for error in form.repeat_password.errors %} 32 | 35 | {% endfor %} 36 | 37 |
38 | {{ form.submit(class="btn btn-lg btn-primary btn-block") }} 39 |
40 | 41 |
42 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/auth/reset_password_request.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block style %} 4 | 5 | {% endblock %} 6 | 7 | {% block JS %} 8 | {% endblock %} 9 | 10 | {% block content %} 11 | 12 |
13 |
14 |

Reset your password

15 | {{ form.hidden_tag() }} 16 |
17 | 18 |
19 | {{ form.email(class="form-control", placeholder="Email", required=true, autofocus=true)}} 20 | {{ form.email.label}} 21 |
22 | {% for error in form.email.errors %} 23 | 26 | {% endfor %} 27 | 28 |
29 | {{ form.submit(class="main-button width-100 ml0") }} 30 |
31 | 32 |
33 | 34 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/dashboard/app_dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block style %} 4 | 5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 |
10 | 14 |
15 | {% block dash %} 16 | {% endblock %} 17 | 18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /app/templates/dashboard/suggestion_dash.html: -------------------------------------------------------------------------------- 1 | {% extends "dashboard/app_dashboard.html" %} 2 | 3 | {% block style %} 4 | 5 | {% endblock %} 6 | {% block dash %} 7 |
8 | {{ form.hidden_tag() }} 9 |
10 | {{form.title( placeholder="Title")}} 11 |
12 |
13 | {{form.url(placeholder="URL")}} 14 |
15 |
16 | {{form.summary(placeholder="Summary")}} 17 |
18 | {{form.submit(class = "main-button")}} 19 | 20 |
21 | 22 | {% for s in suggestions %} 23 |
24 |
25 |

{{ s.title }}

26 |
27 |
28 |
Users: {{ s.users.filter_by(test_account=False).count()}}
29 | 30 |
31 | {% endfor %} 32 | 33 | 34 | 35 | {% endblock %} 36 | 37 | {% block JS %} 38 | 39 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/dashboard/user_dash.html: -------------------------------------------------------------------------------- 1 | {% extends "dashboard/app_dashboard.html" %} 2 | 3 | {% block style %} 4 | 5 | 6 | {% endblock %} 7 | 8 | 9 | 10 | {% block dash %} 11 |
12 |
13 |

{{daily_active}} daily active users

14 |

{{monthly_active}} monthly active users

15 |
16 |
17 | 18 |
19 | 20 | 21 | {% for user in users %} 22 |
23 |
24 |
{{ user['id']}}
25 |
26 |
27 |

Articles: {{user['articles']}}
28 | Highlights: {{user['highlights']}}
29 | Tags: {{user['tags']}}
30 | Topics: {{user['topics']}}
31 | Last Active: {{user['last_active'].strftime("%d %B at %H:%M")}}
32 | Days Old: {{user['days_old']}}
33 | Last Action: {{ user['last_action']}}
34 | {% if user['suggestion']%} 35 | Added: {{ user['suggestion']}}

36 | {% endif %} 37 | 38 | 39 |
40 | 41 |
42 | {% endfor %} 43 |
44 | 45 | {% endblock %} 46 | 47 | {% block JS %} 48 | 58 | 59 | 60 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/dotcom/default.html: -------------------------------------------------------------------------------- 1 | {% extends 'dotcom/base.html' %} 2 | {% block content %} 3 |
4 |
5 |

Real learning is difficult. But effective.

6 |

Lurnby is a studying tool that improves how much you understand and remember of what you read. It uses neuroscience principles and learning methods such as active reading, spaced repetition, recall practice, and chunking.

7 | Oooh, ooh, this is for me! 8 |
9 |
10 | 13 |
14 |
15 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/dotcom/find-out-more-about-lurnby.html: -------------------------------------------------------------------------------- 1 | {% extends 'dotcom/base.html' %} 2 | {% block content %} 3 |
4 |

You have questions? We all do buddy.

5 |
6 |

Q. Is your app broken?

7 |

A. We're in beta. It's possible.

8 |

The app has a feedback button, in the main navigation. It sends us the part of the site you were on, when something went wrong. Send us all the bugs.

9 |
10 |
11 |

Q. How do I use lurnby?

12 |

A. We're working on improving our UX. In the meantime. We have this getting started guide we made.

13 |
14 |
15 |

Q. Tell me what you think about this, I buy my own diamonds and I buy my own rings?

16 |

A. I'm really impressed.

17 |
18 |
19 |

Q. But really, can I talk to a human?

20 |

A. Send an email to team@lurnby.com

21 |
22 |
23 |

Q. Why does your website look like ****?

24 |

A. Lurnby is currently a team of 1. We do what we can.

25 |

26 | We're definitely looking for like-minded people who want to work with us. Send us an email at team@lurnby.com 27 |

28 |
29 | 30 | 31 |
32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /app/templates/dotcom/how-much-lurnby-costs.html: -------------------------------------------------------------------------------- 1 | {% extends 'dotcom/base.html' %} 2 | {% block content %} 3 |
4 |
5 |

We're swimming in investor money. Not.

6 |

We believe everyone who wants to learn better, should be supported by the best tools to do so. We currently operate on a pay-what-you-think-it's-worth model. And we plan to keep this model for as long as it's sustainable.

7 |

This means that if you're not sure how much this is worth for you yet, you can use Lurnby at no cost. If you think that Lurnby is solving a meaningful problem, then please visit Lurnby's Patreon page below to support us.

8 | Support Lurnby on Patreon, please. 9 |

and then,

10 | Can I see it now? 11 | 12 |
13 | 14 |
15 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/dotcom/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'dotcom/base.html' %} 2 | {% block content %} 3 |
4 |
5 |

How much of what you read do you remember?

6 |

Lurnby is a studying tool that improves how much you understand and remember of what you read. It uses neuroscience principles and learning methods such as active reading, spaced repetition, recall practice, and chunking. Read more here.

7 |

We don't always read things we want to remember, but when we do, shouldn't we be able to do so?

8 | Oooh, ooh, this is for me! 9 |
10 |
11 | 14 |
15 |
16 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/dotcom/who-is-lurnby-for.html: -------------------------------------------------------------------------------- 1 | {% extends 'dotcom/base.html' %} 2 | {% block content %} 3 |
4 |
5 |

Do I want to learn better and remember more?

6 |

Lurnby is for people who put in work. It's for driven people who want to improve themselves, learn new skills, and make better decisions.

7 |

It's for people who read a lot. Specifically, it's for people who want to do active reading. The kind where you actively process what you're reading, reflect on it, and make sense of it. If you've read with a pen in hand before, filling your books with marginalia - it's for readers like you.

8 | Yo. That's me. Now tell me how it works. 9 | 10 |
11 | 12 | 13 |
14 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/email/content/recent_highlights.html: -------------------------------------------------------------------------------- 1 | {% extends 'email/email_base.html' %} 2 | {% block css %} 3 | {% endblock %} 4 | 5 | {% block content %} 6 | {% if highlights[0].user.firstname%} 7 |

Dear {{highlights[0].user.firstname}},

8 | {% elif highlights[0].user.username %} 9 |

Dear {{ highlights[0].user.username }},

10 | {% else %} 11 |

Hi,

12 | {% endif %} 13 | 14 |

Here are some of your recent highlights:

15 | {% for h in highlights %} 16 | {% set url = url_for('main.article', uuid=h.article.uuid, _external=True) + '?highlight_id=highlight' + h.id|string %} 17 | 18 |
19 |
From: {{ h.article.title }}
20 | 21 |

{{ h.text | safe}}

22 | 23 | View 24 |
25 | 26 | {% endfor %} 27 |

Sincerely,

28 |

The Lurnby Team

29 | 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /app/templates/email/content/recent_highlights.txt: -------------------------------------------------------------------------------- 1 | {% if highlights[0].user.firstname%} 2 | Dear {{highlights[0].user.firstname}}, 3 | {% elif highlights[0].user.username %} 4 | Dear {{ highlights[0].user.username }}, 5 | {% else %} 6 | Hi, 7 | {% endif %} 8 | 9 | Here are some of your recent highlights: 10 | {% for h in highlights %} 11 | {% set url = url_for('main.article', uuid=h.article.uuid, _external=True) + '?highlight_id=highlight' + h.id|string %} 12 | From: {{ h.article.title }} 13 | 14 | {{ h.text | safe}} 15 | 16 | {{ url }} 17 | 18 | {% endfor %} 19 | Sincerely, 20 | The Lurnby Team -------------------------------------------------------------------------------- /app/templates/email/export_highlights.html: -------------------------------------------------------------------------------- 1 | {% if user.username %} 2 |

Dear {{ user.username }},

3 | {% else %} 4 |

Hi,

5 | {% endif %} 6 |

Please click here to download your exported highlights.

7 |

This link expires on {{delete_date}}. Your files will no longer be available after that.

8 |

Sincerely,

9 |

The Lurnby Team

-------------------------------------------------------------------------------- /app/templates/email/export_highlights.txt: -------------------------------------------------------------------------------- 1 | {% if user.username %} 2 | Dear {{ user.username }}, 3 | {% else %} 4 | Hi! 5 | {% endif %} 6 | Please click the following link to download your exported highlights: 7 | {{url}} 8 | 9 | This link expires on {{delete_date}}. Your files will no longer be available after that. 10 | 11 | Sincerely, 12 | 13 | The Lurnby Team -------------------------------------------------------------------------------- /app/templates/errors/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block style %} 3 | 4 | {% endblock %} 5 | 6 | {% block JS %} 7 | {% endblock %} 8 | 9 | 10 | {% block content %} 11 |
12 |

Not Found

13 |

This resource doesn't exist, please check the url!

14 |

Back

15 | 16 |
17 | 18 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/errors/500.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block style %} 3 | 4 | {% endblock %} 5 | 6 | {% block JS %} 7 | {% endblock %} 8 | 9 | 10 | {% block content %} 11 |
12 |

An unexpected error has occurred

13 |

The administrator has been notified. Sorry for the inconvenience!

14 |

Back

15 | 16 |
17 | 18 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/experiments/uploads.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |
6 |

Choose a file to upload it to AWS S3

7 | 8 |
9 | 10 |
11 |
12 | 13 | {% if msg is defined and msg %} {% endif %} 14 | 15 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/legal/_tos_modal.html: -------------------------------------------------------------------------------- 1 |
2 | 27 |
-------------------------------------------------------------------------------- /app/templates/legal/accept_tos_modal.html: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /app/templates/legal/ipp.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block css %} 3 | {% endblock %} 4 | {% block content %} 5 | {% include "/legal/_ipp.html" %} 6 | {% endblock %} 7 | {% block js %} 8 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/legal/privacy.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block css %} 3 | {% endblock %} 4 | {% block content %} 5 | {% include "/legal/_privacy.html" %} 6 | {% endblock %} 7 | {% block js %} 8 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/legal/tos.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block css %} 3 | {% endblock %} 4 | {% block content %} 5 | {% include "/legal/_tos.html" %} 6 | {% endblock %} 7 | {% block js %} 8 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/settings/email/delete_verify.html: -------------------------------------------------------------------------------- 1 | {% if user.firstname %} 2 |

Dear {{ user.firstname }},

3 | {% elif user.username %} 4 |

Dear {{ user.username }},

5 | {% else %} 6 |

Hello,

7 | {% endif %} 8 | 9 |

Click this link to confirm deleting your Lurnby account.

10 |

11 | If you did not request deleting your account, please urgently change your account password as your account may have been compromised. 12 |

13 |

Sincerely,

14 |

The Lurnby team

-------------------------------------------------------------------------------- /app/templates/settings/email/delete_verify.txt: -------------------------------------------------------------------------------- 1 | 2 | {% if user.firstname %} 3 | Dear {{ user.firstname }}, 4 | {% elif user.username %} 5 | Dear {{ user.username }}, 6 | {% else %} 7 | Hello, 8 | {% endif %} 9 | 10 | To confirm deleting your Lurnby account click on the following link: 11 | 12 | {{ url_for('settings.delete_confirm', token=token, _external=True) }} 13 | 14 | If you did not request deleting your account, please urgently change your account password as your account may have been compromised. 15 | 16 | Sincerely, 17 | The Lurnby team -------------------------------------------------------------------------------- /app/templates/settings/email/verify_email.html: -------------------------------------------------------------------------------- 1 | {% if user.firstname %} 2 |

Dear {{ user.firstname }},

3 | {% elif user.username %} 4 |

Dear {{ user.username }},

5 | {% else %} 6 |

Hello,

7 | {% endif %} 8 | 9 |

Click this link to verify your email.

10 |

If you have not requested an email change simply ignore this message.

11 |

Sincerely,

12 |

The Lurnby team

-------------------------------------------------------------------------------- /app/templates/settings/email/verify_email.txt: -------------------------------------------------------------------------------- 1 | 2 | {% if user.firstname %} 3 | Dear {{ user.firstname }}, 4 | {% elif user.username %} 5 | Dear {{ user.username }}, 6 | {% else %} 7 | Hello, 8 | {% endif %} 9 | 10 | To verify your email click on the following link: 11 | 12 | {{ url_for('settings.verify_email', token=token,email=email, _external=True) }} 13 | 14 | If you have not requested an email change simply ignore this message. 15 | 16 | Sincerely, 17 | The Lurnby team -------------------------------------------------------------------------------- /app/templates/settings/settings_account_email.html: -------------------------------------------------------------------------------- 1 | {% set settings_nav = 'account' %} 2 | {% extends 'settings/settings_base.html' %} 3 | {% block sub_css %} 4 | 7 | {% endblock %} 8 | 9 | {% block settings_content %} 10 |
11 |
12 |

Account

13 |
14 | 15 |
16 |
17 | {{ form.csrf_token }} 18 | 19 |
Update email
20 |
21 | {{ form.email(class="form-control", placeholder="New Email")}} 22 | {{ form.email.label}} 23 |
24 | {% for error in form.email.errors %} 25 | 28 | {% endfor %} 29 | 30 | 31 |
32 | 33 |
34 |
35 | 36 | {% endblock %} 37 | 38 | {% block sub_JS%} 39 | 41 | 42 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/settings/settings_delete_confirm.html: -------------------------------------------------------------------------------- 1 | {% set settings_nav = 'account' %} 2 | {% extends 'settings/settings_base.html' %} 3 | {% block sub_css %} 4 | 7 | {% endblock %} 8 | 9 | {% block settings_content %} 10 |
11 |
12 |

Account

13 |
14 | 15 |
16 | 17 |
18 | {{ form.csrf_token }} 19 | 20 |
Delete my data
21 |
22 |

Clicking the button below will permanently delete your Lurnby account and all of it's associated data.

23 |

If you wish to receive your Lurnby data by email. Please choose an export method below.

24 |

Please consider sharing the reason for your account deletion with our team. It would help us know how we should improve our service.

25 |

26 | Export format 27 |

28 | {{ form.export(class="form-control", placeholder="Choose")}} 29 |
30 | 31 | 32 |
33 | 34 | 35 |
36 |
37 | 38 | {% endblock %} 39 | 40 | {% block sub_JS%} 41 | 42 | {% endblock %} -------------------------------------------------------------------------------- /boot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source venv/bin/activate 3 | flask db upgrade 4 | npm install 5 | exec gunicorn -b :5000 --certfile=./certs/cert.pem --keyfile=./certs/key.pem --access-logfile - --error-logfile - learnbetter:app -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | basedir = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | dotenv_path = os.path.join(basedir, ".env") 7 | load_dotenv(dotenv_path) 8 | 9 | DB_URI = os.getenv("DATABASE_URL") # or other relevant config var 10 | 11 | if DB_URI and DB_URI.startswith("postgres://"): 12 | DB_URI = DB_URI.replace("postgres://", "postgresql://", 1) 13 | 14 | 15 | class Config(object): 16 | SECRET_KEY = os.environ.get("SECRET_KEY") or "Slava-fakes-it-till-he-makes-it" 17 | 18 | # SQL Alchemy Configs 19 | SQLALCHEMY_DATABASE_URI = DB_URI or "sqlite:///" + os.path.join(basedir, "app.db") 20 | SQLALCHEMY_TRACK_MODIFICATIONS = False 21 | SQLALCHEMY_ENGINE_OPTIONS = {"pool_pre_ping": True} 22 | 23 | # Google Client Configs 24 | GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID", None) 25 | GOOGLE_CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET", None) 26 | GOOGLE_DISCOVERY_URL = ( 27 | "https://accounts.google.com/.well-known/openid-configuration" 28 | ) 29 | 30 | LOG_TO_STDOUT = os.environ.get("LOG_TO_STDOUT") 31 | MAIL_SERVER = "smtp.sendgrid.net" 32 | MAIL_PORT = 587 33 | MAIL_USE_TLS = True 34 | MAIL_USERNAME = "apikey" 35 | MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD") 36 | MAIL_DEFAULT_SENDER = os.environ.get("MAIL_DEFAULT_SENDER") 37 | MAIL_DEBUG = False 38 | ADMINS = ["Lurnby "] 39 | REDIS_URL = os.environ.get("REDIS_URL") or "redis://" 40 | SERVER_NAME = os.environ.get("SERVER_NAME") 41 | PREFERRED_URL_SCHEME = "https" 42 | WTF_CSRF_TIME_LIMIT = None 43 | DEV = os.environ.get("DEV") 44 | -------------------------------------------------------------------------------- /dockerRun.md: -------------------------------------------------------------------------------- 1 | ## Postgres 2 | docker run --name lurnby-postgres -e POSTGRES_PASSWORD=mysecretpassword -d postgres 3 | 4 | ## Lurnby 5 | docker run --name lurnby -d -p 8000:5000 --rm -e SECRET_KEY=my-secret-key \ 6 | --link lurnby-postgres:dbserver \ 7 | --mount type=bind,source=./certs,target=/certs \ 8 | lurnby:latest 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /dotcom/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/dotcom/__init__.py -------------------------------------------------------------------------------- /dotcom/ssg.py: -------------------------------------------------------------------------------- 1 | from jinja2 import Environment, FileSystemLoader 2 | import templates 3 | 4 | 5 | env = Environment(loader=FileSystemLoader(next(iter(templates.__path__)))) 6 | 7 | # index 8 | index = env.get_template("index.html") 9 | 10 | with open("default.html", "w") as file: 11 | file.write(index.render(active="index")) 12 | 13 | with open("index.html", "w") as file: 14 | file.write(index.render(active="index")) 15 | 16 | # learn more 17 | more = env.get_template("find-out-more-about-lurnby.html") 18 | with open("find-out-more-about-lurnby.html", "w") as file: 19 | file.write(more.render(active="more")) 20 | 21 | # how it works 22 | works = env.get_template("how-lurnby-works.html") 23 | with open("how-lurnby-works.html", "w") as file: 24 | file.write(works.render(active="works")) 25 | 26 | # how much it costs 27 | costs = env.get_template("how-much-lurnby-costs.html") 28 | with open("how-much-lurnby-costs.html", "w") as file: 29 | file.write(costs.render(active="costs")) 30 | 31 | 32 | # start using 33 | start = env.get_template("how-to-start-using-lurnby.html") 34 | with open("how-to-start-using-lurnby.html", "w") as file: 35 | file.write(start.render(active="start")) 36 | 37 | # tutorials and demos 38 | tutorials = env.get_template("tutorials-and-demos.html") 39 | with open("tutorials-and-demos.html", "w") as file: 40 | file.write(tutorials.render(active="tutorials")) 41 | 42 | # who is lurnby for 43 | who = env.get_template("who-is-lurnby-for.html") 44 | with open("who-is-lurnby-for.html", "w") as file: 45 | file.write(who.render(active="who")) 46 | -------------------------------------------------------------------------------- /dotcom/static/styles.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["styles.scss"],"names":[],"mappings":"AACQ;AAER;EACI;;;AAKJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EAEI;;;AAGJ;EACI;;;AAGJ;EACI;EACA;;;AAIH;EACG;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EAEI;EACA;EAEA;EACA;EACA;;;AAKJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;;;AAIJ;EACI;EAEA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;;;AAIJ;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGJ;EACI;;;AAEJ;AAcA;EACI;IACI;;;EAGJ;IACI;IACA;;;EAEJ;IACI;IACA;IACA;IACA;;;EAGJ;IACI;IACA;;;EAGJ;IACI;IAEA;;;EAIJ;IACI;;;EAKR;IACI;AAA0B;IAC1B;IACA;IACA;IACA;;;EAGA;IACA;;;EAGD;IACI;IACA;IACA;;;AASP;EACI;EACA;EACA;;AAEA;EACI;EACA;;;AAUR;EACI;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA","file":"styles.css"} -------------------------------------------------------------------------------- /dotcom/templates/default.html: -------------------------------------------------------------------------------- 1 | {% extends 'dotcom/base.html' %} 2 | {% block content %} 3 |
4 |
5 |

Real learning is difficult. But effective.

6 |

Lurnby is a studying tool that improves how much you understand and remember of what you read. It uses neuroscience principles and learning methods such as active reading, spaced repetition, recall practice, and chunking.

7 | Oooh, ooh, this is for me! 8 |
9 |
10 | 13 |
14 |
15 | {% endblock %} -------------------------------------------------------------------------------- /dotcom/templates/find-out-more-about-lurnby.html: -------------------------------------------------------------------------------- 1 | {% extends 'dotcom/base.html' %} 2 | {% block content %} 3 |
4 |

You have questions? We all do buddy.

5 |
6 |

Q. Is your app broken?

7 |

A. We're in beta. It's possible.

8 |

The app has a feedback button, in the main navigation. It sends us the part of the site you were on, when something went wrong. Send us all the bugs.

9 |
10 |
11 |

Q. How do I use lurnby?

12 |

A. We're working on improving our UX. In the meantime. We have this getting started guide we made.

13 |
14 |
15 |

Q. Tell me what you think about this, I buy my own diamonds and I buy my own rings?

16 |

A. I'm really impressed.

17 |
18 |
19 |

Q. But really, can I talk to a human?

20 |

A. Send an email to team@lurnby.com

21 |
22 |
23 |

Q. Why does your website look like ****?

24 |

A. Lurnby is currently a team of 1. We do what we can.

25 |

26 | We're definitely looking for like-minded people who want to work with us. Send us an email at team@lurnby.com 27 |

28 |
29 | 30 | 31 |
32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /dotcom/templates/how-much-lurnby-costs.html: -------------------------------------------------------------------------------- 1 | {% extends 'dotcom/base.html' %} 2 | {% block content %} 3 |
4 |
5 |

We're swimming in investor money. Not.

6 |

We believe everyone who wants to learn better, should be supported by the best tools to do so. We currently operate on a pay-what-you-think-it's-worth model. And we plan to keep this model for as long as it's sustainable.

7 |

This means that if you're not sure how much this is worth for you yet, you can use Lurnby at no cost. If you think that Lurnby is solving a meaningful problem, then please visit Lurnby's Patreon page below to support us.

8 | Support Lurnby on Patreon, please. 9 |

and then,

10 | Can I see it now? 11 | 12 |
13 | 14 |
15 | {% endblock %} -------------------------------------------------------------------------------- /dotcom/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |
4 |
5 |

How much of what you read do you remember?

6 |

Lurnby is a studying tool that improves how much you understand and remember of what you read. It uses neuroscience principles and learning methods such as active reading, spaced repetition, recall practice, and chunking. Read more here.

7 |

We don't always read things we want to remember, but when we do, shouldn't we be able to do so?

8 | Oooh, ooh, this is for me! 9 |
10 |
11 | 14 |
15 |
16 | {% endblock %} -------------------------------------------------------------------------------- /dotcom/templates/who-is-lurnby-for.html: -------------------------------------------------------------------------------- 1 | {% extends 'dotcom/base.html' %} 2 | {% block content %} 3 |
4 |
5 |

Do I want to learn better and remember more?

6 |

Lurnby is for people who put in work. It's for driven people who want to improve themselves, learn new skills, and make better decisions.

7 |

It's for people who read a lot. Specifically, it's for people who want to do active reading. The kind where you actively process what you're reading, reflect on it, and make sense of it. If you've read with a pen in hand before, filling your books with marginalia - it's for readers like you.

8 | Yo. That's me. Now tell me how it works. 9 | 10 |
11 | 12 | 13 |
14 | {% endblock %} -------------------------------------------------------------------------------- /install.md: -------------------------------------------------------------------------------- 1 | # Installing locally 2 | To run the following things should be installed on the system. 3 | 4 | - Python 5 | - Redis-server 6 | - node (used by ReadabiliPy to access Mozilla's readability.js) 7 | 8 | ## Installing on a mac 9 | 1. Clone the repo 10 | 1. `cd` into directory 11 | 1. `python3 -m venv venv` // isolates and creates a virtual env 12 | 1. `. venv/bin/activate` // activate venv 13 | 1. `pip install -r requirements.txt` // installs requirements 14 | 1. `npm install` // installs node requirements 15 | 1. `flask db upgrade` // creates the db 16 | 1. `flask run` // starts the flask server 17 | 1. open new terminal tab 18 | 1. `redis-server` // start redis server 19 | 1. open new terminal tab 20 | 1. `. venv/bin/activate` // activate venv 21 | 1. `rq worker lurnby-tasks` // start listening for bg tasks 22 | 23 | ## apis 24 | The app also uses some apis to do what it needs to do. 25 | - amazon s3 for storing images from epubs 26 | - google for auth 27 | - sendgrid for sending emails. 28 | 29 | These need to be set in a .env file, see the .env.example file. 30 | 31 | Some more details for mac are in [mac-install-notes.md](./mac-install-notes.md). 32 | 33 | -------------------------------------------------------------------------------- /learnbetter.py: -------------------------------------------------------------------------------- 1 | from app import create_app, db, cli 2 | 3 | from app.models import ( 4 | User, 5 | Article, 6 | Highlight, 7 | Topic, 8 | highlights_topics, 9 | Tag, 10 | tags_articles, 11 | tags_highlights, 12 | Approved_Sender, 13 | Task, 14 | Notification, 15 | Suggestion, 16 | Event, 17 | Comms, 18 | ) 19 | 20 | app = create_app() 21 | cli.register(app) 22 | 23 | 24 | @app.shell_context_processor 25 | def make_shell_context(): 26 | return { 27 | "db": db, 28 | "User": User, 29 | "Article": Article, 30 | "Highlight": Highlight, 31 | "Topic": Topic, 32 | "highlights_topics": highlights_topics, 33 | "Tag": Tag, 34 | "tags_articles": tags_articles, 35 | "tags_highlights": tags_highlights, 36 | "Task": Task, 37 | "Notification": Notification, 38 | "Approved_Sender": Approved_Sender, 39 | "Suggestion": Suggestion, 40 | "Event": Event, 41 | "Comms": Comms, 42 | } 43 | -------------------------------------------------------------------------------- /mac-install-notes.md: -------------------------------------------------------------------------------- 1 | ## Notes on running this on a mac 2 | 3 | 1. Locally the app runs on ssl, so you will need to generate a local certificate and add it to a `certs/` folder in the root directory. It is used in the `.flaskenv` file. 4 | ``` 5 | openssl req -x509 -newkey rsa:4096 -nodes -out cert.pem -keyout key.pem -days 365 6 | ``` 7 | 1. You need to install a few things in the os. 8 | ``` 9 | brew install mupdf swig freetype redis postgresql 10 | ``` 11 | 1. Some macs have issues with multithreading and cause python to crash. Lurnby uses redis to run background tasks which might cause python to crash. You need to set an environment variable to fix this. 12 | ``` 13 | export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES 14 | ``` 15 | 1. You will need 3 terminal tabs for lurnby to run 16 | ``` 17 | 1. Flask app - flask run 18 | 2. Redis - redis-server 19 | 3. Redis Queue - rq worker lurnby-tasks 20 | ``` -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /migrations/versions/0801537eefe1_added_unique_to_comms.py: -------------------------------------------------------------------------------- 1 | """added unique to comms 2 | 3 | Revision ID: 0801537eefe1 4 | Revises: 755e6e328fcf 5 | Create Date: 2021-11-15 00:05:18.946417 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "0801537eefe1" 14 | down_revision = "755e6e328fcf" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | 22 | with op.batch_alter_table("comms", schema=None) as batch_op: 23 | batch_op.create_unique_constraint(batch_op.f("uq_comms_user_id"), ["user_id"]) 24 | 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | with op.batch_alter_table("comms", schema=None) as batch_op: 31 | batch_op.drop_constraint(batch_op.f("uq_comms_user_id"), type_="unique") 32 | 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /migrations/versions/0c05d3df71bd_added_reflections_to_article.py: -------------------------------------------------------------------------------- 1 | """added reflections to article 2 | 3 | Revision ID: 0c05d3df71bd 4 | Revises: 12f7ab99d9d2 5 | Create Date: 2022-01-04 18:39:55.804465 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "0c05d3df71bd" 14 | down_revision = "12f7ab99d9d2" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | with op.batch_alter_table("article", schema=None) as batch_op: 22 | batch_op.add_column(sa.Column("reflections", sa.Text(), nullable=True)) 23 | 24 | with op.batch_alter_table("message", schema=None) as batch_op: 25 | batch_op.alter_column( 26 | "date", existing_type=sa.DATETIME(), type_=sa.Date(), existing_nullable=True 27 | ) 28 | 29 | # ### end Alembic commands ### 30 | 31 | 32 | def downgrade(): 33 | # ### commands auto generated by Alembic - please adjust! ### 34 | with op.batch_alter_table("message", schema=None) as batch_op: 35 | batch_op.alter_column( 36 | "date", existing_type=sa.Date(), type_=sa.DATETIME(), existing_nullable=True 37 | ) 38 | 39 | with op.batch_alter_table("article", schema=None) as batch_op: 40 | batch_op.drop_column("reflections") 41 | 42 | # ### end Alembic commands ### 43 | -------------------------------------------------------------------------------- /migrations/versions/12f7ab99d9d2_added_messages_table.py: -------------------------------------------------------------------------------- 1 | """added messages table 2 | 3 | Revision ID: 12f7ab99d9d2 4 | Revises: 50e2f2ff2dbd 5 | Create Date: 2021-11-26 22:08:46.608300 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "12f7ab99d9d2" 14 | down_revision = "50e2f2ff2dbd" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table( 22 | "message", 23 | sa.Column("id", sa.Integer(), nullable=False), 24 | sa.Column("user_id", sa.Integer(), nullable=True), 25 | sa.Column("name", sa.String(), nullable=True), 26 | sa.Column("date", sa.DateTime(), nullable=True), 27 | sa.ForeignKeyConstraint( 28 | ["user_id"], ["user.id"], name=op.f("fk_message_user_id_user") 29 | ), 30 | sa.PrimaryKeyConstraint("id", name=op.f("pk_message")), 31 | ) 32 | 33 | # ### end Alembic commands ### 34 | 35 | 36 | def downgrade(): 37 | # ### commands auto generated by Alembic - please adjust! ### 38 | 39 | op.drop_table("message") 40 | # ### end Alembic commands ### 41 | -------------------------------------------------------------------------------- /migrations/versions/202c385e6984_added_uuid_to_highlight.py: -------------------------------------------------------------------------------- 1 | """added uuid to highlight 2 | 3 | Revision ID: 202c385e6984 4 | Revises: 8287a55072a2 5 | Create Date: 2023-02-01 08:02:50.281409 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "202c385e6984" 14 | down_revision = "8287a55072a2" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | with op.batch_alter_table("highlight", schema=None) as batch_op: 22 | batch_op.add_column(sa.Column("uuid", sa.String(), nullable=True)) 23 | 24 | # ### end Alembic commands ### 25 | 26 | 27 | def downgrade(): 28 | # ### commands auto generated by Alembic - please adjust! ### 29 | with op.batch_alter_table("highlight", schema=None) as batch_op: 30 | batch_op.drop_column("uuid") 31 | 32 | # ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /migrations/versions/26b62ee75c23_added_uuid_to_tag.py: -------------------------------------------------------------------------------- 1 | """added uuid to tag 2 | 3 | Revision ID: 26b62ee75c23 4 | Revises: 5e2af35aa067 5 | Create Date: 2023-02-05 00:09:59.861431 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "26b62ee75c23" 14 | down_revision = "5e2af35aa067" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | with op.batch_alter_table("tag", schema=None) as batch_op: 22 | batch_op.add_column(sa.Column("uuid", sa.String(), nullable=True)) 23 | 24 | # ### end Alembic commands ### 25 | 26 | 27 | def downgrade(): 28 | # ### commands auto generated by Alembic - please adjust! ### 29 | with op.batch_alter_table("tag", schema=None) as batch_op: 30 | batch_op.drop_column("uuid") 31 | 32 | # ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /migrations/versions/27e6fca13770_added_review_count_to_user_model_for_.py: -------------------------------------------------------------------------------- 1 | """added review count to user model for determining the number of highlights to show per tier 2 | 3 | Revision ID: 27e6fca13770 4 | Revises: 3edaed8395c0 5 | Create Date: 2021-10-12 23:07:44.770245 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "27e6fca13770" 14 | down_revision = "3edaed8395c0" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | 22 | with op.batch_alter_table("user", schema=None) as batch_op: 23 | batch_op.add_column(sa.Column("review_count", sa.Integer(), nullable=True)) 24 | 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | with op.batch_alter_table("user", schema=None) as batch_op: 31 | batch_op.drop_column("review_count") 32 | 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /migrations/versions/35127e9854c7_added_prompt_to_highlight.py: -------------------------------------------------------------------------------- 1 | """added Prompt to highlight 2 | 3 | Revision ID: 35127e9854c7 4 | Revises: 0c05d3df71bd 5 | Create Date: 2022-03-21 22:38:50.932324 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "35127e9854c7" 14 | down_revision = "0c05d3df71bd" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | 22 | with op.batch_alter_table("highlight", schema=None) as batch_op: 23 | batch_op.add_column(sa.Column("prompt", sa.String(), nullable=True)) 24 | 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | with op.batch_alter_table("highlight", schema=None) as batch_op: 31 | batch_op.drop_column("prompt") 32 | 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /migrations/versions/3edaed8395c0_added_last_used_to_topic_model.py: -------------------------------------------------------------------------------- 1 | """added last_used to topic model 2 | 3 | Revision ID: 3edaed8395c0 4 | Revises: cdddbf0edeb6 5 | Create Date: 2021-09-18 12:35:31.915289 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "3edaed8395c0" 14 | down_revision = "cdddbf0edeb6" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | 22 | with op.batch_alter_table("topic", schema=None) as batch_op: 23 | batch_op.add_column(sa.Column("last_used", sa.DateTime(), nullable=True)) 24 | 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | with op.batch_alter_table("topic", schema=None) as batch_op: 31 | batch_op.drop_column("last_used") 32 | 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /migrations/versions/4796cfdbe050_added_processing_field.py: -------------------------------------------------------------------------------- 1 | """added processing field 2 | 3 | Revision ID: 4796cfdbe050 4 | Revises: 35127e9854c7 5 | Create Date: 2023-01-19 21:59:43.129089 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "4796cfdbe050" 14 | down_revision = "35127e9854c7" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | with op.batch_alter_table("article", schema=None) as batch_op: 22 | batch_op.add_column(sa.Column("processing", sa.Boolean(), nullable=True)) 23 | 24 | # ### end Alembic commands ### 25 | 26 | 27 | def downgrade(): 28 | # ### commands auto generated by Alembic - please adjust! ### 29 | with op.batch_alter_table("article", schema=None) as batch_op: 30 | batch_op.drop_column("processing") 31 | 32 | # ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /migrations/versions/50e2f2ff2dbd_adding_index_on_article_title_lowercase.py: -------------------------------------------------------------------------------- 1 | """adding index on article title lowercase 2 | 3 | Revision ID: 50e2f2ff2dbd 4 | Revises: a544d948cd1b 5 | Create Date: 2021-11-29 00:35:22.478120 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "50e2f2ff2dbd" 14 | down_revision = "a544d948cd1b" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | with op.batch_alter_table("article", schema=None) as batch_op: 22 | batch_op.create_index( 23 | "articles_lower_title_key", [sa.text("lower(title)")], unique=False 24 | ) 25 | 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade(): 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | with op.batch_alter_table("article", schema=None) as batch_op: 32 | batch_op.drop_index("articles_lower_title_key") 33 | 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /migrations/versions/5c5bf645c104_added_tos_to_user_model.py: -------------------------------------------------------------------------------- 1 | """added tos to User model 2 | 3 | Revision ID: 5c5bf645c104 4 | Revises: ac6229daac86 5 | Create Date: 2021-11-17 17:19:57.226207 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "5c5bf645c104" 14 | down_revision = "ac6229daac86" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | 22 | with op.batch_alter_table("user", schema=None) as batch_op: 23 | batch_op.add_column(sa.Column("tos", sa.Boolean(), nullable=True)) 24 | 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | with op.batch_alter_table("user", schema=None) as batch_op: 31 | batch_op.drop_column("tos") 32 | 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /migrations/versions/5e2af35aa067_added_article_count_and_highlight_count_.py: -------------------------------------------------------------------------------- 1 | """added article_count and highlight_count to tags 2 | 3 | Revision ID: 5e2af35aa067 4 | Revises: e5f879371002 5 | Create Date: 2023-02-04 23:53:23.449712 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "5e2af35aa067" 14 | down_revision = "e5f879371002" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | with op.batch_alter_table("tag", schema=None) as batch_op: 22 | batch_op.add_column(sa.Column("highlight_count", sa.Integer(), nullable=True)) 23 | batch_op.add_column(sa.Column("article_count", sa.Integer(), nullable=True)) 24 | 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | with op.batch_alter_table("tag", schema=None) as batch_op: 31 | batch_op.drop_column("article_count") 32 | batch_op.drop_column("highlight_count") 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /migrations/versions/755e6e328fcf_added_comms_model.py: -------------------------------------------------------------------------------- 1 | """added Comms model 2 | 3 | Revision ID: 755e6e328fcf 4 | Revises: 83c88503ad2b 5 | Create Date: 2021-11-14 23:58:38.636851 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "755e6e328fcf" 14 | down_revision = "83c88503ad2b" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table( 22 | "comms", 23 | sa.Column("id", sa.Integer(), nullable=False), 24 | sa.Column("user_id", sa.Integer(), nullable=True), 25 | sa.Column("informational", sa.Boolean(), nullable=True), 26 | sa.Column("educational", sa.Boolean(), nullable=True), 27 | sa.Column("promotional", sa.Boolean(), nullable=True), 28 | sa.Column("highlights", sa.Boolean(), nullable=True), 29 | sa.Column("reminders", sa.Boolean(), nullable=True), 30 | sa.ForeignKeyConstraint( 31 | ["user_id"], ["user.id"], name=op.f("fk_comms_user_id_user") 32 | ), 33 | sa.PrimaryKeyConstraint("id", name=op.f("pk_comms")), 34 | ) 35 | 36 | # ### end Alembic commands ### 37 | 38 | 39 | def downgrade(): 40 | # ### commands auto generated by Alembic - please adjust! ### 41 | op.drop_table("comms") 42 | # ### end Alembic commands ### 43 | -------------------------------------------------------------------------------- /migrations/versions/8287a55072a2_added_source_to_highlight.py: -------------------------------------------------------------------------------- 1 | """added source to highlight 2 | 3 | Revision ID: 8287a55072a2 4 | Revises: eb9ea8694722 5 | Create Date: 2023-01-29 15:18:40.253000 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "8287a55072a2" 14 | down_revision = "eb9ea8694722" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | with op.batch_alter_table("highlight", schema=None) as batch_op: 22 | batch_op.add_column(sa.Column("source", sa.String(), nullable=True)) 23 | 24 | # ### end Alembic commands ### 25 | 26 | 27 | def downgrade(): 28 | # ### commands auto generated by Alembic - please adjust! ### 29 | with op.batch_alter_table("highlight", schema=None) as batch_op: 30 | batch_op.drop_column("source") 31 | 32 | # ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /migrations/versions/83c88503ad2b_added_event_class.py: -------------------------------------------------------------------------------- 1 | """added event class 2 | 3 | Revision ID: 83c88503ad2b 4 | Revises: 27e6fca13770 5 | Create Date: 2021-10-29 15:19:04.990894 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "83c88503ad2b" 14 | down_revision = "27e6fca13770" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table( 22 | "event", 23 | sa.Column("id", sa.Integer(), nullable=False), 24 | sa.Column("user_id", sa.Integer(), nullable=True), 25 | sa.Column("name", sa.String(), nullable=True), 26 | sa.Column("date", sa.DateTime(), nullable=True), 27 | sa.ForeignKeyConstraint( 28 | ["user_id"], ["user.id"], name=op.f("fk_event_user_id_user") 29 | ), 30 | sa.PrimaryKeyConstraint("id", name=op.f("pk_event")), 31 | ) 32 | 33 | # ### end Alembic commands ### 34 | 35 | 36 | def downgrade(): 37 | # ### commands auto generated by Alembic - please adjust! ## 38 | 39 | op.drop_table("event") 40 | # ### end Alembic commands ### 41 | -------------------------------------------------------------------------------- /migrations/versions/a544d948cd1b_added_date_read_date_and_date_read_time.py: -------------------------------------------------------------------------------- 1 | """added date_read_date 2 | 3 | Revision ID: a544d948cd1b 4 | Revises: 5c5bf645c104 5 | Create Date: 2021-11-28 20:52:31.230691 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "a544d948cd1b" 14 | down_revision = "5c5bf645c104" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | with op.batch_alter_table("article", schema=None) as batch_op: 22 | batch_op.add_column(sa.Column("date_read_date", sa.Date(), nullable=True)) 23 | 24 | # ### end Alembic commands ### 25 | 26 | 27 | def downgrade(): 28 | # ### commands auto generated by Alembic - please adjust! ### 29 | with op.batch_alter_table("article", schema=None) as batch_op: 30 | batch_op.drop_column("date_read_date") 31 | 32 | # ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /migrations/versions/ac6229daac86_added_deleted_attribute.py: -------------------------------------------------------------------------------- 1 | """added deleted attribute 2 | 3 | Revision ID: ac6229daac86 4 | Revises: ae7646aac3f6 5 | Create Date: 2021-11-17 16:36:26.437756 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "ac6229daac86" 14 | down_revision = "ae7646aac3f6" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | 22 | with op.batch_alter_table("user", schema=None) as batch_op: 23 | batch_op.add_column(sa.Column("deleted", sa.Boolean(), nullable=True)) 24 | 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | with op.batch_alter_table("user", schema=None) as batch_op: 31 | batch_op.drop_column("deleted") 32 | 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /migrations/versions/ae7646aac3f6_added_backred.py: -------------------------------------------------------------------------------- 1 | """added backref to user - comms 2 | 3 | Revision ID: ae7646aac3f6 4 | Revises: b1bd350dc073 5 | Create Date: 2021-11-15 02:21:14.844569 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "ae7646aac3f6" 14 | down_revision = "b1bd350dc073" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | 22 | with op.batch_alter_table("user", schema=None) as batch_op: 23 | batch_op.drop_constraint("fk_user_comms_comms", type_="foreignkey") 24 | batch_op.drop_column("comms") 25 | 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade(): 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | with op.batch_alter_table("user", schema=None) as batch_op: 32 | batch_op.add_column(sa.Column("comms", sa.INTEGER(), nullable=True)) 33 | batch_op.create_foreign_key("fk_user_comms_comms", "comms", ["comms"], ["id"]) 34 | 35 | # ### end Alembic commands ### 36 | -------------------------------------------------------------------------------- /migrations/versions/b1bd350dc073_changed_comms_to_be_1_1.py: -------------------------------------------------------------------------------- 1 | """changed comms to be 1:1 2 | 3 | Revision ID: b1bd350dc073 4 | Revises: 0801537eefe1 5 | Create Date: 2021-11-15 02:13:53.258474 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "b1bd350dc073" 14 | down_revision = "0801537eefe1" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | 22 | with op.batch_alter_table("user", schema=None) as batch_op: 23 | batch_op.add_column(sa.Column("comms", sa.Integer(), nullable=True)) 24 | batch_op.create_foreign_key( 25 | batch_op.f("fk_user_comms_comms"), "comms", ["comms"], ["id"] 26 | ) 27 | 28 | # ### end Alembic commands ### 29 | 30 | 31 | def downgrade(): 32 | # ### commands auto generated by Alembic - please adjust! ### 33 | with op.batch_alter_table("user", schema=None) as batch_op: 34 | batch_op.drop_constraint(batch_op.f("fk_user_comms_comms"), type_="foreignkey") 35 | batch_op.drop_column("comms") 36 | 37 | # ### end Alembic commands ### 38 | -------------------------------------------------------------------------------- /migrations/versions/cdddbf0edeb6_added_do_not_review_bool_to_highlight_.py: -------------------------------------------------------------------------------- 1 | """added do_not_review bool to highlight model 2 | 3 | Revision ID: cdddbf0edeb6 4 | Revises: 894a4c974216 5 | Create Date: 2021-08-17 15:50:11.156932 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "cdddbf0edeb6" 14 | down_revision = "894a4c974216" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | 22 | with op.batch_alter_table("highlight", schema=None) as batch_op: 23 | batch_op.add_column(sa.Column("do_not_review", sa.Boolean(), nullable=True)) 24 | 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | with op.batch_alter_table("highlight", schema=None) as batch_op: 31 | batch_op.drop_column("do_not_review") 32 | 33 | # ### end Alembic commands ### 34 | -------------------------------------------------------------------------------- /migrations/versions/e5f879371002_added_unique_constraints_to_uuid_cols.py: -------------------------------------------------------------------------------- 1 | """added unique constraints to uuid cols 2 | 3 | Revision ID: e5f879371002 4 | Revises: 202c385e6984 5 | Create Date: 2023-02-01 08:06:21.388363 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "e5f879371002" 14 | down_revision = "202c385e6984" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | with op.batch_alter_table("article", schema=None) as batch_op: 22 | batch_op.drop_index("ix_article_uuid") 23 | batch_op.create_index(batch_op.f("ix_article_uuid"), ["uuid"], unique=True) 24 | 25 | with op.batch_alter_table("highlight", schema=None) as batch_op: 26 | batch_op.create_unique_constraint(batch_op.f("uq_highlight_uuid"), ["uuid"]) 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade(): 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | with op.batch_alter_table("highlight", schema=None) as batch_op: 33 | batch_op.drop_constraint(batch_op.f("uq_highlight_uuid"), type_="unique") 34 | 35 | with op.batch_alter_table("article", schema=None) as batch_op: 36 | batch_op.drop_index(batch_op.f("ix_article_uuid")) 37 | batch_op.create_index("ix_article_uuid", ["uuid"], unique=False) 38 | # ### end Alembic commands ### 39 | -------------------------------------------------------------------------------- /migrations/versions/eb9ea8694722_added_untagged_to_highlight_model.py: -------------------------------------------------------------------------------- 1 | """added untagged to highlight model 2 | 3 | Revision ID: eb9ea8694722 4 | Revises: 4796cfdbe050 5 | Create Date: 2023-01-29 13:53:35.915093 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "eb9ea8694722" 14 | down_revision = "4796cfdbe050" 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | with op.batch_alter_table("highlight", schema=None) as batch_op: 22 | batch_op.add_column(sa.Column("untagged", sa.Boolean(), nullable=True)) 23 | 24 | # ### end Alembic commands ### 25 | 26 | 27 | def downgrade(): 28 | # ### commands auto generated by Alembic - please adjust! ### 29 | with op.batch_alter_table("highlight", schema=None) as batch_op: 30 | batch_op.drop_column("untagged") 31 | 32 | # ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ReadabiliPy", 3 | "version": "0.1.0", 4 | "description": "An augmented Python wrapper for the Mozilla standalone Readability.js package.", 5 | "main": "ExtractArticle.js", 6 | "scripts": {}, 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/alan-turing-institute/ReadabiliPy" 10 | }, 11 | "author": "", 12 | "license": "Apache-2.0", 13 | "bugs": { 14 | "url": "https://github.com/alan-turing-institute/ReadabiliPy/issues" 15 | }, 16 | "engines": { 17 | "node": ">=11.0.0" 18 | }, 19 | "homepage": "https://github.com/alan-turing-institute/ReadabiliPy", 20 | "dependencies": { 21 | "jsdom": ">=12.2.0", 22 | "minimist": "^1.2.6" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.7.5 2 | attrs==21.2.0 3 | beautifulsoup4==4.9.1 4 | black==22.12.0 5 | blinker==1.4 6 | boto3==1.17.12 7 | botocore==1.20.112 8 | certifi==2023.7.22 9 | chardet==3.0.4 10 | charset-normalizer==2.0.12 11 | click==8.1.3 12 | coverage==7.1.0 13 | cssselect==1.1.0 14 | decorator==5.1.0 15 | Deprecated==1.2.13 16 | dnspython==2.1.0 17 | EbookLib==0.17.1 18 | email-validator==1.1.1 19 | exceptiongroup==1.1.0 20 | Faker==8.10.1 21 | flake8==6.0.0 22 | Flask==2.2.5 23 | Flask-Cors==3.0.9 24 | Flask-HTTPAuth==4.7.0 25 | Flask-Login==0.6.2 26 | Flask-Mail==0.9.1 27 | Flask-Migrate==2.5.3 28 | Flask-SQLAlchemy==3.0.2 29 | flask-talisman==0.7.0 30 | Flask-WTF==1.0.1 31 | gunicorn==20.1.0 32 | html5lib==1.1 33 | idna==2.10 34 | importlib-metadata==6.0.0 35 | infinity==1.5 36 | iniconfig==1.1.1 37 | intervals==0.9.2 38 | itsdangerous==2.0.1 39 | Jinja2==3.0.3 40 | jmespath==0.10.0 41 | lxml==4.6.5 42 | Mako==1.1.6 43 | MarkupSafe==2.1.1 44 | mccabe==0.7.0 45 | mypy-extensions==0.4.3 46 | oauthlib==3.1.0 47 | packaging==21.3 48 | pathspec==0.10.3 49 | pipdeptree==2.2.0 50 | platformdirs==2.6.2 51 | pluggy==1.0.0 52 | psycopg2==2.8.5 53 | py==1.11.0 54 | pycodestyle==2.10.0 55 | pyflakes==3.0.1 56 | PyJWT==2.6.0 57 | PyMuPDF==1.19.2 58 | pyngrok==5.1.0 59 | pyparsing==3.0.6 60 | pytest==7.2.1 61 | pytest-cov==4.0.0 62 | python-dateutil==2.8.2 63 | python-dotenv==0.13.0 64 | PyYAML==6.0 65 | readability-lxml==0.8.1 66 | redis==3.5.3 67 | regex==2020.5.14 68 | requests==2.27.1 69 | rq==1.12.0 70 | s3transfer==0.3.7 71 | six==1.16.0 72 | soupsieve==2.3.1 73 | SQLAlchemy==1.4.46 74 | SQLAlchemy-Utils==0.39.0 75 | text-unidecode==1.3 76 | toml==0.10.2 77 | tomli==2.0.1 78 | typing_extensions==4.4.0 79 | urllib3==1.26.5 80 | validators==0.15.0 81 | webencodings==0.5.1 82 | Werkzeug==2.2.2 83 | wrapt==1.13.3 84 | WTForms==2.3.1 85 | WTForms-Components==0.10.4 86 | zipp==3.11.0 87 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.9 -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/tests/__init__.py -------------------------------------------------------------------------------- /tests/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/tests/api/__init__.py -------------------------------------------------------------------------------- /tests/mocks/mock.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/tests/mocks/mock.epub -------------------------------------------------------------------------------- /tests/mocks/mock.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/tests/mocks/mock.pdf -------------------------------------------------------------------------------- /tests/mocks/mock_html_req.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The coolest Title Ever 5 | 6 | 7 | 8 |

Is there anybody going to listen to my story? All about the girl who came to stay?

9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/mocks/mock_redis.py: -------------------------------------------------------------------------------- 1 | class MockRedis: 2 | def __init__(self, cache=dict()): 3 | self.cache = cache 4 | 5 | def get(self, key): 6 | if key in self.cache: 7 | return self.cache[key] 8 | return None # return nil 9 | 10 | def set(self, key, value, *args, **kwargs): 11 | if self.cache: 12 | self.cache[key] = value 13 | return "OK" 14 | return None # return nil in case of some issue 15 | 16 | def hget(self, hash, key): 17 | if hash in self.cache: 18 | if key in self.cache[hash]: 19 | return self.cache[hash][key] 20 | return None # return nil 21 | 22 | def hset(self, hash, key, value, *args, **kwargs): 23 | if self.cache: 24 | self.cache[hash][key] = value 25 | return 1 26 | return None # return nil in case of some issue 27 | 28 | def exists(self, key): 29 | if key in self.cache: 30 | return 1 31 | return 0 32 | 33 | def cache_overwrite(self, cache=dict()): 34 | self.cache = cache 35 | 36 | def ping(self): 37 | return True 38 | -------------------------------------------------------------------------------- /tests/mocks/test.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roznoshchik/Lurnby/0a0288596c58d5053add1e85f1abd9fc03518ae3/tests/mocks/test.pdf -------------------------------------------------------------------------------- /tests/test_email.py: -------------------------------------------------------------------------------- 1 | from flask_mail import Message 2 | import unittest 3 | from unittest.mock import patch, call 4 | from app import db, create_app 5 | from app.email import send_email, mail 6 | from config import Config 7 | 8 | 9 | class TestConfig(Config): 10 | TESTING = True 11 | SQLALCHEMY_DATABASE_URI = "sqlite://" 12 | MAIL_SUPPRESS_SEND = True 13 | 14 | 15 | class SendEmail(unittest.TestCase): 16 | def setUp(self): 17 | self.app = create_app(TestConfig) 18 | self.app_context = self.app.app_context() 19 | self.app_context.push() 20 | db.create_all() 21 | 22 | def tearDown(self): 23 | db.session.remove() 24 | db.drop_all() 25 | self.app_context.pop() 26 | 27 | @patch.object(mail, "send") 28 | def test_send_email(self, mock_send): 29 | with patch.object(mail, "send") as mock_send: 30 | subject = "Test Subject" 31 | sender = "test@example.com" 32 | recipients = ["recipient@example.com"] 33 | text_body = "This is a test message" 34 | html_body = "

This is a test message

" 35 | send_email(subject, sender, recipients, text_body, html_body) 36 | 37 | expected_msg = Message( 38 | subject=subject, sender=sender, recipients=recipients 39 | ) 40 | expected_msg.body = text_body 41 | expected_msg.html = html_body 42 | 43 | sent_msg = mock_send.call_args_list[0][0][0] 44 | 45 | self.assertEqual(sent_msg.subject, expected_msg.subject) 46 | self.assertEqual(sent_msg.sender, expected_msg.sender) 47 | self.assertEqual(sent_msg.recipients, expected_msg.recipients) 48 | self.assertEqual(sent_msg.body, expected_msg.body) 49 | self.assertEqual(sent_msg.html, expected_msg.html) 50 | -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | from app import create_app 2 | from config import Config 3 | 4 | app = create_app(config_class=Config) 5 | 6 | if __name__ == "__main__": 7 | app.run() 8 | --------------------------------------------------------------------------------