├── .gitignore ├── INSTALL.md ├── LICENSE ├── Makefile ├── NEEDS.md ├── README.md ├── docs ├── Makefile ├── make.bat └── source │ ├── _images │ ├── multiple_choice.gif │ ├── share.png │ ├── single_choice.gif │ ├── to_do_list.gif │ └── true_or_false.gif │ ├── conf.py │ ├── goodies.rst │ ├── goodies_colored_expanders.rst │ ├── goodies_echo.rst │ ├── goodies_share.rst │ ├── index.rst │ ├── install.rst │ ├── interactive_activities.rst │ ├── interactive_activities_multiple_choice.rst │ ├── interactive_activities_single_choice.rst │ ├── interactive_activities_to_do_list.rst │ ├── interactive_activities_true_or_false.rst │ ├── intro.rst │ ├── multipaging.rst │ ├── multipaging_book.rst │ ├── multipaging_chapter.rst │ ├── multipaging_documentation.rst │ ├── multipaging_native.rst │ ├── multipaging_single.rst │ └── roadmap.rst ├── examples ├── 01_to_do_list.md ├── 02_true_or_false.md ├── 03_single_choice.md └── 04_multiple_choice.md ├── images ├── avatar.jpeg ├── github.png ├── linkedin.png └── twitter.png ├── requirements.txt ├── setup.py ├── stb_examples └── README.md ├── stbook_methods.py └── streamlit_book ├── __init__.py ├── admin.py ├── answers.py ├── book_config.py ├── chapter_config.py ├── colored_expanders.py ├── echo.py ├── file_reader.py ├── floating_button.py ├── helpers.py ├── keywords.py ├── login.py ├── render.py ├── render_code_input.py ├── render_multiple_choice.py ├── render_single_choice.py ├── render_text_input.py ├── render_to_do_list.py ├── render_true_or_false.py └── social_media.py /.gitignore: -------------------------------------------------------------------------------- 1 | bookmark.txt 2 | stb_examples/ 3 | tmp/ 4 | stb_*.py 5 | 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | venv/ 17 | .Python 18 | .DS_Store 19 | */.DS_Store 20 | */*/.DS_Store 21 | 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | pip-wheel-metadata/ 35 | share/python-wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | 41 | # PyInstaller 42 | # Usually these files are written by a python script from a template 43 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 44 | *.manifest 45 | *.spec 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .nox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | *.py,cover 62 | .hypothesis/ 63 | .pytest_cache/ 64 | 65 | # Translations 66 | *.mo 67 | *.pot 68 | 69 | # Django stuff: 70 | *.log 71 | local_settings.py 72 | db.sqlite3 73 | db.sqlite3-journal 74 | 75 | # Flask stuff: 76 | instance/ 77 | .webassets-cache 78 | 79 | # Scrapy stuff: 80 | .scrapy 81 | 82 | # Sphinx documentation 83 | docs/_build/ 84 | 85 | # PyBuilder 86 | target/ 87 | 88 | # Jupyter Notebook 89 | .ipynb_checkpoints 90 | 91 | # IPython 92 | profile_default/ 93 | ipython_config.py 94 | 95 | # pyenv 96 | .python-version 97 | 98 | # pipenv 99 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 100 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 101 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 102 | # install all needed dependencies. 103 | #Pipfile.lock 104 | 105 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 106 | __pypackages__/ 107 | 108 | # Celery stuff 109 | celerybeat-schedule 110 | celerybeat.pid 111 | 112 | # SageMath parsed files 113 | *.sage.py 114 | 115 | # Environments 116 | .env 117 | .venv 118 | env/ 119 | venv/ 120 | ENV/ 121 | env.bak/ 122 | venv.bak/ 123 | 124 | # Spyder project settings 125 | .spyderproject 126 | .spyproject 127 | 128 | # Rope project settings 129 | .ropeproject 130 | 131 | # mkdocs documentation 132 | /site 133 | 134 | # mypy 135 | .mypy_cache/ 136 | .dmypy.json 137 | dmypy.json 138 | 139 | # Pyre type checker 140 | .pyre/ 141 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # Install requirements with a virtual environment 2 | 3 | ## Crear el ambiente 4 | virtualenv venv 5 | 6 | ## Activar ambiente 7 | source venv/bin/activate 8 | 9 | ## Instalar librerías 10 | pip install -r requirements.txt 11 | 12 | ## Desactivar Ambiente 13 | deactivate 14 | 15 | ## Borrar el ambiente 16 | rm -rf venv/ 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sebastian Flores 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = docs/source 9 | BUILDDIR = docs/build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | make show: 18 | open docs/build/html/index.html 19 | 20 | make install: 21 | # New 22 | pip install -e /srv/pkg 23 | # old 24 | rm -f dist/*.tar.gz 25 | rm -f build/lib/streamlit_book/*.py 26 | rm -f /miniconda3/lib/python3.7/site-packages/streamlit_book/*.py 27 | python setup.py install 28 | 29 | version: 30 | nano streamlit_book/version_file.py 31 | 32 | test.pypi: 33 | rm -f dist/*.tar.gz 34 | python setup.py sdist 35 | python -m twine upload --repository testpypi dist/* 36 | 37 | pypi: 38 | rm -f dist/*.tar.gz 39 | python setup.py sdist 40 | python -m twine upload --repository pypi dist/* 41 | 42 | # Catch-all target: route all unknown targets to Sphinx using the new 43 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 44 | %: Makefile 45 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 46 | -------------------------------------------------------------------------------- /NEEDS.md: -------------------------------------------------------------------------------- 1 | ADD to template. 2 | 3 | Makefile: create env, delete, activate, run, etc. 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # streamlit_book 2 | 3 | `streamlit_book` is a streamlit companion library, written in python+streamlit to create a interactive reader for the content on a given folder. It was developed on November 2021 during streamlit's hackathon - ended up being awarded one of the two best apps! 4 | 5 | ## Documentation 6 | 7 | All the documentation is at [readthedocs](https://streamlit-book.readthedocs.io/en/latest). 8 | 9 | ## Demos 10 | 11 | The list of all demos of the library for release 0.7.0. are: 12 | 13 | * [Demo Methods](https://stbook-methods.streamlit.app/): differente activities and methods. 14 | * [Demo Multipaging](https://stbook-multipaging.streamlit.app/): Different multipaging options (native and stbook). 15 | 16 | ## Examples 17 | 18 | Some apps using the library are: 19 | 20 | * [Happy Birds](https://notangrybirds.streamlit.app/) : A self contained example that mixes features of the library with a funny twist. 21 | * [The (confusion) Matrix](https://confusion-matrix.streamlit.app/): Take the blue pill to learn all about the confusion matrix. 22 | * [The Streamlitsaurus Rex](https://datasaurus.streamlit.app/): Will teach you to always visualize your data, and exhibits the mythical Datasaurus. 23 | 24 | ## How to use it 25 | 26 | Install it: 27 | 28 | ```bash 29 | pip install streamlit_book 30 | ``` 31 | 32 | There are [different ways to use it](https://streamlit-book.readthedocs.io/en/latest/config.html), but in short just add to `streamlit_app.py` the function that list the files to be read (and other properties): 33 | 34 | ```python 35 | import streamlit as st 36 | import streamlit_book as stb 37 | 38 | # Streamlit page properties 39 | st.set_page_config() 40 | 41 | # Streamit book properties 42 | stb.set_book_config(menu_title="streamlit_book", 43 | menu_icon="lightbulb", 44 | options=[ 45 | "What's new on v0.7.0?", 46 | "Core Features", 47 | ], 48 | paths=[ 49 | "pages/00_whats_new.py", # single file 50 | "pages/01 Multitest", # a folder 51 | ], 52 | icons=[ 53 | "code", 54 | "robot", 55 | ], 56 | save_answers=True, 57 | ) 58 | ``` 59 | 60 | 61 | ## Star History 62 | 63 | [![Star History Chart](https://api.star-history.com/svg?repos=sebastiandres/streamlit_book&type=Date)](https://star-history.com/#sebastiandres/streamlit_book&Date) 64 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/_images/multiple_choice.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastiandres/streamlit_book/65e6e1896253d0a326f4d3e7af143aabc0139c76/docs/source/_images/multiple_choice.gif -------------------------------------------------------------------------------- /docs/source/_images/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastiandres/streamlit_book/65e6e1896253d0a326f4d3e7af143aabc0139c76/docs/source/_images/share.png -------------------------------------------------------------------------------- /docs/source/_images/single_choice.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastiandres/streamlit_book/65e6e1896253d0a326f4d3e7af143aabc0139c76/docs/source/_images/single_choice.gif -------------------------------------------------------------------------------- /docs/source/_images/to_do_list.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastiandres/streamlit_book/65e6e1896253d0a326f4d3e7af143aabc0139c76/docs/source/_images/to_do_list.gif -------------------------------------------------------------------------------- /docs/source/_images/true_or_false.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastiandres/streamlit_book/65e6e1896253d0a326f4d3e7af143aabc0139c76/docs/source/_images/true_or_false.gif -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('../../')) 16 | sys.path.insert(0, os.path.abspath('../../streamlit_book/')) 17 | 18 | print("sys.path:", sys.path) 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = 'streamlit_book' 22 | copyright = '2022, Sebastian Flores Benner' 23 | author = 'Sebastian Flores Benner' 24 | 25 | # The full version, including alpha/beta/rc tags 26 | # Note: We import this to only define this in the src folder 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Read the Docs will set master doc to index instead 31 | # (or whatever it is you have specified in your settings). 32 | # See: https://stackoverflow.com/questions/56336234/build-fail-sphinx-error-contents-rst-not-found 33 | master_doc = 'index' 34 | 35 | # Add any Sphinx extension module names here, as strings. They can be 36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 37 | # ones. 38 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.autosectionlabel'] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # List of patterns, relative to source directory, that match files and 44 | # directories to ignore when looking for source files. 45 | # This pattern also affects html_static_path and html_extra_path. 46 | exclude_patterns = [] 47 | 48 | 49 | # -- Options for HTML output ------------------------------------------------- 50 | 51 | # The theme to use for HTML and HTML Help pages. See the documentation for 52 | # a list of builtin themes. 53 | # 54 | 55 | html_theme = 'sphinx_rtd_theme' 56 | #html_theme = 'sphinx_documatt_theme' 57 | 58 | # Add any paths that contain custom static files (such as style sheets) here, 59 | # relative to this directory. They are copied after the builtin static files, 60 | # so a file named "default.css" will overwrite the builtin "default.css". 61 | html_static_path = ['_static'] 62 | 63 | add_module_names = False -------------------------------------------------------------------------------- /docs/source/goodies.rst: -------------------------------------------------------------------------------- 1 | Other goodies 2 | =============================================== 3 | 4 | But wait, there's more! 5 | 6 | .. include:: goodies_colored_expanders.rst 7 | 8 | .. include:: goodies_echo.rst 9 | 10 | .. include:: goodies_share.rst 11 | -------------------------------------------------------------------------------- /docs/source/goodies_colored_expanders.rst: -------------------------------------------------------------------------------- 1 | Colored Expanders 2 | -------------------- 3 | 4 | More colors to your expander!!! You just have to start the expander header with the right keyword. 5 | 6 | .. raw:: html 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/source/goodies_echo.rst: -------------------------------------------------------------------------------- 1 | Echo 2 | ------ 3 | 4 | The same old echo function from streamlit, but with an additional boolean parameter to control whether the output is shown or not. Intended to be paired with a checkbox, as shown on the example. 5 | 6 | .. autofunction:: __init__.echo 7 | 8 | **Interactive example:** 9 | 10 | .. raw:: html 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/source/goodies_share.rst: -------------------------------------------------------------------------------- 1 | Share on Social Media 2 | ----------------------- 3 | 4 | You can create share buttons for social media. This is based on the code I found at https://sharingbuttons.io/, 5 | which after a couple of hours of googling turned out to be the simplest way to share the content. 6 | Kudos to creator. It has some minor modifications, but it's mostly a wrapping of the code for stremlit. 7 | 8 | It requires as arguments the message and the url. 9 | 10 | .. autofunction:: __init__.share 11 | 12 | .. raw:: html 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome 2 | =============================================== 3 | 4 | **streamlit_book** is an extension to the library streamlit, allowing an easier creation of streamlit apps with multipages and interactive activities. 5 | Jump to the introduction to see some examples or go to the documentation to see how to use it. 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | :caption: Content 10 | 11 | intro 12 | install 13 | multipaging 14 | multipaging_documentation 15 | interactive_activities 16 | goodies 17 | roadmap -------------------------------------------------------------------------------- /docs/source/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ==================== 3 | 4 | The repository for the code is hosted at ``_. 5 | 6 | From pypi 7 | ------------ 8 | 9 | You can install the library from `pypi `_. 10 | This is the safe way. Don't stray from this path. 11 | 12 | .. code-block:: bash 13 | 14 | pip install streamlit_book 15 | 16 | On the `requirements.txt` file it should be simply the line: 17 | 18 | .. code-block:: none 19 | 20 | streamlit_book 21 | 22 | Or, optionally, for the specific version X.Y.Z: 23 | 24 | .. code-block:: none 25 | 26 | streamlit_book==X.Y.Z 27 | 28 | From repository 29 | -------------------- 30 | 31 | You can install the library directly from the latest available version on github. 32 | This is good for testing the library, but might encounter some bugs, in which case you should let us know! 33 | 34 | .. code-block:: bash 35 | 36 | pip install git+https://github.com/sebastiandres/streamlit_book.git 37 | 38 | On the `requirements.txt` file it should be simply the line: 39 | 40 | .. code-block:: none 41 | 42 | git+https://github.com/sebastiandres/streamlit_book.git 43 | -------------------------------------------------------------------------------- /docs/source/interactive_activities.rst: -------------------------------------------------------------------------------- 1 | Interactive Activities 2 | =============================================== 3 | 4 | There are different interactive activities to be used in your apps: 5 | 6 | .. include:: interactive_activities_true_or_false.rst 7 | .. include:: interactive_activities_single_choice.rst 8 | .. include:: interactive_activities_multiple_choice.rst 9 | .. include:: interactive_activities_to_do_list.rst -------------------------------------------------------------------------------- /docs/source/interactive_activities_multiple_choice.rst: -------------------------------------------------------------------------------- 1 | Multiple choice question 2 | ---------------------------- 3 | 4 | This functionality allows to ask a multiple choice question. 5 | It requires a question, the alternatives and the answer(s). 6 | Optionally, the texts for success, error and button can be customized. 7 | 8 | .. autofunction:: __init__.multiple_choice 9 | 10 | .. raw:: html 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/source/interactive_activities_single_choice.rst: -------------------------------------------------------------------------------- 1 | Single choice question 2 | ------------------------ 3 | 4 | This functionality allows to ask a single choice question. 5 | It requires a question, the alternatives and the (unique) answer. 6 | Optionally, the texts for success, error and button can be customized. 7 | 8 | .. autofunction:: __init__.single_choice 9 | 10 | .. raw:: html 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/source/interactive_activities_to_do_list.rst: -------------------------------------------------------------------------------- 1 | To do list 2 | ------------- 3 | 4 | This functionality allows to create to-do list. 5 | It only has as optional argument the text for success. 6 | 7 | .. autofunction:: __init__.to_do_list 8 | 9 | .. raw:: html 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/source/interactive_activities_true_or_false.rst: -------------------------------------------------------------------------------- 1 | True or False question 2 | -------------------------- 3 | 4 | This functionality allows to ask a true/false type of question. 5 | It requires a question and the solution as a True/False value. 6 | Optionally, the texts for success, error and button can be customized. 7 | 8 | .. autofunction:: __init__.true_or_false 9 | 10 | .. raw:: html 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/source/intro.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | streamlit_book provide tools to create interactive multipaged apps 5 | on streamlit apps using python and simple folder convention. 6 | 7 | Features 8 | ---------- 9 | 10 | * Reads and renders python files of the provided folders. 11 | * Multipaging compatible with native streamlit multipage. It can renders chapters (previous, next and reload buttons) or a book (collection of chapters). 12 | * Interactive activities: true or false question, single choice question, multiple choice question and to do list. 13 | * User answers can (optionally) be saved. 14 | * User's token to persist a session. 15 | * Admin view: stats on users and saved answers. 16 | 17 | Use streamlit_book to create more rich and interactive apps, activities and quizzes! 18 | 19 | Examples 20 | ---------- 21 | 22 | * `Happy Birds `_ : A self contained example that mixes features of the library with a funny twist. 23 | * `The (confusion) Matrix `_: Take the blue pill to learn all about the confusion matrix. 24 | * `The Streamlitsaurus Rex `_: Will teach you to always visualize your data, and exhibits the mythical Datasaurus. 25 | * `SatSchool `_: interactive app how to manipulate and understand data from satellites in a range of environmental contexts. 26 | * `Template for multipaging `_: A template for a multipage app using streamlit_book, with public and private sections. 27 | * `stbook-methods.streamlitapp.com `_ : All the activities and goodies in streamlit_book! 28 | * `stbook-multipaging.streamlitapp.com `_ : How to do multipage with streamlit book! 29 | 30 | Are you using streamlit_book? `Let me know `_ and I will add it! 31 | -------------------------------------------------------------------------------- /docs/source/multipaging.rst: -------------------------------------------------------------------------------- 1 | Multipaging: A short guide 2 | =============================================== 3 | 4 | There are ways to have a multipage content 5 | 6 | .. include:: multipaging_native.rst 7 | .. include:: multipaging_single.rst 8 | .. include:: multipaging_chapter.rst 9 | .. include:: multipaging_book.rst 10 | -------------------------------------------------------------------------------- /docs/source/multipaging_book.rst: -------------------------------------------------------------------------------- 1 | Book: Having several chapters 2 | ------------------------------- 3 | 4 | Requires a sidebar menu (like this demo), where each topic required a previous/next buttons. 5 | 6 | Use `stb.set_book_config`` to set the path and the configuration for the book. 7 | 8 | 9 | .. code-block:: python 10 | 11 | import streamlit as st 12 | import streamlit_book as stb 13 | 14 | # Streamlit webpage properties 15 | st.set_page_config() 16 | 17 | # Streamit book properties 18 | stb.set_book_config(menu_title="streamlit_book", 19 | menu_icon="lightbulb", 20 | options=[ 21 | "What's new on v0.7.0?", 22 | "Core Features", 23 | ], 24 | paths=[ 25 | "pages/00_whats_new.py", # single file 26 | "pages/01 Multitest", # a folder 27 | ], 28 | icons=[ 29 | "code", 30 | "robot", 31 | ], 32 | save_answers=True, 33 | ) 34 | 35 | Using the function `set_book_config`: 36 | 37 | * Will setup a sidebar menu with each of the chapter. 38 | * Each chapter is build with pagination for the provided file/folder. 39 | 40 | The capability to render several chapters was added in version 0.7.0, and makes a direct use of 41 | an awesome library to add a sidebar menu called (`streamlit_option_menu `_). 42 | Kudos to the creator. It delivers a professional look, and allows to add `icons by name `_ makes it a lot more user-friendly. 43 | 44 | See the function `set_book_config` required and optional parameters on the :ref:`Multipaging Documentation`. 45 | 46 | .. raw:: html 47 | 48 | 49 |

-------------------------------------------------------------------------------- /docs/source/multipaging_chapter.rst: -------------------------------------------------------------------------------- 1 | Chapter: A single document with multiple connected pages 2 | ---------------------------------------------------------- 3 | 4 | * **Problem**: You want cute multipages, but you only need previous/next buttons. 5 | * **Solution**: Use method `set_chapter_config`` to set the path and other chapter configurations. 6 | 7 | 8 | .. code-block:: python 9 | 10 | import streamlit as st 11 | import streamlit_book as stb 12 | 13 | # Set wide display 14 | st.set_page_config() 15 | 16 | # Set multipage 17 | stb.set_chapter_config(path="pages/", save_answers=True) 18 | 19 | Using the function `set_chapter_config`: 20 | 21 | * Will setup the page navigation text/icons for a unique chapter. 22 | * Will sort the python and markdown files of the given path on lexigraphic order. 23 | * Will read and render the files into streamlit, allowing a enriched markdown/python of the implemented activities. 24 | 25 | See the function `set_chapter_config` required and optional parameters on the :ref:`Multipaging Documentation`. 26 | 27 | .. raw:: html 28 | 29 | 30 |

-------------------------------------------------------------------------------- /docs/source/multipaging_documentation.rst: -------------------------------------------------------------------------------- 1 | .. Multipaging Documentation: 2 | 3 | Multipaging Documentation 4 | ------------------------------- 5 | 6 | .. autofunction:: __init__.set_book_config 7 | 8 | .. autofunction:: __init__.set_chapter_config 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/source/multipaging_native.rst: -------------------------------------------------------------------------------- 1 | Native multipaging 2 | --------------------- 3 | 4 | * **Problem**: You don't want streamlit_book multipaging, but streamlit's native multipaging. 5 | * **Solution**: Just pust your files on `pages/` and import streamlit_book on the files where you need them. You can mix streamlit_book and streamlit's native multipaging. 6 | 7 | .. raw:: html 8 | 9 | 10 |

-------------------------------------------------------------------------------- /docs/source/multipaging_single.rst: -------------------------------------------------------------------------------- 1 | Single page 2 | ------------- 3 | 4 | * **Problem**: You have one page to show, but you want to use the activities/questions provided on streamlit_book. 5 | * **Solution**: Just import the library streamlit_book without the setup and call the required functions. 6 | 7 | .. code-block:: python 8 | 9 | import streamlit as st 10 | import streamlit_book as stb 11 | 12 | st.set_page_config() 13 | 14 | # No need to initialize the streamlit_book library 15 | stb.true_or_false("Are you a robot?", False) 16 | 17 | .. raw:: html 18 | 19 | 20 |

-------------------------------------------------------------------------------- /docs/source/roadmap.rst: -------------------------------------------------------------------------------- 1 | Roadmap 2 | ============ 3 | 4 | This is the roadmap for the next version of the library. 5 | 6 | * Preserving answers to questions - it currently resets the answers to the default values, even if answers have been provided/saved. 7 | * Setting a limit number of answers to a question - it currently allows unlimited answers. 8 | * Reimplement the Table of Contents. 9 | * Save when a user has accepted (do not repeat the dismiss). 10 | * More activities: Text entry, Code entry, File upload, ... -------------------------------------------------------------------------------- /examples/01_to_do_list.md: -------------------------------------------------------------------------------- 1 | stb.to_do 2 | A (completely optional) description: 3 | - [x] task 1 4 | - [ ] task 2 5 | - [ ] task 3 6 | success: Congrats! You did it! -------------------------------------------------------------------------------- /examples/02_true_or_false.md: -------------------------------------------------------------------------------- 1 | stb.true_or_false 2 | Is "Indiana Jones and the Last Crusade" one of the best movies? 3 | True 4 | success: You have chosen wisely 5 | failure: You chose poorly 6 | button: You must choose -------------------------------------------------------------------------------- /examples/03_single_choice.md: -------------------------------------------------------------------------------- 1 | stb.single_choice 2 | Which is the current version of streamlit? 3 | - 0.0.1 4 | - 0.5.5 5 | + 1.2.0 6 | - 9.9.9 7 | success: Correct! As of Nov 13th, 2021, the current version of streamlit is 1.2.0. 8 | error: Wrong. Check the web! 9 | button: Check! -------------------------------------------------------------------------------- /examples/04_multiple_choice.md: -------------------------------------------------------------------------------- 1 | stb.multiple-choice 2 | Which of the following are python libraries? 3 | [T] streamlit 4 | [F] pikachu 5 | [T] numpy 6 | [F] psyduck 7 | [T] matplotlib -------------------------------------------------------------------------------- /images/avatar.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastiandres/streamlit_book/65e6e1896253d0a326f4d3e7af143aabc0139c76/images/avatar.jpeg -------------------------------------------------------------------------------- /images/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastiandres/streamlit_book/65e6e1896253d0a326f4d3e7af143aabc0139c76/images/github.png -------------------------------------------------------------------------------- /images/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastiandres/streamlit_book/65e6e1896253d0a326f4d3e7af143aabc0139c76/images/linkedin.png -------------------------------------------------------------------------------- /images/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastiandres/streamlit_book/65e6e1896253d0a326f4d3e7af143aabc0139c76/images/twitter.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | streamlit 2 | git+https://github.com/sebastiandres/streamlit_book.git 3 | #streamlit_book==0.7.3 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import setuptools 4 | 5 | this_directory = Path(__file__).parent 6 | long_description = (this_directory / "README.md").read_text() 7 | 8 | setuptools.setup( 9 | name="streamlit-slides", 10 | version="0.7.8", 11 | author="Sebastián Flores Benner", 12 | author_email="sebastiandres@gmail.com", 13 | description="A streamlit companion library to create a interactive reader for the content on a given folder", 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | url="https://github.com/sebastiandres/streamlit_book", 17 | packages=setuptools.find_packages(), 18 | include_package_data=True, 19 | classifiers=[], 20 | python_requires=">=3.7", 21 | install_requires=[ 22 | "streamlit >= 0.63", 23 | 'streamlit-option-menu', 24 | ], 25 | extras_require={ 26 | "devel": [ 27 | "wheel", 28 | "pytest==7.4.0", 29 | "playwright==1.36.0", 30 | "requests==2.31.0", 31 | "pytest-playwright-snapshot==1.0", 32 | "pytest-rerunfailures==12.0", 33 | ] 34 | } 35 | ) -------------------------------------------------------------------------------- /stb_examples/README.md: -------------------------------------------------------------------------------- 1 | The stb_examples folder contains two typical examples of how to use streamlit-book. 2 | In order to test the examples on their own, and to avoid duplication, these are separated repos 3 | that should be downloaded by the interested contributor. 4 | 5 | Methods: 6 | ``` 7 | cd stb_examples 8 | git clone git@github.com:sebastiandres/stbook_methods.git 9 | ``` 10 | 11 | Multipaging: 12 | ``` 13 | cd stb_examples 14 | git clone git@github.com:sebastiandres/stbook_multipaging.git 15 | ``` 16 | -------------------------------------------------------------------------------- /stbook_methods.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | from stb_examples.stbook_methods import streamlit_app 3 | 4 | streamlit_app.main() -------------------------------------------------------------------------------- /streamlit_book/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | sys.path.insert(0, os.path.abspath('.')) 4 | 5 | # The version of the library 6 | try: 7 | ### The main function to be used to read the files 8 | from chapter_config import set_chapter_config 9 | from book_config import set_book_config 10 | # General rendering 11 | from render import render_file 12 | # Some additional interesting functions 13 | from render_true_or_false import true_or_false 14 | from render_to_do_list import to_do_list 15 | from render_single_choice import single_choice 16 | from render_multiple_choice import multiple_choice 17 | from render_text_input import text_input 18 | from render_code_input import code_input 19 | # Other goodies 20 | from social_media import share 21 | from colored_expanders import add_color_to_expanders 22 | from floating_button import floating_button 23 | from echo import echo 24 | except: 25 | ### The main function to be used to read the files 26 | from .chapter_config import set_chapter_config 27 | from .book_config import set_book_config 28 | # General rendering 29 | from .render import render_file 30 | # Some additional interesting functions 31 | from .render_true_or_false import true_or_false 32 | from .render_to_do_list import to_do_list 33 | from .render_single_choice import single_choice 34 | from .render_multiple_choice import multiple_choice 35 | from .render_text_input import text_input 36 | from .render_code_input import code_input 37 | # Other goodies 38 | from .social_media import share 39 | from .colored_expanders import add_color_to_expanders 40 | from .floating_button import floating_button 41 | from .echo import echo 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /streamlit_book/admin.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import os 3 | import pandas as pd 4 | import altair as alt 5 | try: 6 | from keywords import USERS_FILENAME, ANSWER_FILENAME 7 | from helpers import password_login, download_file, get_git_revision_short_hash 8 | except: 9 | from .keywords import USERS_FILENAME, ANSWER_FILENAME, ADMIN_PASSWORD 10 | from .helpers import password_login, download_file, get_git_revision_short_hash 11 | 12 | def admin_page(): 13 | """" 14 | Shows a password page to the admin. 15 | """ 16 | if "password_correct" not in st.session_state or not st.session_state["password_correct"]: 17 | admin_login_page() 18 | else: 19 | c1, c2 = st.columns(2) 20 | c1.title("Admin page") 21 | # Show configuration 22 | options = ["Configuration", "User info", "Answer info"] 23 | option = c2.selectbox("Choose an option", options) 24 | # Show users 25 | if option == options[0]: 26 | configuration() 27 | if option == options[1]: 28 | user_info() 29 | if option == options[2]: 30 | answers_info() 31 | return 32 | 33 | def configuration(): 34 | """ 35 | Shows available info on users. 36 | """ 37 | st.header("Configuration") 38 | c1, c2, c3 = st.columns(3) 39 | c1.metric(label="Save answers?", value=f"{st.session_state.save_answers}") 40 | commit_hash = get_git_revision_short_hash() 41 | c2.metric(label="Current commit", value=f"{commit_hash}") 42 | 43 | 44 | def user_info(): 45 | """ 46 | Shows available info on users. 47 | """ 48 | if os.path.exists(USERS_FILENAME): 49 | # If file exists, read the file 50 | df = pd.read_csv(USERS_FILENAME) 51 | # Provide some basic stats 52 | n_users = len(df) 53 | first_user = df.datetime.min() 54 | last_user = df.datetime.max() 55 | c1, c2, c3 = st.columns(3) 56 | c1.metric(label="# users", value=f"{n_users}") 57 | c2.metric(label="First user created on", value=f"{first_user}") 58 | c3.metric(label="Last user created on", value=f"{last_user}") 59 | # Plot the users by day 60 | df['day'] = pd.to_datetime(df['datetime'], format="%Y-%m-%d %H:%M:%S").dt.strftime('%Y-%m-%d') 61 | alt_df = df.groupby("day").count().rename(columns={"user_id": "# users"}).reset_index() 62 | alt_plot = alt.Chart(alt_df).mark_line(point=alt.OverlayMarkDef(color="blue")).encode(x="day", y="# users") 63 | #st.write(alt_df) 64 | st.altair_chart(alt_plot) 65 | # Allow to download the file 66 | download_file(df, button_label="Download user data", filename="users.csv") 67 | # Show the file or a graph 68 | #st.write(df) 69 | else: 70 | st.warning("No users file found.") 71 | 72 | def answers_info(): 73 | """ 74 | Shows available info on answers. 75 | """ 76 | # Show answers 77 | if os.path.exists(ANSWER_FILENAME): 78 | df = pd.read_csv(ANSWER_FILENAME) 79 | # Provide some basic stats 80 | n_answers = len(df) 81 | n_unique_users = df["user_id"].nunique() 82 | n_unique_questions = df["question"].nunique() 83 | correctness = df["correct?"].mean() 84 | c1, c2, c3, c4 = st.columns(4) 85 | c1.metric(label="# answers", value=f"{n_answers}") 86 | c2.metric(label="# users on answers", value=f"{n_unique_users}") 87 | c3.metric(label="# questions", value=f"{n_unique_questions}") 88 | c4.metric(label="Average correct?", value=f"{100*correctness}%") 89 | # Plot the users by day 90 | df['day'] = pd.to_datetime(df['datetime'], format="%Y-%m-%d %H:%M:%S").dt.strftime('%Y-%m-%d') 91 | alt_df = df.groupby("day").count().rename(columns={"user_id": "# users"}).reset_index() 92 | alt_plot = alt.Chart(alt_df).mark_line(point=alt.OverlayMarkDef(color="blue")).encode(x="day", y="# users") 93 | #st.write(alt_df) 94 | st.altair_chart(alt_plot) 95 | # Allow to download the file 96 | c1, c2 = st.columns(2) 97 | df_last_answer = df.drop_duplicates(keep="last") 98 | download_file(df, "Download all answers", "all_answers.csv", where=c1) 99 | download_file(df_last_answer, "Download last answers", "last_answers.csv", where=c2) 100 | # Show the file or a graph 101 | #st.write(df) 102 | else: 103 | st.warning("No answers file found.") 104 | 105 | def admin_login_page(): 106 | """" 107 | Shows a password page to the admin. 108 | """ 109 | st.title("Admin Login") 110 | st.write("Please enter the password to access the admin page.") 111 | password_login("admin") -------------------------------------------------------------------------------- /streamlit_book/answers.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import os 3 | 4 | try: 5 | from helpers import get_datetime_string, get_git_revision_short_hash 6 | from keywords import ANSWER_FILENAME 7 | except: 8 | from .helpers import get_datetime_string, get_git_revision_short_hash 9 | from .keywords import ANSWER_FILENAME 10 | 11 | def create_answer_file(): 12 | if not os.path.exists(ANSWER_FILENAME): 13 | with open(ANSWER_FILENAME, "w") as f: 14 | f.write("commit_hash,datetime,user_id,question,correct?,user_answer,correct_answer\n") 15 | return 16 | 17 | def save_answer(question, is_correct, user_answer, correct_answer): 18 | """ 19 | Save the answer to the question. 20 | """ 21 | # Parameters 22 | create_answer_file() 23 | commit_hash = st.session_state.commit_hash 24 | dt_string = get_datetime_string() 25 | user_id = st.session_state.user_id 26 | # Clean question and answers 27 | question = question.replace("\n", "\\n") 28 | user_answer = str(user_answer).replace("\n", "\\n") 29 | correct_answer = str(correct_answer).replace("\n", "\\n") 30 | with open(ANSWER_FILENAME, "a") as f: 31 | f.write(f'"{commit_hash}","{dt_string}","{user_id}","{question}","{is_correct}","{user_answer}","{correct_answer}"\n') 32 | return -------------------------------------------------------------------------------- /streamlit_book/book_config.py: -------------------------------------------------------------------------------- 1 | 2 | import streamlit as st 3 | from glob import glob 4 | import os 5 | import pandas as pd 6 | 7 | try: 8 | from .file_reader import get_all_files, create_buttons 9 | from .login import create_new_user_and_token, get_user_from_token, get_token_from_user 10 | from .admin import admin_page 11 | from .helpers import get_git_revision_short_hash 12 | from .render import render_file 13 | from .chapter_config import set_chapter_config 14 | except Exception as e: 15 | print("Cannot import from . ", e) 16 | from file_reader import get_all_files, create_buttons 17 | from login import create_new_user_and_token, get_user_from_token, get_token_from_user 18 | from admin import admin_page 19 | from helpers import get_git_revision_short_hash 20 | from render import render_file 21 | from chapter_config import set_chapter_config 22 | except Exception as e: 23 | print("Cannot import! ", e) 24 | 25 | def set_book_config(options, paths, 26 | menu_title="Select a chapter", 27 | menu_icon="book", 28 | icons=None, 29 | orientation=None, 30 | styles=None, 31 | save_answers=False, 32 | display_page_info=True, 33 | ): 34 | """Creates a book using the streamlit_option_menu library. 35 | Renders each of the corresponding chapters based on their properties. 36 | Uses the same configurations used by `streamlit-option-menu 37 | `_ 38 | and icons from `bootstrap-icons `_. 39 | 40 | :param options: List of chapter names to be displayed 41 | :type options: list of str 42 | :param paths: List of chapter paths containging the pages (py, md) to be displayed 43 | :type paths: list of str 44 | :param menu_title: Title of the menu, can be empty to be skipped. 45 | :type menu_title: str 46 | :param menu_icon: Icon to be used on the menu, from bootstrap icons. 47 | :type menu_icon: str 48 | :param icons: Icons to be used. Can be a single one used for all books, or a list of icons for each book. 49 | :type menu_icon: str or list of str 50 | :param orientation: Orientation of the menu. Can be "horizontal" or "vertical". 51 | :type orientation: str 52 | :param styles: Styles to be used. See the documentation of streamlit_option_menu. 53 | :type styles: dict 54 | :param save_answers: If True, it will save the answers in a csv file. Defaults to False. 55 | :type save_answers: bool 56 | :param display_page_info: If True, it will display the page info with the name and number. Defaults to True. 57 | :type display_page_info: bool 58 | :return: None 59 | """ 60 | from streamlit_option_menu import option_menu 61 | 62 | # Initialize variables 63 | if "page_number" not in st.session_state: 64 | st.session_state.page_number = 0 65 | if "book_number" not in st.session_state: 66 | st.session_state.book_number = 0 67 | 68 | # Pack the arguments 69 | execution_dict = { 70 | "menu_title": menu_title, 71 | "options": options, 72 | } 73 | args = [menu_icon, icons, orientation, styles] 74 | if menu_icon is not None: execution_dict["menu_icon"] = menu_icon 75 | if icons is not None: execution_dict["icons"] = icons 76 | if orientation is not None: execution_dict["orientation"] = orientation 77 | if styles is not None: execution_dict["styles"] = styles 78 | 79 | # Execute the menu 80 | if orientation=="horizontal": 81 | selected_book_name = option_menu(**execution_dict) 82 | else: 83 | with st.sidebar: 84 | selected_book_name = option_menu(**execution_dict) 85 | 86 | # Update variables 87 | selected_book_number = options.index(selected_book_name) 88 | if st.session_state.book_number != selected_book_number: 89 | st.session_state.book_number = selected_book_number 90 | st.session_state.page_number = 0 91 | 92 | # Launch the corresponding chapter 93 | set_chapter_config(path=paths[selected_book_number], save_answers=save_answers, display_page_info=display_page_info) 94 | -------------------------------------------------------------------------------- /streamlit_book/chapter_config.py: -------------------------------------------------------------------------------- 1 | 2 | import streamlit as st 3 | from glob import glob 4 | import os 5 | import pandas as pd 6 | 7 | try: 8 | from .file_reader import get_all_files, create_buttons 9 | from .login import create_new_user_and_token, get_user_from_token, get_token_from_user 10 | from .admin import admin_page 11 | from .helpers import get_git_revision_short_hash 12 | from .render import render_file 13 | except Exception as e: 14 | print("Cannot import from . ", e) 15 | from file_reader import get_all_files, create_buttons 16 | from login import create_new_user_and_token, get_user_from_token, get_token_from_user 17 | from admin import admin_page 18 | from helpers import get_git_revision_short_hash 19 | from render import render_file 20 | except Exception as e: 21 | print("Cannot import! ", e) 22 | 23 | def get_query_params(): 24 | """ 25 | Depending on streamlit's version, uses the previous or the new way to get the query params. 26 | Previous: st.experimental_get_query_params() 27 | New: st.query_params() 28 | """ 29 | if hasattr(st, "query_params"): 30 | return st.query_params.to_dict() 31 | else: 32 | return st.experimental_get_query_params() 33 | 34 | def set_query_params(**kwargs): 35 | """ 36 | Depending on streamlit's version, uses the previous or the new way to set the query params. 37 | Previous: st.experimental_set_query_params() 38 | New: st.set_query_params() 39 | """ 40 | if hasattr(st, "query_params"): 41 | st.query_params.from_dict(**kwargs) 42 | else: 43 | st.experimental_set_query_params(**kwargs) 44 | 45 | def set_chapter_config( 46 | path="pages", 47 | toc=False, 48 | button="top", 49 | button_previous="⬅️", 50 | button_next="➡️", 51 | button_refresh="🔄", 52 | on_load_header=None, 53 | on_load_footer=None, 54 | save_answers=False, 55 | display_page_info=True, 56 | ): 57 | """Sets the book configuration, and displays the selected file. 58 | 59 | :param path: The path to root directory of the the files (py or md) to be rendered as pages of the book. 60 | :type path: string, dict 61 | :param toc: If True, it will display the table of contents for the files on the path. Defaults to False. 62 | :type toc: bool 63 | :param button: "top" (default behavior) or "bottom". 64 | :type button: str 65 | :param button_previous: icon or text for the previous button. 66 | :type button_previous: str 67 | :param button_next: icon or text for the next button. 68 | :type button_next: str 69 | :param button_refresh: icon or text for the refresh button. 70 | :type button_refresh: str 71 | :param on_load_header: function to be called before the page is loaded. 72 | :type on_load_header: function 73 | :param on_load_footer: function to be called after the page is loaded. 74 | :type on_load_footer: function 75 | :param save_answers: If True, it will save the answers in a csv file. Defaults to False. 76 | :type save_answers: bool 77 | :param display_page_info: If True, it will display the page info with the name and number. Defaults to True. 78 | :type display_page_info: bool 79 | :return: None 80 | """ 81 | # Observation: File number goes from 0 to n-1. 82 | 83 | # Quick and dirty 84 | path = str(path) 85 | 86 | # Store the parameters 87 | if "save_answers" not in st.session_state: 88 | st.session_state.save_answers = save_answers 89 | if "commit_hash" not in st.session_state: 90 | st.session_state.commit_hash = get_git_revision_short_hash() 91 | 92 | # Admin Login: if requested by the user on the url, it redirects to a specific amin page 93 | # Never shows the content, it will stay on the admin page. 94 | query_params = get_query_params() 95 | if "user" in query_params and "admin" in query_params["user"]: 96 | # Here we handle everything related to the admin 97 | admin_page() 98 | return # Don't show anything else! 99 | 100 | # User login 101 | if "user_id" not in st.session_state: 102 | query_params = get_query_params() 103 | if "token" in query_params: 104 | # Here we must handle the user session 105 | token = query_params["token"][0] #Just consider the first one 106 | ## If the token is wrong, notify the user. 107 | user_id = get_user_from_token(token) 108 | if user_id is None: 109 | # Wrong token 110 | st.error("Wrong token! Use the regular url.") 111 | return 112 | else: 113 | # Correct token 114 | st.session_state.user_id = user_id 115 | st.session_state.token = token 116 | # Redirect to avoid the url with the token 117 | del query_params["token"] 118 | st.experimental_set_query_params(**query_params) 119 | else: 120 | # Token not in query params: create a new user and token 121 | user_id, token = create_new_user_and_token() 122 | st.session_state.user_id = user_id 123 | st.session_state.token = token 124 | 125 | # Save answers behavior 126 | if st.session_state.save_answers: 127 | if "warned_about_save_answers" not in st.session_state: 128 | def on_click(): 129 | st.session_state.warned_about_save_answers = True 130 | c1, c2 = st.columns([8,1]) 131 | user_url = f"?token={st.session_state.token}" 132 | c1.warning(f"Your answers will be saved. You can relaunch the app using the following custom url (Save it!).\n\n{user_url}") 133 | c2.markdown("\n\n") 134 | c2.markdown("\n\n") 135 | c2.button("Dismiss", on_click=on_click) 136 | 137 | # Get the files at path level (only files, not folders) 138 | file_list = get_all_files(path) 139 | 140 | # Check that we have at least 1 file to render 141 | if len(file_list) == 0: 142 | st.error(f"No files were found at the given path. Please check the provided path: {path}") 143 | return 144 | 145 | # Initialize the session state variables 146 | if "toc" not in st.session_state: 147 | st.session_state.toc = toc 148 | 149 | # Save button configuration 150 | if "button" not in st.session_state: 151 | st.session_state.button = button 152 | 153 | # Page number 154 | if "page_number" not in st.session_state: 155 | st.session_state.page_number = 0 156 | 157 | # Update file_fullpath 158 | selected_file_fullpath = file_list[st.session_state.page_number] 159 | caption_text = f"Page {st.session_state.page_number+1} of {st.session_state.total_files}. File: {selected_file_fullpath}" 160 | 161 | if st.session_state.toc: 162 | option = st.radio("Table of contents", options=file_list) 163 | st.session_state.page_number = file_list.index(option) 164 | st.button("Go to page", on_click=on_toc_table_click, key="gotopage") 165 | else: 166 | # Execute the on_load_header function 167 | if on_load_header: 168 | on_load_header() 169 | 170 | # If required, put the button on top of the page. Use columns for alignment 171 | if st.session_state.button in ["top", "both"]: 172 | create_buttons(caption_text, 173 | button_previous, button_next, button_refresh, 174 | display_page_info, 175 | key="top") 176 | 177 | # Render the file using the magic 178 | try: 179 | render_file(selected_file_fullpath) 180 | except Exception as e: 181 | st.exception(e) 182 | 183 | # If required, put the button on the bottom of the page. Use columns for alignment 184 | if st.session_state.button in ["bottom", "both"]: 185 | create_buttons(caption_text, 186 | button_previous, button_next, button_refresh, 187 | display_page_info, 188 | key="bottom") 189 | 190 | # Execute the on_load_footer function 191 | if on_load_footer: 192 | on_load_footer() 193 | return -------------------------------------------------------------------------------- /streamlit_book/colored_expanders.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | 3 | def add_color_to_expanders(): 4 | """ 5 | Adds color to the expanders. 6 | Users don't need to call this function, is executed by default. 7 | """ 8 | # Define your javascript 9 | my_js = """ 10 | // Define the colors 11 | const color_dict = { 12 | "info": ["#EAF2FC", "#1D6777"], 13 | "success": ["#D7EED9", "#156C36"], 14 | "ok": ["#D7EED9", "#156C36"], 15 | "warning": ["#FFF4D9", "#947C2D"], 16 | "error": ["#FFDDDC", "#9D282C"] 17 | }; 18 | // Get all the expander elements 19 | var expanderHeaders = window.parent.document.querySelectorAll('.streamlit-expanderHeader'); 20 | var expanderContents = window.parent.document.querySelectorAll('.streamlit-expanderContent'); 21 | for (var i = 0; i < expanderHeaders.length; i++) { 22 | let header = expanderHeaders[i]; 23 | let content = expanderContents[i]; 24 | text = header.innerText || header.textContent; 25 | // Check for text content 26 | for (let color in color_dict) { 27 | if (text.toLowerCase().startsWith(color)) { 28 | header.style.backgroundColor = color_dict[color][0]; 29 | content.style.backgroundColor = color_dict[color][0]; 30 | header.style.color = color_dict[color][1]; 31 | content.style.color = color_dict[color][1]; 32 | header.addEventListener("mouseover", function(event){header.style.color = 'red'}) 33 | header.addEventListener("mouseout", function( event ) {header.style.color = color_dict[color][1]}) 34 | } 35 | } 36 | } 37 | """ 38 | 39 | # Wrapt the javascript as html code 40 | my_html = f"" 41 | 42 | # Execute your app 43 | st.components.v1.html(my_html, height=0, width=0) 44 | -------------------------------------------------------------------------------- /streamlit_book/echo.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import re 3 | import textwrap 4 | import traceback 5 | from typing import List, Iterable, Optional 6 | 7 | _SPACES_RE = re.compile("\\s*") 8 | _EMPTY_LINE_RE = re.compile("\\s*\n") 9 | 10 | @contextlib.contextmanager 11 | def echo(code_location="above", show=True): 12 | """Whether to show the echoed code before or after the results of the executed code block. 13 | :param code_location: "above" or "below" 14 | :type lines: str 15 | :param show: Boolean to show or hide the code block 16 | :type lines: bool 17 | :return: None 18 | :rtype: none 19 | Copied and improved from `Streamlit's github `_ 20 | """ 21 | 22 | from streamlit import code, warning, empty, source_util 23 | 24 | if code_location == "below": 25 | show_code = code 26 | show_warning = warning 27 | else: 28 | placeholder = empty() 29 | show_code = placeholder.code 30 | show_warning = placeholder.warning 31 | 32 | try: 33 | # Get stack frame *before* running the echoed code. The frame's 34 | # line number will point to the `st.echo` statement we're running. 35 | frame = traceback.extract_stack()[-3] 36 | filename, start_line = frame.filename, frame.lineno 37 | 38 | # Read the file containing the source code of the echoed statement. 39 | with source_util.open_python_file(filename) as source_file: 40 | source_lines = source_file.readlines() 41 | 42 | # Get the indent of the first line in the echo block, skipping over any 43 | # empty lines. 44 | initial_indent = _get_initial_indent(source_lines[start_line:]) 45 | 46 | # Iterate over the remaining lines in the source file 47 | # until we find one that's indented less than the rest of the 48 | # block. That's our end line. 49 | # 50 | # Note that this is *not* a perfect strategy, because 51 | # de-denting is not guaranteed to signal "end of block". (A 52 | # triple-quoted string might be dedented but still in the 53 | # echo block, for example.) 54 | # TODO: rewrite this to parse the AST to get the *actual* end of the block. 55 | lines_to_display: List[str] = [] 56 | for line in source_lines[start_line:]: 57 | indent = _get_indent(line) 58 | if indent is not None and indent < initial_indent: 59 | break 60 | lines_to_display.append(line) 61 | 62 | code_string = textwrap.dedent("".join(lines_to_display)) 63 | 64 | # Run the echoed code... 65 | yield 66 | 67 | # And draw the code string to the app! 68 | if show: # Improvement: only show if required!!! 69 | show_code(code_string, "python") 70 | 71 | except FileNotFoundError as err: 72 | show_warning("Unable to display code. %s" % err) 73 | 74 | 75 | 76 | def _get_initial_indent(lines: Iterable[str]) -> int: 77 | """Return the indent of the first non-empty line in the list. 78 | If all lines are empty, return 0. 79 | """ 80 | for line in lines: 81 | indent = _get_indent(line) 82 | if indent is not None: 83 | return indent 84 | 85 | return 0 86 | 87 | 88 | def _get_indent(line: str) -> Optional[int]: 89 | """Get the number of whitespaces at the beginning of the given line. 90 | If the line is empty, or if it contains just whitespace and a newline, 91 | return None. 92 | """ 93 | if _EMPTY_LINE_RE.match(line) is not None: 94 | return None 95 | 96 | match = _SPACES_RE.match(line) 97 | return match.end() if match is not None else 0 -------------------------------------------------------------------------------- /streamlit_book/file_reader.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | from glob import glob 3 | import os 4 | import pandas as pd 5 | 6 | try: 7 | from .render import render_file 8 | except: 9 | from render import render_file 10 | 11 | def on_toc_table_click(): 12 | st.session_state.toc = False 13 | return 14 | 15 | def on_previous_click(): 16 | """ 17 | Updates the values of update_from_selectbox and update_from_button. 18 | Updates the page number to +1, or 0 if the last page was reached. 19 | """ 20 | st.session_state.page_number = (st.session_state.page_number - 1) % st.session_state.total_files 21 | return 22 | 23 | def on_next_click(): 24 | """ 25 | Updates the values of update_from_selectbox and update_from_button. 26 | Updates the page number to +1, or 0 if the last page was reached. 27 | """ 28 | st.session_state.page_number = (st.session_state.page_number + 1) % st.session_state.total_files 29 | return 30 | 31 | def on_refresh_click(): 32 | return 33 | 34 | def create_buttons(caption_text, 35 | button_previous, 36 | button_next, 37 | button_refresh, 38 | display_page_info, 39 | key=""): 40 | """ 41 | Function to create the navigation buttons. 42 | Only applies if more than 1 file is to be rendered. 43 | """ 44 | # Skip if there is only one file 45 | if st.session_state.total_files <= 1: 46 | return 47 | # We have at least 2 files, so render the buttons 48 | if display_page_info: 49 | st.caption(caption_text) 50 | p = len(button_previous) 51 | n = len(button_next) 52 | r = len(button_refresh) 53 | b = max(p,n,r) 54 | # Dinamic resizing based on length of the button names 55 | # Would be even better if layout type was known 56 | c1, c2, c3, c4 = st.columns([b, b, b, max(b, 20-3*b)]) 57 | if st.session_state.page_number!=0: 58 | c1.button(button_previous, 59 | help="Previous page", on_click=on_previous_click, key="previous_button_top"+key) 60 | if st.session_state.page_number!=st.session_state.total_files-1: 61 | c2.button(button_next, 62 | help="Next page", on_click=on_next_click, key="next_button_top"+key) 63 | c3.button(button_refresh, 64 | help="Refresh current page", on_click=on_refresh_click, key="switch_button_top"+key) 65 | return 66 | 67 | def get_all_files(path): 68 | """ 69 | Returns a list of all files (python, markdown) in the given path, 70 | considering recursively all subfolders. 71 | The path can be a single file. 72 | It does not considers folders. 73 | Excludes files and folders starting with WIP (work in progress). 74 | It stores the total number of files (pages) in the session_state. 75 | """ 76 | if path.endswith(".py"): 77 | py_files = [ path, ] 78 | else: 79 | py_files = glob(f"{path}/**/*.py", recursive=True) 80 | if path.endswith(".md"): 81 | md_files = [ path, ] 82 | else: 83 | md_files = glob(f"{path}/**/*.md", recursive=True) 84 | all_files = [_ for _ in sorted(py_files + md_files) if "/WIP" not in _] 85 | st.session_state.total_files = len(all_files) 86 | return all_files 87 | 88 | def is_file(label): 89 | """ 90 | Checks if the given label is a file. 91 | """ 92 | return label.endswith(".py") or label.endswith(".md") 93 | 94 | def get_options_at_path(user_selected_path: str, file_list: list): 95 | """ 96 | Returns all the alternative items (folders and files) on the provided path 97 | """ 98 | # Get the number of folders at the current level 99 | n_folders = len(user_selected_path.split("/")) 100 | # Get all the files at level 101 | subfolder_options = [] 102 | for filepath in file_list: 103 | subfolder = "/".join(filepath.split("/")[:(n_folders+1)]) 104 | if filepath.startswith(user_selected_path) and subfolder not in subfolder_options: 105 | subfolder_options.append(subfolder) 106 | options = [_.split("/")[-1] for _ in subfolder_options] 107 | return options 108 | 109 | def get_selection_box_args_at_path(user_selected_path: str, file_list: list, level: int, selected_option: str=""): 110 | """ 111 | Search for items: folders, python files or markdown files. 112 | Only looks for files at the given path. 113 | It returns a dictionary, with the required keys and values required by the selectbox 114 | """ 115 | # Get all options at certain level 116 | options = get_options_at_path(user_selected_path, file_list) 117 | # Get the current label index 118 | if selected_option: 119 | index = options.index(selected_option) 120 | else: 121 | index = 0 122 | # Get all options at certain level 123 | selection_box_args = { 124 | "label": f"Select {book_parts_convention(level)}:", 125 | "options": options, 126 | "index": index, 127 | "preset_option": options[index], 128 | "key": f"select_box_{level}", 129 | } 130 | return selection_box_args 131 | 132 | def get_selection_boxes_args_from_filepath(file_fullpath: str, base_path: str, file_list: list): 133 | """ 134 | Creates a list of dictionaries, with the required keys and values required by the selectbox. 135 | """ 136 | # The relative path are only the folders in the filepath not on the base_path (but not the file itself) 137 | relative_path = file_fullpath.replace(f"{base_path}/", "") 138 | # Get the intermediate folders, but not the file itself 139 | selected_labels = relative_path.split("/") 140 | # Initialize and populate the list of dictionaries 141 | selection_boxes_args = [] 142 | working_path = base_path 143 | for level, label in enumerate(selected_labels): 144 | # Get the options of the selectbox 145 | selection_box_args = get_selection_box_args_at_path(working_path, file_list, level, label) 146 | selection_boxes_args.append(selection_box_args) 147 | # Increate the path 148 | working_path = working_path + "/" + label 149 | return selection_boxes_args 150 | 151 | def get_items(path: str): 152 | """ 153 | # Search for folders, python files or markdown files. 154 | Only looks at precisely the level of the given path. 155 | It returns a dictionary, with keys the name to be rendered in the selectbox 156 | and with values another dictionary, with the type (file/folder) and the fullpath. 157 | """ 158 | # Get all the files 159 | items_at_path = glob(f"{path}/*/") + glob(f"{path}/*.md") + glob(f"{path}/*.py") 160 | # Filter files that start with WIP (work in progress) 161 | items_at_path = [item for item in items_at_path if ("/WIP" not in item)] 162 | # Create 163 | item_dict = {} 164 | for i, my_item in enumerate(items_at_path): 165 | if my_item[-3:] in (".py", ".md"): 166 | item_name = my_item.split("/")[-1] 167 | render_name = item_name 168 | item_dict[render_name] = {"type":"file", "path":my_item} 169 | else: 170 | item_name = my_item.split("/")[-2] 171 | render_name = item_name 172 | item_dict[render_name] = {"type":"folder", "path":my_item} 173 | return item_dict 174 | -------------------------------------------------------------------------------- /streamlit_book/floating_button.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | 3 | def add_floating_button(url, bootstrap_icon, icon_color, background_color): 4 | """ 5 | Adds a floating button that opens a link to feedback. 6 | """ 7 | url_html = f'' 8 | button_html = """ 9 | 11 | 24 | my_url 25 | """ 26 | button_html = button_html.replace("my_background_color", background_color) 27 | button_html = button_html.replace("my_color", icon_color) 28 | button_html = button_html.replace("my_url", url_html) 29 | st.components.v1.html(button_html, height=100) 30 | return url, bootstrap_icon, icon_color, background_color 31 | 32 | def make_it_float(url, bootstrap_icon, icon_color, background_color): 33 | # Define your javascript 34 | my_js = """ 35 | var containers = window.parent.document.querySelectorAll('.element-container'); 36 | for (var i = 0; i < containers.length; i++) { 37 | //alert(i); 38 | let container = containers[i]; 39 | let child = container.firstChild 40 | let name = child.tagName; 41 | if (name == 'IFRAME') { 42 | let docsrc = child.srcdoc; 43 | bool1 = docsrc.includes('my_url'); 44 | bool2 = docsrc.includes('my_bootstrap_icon'); 45 | bool3 = docsrc.includes('my_icon_color'); 46 | bool4 = docsrc.includes('my_background_color'); 47 | if (bool1 && bool2 && bool3 && bool4) { 48 | child.style.position = "fixed"; 49 | child.style.bottom = "10px"; 50 | child.style.right = "10px"; 51 | child.style.zindex = "2000"; 52 | } 53 | } 54 | } 55 | """ 56 | my_js = my_js.replace("my_url", url) 57 | my_js = my_js.replace("my_bootstrap_icon", bootstrap_icon) 58 | my_js = my_js.replace("my_icon_color", icon_color) 59 | my_js = my_js.replace("my_background_color", background_color) 60 | # Wrapt the javascript as html code 61 | my_html = f"" 62 | # Execute your app 63 | st.components.v1.html(my_html, height=0, width=2000) 64 | 65 | def floating_button(url, bootstrap_icon="chat-left-text-fill", 66 | icon_color="white", background_color="gray"): 67 | """ 68 | """ 69 | # Fix bootstrap icon if does not starts with "bi" 70 | if not bootstrap_icon.startswith("bi-"): 71 | bootstrap_icon = "bi-" + bootstrap_icon 72 | # Wrapt the javascript as html code 73 | properties = add_floating_button(url, bootstrap_icon, icon_color, background_color) 74 | make_it_float(*properties) 75 | return -------------------------------------------------------------------------------- /streamlit_book/helpers.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import streamlit as st 3 | import subprocess 4 | import random 5 | import os 6 | 7 | try: 8 | from keywords import ADMIN_PASSWORD 9 | except: 10 | from .keywords import ADMIN_PASSWORD 11 | 12 | def password_entered(): 13 | """ 14 | Checks whether a password entered by the user is correct. 15 | Checks agains secrets (if defined) otherwise checks against hardcoded password. 16 | """ 17 | # Get the password 18 | try: 19 | PASSWORD = st.secrets["ADMIN_PASSWORD"] 20 | except: 21 | PASSWORD = ADMIN_PASSWORD 22 | # Compare against password entered by the user 23 | if st.session_state["password"] == PASSWORD: 24 | st.session_state["password_correct"] = True 25 | del st.session_state["password"] # don't store password 26 | else: 27 | st.session_state["password_correct"] = False 28 | # Delete password from session state 29 | del PASSWORD 30 | return 31 | 32 | def password_login(user_id): 33 | if "password_correct" not in st.session_state: 34 | # First run, show input for password. 35 | st.text_input("Password", type="password", on_change=password_entered, key="password") 36 | elif not st.session_state["password_correct"]: 37 | # Password not correct, show input + error. 38 | st.text_input("Password", type="password", on_change=password_entered, key="password") 39 | st.error("😕 Password incorrect") 40 | 41 | def get_datetime_string(): 42 | # datetime object containing current date and time 43 | dt_string = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 44 | return dt_string 45 | 46 | def get_git_revision_short_hash() -> str: 47 | """" 48 | Improved upon version found at 49 | https://stackoverflow.com/questions/14989858/get-the-current-git-hash-in-a-python-script/21901260#21901260" 50 | """ 51 | try: 52 | return subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('ascii').strip() 53 | except: 54 | return "not_git" 55 | 56 | def download_file(df, button_label, filename, where=None): 57 | """ 58 | """ 59 | data_csv = df.to_csv().encode('utf-8') 60 | if where: 61 | btn = where.download_button( 62 | label=button_label, 63 | data=data_csv, 64 | file_name=filename, 65 | ) 66 | else: 67 | btn = st.download_button( 68 | label=button_label, 69 | data=data_csv, 70 | file_name=filename, 71 | ) 72 | return 73 | 74 | def get_token(): 75 | t1 = get_random_string(3, "letters") 76 | t2 = get_random_string(3, "numbers") 77 | return t1 + t2 78 | 79 | def get_random_string(length, elements): 80 | if elements == "letters": 81 | elements = "abcdefghkmnopqrstxyz" # no confusing characters 82 | elif elements == "numbers": 83 | elements = "0123456789" 84 | else: 85 | elements = string.digits + string.ascii_letters 86 | # With combination of lower and upper case 87 | result_str = ''.join(random.choice(elements) for i in range(length)) 88 | # print random string 89 | return result_str -------------------------------------------------------------------------------- /streamlit_book/keywords.py: -------------------------------------------------------------------------------- 1 | 2 | def check_keyword(line, keyword): 3 | """ 4 | Check if the line starts with the keyword 5 | Replaces - for _ for backwards compatibility. 6 | """ 7 | new_line = line.replace("-", "_") 8 | return new_line.startswith(keyword) 9 | 10 | TODO_KEYWORD = "stb.to_do" 11 | TODO_COMPLETED = "- [x] " 12 | TODO_INCOMPLETE = "- [ ] " 13 | TODO_SUCCESS = "Bravo!" 14 | 15 | TRUE_FALSE_KEYWORD = "stb.true_or_false" 16 | 17 | SINGLE_CHOICE_KEYWORD = "stb.single_choice" 18 | SINGLE_CHOICE_CORRECT = "+ " 19 | SINGLE_CHOICE_WRONG = "- " 20 | 21 | MULTIPLE_CHOICE_KEYWORD = "stb.multiple_choice" 22 | MULTIPLE_CHOICE_TRUE = "[T] " 23 | MULTIPLE_CHOICE_FALSE = "[F] " 24 | 25 | TEXT_INPUT_KEYWORD = "stb.text_input" 26 | 27 | CODE_INPUT_KEYWORD = "stb.code_input" 28 | 29 | FILE_UPLOAD_KEYWORD = "stb.file_upload" 30 | 31 | SHARE_KEYWORD = "stb.share" 32 | 33 | BUTTON = "button:" 34 | DEFAULT_BUTTON_MESSAGE = "Check answer" 35 | SUCCESS = "success:" 36 | DEFAULT_SUCCESS_MESSAGE = "Correct answer" 37 | ERROR = "error:" 38 | DEFAULT_ERROR_MESSAGE = "Wrong answer" 39 | 40 | EXACT_TEXT = "equals:" 41 | CONTAINS_TEXT = "contains:" 42 | STARTS_WITH = "starts_with:" 43 | ENDS_WITH = "ends_with:" 44 | 45 | ASSERT = "assert:" 46 | 47 | USERS_FILENAME = "./tmp/users.csv" 48 | ANSWER_FILENAME = "./tmp/answers.csv" 49 | ADMIN_PASSWORD = "2lazy2change1password" -------------------------------------------------------------------------------- /streamlit_book/login.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pandas as pd 3 | import streamlit as st 4 | 5 | try: 6 | from helpers import get_datetime_string, download_file, get_token 7 | from keywords import USERS_FILENAME, ANSWER_FILENAME 8 | except: 9 | from .helpers import get_datetime_string, download_file, get_token 10 | from .keywords import USERS_FILENAME, ANSWER_FILENAME 11 | 12 | def create_user_df(user_id, token): 13 | """ 14 | Create a dataframe with the user information. 15 | """ 16 | dt_string = get_datetime_string() 17 | user_df = pd.DataFrame(columns=["user_id", "token", "datetime"], data=[[user_id, token, dt_string]]) 18 | return user_df 19 | 20 | def get_user_from_token(query_token): 21 | df = pd.read_csv(USERS_FILENAME) 22 | m = df["token"] == query_token 23 | if m.any(): 24 | user_for_token = df.user_id[m].values[0] # the first (and only) one 25 | return user_for_token 26 | else: 27 | return None 28 | 29 | def create_new_user_and_token(): 30 | """ 31 | Returns the user id and token. 32 | """ 33 | # Create folder, if not there 34 | try: 35 | os.mkdir("./tmp/") 36 | except: 37 | pass 38 | # Check for user file, create if not there 39 | if not os.path.exists("./tmp/users.csv"): 40 | df0 = create_user_df(0,"abc123") 41 | df0.to_csv(USERS_FILENAME, index=False) 42 | # Take a new user 43 | df_users = pd.read_csv(USERS_FILENAME) 44 | user_id = df_users["user_id"].max() + 1 45 | user_token = get_token() 46 | df_new_user = create_user_df(user_id, user_token) 47 | df_users = pd.concat([df_users, df_new_user]) 48 | df_users.to_csv(USERS_FILENAME, index=False) 49 | # Create the token 50 | return (user_id, user_token) 51 | 52 | def get_token_from_user(user_id): 53 | df = pd.read_csv(USERS_FILENAME) 54 | m = df["user_id"] == user_id 55 | if m.any(): 56 | token = df.token[m].values[0] 57 | return token 58 | else: 59 | return None -------------------------------------------------------------------------------- /streamlit_book/render.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import types 3 | 4 | import streamlit as st 5 | 6 | try: 7 | from colored_expanders import add_color_to_expanders 8 | except: 9 | from .colored_expanders import add_color_to_expanders 10 | 11 | 12 | def _new_module(name: str) -> types.ModuleType: 13 | """Create a new module with the given name.""" 14 | return types.ModuleType(name) 15 | 16 | 17 | def render_file(fullpath): 18 | """ 19 | Renders the file (it's always a file and not a folder) 20 | given the fullpath (path and filename). 21 | Only admits python or markdown, also by construction. 22 | """ 23 | fullpath_str = str(fullpath) 24 | if fullpath_str.endswith(".py"): 25 | # Create fake module. This gives us a name global namespace to 26 | # execute the code in. 27 | module = _new_module("__main__") 28 | 29 | # Install the fake module as the __main__ module. This allows 30 | # the pickle module to work inside the user's code, since it now 31 | # can know the module where the pickled objects stem from. 32 | # IMPORTANT: This means we can't use "if __name__ == '__main__'" in 33 | # our code, as it will point to the wrong module!!! 34 | sys.modules["__main__"] = module 35 | 36 | # Add special variables to the module's globals dict. 37 | # Note: The following is a requirement for the CodeHasher to 38 | # work correctly. The CodeHasher is scoped to 39 | # files contained in the directory of __main__.__file__, which we 40 | # assume is the main script directory. 41 | module.__dict__["__file__"] = fullpath 42 | 43 | # Execute as a regular python file 44 | with open(fullpath, "rb") as source_file: 45 | code = compile(source_file.read(), fullpath, "exec") 46 | exec(code, module.__dict__) 47 | 48 | add_color_to_expanders() 49 | elif fullpath_str.endswith(".md"): 50 | with open(fullpath, "r") as source_file: 51 | code = source_file.read() 52 | st.markdown(code) 53 | add_color_to_expanders() 54 | else: 55 | st.warning(f" ah: File extention not supported for file {fullpath}") 56 | -------------------------------------------------------------------------------- /streamlit_book/render_code_input.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | 3 | try: 4 | from keywords import * 5 | from answers import save_answer 6 | except: 7 | from .keywords import * 8 | from .answers import save_answer 9 | 10 | def code_input(question, initial_code, 11 | contains=[], 12 | equals="", 13 | normalization_function=None, 14 | verification_function=None, 15 | success=DEFAULT_SUCCESS_MESSAGE, 16 | error=DEFAULT_ERROR_MESSAGE, 17 | button=DEFAULT_BUTTON_MESSAGE): 18 | """Render a text input question from the given parameters. 19 | 20 | :param question: question to be displayed before the multiple-choice options 21 | :type question: str 22 | :param initial_code: 23 | :type initial_code: str 24 | :param contains: 25 | :type contains: list of str 26 | :param equals: 27 | :type equals: str 28 | :param normalization_function: 29 | :type normalization_function: function 30 | :param verification_function: 31 | :type verification_function: function 32 | :param success: message to be displayed when the user answers correctly 33 | :type success: str, optional 34 | :param error: message to be displayed when the user answers incorrectly 35 | :type error: str, optional 36 | :param button: message to be displayed on the button that checks the answer 37 | :type button: str, optional 38 | :return: tuple of booleans with button press status and correctness of answer 39 | :rtype: tuple of bool 40 | """ 41 | user_code = st.text_input(question, initial_code) 42 | # Check if the correct checkboxes are checked 43 | key = ("text-input:" + question + "/" + initial_code).lower().replace(" ", "_") 44 | if st.button(button, key=key): 45 | # Check answers 46 | if user_code == equals: 47 | if success: 48 | st.success(success) 49 | is_correct = True 50 | else: 51 | if error: 52 | st.error(error) 53 | is_correct = False 54 | # Save the answers, if required 55 | if "save_answers" in st.session_state and st.session_state.save_answers: 56 | correct_answer = "Depends, dynamically generated" 57 | save_answer(question, is_correct=is_correct, user_answer=user_code, correct_answer=correct_answer) 58 | # Return if button is pressed and answer evaluation 59 | return True, is_correct 60 | else: 61 | # Return if button is not pressed 62 | return False, None -------------------------------------------------------------------------------- /streamlit_book/render_multiple_choice.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | 3 | try: 4 | from keywords import check_keyword 5 | from keywords import MULTIPLE_CHOICE_KEYWORD, MULTIPLE_CHOICE_FALSE, MULTIPLE_CHOICE_TRUE 6 | from keywords import BUTTON, SUCCESS, ERROR, DEFAULT_SUCCESS_MESSAGE, DEFAULT_ERROR_MESSAGE, DEFAULT_BUTTON_MESSAGE 7 | from answers import save_answer 8 | except: 9 | from .keywords import check_keyword 10 | from .keywords import MULTIPLE_CHOICE_KEYWORD, MULTIPLE_CHOICE_FALSE, MULTIPLE_CHOICE_TRUE 11 | from .keywords import BUTTON, SUCCESS, ERROR, DEFAULT_SUCCESS_MESSAGE, DEFAULT_ERROR_MESSAGE, DEFAULT_BUTTON_MESSAGE 12 | from .answers import save_answer 13 | 14 | 15 | def multiple_choice(question, options_dict, 16 | success=DEFAULT_SUCCESS_MESSAGE, 17 | error=DEFAULT_ERROR_MESSAGE, 18 | button=DEFAULT_BUTTON_MESSAGE): 19 | """Render a multiple choice question from the given parameters. 20 | 21 | :param question: question to be displayed before the multiple-choice options 22 | :type question: str 23 | :param options_dict: dictionary of options to be displayed, with the option text as key and the boolean answer as value 24 | :type options: dict 25 | :param answer: index (starting at 0) of the expected answer to the question 26 | :type answer: int 27 | :param success: message to be displayed when the user answers correctly 28 | :type success: str, optional 29 | :param error: message to be displayed when the user answers incorrectly 30 | :type error: str, optional 31 | :param button: message to be displayed on the button that checks the answer 32 | :type button: str, optional 33 | :return: tuple of booleans with button press status and correctness of answer 34 | :rtype: tuple of bool 35 | """ 36 | cb_list = [] 37 | if len(question)==0: 38 | st.error("Please provide a question") 39 | return None, None 40 | elif len(options_dict)<2: 41 | st.error("Must have at least 2 options") 42 | return None, None 43 | else: 44 | # Write the question 45 | st.markdown(question) 46 | # Write the options and append the user answers into the list cb_list 47 | for option, answer in options_dict.items(): 48 | key = (question + option + str(answer)).lower().replace(" ", "_") 49 | # Post the option with no option 50 | cb = st.checkbox(option, value=False, key=key) 51 | cb_list.append(cb) 52 | # Check if the correct checkboxes are checked 53 | key = ("multiple-choice:" + question + "".join(options_dict.keys())).lower().replace(" ", "_") 54 | if st.button(button, key=key): 55 | # Check answers 56 | if all(u==a for u, a in zip(cb_list, options_dict.values())): 57 | if success: 58 | st.success(success) 59 | is_correct = True 60 | else: 61 | if error: 62 | st.error(error) 63 | is_correct = False 64 | # Save the answers, if required 65 | if "save_answers" in st.session_state and st.session_state.save_answers: 66 | user_answer = [list(options_dict.keys())[i] for i, cb in enumerate(cb_list) if cb] 67 | correct_answer = [key for key, val in options_dict.items() if val] 68 | save_answer(question, is_correct=is_correct, user_answer=user_answer, correct_answer=correct_answer) 69 | # Return if button is pressed and answer evaluation 70 | return True, is_correct 71 | else: 72 | # Return if button is not pressed 73 | return False, None -------------------------------------------------------------------------------- /streamlit_book/render_single_choice.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | 3 | try: 4 | from keywords import check_keyword 5 | from keywords import SINGLE_CHOICE_KEYWORD, SINGLE_CHOICE_CORRECT, SINGLE_CHOICE_WRONG 6 | from keywords import BUTTON, SUCCESS, ERROR, DEFAULT_SUCCESS_MESSAGE, DEFAULT_ERROR_MESSAGE, DEFAULT_BUTTON_MESSAGE 7 | from answers import save_answer 8 | except: 9 | from .keywords import check_keyword 10 | from .keywords import SINGLE_CHOICE_KEYWORD, SINGLE_CHOICE_CORRECT, SINGLE_CHOICE_WRONG, BUTTON, SUCCESS, ERROR 11 | from .keywords import BUTTON, SUCCESS, ERROR, DEFAULT_SUCCESS_MESSAGE, DEFAULT_ERROR_MESSAGE, DEFAULT_BUTTON_MESSAGE 12 | from .answers import save_answer 13 | 14 | 15 | def single_choice(question, options, answer_index, 16 | success=DEFAULT_SUCCESS_MESSAGE, 17 | error=DEFAULT_ERROR_MESSAGE, 18 | button=DEFAULT_BUTTON_MESSAGE): 19 | """Renders a single-choice question from arguments. 20 | 21 | :param question: question to be displayed before the single-choice options 22 | :type question: str 23 | :param options: list of options to be displayed. 24 | :type options: str 25 | :param answer: index (starting at 0) of the expected answer to the question 26 | :type answer: int 27 | :param success: message to be displayed when the user answers correctly 28 | :type success: str, optional 29 | :param error: message to be displayed when the user answers incorrectly 30 | :type error: str, optional 31 | :param button: message to be displayed on the button that checks the answer 32 | :type button: str, optional 33 | :return: tuple of booleans with button press status and correctness of answer 34 | :rtype: tuple of bool 35 | """ 36 | if len(question)==0: 37 | st.error("Please provide a question") 38 | return None, None 39 | elif len(options)<2: 40 | st.error("Must have at least 2 options") 41 | return None, None 42 | else: 43 | # Write the options with radio button: only one option can be selected 44 | user_answer = st.radio(question, options=options) 45 | # Check if correct answer 46 | key = ("single-choice:" + question + "".join(options)).lower().replace(" ", "_") 47 | if st.button(button, key=key): 48 | # Check ansers 49 | if options.index(user_answer) == answer_index: 50 | if success: 51 | st.success(success) 52 | is_correct = True 53 | else: 54 | if error: 55 | st.error(error) 56 | is_correct = False 57 | # Save the answers, if required 58 | if "save_answers" in st.session_state and st.session_state.save_answers: 59 | save_answer(question, is_correct=is_correct, user_answer=user_answer, correct_answer=options[answer_index]) 60 | # Return if button is pressed and answer evaluation 61 | return True, is_correct 62 | else: 63 | # Return if button is not pressed 64 | return False, None 65 | -------------------------------------------------------------------------------- /streamlit_book/render_text_input.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | 3 | try: 4 | from keywords import * 5 | from answers import save_answer 6 | except: 7 | from .keywords import * 8 | from .answers import save_answer 9 | 10 | 11 | def text_input(question, initial_answer, 12 | contains=[], 13 | equals="", 14 | normalization_function=None, 15 | verification_function=None, 16 | success=DEFAULT_SUCCESS_MESSAGE, 17 | error=DEFAULT_ERROR_MESSAGE, 18 | button=DEFAULT_BUTTON_MESSAGE): 19 | """Render a text input question from the given parameters. 20 | 21 | :param question: question to be displayed before the multiple-choice options 22 | :type question: str 23 | :param initial_answer: 24 | :type initial_answer: str 25 | :param contains: 26 | :type contains: list of str 27 | :param equals: 28 | :type equals: str 29 | :param normalization_function: 30 | :type normalization_function: function 31 | :param verification_function: 32 | :type normalization_function: function 33 | :param success: message to be displayed when the user answers correctly 34 | :type success: str, optional 35 | :param error: message to be displayed when the user answers incorrectly 36 | :type error: str, optional 37 | :param button: message to be displayed on the button that checks the answer 38 | :type button: str, optional 39 | :return: tuple of booleans with button press status and correctness of answer 40 | :rtype: tuple of bool 41 | """ 42 | user_answer = st.text_input(question, initial_answer) 43 | # Check if the correct checkboxes are checked 44 | key = ("text-input:" + question + "/" + initial_answer).lower().replace(" ", "_") 45 | if st.button(button, key=key): 46 | # Check answers 47 | if user_answer == equals: 48 | if success: 49 | st.success(success) 50 | is_correct = True 51 | else: 52 | if error: 53 | st.error(error) 54 | is_correct = False 55 | # Save the answers, if required 56 | if "save_answers" in st.session_state and st.session_state.save_answers: 57 | correct_answer = "Depends, dynamically generated" 58 | save_answer(question, is_correct=is_correct, user_answer=user_answer, correct_answer=correct_answer) 59 | # Return if button is pressed and answer evaluation 60 | return True, is_correct 61 | else: 62 | # Return if button is not pressed 63 | return False, None -------------------------------------------------------------------------------- /streamlit_book/render_to_do_list.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | 3 | try: 4 | from keywords import check_keyword 5 | from keywords import TODO_KEYWORD, TODO_COMPLETED, TODO_INCOMPLETE, TODO_SUCCESS, SUCCESS 6 | except: 7 | from .keywords import check_keyword 8 | from .keywords import TODO_KEYWORD, TODO_COMPLETED, TODO_INCOMPLETE, TODO_SUCCESS, SUCCESS 9 | 10 | def to_do_list(tasks, header="", success=TODO_SUCCESS): 11 | """ Renders the tasks as a to-do list, with optional header and success message. 12 | The tasks are a dictionary of tasks (supposed to be ordered as Python +3.6) 13 | with their completed (True) or to-do (False) status as a checkbox. 14 | 15 | :param tasks: dictionary of tasks in format string:bool 16 | :type tasks: dict 17 | :param header: description of the tasks 18 | :type header: str, optional 19 | :param success: success message 20 | :type success: str, optional 21 | :return: boolean with the exit status of the function 22 | :rtype: bool 23 | """ 24 | if len(tasks)==0: 25 | st.error("There are no tasks to display.") 26 | cb_list = [] 27 | st.markdown(header) 28 | for task, status in tasks.items(): 29 | key = (header + task).lower().replace(" ", "_") 30 | cb = st.checkbox(task, value=status, key=key) 31 | cb_list.append(cb) 32 | if len(cb_list)>0 and all(cb for cb in cb_list): 33 | st.balloons() 34 | st.success(success) 35 | return True 36 | else: 37 | return False -------------------------------------------------------------------------------- /streamlit_book/render_true_or_false.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | 3 | try: 4 | from keywords import check_keyword 5 | from keywords import TRUE_FALSE_KEYWORD, BUTTON, SUCCESS, ERROR, DEFAULT_BUTTON_MESSAGE, DEFAULT_SUCCESS_MESSAGE, DEFAULT_ERROR_MESSAGE 6 | from answers import save_answer 7 | except: 8 | from .keywords import check_keyword 9 | from .keywords import TRUE_FALSE_KEYWORD, BUTTON, SUCCESS, ERROR, DEFAULT_BUTTON_MESSAGE, DEFAULT_SUCCESS_MESSAGE, DEFAULT_ERROR_MESSAGE 10 | from .answers import save_answer 11 | 12 | 13 | def true_or_false(question, answer, 14 | success=DEFAULT_SUCCESS_MESSAGE, 15 | error=DEFAULT_ERROR_MESSAGE, 16 | button=DEFAULT_BUTTON_MESSAGE): 17 | """Renders a true or false question from arguments. 18 | 19 | :param question: question to be displayed before the true or false options 20 | :type question: str 21 | :param answer: expected answer to the question, can be True or False 22 | :type answer: str 23 | :param success: message to be displayed when the user answers correctly. If empty, no message is displayed. 24 | :type success: str, optional 25 | :param error: message to be displayed when the user answers incorrectly. If empty, no message is displayed. 26 | :type error: str, optional 27 | :param button: message to be displayed on the button that checks the answer 28 | :type button: str, optional 29 | :return: tuple of booleans(button_pressed, answer_correct) with the button status and correctness of answer 30 | :rtype: tuple of bool 31 | """ 32 | if len(question)==0: 33 | st.error("Please provide a question") 34 | return None, None 35 | else: 36 | key = question.lower().replace(" ","") 37 | user_answer_str = st.radio(question, options=["True", "False"], key="TF"+key) 38 | user_answer = (user_answer_str == "True") # Convert to boolean 39 | if st.button(button, key="button"+key): 40 | # Reveal the answer 41 | if user_answer == answer: 42 | if success: 43 | st.success(success) 44 | is_correct = True 45 | else: 46 | if error: 47 | st.error(error) 48 | is_correct = False 49 | # Save the answer, if required 50 | if "save_answers" in st.session_state and st.session_state.save_answers: 51 | save_answer(question, is_correct=is_correct, user_answer=user_answer, correct_answer=answer) 52 | # Return if button is pressed and answer evaluation 53 | return True, is_correct 54 | else: 55 | # Not pressed yet 56 | return False, None -------------------------------------------------------------------------------- /streamlit_book/social_media.py: -------------------------------------------------------------------------------- 1 | from streamlit.components.v1 import html 2 | 3 | try: 4 | from keywords import SHARE_KEYWORD 5 | from keywords import check_keyword 6 | except: 7 | from .keywords import SHARE_KEYWORD 8 | from .keywords import check_keyword 9 | 10 | def share_parser(lines): 11 | """Parses a list of lines into a dictionary with the parsed values. 12 | 13 | :param lines: list of lines 14 | :type lines: list 15 | :return: parsed values for text and url 16 | :rtype: dict 17 | """ 18 | # Dict to store the parsed values 19 | parse_dict = { 20 | "my_text":"", 21 | "my_url":"", 22 | } 23 | for i, line in enumerate(lines): 24 | if i==0: 25 | if check_keyword(line, SHARE_KEYWORD): 26 | continue 27 | else: 28 | break 29 | elif i==1: 30 | parse_dict["my_text"] = line.strip() 31 | elif i==2: 32 | parse_dict["my_url"] = line.strip() 33 | return parse_dict 34 | 35 | def share_from_lines(lines): 36 | """Renders the share buttons from a list of lines. 37 | 38 | :param lines: list of lines 39 | :type lines: list 40 | :return: None 41 | """ 42 | parse_dict = share_parser(lines) 43 | share(my_text=parse_dict["my_text"], 44 | my_url=parse_dict["my_url"], ) 45 | return 46 | 47 | def share(my_text, my_url): 48 | """ 49 | This function takes a url and a text and displays 50 | clickable sharing buttons in html. 51 | 52 | :param my_text: the text to share on social media 53 | :type my_text: str 54 | :param my_url: the url to share on social media 55 | :type my_url: str 56 | """ 57 | 58 | # Define the css style for the sharing buttons 59 | my_css = """ 60 | .resp-sharing-button__link, 61 | .resp-sharing-button__icon { 62 | display: inline-block 63 | } 64 | 65 | .resp-sharing-button__link { 66 | text-decoration: none; 67 | color: #fff; 68 | margin: 0.5em 69 | } 70 | 71 | .resp-sharing-button { 72 | border-radius: 5px; 73 | transition: 25ms ease-out; 74 | padding: 0.5em 0.75em; 75 | font-family: Helvetica Neue,Helvetica,Arial,sans-serif 76 | } 77 | 78 | .resp-sharing-button__icon svg { 79 | width: 1em; 80 | height: 1em; 81 | margin-right: 0.4em; 82 | vertical-align: top 83 | } 84 | 85 | .resp-sharing-button--small svg { 86 | margin: 0; 87 | vertical-align: middle 88 | } 89 | 90 | /* Non solid icons get a stroke */ 91 | .resp-sharing-button__icon { 92 | stroke: #fff; 93 | fill: none 94 | } 95 | 96 | /* Solid icons get a fill */ 97 | .resp-sharing-button__icon--solid, 98 | .resp-sharing-button__icon--solidcircle { 99 | fill: #fff; 100 | stroke: none 101 | } 102 | 103 | .resp-sharing-button--twitter { 104 | background-color: #55acee 105 | } 106 | 107 | .resp-sharing-button--twitter:hover { 108 | background-color: #2795e9 109 | } 110 | 111 | .resp-sharing-button--pinterest { 112 | background-color: #bd081c 113 | } 114 | 115 | .resp-sharing-button--pinterest:hover { 116 | background-color: #8c0615 117 | } 118 | 119 | .resp-sharing-button--facebook { 120 | background-color: #3b5998 121 | } 122 | 123 | .resp-sharing-button--facebook:hover { 124 | background-color: #2d4373 125 | } 126 | 127 | .resp-sharing-button--tumblr { 128 | background-color: #35465C 129 | } 130 | 131 | .resp-sharing-button--tumblr:hover { 132 | background-color: #222d3c 133 | } 134 | 135 | .resp-sharing-button--reddit { 136 | background-color: #5f99cf 137 | } 138 | 139 | .resp-sharing-button--reddit:hover { 140 | background-color: #3a80c1 141 | } 142 | 143 | .resp-sharing-button--google { 144 | background-color: #dd4b39 145 | } 146 | 147 | .resp-sharing-button--google:hover { 148 | background-color: #c23321 149 | } 150 | 151 | .resp-sharing-button--linkedin { 152 | background-color: #0077b5 153 | } 154 | 155 | .resp-sharing-button--linkedin:hover { 156 | background-color: #046293 157 | } 158 | 159 | .resp-sharing-button--email { 160 | background-color: #777 161 | } 162 | 163 | .resp-sharing-button--email:hover { 164 | background-color: #5e5e5e 165 | } 166 | 167 | .resp-sharing-button--xing { 168 | background-color: #1a7576 169 | } 170 | 171 | .resp-sharing-button--xing:hover { 172 | background-color: #114c4c 173 | } 174 | 175 | .resp-sharing-button--whatsapp { 176 | background-color: #25D366 177 | } 178 | 179 | .resp-sharing-button--whatsapp:hover { 180 | background-color: #1da851 181 | } 182 | 183 | .resp-sharing-button--hackernews { 184 | background-color: #FF6600 185 | } 186 | .resp-sharing-button--hackernews:hover, .resp-sharing-button--hackernews:focus { background-color: #FB6200 } 187 | 188 | .resp-sharing-button--vk { 189 | background-color: #507299 190 | } 191 | 192 | .resp-sharing-button--vk:hover { 193 | background-color: #43648c 194 | } 195 | 196 | .resp-sharing-button--facebook { 197 | background-color: #3b5998; 198 | border-color: #3b5998; 199 | } 200 | 201 | .resp-sharing-button--facebook:hover, 202 | .resp-sharing-button--facebook:active { 203 | background-color: #2d4373; 204 | border-color: #2d4373; 205 | } 206 | 207 | .resp-sharing-button--twitter { 208 | background-color: #55acee; 209 | border-color: #55acee; 210 | } 211 | 212 | .resp-sharing-button--twitter:hover, 213 | .resp-sharing-button--twitter:active { 214 | background-color: #2795e9; 215 | border-color: #2795e9; 216 | } 217 | 218 | .resp-sharing-button--email { 219 | background-color: #777777; 220 | border-color: #777777; 221 | } 222 | 223 | .resp-sharing-button--email:hover, 224 | .resp-sharing-button--email:active { 225 | background-color: #5e5e5e; 226 | border-color: #5e5e5e; 227 | } 228 | 229 | .resp-sharing-button--linkedin { 230 | background-color: #0077b5; 231 | border-color: #0077b5; 232 | } 233 | 234 | .resp-sharing-button--linkedin:hover, 235 | .resp-sharing-button--linkedin:active { 236 | background-color: #046293; 237 | border-color: #046293; 238 | } 239 | 240 | .resp-sharing-button--whatsapp { 241 | background-color: #25D366; 242 | border-color: #25D366; 243 | } 244 | 245 | .resp-sharing-button--whatsapp:hover, 246 | .resp-sharing-button--whatsapp:active { 247 | background-color: #1DA851; 248 | border-color: #1DA851; 249 | } 250 | 251 | .resp-sharing-button--telegram { 252 | background-color: #54A9EB; 253 | } 254 | 255 | .resp-sharing-button--telegram:hover { 256 | background-color: #4B97D1;} 257 | """ 258 | 259 | # Define the html 260 | my_html = f""" 261 | 262 | 263 | 264 | 268 | 269 | 270 | 271 | 272 | 276 | 277 | 278 | 279 | 280 | 284 | 285 | 286 | 287 | 288 |
293 |
294 |
295 | 296 | 297 | 298 |
301 |
302 |
303 | 304 | 305 | 306 |
309 |
310 |
311 |
312 | """ 313 | 314 | # Define button html 315 | my_share_button = f""" 316 | 319 | {my_html} 320 | """ 321 | 322 | # Render 323 | html(my_share_button) 324 | 325 | return --------------------------------------------------------------------------------