├── .github └── workflows │ ├── python-app.yml │ └── translatable-string-extract.yml ├── .pre-commit-config.yaml ├── AUTHORS.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── __init__.py ├── babel_mapping.ini ├── config.py.example ├── database │ ├── __init__.py │ ├── models.py │ └── schemas.py ├── dependencies.py ├── internal │ ├── __init__.py │ ├── agenda_events.py │ ├── astronomy.py │ ├── calendar_privacy.py │ ├── celebrity.py │ ├── comment.py │ ├── daily_quotes.py │ ├── email.py │ ├── emotion.py │ ├── event.py │ ├── export.py │ ├── friend_view.py │ ├── google_connect.py │ ├── import_file.py │ ├── import_holidays.py │ ├── json_data_loader.py │ ├── languages.py │ ├── logger_customizer.py │ ├── on_this_day_events.py │ ├── search.py │ ├── security │ │ ├── __init__.py │ │ ├── dependancies.py │ │ ├── ouath2.py │ │ └── schema.py │ ├── translation.py │ ├── user │ │ └── availability.py │ ├── utils.py │ ├── weather_forecast.py │ ├── week.py │ └── zodiac.py ├── locales │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── base.mo │ │ │ └── base.po │ └── he │ │ └── LC_MESSAGES │ │ ├── base.mo │ │ └── base.po ├── main.py ├── media │ ├── example.png │ ├── free-python-course-4k.png │ ├── profile.png │ └── user1.png ├── resources │ ├── credits.json │ ├── quotes.json │ └── zodiac.json ├── routers │ ├── __init__.py │ ├── about_us.py │ ├── agenda.py │ ├── calendar.py │ ├── calendar_grid.py │ ├── categories.py │ ├── celebrity.py │ ├── credits.py │ ├── currency.py │ ├── dayview.py │ ├── email.py │ ├── event.py │ ├── event_images.py │ ├── export.py │ ├── four_o_four.py │ ├── friendview.py │ ├── google_connect.py │ ├── invitation.py │ ├── login.py │ ├── logout.py │ ├── profile.py │ ├── register.py │ ├── salary │ │ ├── __init__.py │ │ ├── config.py │ │ ├── routes.py │ │ └── utils.py │ ├── search.py │ ├── share.py │ ├── telegram.py │ ├── user.py │ ├── weekview.py │ └── whatsapp.py ├── static │ ├── about.css │ ├── agenda_style.css │ ├── celebrity.css │ ├── celebrity.js │ ├── credits_pictures │ │ ├── Adi Faibish.PNG │ │ ├── Adva Alkalay.PNG │ │ ├── Anna Shtirberg.PNG │ │ ├── Aviad Amar.PNG │ │ ├── Elior Digmi.PNG │ │ ├── Elor Shoshan.PNG │ │ ├── Hagai Kraus.PNG │ │ ├── Idan Pelled.PNG │ │ ├── Nadav Pesach.PNG │ │ ├── Nir Perelshtein.PNG │ │ ├── Odelia Yechiel.PNG │ │ ├── Ori Hirshfeld.PNG │ │ ├── RonHuberfeld.PNG │ │ ├── Sagi Zaid Or.PNG │ │ ├── Yaakov Fogel.PNG │ │ ├── Yam Mesicka.PNG │ │ ├── YuvalCagan.PNG │ │ ├── Zohar Yamin.PNG │ │ └── profile.png │ ├── credits_style.css │ ├── currency.css │ ├── currency.js │ ├── dayview.css │ ├── event │ │ ├── eventedit.css │ │ └── eventview.css │ ├── event_flairs │ │ ├── birthday.jpg │ │ ├── christmas.jpg │ │ ├── coffee.jpg │ │ ├── concert.jpg │ │ ├── cycle.jpg │ │ ├── dentist.jpg │ │ ├── drank.jpg │ │ ├── food.jpg │ │ ├── golf.jpg │ │ ├── graduate.jpg │ │ ├── gym.jpg │ │ ├── haircut.jpg │ │ ├── halloween.jpg │ │ ├── hike.jpg │ │ ├── kayak.jpg │ │ ├── manicure.jpg │ │ ├── massage.jpg │ │ ├── music.jpg │ │ ├── pill.jpg │ │ ├── pingpong.jpg │ │ ├── plan.jpg │ │ ├── pokemon.jpg │ │ ├── ran.jpg │ │ ├── read.jpg │ │ ├── repair.jpg │ │ ├── sail.jpg │ │ ├── santa.jpg │ │ ├── ski.jpg │ │ ├── soccer.jpg │ │ ├── swam.jpg │ │ ├── tennis.jpg │ │ ├── thanksgiving.jpg │ │ ├── wed.jpg │ │ └── yoga.jpg │ ├── eventdisplay.js │ ├── friendview.css │ ├── global.css │ ├── grid_style.css │ ├── horoscope.js │ ├── images │ │ ├── calendar.jpg │ │ ├── icons │ │ │ ├── calendar-outline.svg │ │ │ ├── close_sidebar.svg │ │ │ ├── pencil.svg │ │ │ ├── plus.svg │ │ │ ├── trash-can.svg │ │ │ └── view.svg │ │ └── zodiac │ │ │ ├── Aquarius.svg │ │ │ ├── Aries.svg │ │ │ ├── Cancer.svg │ │ │ ├── Capricorn.svg │ │ │ ├── Gemini.svg │ │ │ ├── Leo.svg │ │ │ ├── Libra.svg │ │ │ ├── Pisces.svg │ │ │ ├── Sagittarius.svg │ │ │ ├── Scorpio.svg │ │ │ ├── Taurus.svg │ │ │ └── Virgo.svg │ ├── js │ │ ├── grid_navigation.js │ │ └── grid_scripts.js │ ├── popover.js │ ├── share_event.css │ ├── style.css │ ├── swagger │ │ ├── swagger-ui-bundle.js │ │ └── swagger-ui.css │ ├── text_editor.js │ ├── voice.js │ └── weekview.css ├── telegram │ ├── __init__.py │ ├── bot.py │ ├── handlers.py │ ├── keyboards.py │ └── models.py ├── templates │ ├── about_us.html │ ├── agenda.html │ ├── base.html │ ├── calendar │ │ ├── add_week.html │ │ ├── calendar.html │ │ └── layout.html │ ├── calendar_day_view.html │ ├── calendar_monthly_view.html │ ├── celebrity.html │ ├── credits.html │ ├── currency.html │ ├── dayview.html │ ├── demo │ │ └── home_email.html │ ├── event │ │ └── partials │ │ │ └── edit_event_details_tab.html │ ├── eventedit.html │ ├── eventview.html │ ├── four_o_four.j2 │ ├── friendview.html │ ├── hello.html │ ├── home.html │ ├── import_holidays.html │ ├── index.html │ ├── invitations.html │ ├── invite_mail.html │ ├── login.html │ ├── mail_base.html │ ├── on_this_day.html │ ├── partials │ │ ├── base.html │ │ ├── calendar │ │ │ ├── calendar_base.html │ │ │ ├── event │ │ │ │ ├── comments_tab.html │ │ │ │ ├── edit_event_details_tab.html │ │ │ │ ├── text_editor_partial_body.html │ │ │ │ ├── text_editor_partial_head.html │ │ │ │ └── view_event_details_tab.html │ │ │ ├── feature_settings │ │ │ │ └── example.html │ │ │ ├── monthly_view │ │ │ │ ├── add_week.html │ │ │ │ └── monthly_grid.html │ │ │ └── navigation.html │ │ └── index │ │ │ ├── index_base.html │ │ │ └── navigation.html │ ├── profile.html │ ├── register.html │ ├── salary │ │ ├── month.j2 │ │ ├── pick.j2 │ │ ├── settings.j2 │ │ └── view.j2 │ ├── search.html │ ├── share_event.html │ └── weekview.html └── utils │ ├── __init__.py │ └── extending_openapi.py ├── git ├── mypy.ini ├── pyproject.toml ├── requirements.txt ├── schema.md ├── tests ├── __init__.py ├── association_fixture.py ├── asyncio_fixture.py ├── calendar-discovery.json ├── calendar-linux.json ├── category_fixture.py ├── client_fixture.py ├── comment_fixture.py ├── conftest.py ├── dayview_fixture.py ├── event_fixture.py ├── files_for_import_file_tests │ ├── sample.ics │ ├── sample2.blabla │ ├── sample2.ics │ ├── sample3.ics │ ├── sample_above_5mb.txt │ ├── sample_below_1mb.txt │ ├── sample_calendar_data.csv │ ├── sample_calendar_data.txt │ ├── sample_data_invalid.txt │ ├── sample_date2_ver.txt │ ├── sample_date_mix.txt │ ├── sample_rng_invalid.txt │ ├── ‏‏sample_below_1mb.csv │ └── ‏‏sample_loc_ver.txt ├── invitation_fixture.py ├── logger_fixture.py ├── quotes_fixture.py ├── resources │ ├── ics_example.txt │ └── wrong_ics_example.txt ├── salary │ ├── conftest.py │ ├── test_routes.py │ └── test_utils.py ├── security_testing_routes.py ├── test_a_telegram_asyncio.py ├── test_about.py ├── test_agenda_internal.py ├── test_agenda_route.py ├── test_app.py ├── test_association.py ├── test_astronomy.py ├── test_calendar_grid.py ├── test_calendar_privacy.py ├── test_categories.py ├── test_celebrity.py ├── test_comment.py ├── test_credits.py ├── test_currency.py ├── test_dayview.py ├── test_email.py ├── test_emotion.py ├── test_event.py ├── test_event_images.py ├── test_export.py ├── test_friendview.py ├── test_google_connect.py ├── test_holidays.py ├── test_home.py ├── test_import_file.py ├── test_invitation.py ├── test_json_data_loader.py ├── test_language.py ├── test_logger.py ├── test_login.py ├── test_on_this_day_events.py ├── test_profile.py ├── test_psql_environment.py ├── test_quotes.py ├── test_register.py ├── test_search.py ├── test_share_event.py ├── test_translation.py ├── test_user.py ├── test_utils.py ├── test_weather_forecast.py ├── test_weekview.py ├── test_whatsapp.py ├── test_zodiac.py ├── user_fixture.py ├── utils.py └── zodiac_fixture.py └── tox.ini /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python application 5 | 6 | on: 7 | push: 8 | branches: [ main, develop ] 9 | pull_request: 10 | branches: [ main, develop ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python 3.9 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: 3.9 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install flake8 pytest pytest-cov 27 | cp app/config.py.example app/config.py 28 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 29 | - name: Lint with flake8 30 | run: | 31 | # stop the build if there are Python syntax errors or undefined names 32 | flake8 . --count --show-source --statistics 33 | # exit-zero treats all errors as warnings. 34 | flake8 . --count --exit-zero --max-complexity=10 --statistics 35 | - name: Test with pytest 36 | run: | 37 | pytest -vvv --junitxml=junit/test-results.xml --cov-report=xml --cov=app ./tests 38 | - name: Upload coverage to Codecov 39 | uses: codecov/codecov-action@v1.0.13 40 | with: 41 | file: coverage.xml 42 | - name: Cleanup 43 | run: | 44 | rm app/config.py 45 | -------------------------------------------------------------------------------- /.github/workflows/translatable-string-extract.yml: -------------------------------------------------------------------------------- 1 | # This workflow will extract new translatable strings from files under /app and /tests into a base.pot file, 2 | # and update the 'en' and 'he' base.po and base.mo files. 3 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 4 | 5 | name: update-translations 6 | 7 | on: 8 | # Trigger the workflow on push request, 9 | # but only for the main branch 10 | push: 11 | branches: 12 | - main 13 | 14 | jobs: 15 | update-translations: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | 22 | - name: Set up Python 3.x 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: '3.x' 26 | 27 | - name: Install prerequesits 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install Babel Jinja2 31 | 32 | - name: Create base.pot file 33 | run: pybabel extract --mapping-file=app/babel_mapping.ini app tests -o app/locales/base.pot -c i18n 34 | 35 | - name: Update all language base.po files 36 | run: pybabel update -i app/locales/base.pot -d app/locales -D base 37 | 38 | - name: Update all .mo files 39 | run: pybabel compile -d app/locales -D base 40 | 41 | # https://github.com/stefanzweifel/git-auto-commit-action 42 | - name: Commit changes 43 | uses: stefanzweifel/git-auto-commit-action@v4 44 | with: 45 | commit_message: Apply automatic translatable string changes 46 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | # Flake8 to check style is OK 3 | - repo: https://gitlab.com/pycqa/flake8 4 | rev: 3.8.4 5 | hooks: 6 | - id: flake8 7 | # yapf to fix many style mistakes 8 | - repo: https://github.com/ambv/black 9 | rev: 20.8b1 10 | hooks: 11 | - id: black 12 | entry: black 13 | language: python 14 | language_version: python3 15 | require_serial: true 16 | types_or: [python, pyi] 17 | # More built in style checks and fixes 18 | - repo: https://github.com/pre-commit/pre-commit-hooks 19 | rev: v3.4.0 20 | hooks: 21 | - id: trailing-whitespace 22 | - id: check-docstring-first 23 | - id: check-json 24 | - id: check-added-large-files 25 | - id: check-yaml 26 | - id: debug-statements 27 | - id: requirements-txt-fixer 28 | - id: check-merge-conflict 29 | - id: end-of-file-fixer 30 | - id: sort-simple-yaml 31 | - repo: meta 32 | hooks: 33 | - id: check-useless-excludes 34 | - repo: https://github.com/asottile/add-trailing-comma 35 | rev: v2.1.0 36 | hooks: 37 | - id: add-trailing-comma 38 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | 🐍 This document only reflects the team at the time of the original release in February 2021. 2 | 3 | # Our Authors 4 | 5 | * Yam Mesicka - Leader 6 | * Adva Alkalay - Developer 7 | * Aviad Amar - Developer 8 | * Efrat Bar Yehuda - Developer 9 | * Michael Ben David - Developer 10 | * Tamar Berger - Developer 11 | * Yuval Cagan - Developer 12 | * Elior Digmi - Developer 13 | * Adi Faibish - Developer 14 | * Yaakov Fogel - Developer 15 | * Ori Hirshfeld - Developer 16 | * Hadas Kedar - Developer 17 | * Hagai Kraus - Developer 18 | * Eyal Merav - Developer 19 | * Idan Pelled - Developer 20 | * Nadav Pesach - Developer 21 | * Nir Perelshtein - Developer 22 | * Elor Shoshan - Developer 23 | * Anna Shtirberg - Developer 24 | * Zohar Yamin - Developer 25 | * Odelia Yechiel - Developer 26 | * Sagi Zaid Or - Developer 27 | * Ap1234567 - Developer 28 | * ellenc345 - Developer 29 | * Gonzom - Developer 30 | * ivarshav - Developer 31 | * Liad-n - Developer 32 | * leddest - Developer 33 | * PureDreamer - Developer 34 | * ShiZinDle - Developer 35 | * YairEn - Developer 36 | 37 | # Special thanks to 38 | 39 | Yam Mesicka for dedicated accompaniment throughout the year of fascinating Python studies. 40 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 👍🎉 First off, thanks for taking the time to contribute! 🎉👍 3 | 4 | When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a [code of conduct](https://github.com/PythonFreeCourse/calendar/blob/master/CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. 7 | 8 | ## Pull Requests 9 | Create feature branches. 10 | 11 | One pull request per feature - If you want to do more than one thing, send multiple pull requests. 12 | 13 | Send coherent history - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 14 | 15 | ## Guide 16 | 1. Fork the project. 17 | 2. Create a new branch (`git checkout -b 'branch name'`). 18 | 3. Commit your changes (`git commit -m 'Add something new'`). 19 | 4. Push to the branch (`git push -u origin 'branch name'`). 20 | 5. Open a Pull Request. 21 | 22 | ## Style Guide 23 | Follow the [commit messages specification](https://www.conventionalcommits.org/en/v1.0.0/). 24 | 25 | Happy coding! 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyLander 2 | 3 |

4 | License Apache-2.0 icon 5 |

6 | 7 | 👋 Welcome to Open Source Calendar built with Python. 🐍 8 | 9 | * [Project's objectives](#Project's-objectives) 10 | * [Creating development environment](#creating-development-environment) 11 | * [Contributing](#contributing) 12 | ### Project's objectives 13 | 1. Develop open source calendar tool using new technics while trying new things. 14 | 2. Using Python as main programming language and plain HTML/JS for GUI. 15 | 3. Create bonding in our community. 16 | 17 | ## Creating development environment 18 | ### Prerequisites 19 | 1. Windows or Linux based system - either [WSL on windows](https://docs.microsoft.com/en-us/windows/wsl/install-win10) or full blown linux. 20 | 2. [Python](https://www.python.org/downloads/release/python-385/) 21 | 3. Install python's requirements: 22 | ```shell 23 | pip install -r requirements.txt 24 | ``` 25 | 4. Install pre-commit hooks: 26 | ```shell 27 | pre-commit install 28 | ``` 29 | 30 | ### Running on Windows 31 | 32 | ```shell 33 | virtualenv env 34 | .\env\Scripts\activate.bat 35 | pip install -r requirements.txt 36 | # Copy app\config.py.example to app\config.py. 37 | # Edit the variables' values. 38 | # Rendering JWT_KEY: 39 | python -c "import secrets; from pathlib import Path; f = Path('app/config.py'); f.write_text(f.read_text().replace('JWT_KEY_PLACEHOLDER', secrets.token_hex(32), 1));" 40 | uvicorn app.main:app --reload 41 | ``` 42 | 43 | ### Running on Linux 44 | ```shell 45 | python -m venv venv 46 | source venv/bin/activate 47 | pip install -r requirements.txt 48 | cp app/config.py.example app/config.py 49 | # Edit the variables' values. 50 | # Rendering JWT_KEY: 51 | python -c "import secrets; from pathlib import Path; f = Path('app/config.py'); f.write_text(f.read_text().replace('JWT_KEY_PLACEHOLDER', secrets.token_hex(32), 1));" 52 | 53 | ### Running tox 54 | ```shell 55 | # Standard tests: 'coverage' and 'flake8' 56 | tox 57 | # Only flake8 tests 58 | tox -e flake8 59 | # Coverage with html reports 60 | tox -e rep 61 | ``` 62 | 63 | ## Contributing 64 | View [contributing guidelines](https://github.com/PythonFreeCourse/calendar/blob/master/CONTRIBUTING.md). 65 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/__init__.py -------------------------------------------------------------------------------- /app/babel_mapping.ini: -------------------------------------------------------------------------------- 1 | # Extraction from Python source files 2 | [python: **.py] 3 | 4 | # Extraction from Jinja2 HTML and text templates 5 | [jinja2: **/templates/**.html] 6 | extensions = jinja2.ext.i18n,jinja2.ext.autoescape,jinja2.ext.with_ 7 | -------------------------------------------------------------------------------- /app/database/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from sqlalchemy import create_engine 4 | from sqlalchemy.orm import sessionmaker 5 | 6 | from app import config 7 | 8 | SQLALCHEMY_DATABASE_URL = os.getenv( 9 | "DATABASE_CONNECTION_STRING", config.DEVELOPMENT_DATABASE_STRING) 10 | 11 | 12 | def create_env_engine(psql_environment, sqlalchemy_database_url): 13 | if not psql_environment: 14 | return create_engine( 15 | sqlalchemy_database_url, connect_args={"check_same_thread": False}) 16 | 17 | return create_engine(sqlalchemy_database_url) 18 | 19 | 20 | engine = create_env_engine(config.PSQL_ENVIRONMENT, SQLALCHEMY_DATABASE_URL) 21 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 22 | -------------------------------------------------------------------------------- /app/dependencies.py: -------------------------------------------------------------------------------- 1 | import os 2 | from functools import lru_cache 3 | 4 | from fastapi.templating import Jinja2Templates 5 | from sqlalchemy.orm import Session 6 | 7 | from app import config 8 | from app.database import SessionLocal 9 | from app.internal.logger_customizer import LoggerCustomizer 10 | 11 | GOOGLE_ERROR = config.CLIENT_SECRET_FILE is None 12 | APP_PATH = os.path.dirname(os.path.realpath(__file__)) 13 | MEDIA_PATH = os.path.join(APP_PATH, config.MEDIA_DIRECTORY) 14 | STATIC_PATH = os.path.join(APP_PATH, "static") 15 | TEMPLATES_PATH = os.path.join(APP_PATH, "templates") 16 | 17 | templates = Jinja2Templates(directory=TEMPLATES_PATH) 18 | templates.env.add_extension('jinja2.ext.i18n') 19 | 20 | # Configure logger 21 | logger = LoggerCustomizer.make_logger(config.LOG_PATH, 22 | config.LOG_FILENAME, 23 | config.LOG_LEVEL, 24 | config.LOG_ROTATION_INTERVAL, 25 | config.LOG_RETENTION_INTERVAL, 26 | config.LOG_FORMAT) 27 | 28 | 29 | def get_db() -> Session: 30 | db = SessionLocal() 31 | try: 32 | yield db 33 | finally: 34 | db.close() 35 | 36 | 37 | @lru_cache() 38 | def get_settings(): 39 | return config.Settings() 40 | -------------------------------------------------------------------------------- /app/internal/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/internal/__init__.py -------------------------------------------------------------------------------- /app/internal/calendar_privacy.py: -------------------------------------------------------------------------------- 1 | from app.dependencies import get_db 2 | from app.database.models import User 3 | # TODO switch to using this when the user system is merged 4 | # from app.internal.security.dependancies import ( 5 | # current_user, CurrentUser) 6 | 7 | from fastapi import Depends 8 | 9 | 10 | # TODO add privacy as an attribute in current user 11 | # in app.internal.security.dependancies 12 | # when user system is merged 13 | def can_show_calendar( 14 | requested_user_username: str, 15 | db: Depends(get_db), 16 | current_user: User 17 | # TODO to be added after user system is merged: 18 | # CurrentUser = Depends(current_user) 19 | ) -> bool: 20 | """Check whether current user can show the requested calendar""" 21 | requested_user = db.query(User).filter( 22 | User.username == requested_user_username 23 | ).first() 24 | privacy = current_user.privacy 25 | is_current_user = current_user.username == requested_user.username 26 | if privacy == 'Private' and is_current_user: 27 | return True 28 | 29 | elif privacy == 'Public': 30 | return True 31 | 32 | return False 33 | -------------------------------------------------------------------------------- /app/internal/celebrity.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | 4 | def get_today_month_and_day() -> str: 5 | """Returns today's month and day in the format: %m-%d""" 6 | return datetime.date.today().strftime("%m-%d") 7 | -------------------------------------------------------------------------------- /app/internal/daily_quotes.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | from typing import Dict, Optional 3 | 4 | from sqlalchemy.orm import Session 5 | from sqlalchemy.sql.expression import func 6 | 7 | from app.database.models import Quote 8 | 9 | TOTAL_DAYS = 366 10 | 11 | 12 | def get_quote(quote_: Dict[str, Optional[str]]) -> Quote: 13 | """Returns a Quote object from the dictionary data. 14 | 15 | Args: 16 | quote_: A dictionary quote related information. 17 | 18 | Returns: 19 | A new Quote object. 20 | """ 21 | return Quote( 22 | text=quote_['text'], 23 | author=quote_['author'], 24 | ) 25 | 26 | 27 | def get_quote_of_day( 28 | session: Session, requested_date: date = date.today() 29 | ) -> Optional[Quote]: 30 | """Returns the Quote object for the specific day. 31 | 32 | The quote is randomly selected from a set of quotes matching the given day. 33 | 34 | Args: 35 | session: The database connection. 36 | requested_date: Optional; The requested date. 37 | 38 | Returns: 39 | A Quote object. 40 | """ 41 | day_number = requested_date.timetuple().tm_yday 42 | quote = (session.query(Quote) 43 | .filter(Quote.id % TOTAL_DAYS == day_number) 44 | .order_by(func.random()) 45 | .first() 46 | ) 47 | return quote 48 | -------------------------------------------------------------------------------- /app/internal/friend_view.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from sqlalchemy.orm import Session 4 | 5 | from app.database.models import Event 6 | from app.routers.event import sort_by_date 7 | from app.routers.user import get_all_user_events 8 | 9 | 10 | def get_events_per_friend( 11 | session: Session, 12 | user_id: int, 13 | my_friend: str, 14 | ) -> List[Event]: 15 | """ My_friend is the name of a person that appears in the invite list of 16 | events. He is not necessarily a registered userץ The variable is used to 17 | show all events where we are both in the invitees list""" 18 | 19 | events_together = [] 20 | sorted_events = sort_by_date(get_all_user_events(session, user_id)) 21 | for event in sorted_events: 22 | if my_friend in event.invitees.split(','): 23 | events_together.append(event) 24 | return events_together 25 | -------------------------------------------------------------------------------- /app/internal/import_holidays.py: -------------------------------------------------------------------------------- 1 | import re 2 | from datetime import datetime, timedelta 3 | 4 | from app.database.models import User, Event, UserEvent 5 | from sqlalchemy.orm import Session 6 | from typing import List, Match 7 | 8 | REGEX_EXTRACT_HOLIDAYS = re.compile( 9 | r'SUMMARY:(?P.*)(\n.*){1,8}DTSTAMP:(?P<date>\w{8})', 10 | re.MULTILINE) 11 | 12 | 13 | def get_holidays_from_file(file: List[Event], session: Session) -> List[Event]: 14 | """ 15 | This function using regex to extract holiday title 16 | and date from standrd ics file 17 | :param file:standard ics file 18 | :param session:current connection 19 | :return:list of holidays events 20 | """ 21 | parsed_holidays = REGEX_EXTRACT_HOLIDAYS.finditer(file) 22 | holidays = [] 23 | for holiday in parsed_holidays: 24 | holiday_event = create_holiday_event( 25 | holiday, session.query(User).filter_by(id=1).first().id) 26 | holidays.append(holiday_event) 27 | return holidays 28 | 29 | 30 | def create_holiday_event(holiday: Match[str], owner_id: int) -> Event: 31 | valid_ascii_chars_range = 128 32 | title = holiday.groupdict()['title'].strip() 33 | title_to_save = ''.join(i if ord(i) < valid_ascii_chars_range 34 | else '' for i in title) 35 | date = holiday.groupdict()['date'].strip() 36 | format_string = '%Y%m%d' 37 | holiday = Event( 38 | title=title_to_save, 39 | start=datetime.strptime(date, format_string), 40 | end=datetime.strptime(date, format_string) + timedelta(days=1), 41 | content='holiday', 42 | owner_id=owner_id 43 | ) 44 | return holiday 45 | 46 | 47 | def save_holidays_to_db(holidays: List[Event], session: Session): 48 | """ 49 | this function saves holiday list into database. 50 | :param holidays: list of holidays events 51 | :param session: current connection 52 | """ 53 | session.add_all(holidays) 54 | session.commit() 55 | session.flush(holidays) 56 | userevents = [] 57 | for holiday in holidays: 58 | userevent = UserEvent( 59 | user_id=holiday.owner_id, 60 | event_id=holiday.id 61 | ) 62 | userevents.append(userevent) 63 | session.add_all(userevents) 64 | session.commit() 65 | -------------------------------------------------------------------------------- /app/internal/on_this_day_events.py: -------------------------------------------------------------------------------- 1 | from datetime import date, datetime 2 | import json 3 | from typing import Any, Dict 4 | 5 | from fastapi import Depends 6 | from loguru import logger 7 | import requests 8 | from sqlalchemy import func 9 | from sqlalchemy.exc import SQLAlchemyError 10 | from sqlalchemy.orm import Session 11 | from sqlalchemy.orm.exc import NoResultFound 12 | 13 | from app.database.models import WikipediaEvents 14 | from app.dependencies import get_db 15 | 16 | 17 | def insert_on_this_day_data( 18 | db: Session = Depends(get_db) 19 | ) -> Dict[str, Any]: 20 | now = datetime.now() 21 | day, month = now.day, now.month 22 | 23 | res = requests.get( 24 | f'https://byabbe.se/on-this-day/{month}/{day}/events.json') 25 | text = json.loads(res.text) 26 | res_events = text.get('events') 27 | res_date = text.get('date') 28 | res_wiki = text.get('wikipedia') 29 | db.add(WikipediaEvents(events=res_events, 30 | date_=res_date, wikipedia=res_wiki)) 31 | db.commit() 32 | return text 33 | 34 | 35 | def get_on_this_day_events( 36 | db: Session = Depends(get_db) 37 | ) -> Dict[str, Any]: 38 | try: 39 | data = (db.query(WikipediaEvents). 40 | filter( 41 | func.date(WikipediaEvents.date_inserted) == date.today()). 42 | one()) 43 | 44 | except NoResultFound: 45 | data = insert_on_this_day_data(db) 46 | except (SQLAlchemyError, AttributeError) as e: 47 | logger.error(f'on this day failed with error: {e}') 48 | data = {'events': [], 'wikipedia': 'https://en.wikipedia.org/'} 49 | return data 50 | -------------------------------------------------------------------------------- /app/internal/search.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from sqlalchemy.exc import SQLAlchemyError 4 | from sqlalchemy.orm.session import Session 5 | 6 | from app.database.models import Event 7 | 8 | 9 | def get_results_by_keywords( 10 | session: Session, keywords: str, owner_id: int 11 | ) -> List[Event]: 12 | """Returns a list of Events matching the search query. 13 | 14 | The results are limited to Events owned by the current user. 15 | Uses PostgreSQL's built in 'Full-text search' feature, and 16 | doesn't work with SQLite. 17 | 18 | Args: 19 | session: The database connection. 20 | keywords: The search keywords. 21 | owner_id: The current user ID. 22 | 23 | Returns: 24 | A list of Events matching the search query. 25 | """ 26 | keywords = _get_stripped_keywords(keywords) 27 | 28 | try: 29 | return (session.query(Event) 30 | .filter(Event.owner_id == owner_id, 31 | Event.events_tsv.match(keywords)) 32 | .all()) 33 | 34 | except (SQLAlchemyError, AttributeError): 35 | return [] 36 | 37 | 38 | def _get_stripped_keywords(keywords: str) -> str: 39 | """Returns a valid database search keywords string. 40 | 41 | Args: 42 | keywords: The search keywords. 43 | 44 | Returns: 45 | A valid database search keywords string. 46 | """ 47 | keywords = " ".join(keywords.split()) 48 | keywords = keywords.replace(" ", ":* & ") + ":*" 49 | return keywords 50 | -------------------------------------------------------------------------------- /app/internal/security/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/internal/security/__init__.py -------------------------------------------------------------------------------- /app/internal/security/schema.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class CurrentUser(BaseModel): 7 | """ 8 | Validating fields types 9 | Returns a user details as a class. 10 | """ 11 | user_id: Optional[int] 12 | username: str 13 | 14 | class Config: 15 | orm_mode = True 16 | 17 | 18 | class LoginUser(CurrentUser): 19 | """ 20 | Validating fields types 21 | Returns a User object for signing in. 22 | """ 23 | is_manager: Optional[bool] 24 | password: str 25 | -------------------------------------------------------------------------------- /app/internal/user/availability.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy.orm import Session 4 | 5 | from app.database.models import Event, User 6 | # from app.internal.utils import get_current_user 7 | 8 | 9 | def disable(session: Session, user_id: int) -> bool: 10 | """this functions changes user status to disabled. 11 | returns: 12 | True if function worked properly 13 | False if it didn't.""" 14 | future_events_user_owns = session.query(Event).filter( 15 | Event.start > datetime.now(), Event.owner_id == user_id).all() 16 | 17 | if future_events_user_owns: 18 | return False 19 | # if get_current_user(session) != user_id: 20 | # return False 21 | """line above makes sure the user disabled is the current user logged 22 | & doesn't own any event. 23 | currently it doesn't uses get_current_user since logger is not 24 | merged yet, Ill add it when its impossible to mock a logged user.""" 25 | 26 | user_disabled = session.query(User).get(user_id) 27 | user_disabled.disabled = True 28 | session.commit() 29 | return True 30 | 31 | 32 | def enable(session: Session, user_id: int) -> bool: 33 | """this functions enables user- doesn't return anything. 34 | currently it doesn't uses get_current_user since logger is not 35 | merged yet, Ill add it when its impossible to mock a logged user.""" 36 | # if get_current_user(session) != user_id: 37 | # return False 38 | user_enabled = session.query(User).get(user_id) 39 | user_enabled.disabled = False 40 | session.commit() 41 | return True 42 | -------------------------------------------------------------------------------- /app/internal/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Optional 2 | 3 | from sqlalchemy.orm import Session 4 | 5 | from app.database.models import Base, User 6 | 7 | 8 | def save(session: Session, instance: Base) -> bool: 9 | """Commits an instance to the db. 10 | source: app.database.models.Base""" 11 | if issubclass(instance.__class__, Base): 12 | session.add(instance) 13 | session.commit() 14 | return True 15 | return False 16 | 17 | 18 | def create_model(session: Session, model_class: Base, **kwargs: Any) -> Base: 19 | """Creates and saves a db model.""" 20 | instance = model_class(**kwargs) 21 | save(session, instance) 22 | return instance 23 | 24 | 25 | def delete_instance(session: Session, instance: Base) -> None: 26 | """Deletes an instance from the db.""" 27 | session.delete(instance) 28 | session.commit() 29 | 30 | 31 | def get_current_user(session: Session) -> User: 32 | """Mock function for current user information retrival.""" 33 | # Code revision required after user login feature is added 34 | new_user = get_placeholder_user() 35 | user = session.query(User).first() 36 | if not user: 37 | save(session, new_user) 38 | user = session.query(User).first() 39 | 40 | return user 41 | 42 | 43 | def get_available_users(session: Session) -> List[User]: 44 | """this function return all availible users.""" 45 | return session.query(User).filter(not User.disabled).all() 46 | 47 | 48 | def get_user(session: Session, user_id: int) -> Optional[User]: 49 | """Returns User instance for `user_id` if exists, None otherwise. 50 | 51 | Args: 52 | session (Session): DB session. 53 | user_id (int): ID of user to return. 54 | 55 | Returns: 56 | (User | None): User instance for `user_id` if exists, None otherwise. 57 | """ 58 | return session.query(User).filter_by(id=user_id).first() 59 | 60 | 61 | def get_placeholder_user() -> User: 62 | """Creates a mock user. 63 | 64 | This is a temporarily function which should be removed when a 65 | real system is created. 66 | 67 | Returns: 68 | A User object. 69 | """ 70 | return User( 71 | username='new_user', 72 | email='my@email.po', 73 | password='1a2s3d4f5g6', 74 | full_name='My Name', 75 | language_id=1, 76 | telegram_id='', 77 | ) 78 | -------------------------------------------------------------------------------- /app/internal/week.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/internal/week.py -------------------------------------------------------------------------------- /app/internal/zodiac.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Dict, Union 3 | 4 | from sqlalchemy import and_, or_ 5 | from sqlalchemy.orm import Session 6 | 7 | from app.database.models import Zodiac 8 | 9 | 10 | def get_zodiac(zodiac_: Dict[str, Union[str, int]]) -> Zodiac: 11 | """Returns a Zodiac object from the dictionary data. 12 | 13 | Args: 14 | zodiac_: A dictionary zodiac related information. 15 | 16 | Returns: 17 | A new Zodiac object. 18 | """ 19 | return Zodiac( 20 | name=zodiac_['name'], 21 | start_month=zodiac_['start_month'], 22 | start_day_in_month=zodiac_['start_day_in_month'], 23 | end_month=zodiac_['end_month'], 24 | end_day_in_month=zodiac_['end_day_in_month'], 25 | ) 26 | 27 | 28 | def get_zodiac_of_day(session: Session, date: datetime) -> Zodiac: 29 | """Returns the Zodiac object for the specific day. 30 | 31 | Args: 32 | session: The database connection. 33 | date: The requested date. 34 | 35 | Returns: 36 | A Zodiac object. 37 | """ 38 | first_month_of_sign_filter = and_( 39 | Zodiac.start_month == date.month, 40 | Zodiac.start_day_in_month <= date.day) 41 | 42 | second_month_of_sign_filter = and_( 43 | Zodiac.end_month == date.month, 44 | Zodiac.end_day_in_month >= date.day) 45 | 46 | zodiac = (session.query(Zodiac) 47 | .filter(or_(first_month_of_sign_filter, 48 | second_month_of_sign_filter)) 49 | .first() 50 | ) 51 | 52 | return zodiac 53 | 54 | 55 | # TODO: Call this function from the month view 56 | def get_zodiac_of_month(session: Session, date: datetime) -> Zodiac: 57 | """Returns the Zodiac object for the specific month. 58 | 59 | Args: 60 | session: The database connection. 61 | date: The requested date. 62 | 63 | Returns: 64 | A Zodiac object. 65 | """ 66 | zodiac = (session 67 | .query(Zodiac) 68 | .filter(Zodiac.end_month == date.month) 69 | .first() 70 | ) 71 | return zodiac 72 | -------------------------------------------------------------------------------- /app/locales/en/LC_MESSAGES/base.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/locales/en/LC_MESSAGES/base.mo -------------------------------------------------------------------------------- /app/locales/he/LC_MESSAGES/base.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/locales/he/LC_MESSAGES/base.mo -------------------------------------------------------------------------------- /app/media/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/media/example.png -------------------------------------------------------------------------------- /app/media/free-python-course-4k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/media/free-python-course-4k.png -------------------------------------------------------------------------------- /app/media/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/media/profile.png -------------------------------------------------------------------------------- /app/media/user1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/media/user1.png -------------------------------------------------------------------------------- /app/resources/zodiac.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Capricorn", 4 | "start_month": 12, 5 | "start_day_in_month": 22, 6 | "end_month": 1, 7 | "end_day_in_month": 19 8 | }, 9 | { 10 | "name": "Aquarius", 11 | "start_month": 1, 12 | "start_day_in_month": 20, 13 | "end_month": 2, 14 | "end_day_in_month": 18 15 | }, 16 | { 17 | "name": "Pisces", 18 | "start_month": 2, 19 | "start_day_in_month": 19, 20 | "end_month": 3, 21 | "end_day_in_month": 20 22 | }, 23 | { 24 | "name": "Aries", 25 | "start_month": 3, 26 | "start_day_in_month": 21, 27 | "end_month": 4, 28 | "end_day_in_month": 19 29 | }, 30 | { 31 | "name": "Taurus", 32 | "start_month": 4, 33 | "start_day_in_month": 20, 34 | "end_month": 5, 35 | "end_day_in_month": 20 36 | }, 37 | { 38 | "name": "Gemini", 39 | "start_month": 5, 40 | "start_day_in_month": 21, 41 | "end_month": 6, 42 | "end_day_in_month": 20 43 | }, 44 | { 45 | "name": "Cancer", 46 | "start_month": 6, 47 | "start_day_in_month": 21, 48 | "end_month": 7, 49 | "end_day_in_month": 22 50 | }, 51 | { 52 | "name": "Leo", 53 | "start_month": 7, 54 | "start_day_in_month": 23, 55 | "end_month": 8, 56 | "end_day_in_month": 22 57 | }, 58 | { 59 | "name": "Virgo", 60 | "start_month": 8, 61 | "start_day_in_month": 23, 62 | "end_month": 9, 63 | "end_day_in_month": 22 64 | }, 65 | { 66 | "name": "Libra", 67 | "start_month": 9, 68 | "start_day_in_month": 23, 69 | "end_month": 10, 70 | "end_day_in_month": 22 71 | }, 72 | { 73 | "name": "Scorpio", 74 | "start_month": 10, 75 | "start_day_in_month": 23, 76 | "end_month": 11, 77 | "end_day_in_month": 21 78 | }, 79 | { 80 | "name": "Sagittarius", 81 | "start_month": 11, 82 | "start_day_in_month": 22, 83 | "end_month": 12, 84 | "end_day_in_month": 21 85 | } 86 | ] -------------------------------------------------------------------------------- /app/routers/__init__.py: -------------------------------------------------------------------------------- 1 | import nltk 2 | 3 | nltk.download('punkt') 4 | -------------------------------------------------------------------------------- /app/routers/about_us.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request 2 | 3 | from app.dependencies import templates 4 | 5 | 6 | router = APIRouter() 7 | 8 | 9 | @router.get("/about") 10 | def about(request: Request): 11 | return templates.TemplateResponse("about_us.html", { 12 | "request": request, 13 | }) 14 | -------------------------------------------------------------------------------- /app/routers/agenda.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from datetime import date, timedelta 3 | from typing import Optional, Tuple 4 | 5 | from fastapi import APIRouter, Depends, Request 6 | from sqlalchemy.orm import Session 7 | from starlette.templating import _TemplateResponse 8 | 9 | from app.dependencies import get_db, templates 10 | from app.internal import agenda_events 11 | 12 | router = APIRouter() 13 | 14 | 15 | def calc_dates_range_for_agenda( 16 | start: Optional[date], 17 | end: Optional[date], 18 | days: Optional[int], 19 | ) -> Tuple[date, date]: 20 | """Create start and end dates according to the parameters in the page.""" 21 | if days is not None: 22 | start = date.today() 23 | end = start + timedelta(days=days) 24 | elif start is None or end is None: 25 | start = date.today() 26 | end = date.today() 27 | return start, end 28 | 29 | 30 | @router.get("/agenda", include_in_schema=False) 31 | def agenda( 32 | request: Request, 33 | db: Session = Depends(get_db), 34 | start_date: Optional[date] = None, 35 | end_date: Optional[date] = None, 36 | days: Optional[int] = None, 37 | ) -> _TemplateResponse: 38 | """Route for the agenda page, using dates range or exact amount of days.""" 39 | 40 | user_id = 1 # there is no user session yet, so I use user id- 1. 41 | start_date, end_date = calc_dates_range_for_agenda( 42 | start_date, end_date, days 43 | ) 44 | 45 | events_objects = agenda_events.get_events_per_dates( 46 | db, user_id, start_date, end_date 47 | ) 48 | events = defaultdict(list) 49 | for event_obj in events_objects: 50 | event_duration = agenda_events.get_time_delta_string( 51 | event_obj.start, event_obj.end 52 | ) 53 | events[event_obj.start.date()].append((event_obj, event_duration)) 54 | 55 | return templates.TemplateResponse("agenda.html", { 56 | "request": request, 57 | "events": events, 58 | "start_date": start_date, 59 | "end_date": end_date, 60 | }) 61 | -------------------------------------------------------------------------------- /app/routers/calendar.py: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | 3 | from fastapi import APIRouter, Request 4 | from fastapi.responses import HTMLResponse 5 | from starlette.responses import Response 6 | 7 | from app.dependencies import templates 8 | from app.routers import calendar_grid as cg 9 | 10 | router = APIRouter( 11 | prefix="/calendar/month", 12 | tags=["calendar"], 13 | responses={404: {"description": "Not found"}}, 14 | include_in_schema=False 15 | ) 16 | 17 | 18 | @router.get("/") 19 | async def calendar(request: Request) -> Response: 20 | user_local_time = cg.Day.get_user_local_time() 21 | day = cg.create_day(user_local_time) 22 | return templates.TemplateResponse( 23 | "calendar_monthly_view.html", 24 | { 25 | "request": request, 26 | "day": day, 27 | "week_days": cg.Week.DAYS_OF_THE_WEEK, 28 | "weeks_block": cg.get_month_block(day) 29 | } 30 | ) 31 | 32 | 33 | @router.get("/add/{date}") 34 | async def update_calendar( 35 | request: Request, date: str, days: int 36 | ) -> HTMLResponse: 37 | last_day = cg.Day.convert_str_to_date(date) 38 | next_weeks = cg.create_weeks(cg.get_n_days(last_day, days)) 39 | template = templates.get_template( 40 | 'partials/calendar/monthly_view/add_week.html') 41 | content = template.render(weeks_block=next_weeks) 42 | return HTMLResponse(content=content, status_code=HTTPStatus.OK) 43 | -------------------------------------------------------------------------------- /app/routers/celebrity.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request 2 | from fastapi.responses import Response 3 | 4 | from app.dependencies import templates 5 | from app.internal.celebrity import get_today_month_and_day 6 | 7 | router = APIRouter() 8 | 9 | 10 | @router.get("/celebrity") 11 | def celebrity(request: Request) -> Response: 12 | """Returns the Celebrity page route. 13 | 14 | Args: 15 | request: The HTTP request. 16 | 17 | Returns: 18 | The Celebrity HTML page. 19 | """ 20 | today = get_today_month_and_day() 21 | 22 | return templates.TemplateResponse("celebrity.html", { 23 | "request": request, 24 | "date": today, 25 | }) 26 | -------------------------------------------------------------------------------- /app/routers/credits.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request 2 | import json 3 | from typing import List 4 | 5 | from loguru import logger 6 | from starlette.templating import _TemplateResponse 7 | 8 | from app.config import RESOURCES_DIR 9 | from app.dependencies import templates 10 | 11 | router = APIRouter() 12 | 13 | 14 | def credits_from_json() -> List: 15 | path = RESOURCES_DIR / "credits.json" 16 | try: 17 | with open(path, 'r') as json_file: 18 | json_list = json.load(json_file) 19 | except (IOError, ValueError): 20 | logger.exception( 21 | "An error occurred during reading of json file") 22 | return [] 23 | return json_list 24 | 25 | 26 | @router.get("/credits") 27 | def credits(request: Request) -> _TemplateResponse: 28 | credit_list = credits_from_json() 29 | return templates.TemplateResponse("credits.html", { 30 | "request": request, 31 | "credit_list": credit_list 32 | }) 33 | -------------------------------------------------------------------------------- /app/routers/currency.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from fastapi import APIRouter, Request 4 | 5 | from app.dependencies import templates 6 | 7 | router = APIRouter() 8 | 9 | 10 | # TODO: Add this as a feature to the calendar view/ 11 | # day view/features panel frontend 12 | 13 | 14 | @router.get("/currency") 15 | def today_currency(request: Request): 16 | """Current day currency router""" 17 | 18 | date = datetime.date.today().strftime("%Y-%m-%d") 19 | return currency(request, date) 20 | 21 | 22 | @router.get("/currency/{date}") 23 | def currency(request: Request, date: str): 24 | """Custom date currency router""" 25 | 26 | # TODO: get user default/preferred currency 27 | base = "USD" 28 | 29 | return templates.TemplateResponse("currency.html", { 30 | "request": request, 31 | "base": base, 32 | "date": date 33 | }) 34 | -------------------------------------------------------------------------------- /app/routers/export.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | from io import BytesIO 3 | from typing import Union 4 | 5 | from fastapi import APIRouter, Depends, status 6 | from fastapi.responses import StreamingResponse 7 | from sqlalchemy.orm import Session 8 | 9 | from app.dependencies import get_db 10 | from app.internal.agenda_events import get_events_in_time_frame 11 | from app.internal.export import get_icalendar_with_multiple_events 12 | from app.internal.utils import get_current_user 13 | 14 | router = APIRouter( 15 | prefix="/export", 16 | tags=["export"], 17 | responses={status.HTTP_404_NOT_FOUND: {"description": _("Not found")}}, 18 | ) 19 | 20 | 21 | @router.get("/") 22 | def export( 23 | start_date: Union[date, str], 24 | end_date: Union[date, str], 25 | db: Session = Depends(get_db), 26 | ) -> StreamingResponse: 27 | """Returns the Export page route. 28 | 29 | Args: 30 | start_date: A date or an empty string. 31 | end_date: A date or an empty string. 32 | db: Optional; The database connection. 33 | 34 | Returns: 35 | # TODO add description 36 | """ 37 | # TODO: connect to real user 38 | user = get_current_user(db) 39 | events = get_events_in_time_frame(start_date, end_date, user.id, db) 40 | file = BytesIO(get_icalendar_with_multiple_events(db, list(events))) 41 | return StreamingResponse( 42 | content=file, 43 | media_type="text/calendar", 44 | headers={ 45 | # Change filename to "pylandar.ics". 46 | "Content-Disposition": "attachment;filename=pylandar.ics", 47 | }, 48 | ) 49 | -------------------------------------------------------------------------------- /app/routers/four_o_four.py: -------------------------------------------------------------------------------- 1 | from app.dependencies import templates 2 | from fastapi import APIRouter 3 | from starlette.requests import Request 4 | 5 | router = APIRouter( 6 | prefix="/404", 7 | tags=["404"], 8 | responses={404: {"description": "Not found"}}, 9 | ) 10 | 11 | 12 | @router.get("/") 13 | async def not_implemented(request: Request): 14 | return templates.TemplateResponse("four_o_four.j2", 15 | {"request": request}) 16 | -------------------------------------------------------------------------------- /app/routers/friendview.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, Request 2 | from sqlalchemy.orm import Session 3 | from starlette.templating import _TemplateResponse 4 | from typing import Union 5 | 6 | from app.dependencies import get_db, templates 7 | from app.internal import friend_view 8 | 9 | 10 | router = APIRouter(tags=["friendview"]) 11 | 12 | 13 | @router.get("/friendview") 14 | def friendview( 15 | request: Request, 16 | db: Session = Depends(get_db), 17 | my_friend: Union[str, None] = None, 18 | ) -> _TemplateResponse: 19 | 20 | # TODO: Waiting for user registration 21 | user_id = 1 22 | events_list = friend_view.get_events_per_friend(db, user_id, my_friend) 23 | 24 | return templates.TemplateResponse("friendview.html", { 25 | "request": request, 26 | "events": events_list, 27 | "my_friend": my_friend, 28 | }) 29 | -------------------------------------------------------------------------------- /app/routers/google_connect.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends, APIRouter, Request 2 | from starlette.responses import RedirectResponse 3 | from loguru import logger 4 | 5 | from app.internal.utils import get_current_user 6 | from app.dependencies import get_db 7 | from app.internal.google_connect import get_credentials, fetch_save_events 8 | from app.routers.profile import router as profile 9 | 10 | router = APIRouter( 11 | prefix="/google", 12 | tags=["sync"], 13 | responses={404: {"description": "Not found"}}, 14 | ) 15 | 16 | 17 | @router.get("/sync") 18 | async def google_sync(request: Request, 19 | session=Depends(get_db)) -> RedirectResponse: 20 | '''Sync with Google - if user never synced with google this funcion will take 21 | the user to a consent screen to use his google calendar data with the app. 22 | ''' 23 | 24 | user = get_current_user(session) # getting active user 25 | 26 | # getting valid credentials 27 | credentials = get_credentials(user=user, session=session) 28 | 29 | if credentials is None: 30 | # in case credentials is none, this is mean there isn't a client_secret 31 | logger.error("GoogleSync isn't available - missing client_secret.json") 32 | 33 | # fetch and save the events com from Google Calendar 34 | fetch_save_events(credentials=credentials, user=user, session=session) 35 | 36 | url = profile.url_path_for('profile') 37 | return RedirectResponse(url=url) 38 | -------------------------------------------------------------------------------- /app/routers/login.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union 2 | 3 | from fastapi import APIRouter, Depends, Request 4 | from sqlalchemy.orm import Session 5 | from starlette.responses import RedirectResponse 6 | from starlette.status import HTTP_302_FOUND 7 | 8 | from app.dependencies import get_db, templates 9 | from app.internal.security.ouath2 import ( 10 | authenticate_user, create_jwt_token) 11 | from app.internal.security import schema 12 | 13 | 14 | router = APIRouter( 15 | prefix="", 16 | tags=["/login"], 17 | responses={404: {"description": "Not found"}}, 18 | ) 19 | 20 | 21 | @router.get("/login") 22 | async def login_user_form( 23 | request: Request, message: Optional[str] = "") -> templates: 24 | """rendering login route get method""" 25 | return templates.TemplateResponse("login.html", { 26 | "request": request, 27 | "message": message, 28 | 'current_user': "logged in" 29 | }) 30 | 31 | 32 | @router.post('/login') 33 | async def login( 34 | request: Request, 35 | next: Optional[str] = "/", 36 | db: Session = Depends(get_db), 37 | existing_jwt: Union[str, bool] = False) -> RedirectResponse: 38 | """rendering login route post method.""" 39 | form = await request.form() 40 | form_dict = dict(form) 41 | # creating pydantic schema object out of form data 42 | 43 | user = schema.LoginUser(**form_dict) 44 | """ 45 | Validaiting login form data, 46 | if user exist in database, 47 | if password correct. 48 | """ 49 | if user: 50 | user = await authenticate_user(db, user) 51 | if not user: 52 | return templates.TemplateResponse("login.html", { 53 | "request": request, 54 | "message": 'Please check your credentials' 55 | }) 56 | # creating HTTPONLY cookie with jwt-token out of user unique data 57 | # for testing 58 | if not existing_jwt: 59 | jwt_token = create_jwt_token(user) 60 | else: 61 | jwt_token = existing_jwt 62 | if not next.startswith("/"): 63 | next = "/" 64 | response = RedirectResponse(next, status_code=HTTP_302_FOUND) 65 | response.set_cookie( 66 | "Authorization", 67 | value=jwt_token, 68 | httponly=True, 69 | ) 70 | return response 71 | -------------------------------------------------------------------------------- /app/routers/logout.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request 2 | from starlette.responses import RedirectResponse 3 | from starlette.status import HTTP_302_FOUND 4 | 5 | 6 | router = APIRouter( 7 | prefix="", 8 | tags=["/logout"], 9 | responses={404: {"description": "Not found"}}, 10 | ) 11 | 12 | 13 | @router.get('/logout') 14 | async def logout(request: Request): 15 | response = RedirectResponse(url="/login", status_code=HTTP_302_FOUND) 16 | response.delete_cookie("Authorization") 17 | return response 18 | -------------------------------------------------------------------------------- /app/routers/salary/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/routers/salary/__init__.py -------------------------------------------------------------------------------- /app/routers/salary/config.py: -------------------------------------------------------------------------------- 1 | from datetime import time 2 | from typing import Union 3 | 4 | MINIMUM_WAGE = 29.12 5 | ISRAELI_JEWISH = 1 # Revision required after holiday times feature is added 6 | REGULAR_HOUR_BASIS = 8 7 | FIRST_OVERTIME_AMOUNT = 2 8 | FIRST_OVERTIME_PAY = 1.25 9 | SECOND_OVERTIME_PAY = 1.50 10 | NIGHT_HOUR_BASIS = 7 11 | NIGHT_START = time(hour=22) 12 | NIGHT_END = time(hour=6) 13 | NIGHT_MIN_LEN = time(hour=2) 14 | WEEK_WORKING_HOURS = 42 15 | STANDARD_TRANSPORT = 11.8 16 | MAXIMUM_TRANSPORT = 22.6 17 | 18 | OFF_DAY_ADDITION = 0.5 19 | SATURDAY = 5 20 | HOURS_SECONDS_RATIO = 3600 21 | 22 | NUMERIC = Union[float, int] 23 | HOUR_FORMAT = '%H:%M:%S' 24 | ALT_HOUR_FORMAT = '%H:%M' 25 | -------------------------------------------------------------------------------- /app/routers/search.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, Form, Request 2 | from fastapi.responses import Response 3 | from sqlalchemy.orm import Session 4 | 5 | from app.dependencies import get_db, templates 6 | from app.internal.search import get_results_by_keywords 7 | from app.internal.utils import get_current_user 8 | 9 | router = APIRouter() 10 | 11 | 12 | @router.get("/search", include_in_schema=False) 13 | def search(request: Request, db: Session = Depends(get_db)) -> Response: 14 | """Returns the Search page route. 15 | 16 | Args: 17 | request: The HTTP request. 18 | db: Optional; The database connection. 19 | 20 | Returns: 21 | The Search HTML page. 22 | """ 23 | # TODO: connect to current user 24 | user = get_current_user(db) 25 | 26 | return templates.TemplateResponse("search.html", { 27 | "request": request, 28 | "username": user.username, 29 | }) 30 | 31 | 32 | @router.post("/search", include_in_schema=False) 33 | async def show_results( 34 | request: Request, 35 | keywords: str = Form(None), 36 | db: Session = Depends(get_db), 37 | ) -> Response: 38 | """Returns the Search page route. 39 | 40 | Args: 41 | request: The HTTP request. 42 | keywords: The search keywords. 43 | db: Optional; The database connection. 44 | 45 | Returns: 46 | The Search HTML page. 47 | """ 48 | # TODO: connect to current user 49 | user = get_current_user(db) 50 | 51 | message = "" 52 | if not keywords: 53 | message = _("Invalid request.") 54 | results = None 55 | else: 56 | results = get_results_by_keywords(db, keywords, owner_id=user.id) 57 | if not results: 58 | message = _("No matching results for '{keywords}'.") 59 | message = message.format(keywords=keywords) 60 | 61 | return templates.TemplateResponse("search.html", { 62 | "request": request, 63 | "username": user.username, 64 | "message": message, 65 | "results": results, 66 | "keywords": keywords, 67 | }) 68 | -------------------------------------------------------------------------------- /app/routers/telegram.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Body, Depends 2 | 3 | from app.database.models import User 4 | from app.dependencies import get_db 5 | from app.telegram.handlers import MessageHandler, reply_unknown_user 6 | from app.telegram.models import Chat 7 | 8 | router = APIRouter( 9 | prefix="/telegram", 10 | tags=["telegram"], 11 | responses={404: {"description": "Not found"}}, 12 | ) 13 | 14 | 15 | @router.post("/", include_in_schema=False) 16 | async def bot_client(req: dict = Body(...), session=Depends(get_db)): 17 | chat = Chat(req) 18 | 19 | # Check if current chatter is registered to use the bot 20 | user = session.query(User).filter_by(telegram_id=chat.user_id).first() 21 | if user is None: 22 | return await reply_unknown_user(chat) 23 | 24 | message = MessageHandler(chat, user) 25 | return await message.process_callback() 26 | -------------------------------------------------------------------------------- /app/routers/weekview.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from itertools import accumulate 3 | from typing import Iterator, NamedTuple, Tuple 4 | 5 | from fastapi import APIRouter, Depends, Request 6 | from fastapi.templating import Jinja2Templates 7 | from sqlalchemy.orm.session import Session 8 | 9 | from app.database.models import Event, User 10 | from app.dependencies import get_db, TEMPLATES_PATH 11 | from app.routers.dayview import ( 12 | DivAttributes, dayview, get_events_and_attributes 13 | ) 14 | 15 | 16 | templates = Jinja2Templates(directory=TEMPLATES_PATH) 17 | 18 | 19 | router = APIRouter() 20 | 21 | 22 | class DayEventsAndAttrs(NamedTuple): 23 | day: datetime 24 | template: Jinja2Templates.TemplateResponse 25 | events_and_attrs: Tuple[Event, DivAttributes] 26 | 27 | 28 | def get_week_dates(firstday: datetime) -> Iterator[datetime]: 29 | rest_of_days = [timedelta(days=1) for _ in range(6)] 30 | rest_of_days.insert(0, firstday) 31 | return accumulate(rest_of_days) 32 | 33 | 34 | async def get_day_events_and_attributes( 35 | request: Request, day: datetime, session: Session, user: User, 36 | ) -> DayEventsAndAttrs: 37 | template = await dayview( 38 | request=request, 39 | date=day.strftime('%Y-%m-%d'), 40 | view='week', 41 | session=session 42 | ) 43 | events_and_attrs = get_events_and_attributes( 44 | day=day, session=session, user_id=user.id) 45 | return DayEventsAndAttrs(day, template, events_and_attrs) 46 | 47 | 48 | @router.get('/week/{firstday}') 49 | async def weekview( 50 | request: Request, firstday: str, session=Depends(get_db) 51 | ): 52 | user = session.query(User).filter_by(username='test_username').first() 53 | firstday = datetime.strptime(firstday, '%Y-%m-%d') 54 | week_days = get_week_dates(firstday) 55 | week = [await get_day_events_and_attributes( 56 | request, day, session, user 57 | ) for day in week_days] 58 | return templates.TemplateResponse("weekview.html", { 59 | "request": request, 60 | "week": week, 61 | }) 62 | -------------------------------------------------------------------------------- /app/routers/whatsapp.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from fastapi import APIRouter 4 | from urllib.parse import urlencode 5 | 6 | router = APIRouter(tags=["utils"]) 7 | 8 | 9 | @router.get("/whatsapp") 10 | def make_link(phone_number: Optional[str], message: Optional[str]) -> str: 11 | """Returns a WhatsApp message URL. 12 | 13 | The message URL is built with the phone number and text message. 14 | 15 | Args: 16 | phone_number: Optional; The phone number to send the message to. 17 | message: Optional; The message sent. 18 | 19 | Returns: 20 | A WhatsApp message URL. 21 | """ 22 | api = 'https://api.whatsapp.com/send?' 23 | url_query = {'phone': phone_number, 'text': message} 24 | link = api + urlencode(url_query) 25 | return link 26 | -------------------------------------------------------------------------------- /app/static/about.css: -------------------------------------------------------------------------------- 1 | .moving-words { 2 | margin-top: 4rem; 3 | text-transform: uppercase; 4 | font-size: 6rem; 5 | letter-spacing: 0.1rem; 6 | overflow: hidden; 7 | background: linear-gradient(90deg, #000, #fff, #000); 8 | background-repeat: no-repeat; 9 | background-size: 80%; 10 | animation: animate 4s linear infinite; 11 | -webkit-background-clip: text; 12 | background-clip: text; 13 | -webkit-text-fill-color: rgba(255, 255, 255, 0); 14 | } 15 | 16 | @keyframes animate { 17 | 0% { 18 | background-position: -500%; 19 | } 20 | 100% { 21 | background-position: 500%; 22 | } 23 | } 24 | 25 | 26 | /* credit for the animation goes to FrankieDoodie */ 27 | 28 | .infographic { 29 | display: block; 30 | margin-left: auto; 31 | margin-right: auto; 32 | width: 100%; 33 | } 34 | 35 | .seprator { 36 | margin-top: 8rem; 37 | margin-bottom: 8rem; 38 | } 39 | 40 | .about-text { 41 | font-size: 1.2rem; 42 | text-align: justify; 43 | } -------------------------------------------------------------------------------- /app/static/agenda_style.css: -------------------------------------------------------------------------------- 1 | .exact_date { 2 | display: flex; 3 | } 4 | 5 | #dates { 6 | text-align: center; 7 | } 8 | 9 | .event_line { 10 | width: 80%; 11 | margin-left: 2em; 12 | margin-bottom: 0.5em; 13 | border: 1px solid black; 14 | padding: 0.2em; 15 | border-radius: 5px; 16 | display: grid; 17 | grid-template-columns: 1em 1fr; 18 | grid-gap: 0.6em; 19 | } 20 | 21 | .duration { 22 | font-size: small; 23 | } 24 | 25 | .event-title { 26 | text-decoration: none; 27 | } 28 | 29 | /** Event availability */ 30 | .busy { 31 | background-color: rgb(96, 155, 235) 32 | } 33 | 34 | .free { 35 | background-color: rgba(241, 243, 244, 0.74) 36 | } 37 | -------------------------------------------------------------------------------- /app/static/celebrity.css: -------------------------------------------------------------------------------- 1 | .celebs { 2 | display: flex; 3 | flex-flow: row wrap; 4 | } 5 | 6 | .top-div { 7 | display: flex; 8 | width: 11.250em; 9 | margin: 0.2rem; 10 | text-align: center; 11 | } 12 | 13 | .div-footer { 14 | padding: 0.063em; 15 | text-align: center; 16 | } 17 | 18 | .div-job { 19 | display: inline; 20 | } -------------------------------------------------------------------------------- /app/static/credits_pictures/Adi Faibish.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/credits_pictures/Adi Faibish.PNG -------------------------------------------------------------------------------- /app/static/credits_pictures/Adva Alkalay.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/credits_pictures/Adva Alkalay.PNG -------------------------------------------------------------------------------- /app/static/credits_pictures/Anna Shtirberg.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/credits_pictures/Anna Shtirberg.PNG -------------------------------------------------------------------------------- /app/static/credits_pictures/Aviad Amar.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/credits_pictures/Aviad Amar.PNG -------------------------------------------------------------------------------- /app/static/credits_pictures/Elior Digmi.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/credits_pictures/Elior Digmi.PNG -------------------------------------------------------------------------------- /app/static/credits_pictures/Elor Shoshan.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/credits_pictures/Elor Shoshan.PNG -------------------------------------------------------------------------------- /app/static/credits_pictures/Hagai Kraus.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/credits_pictures/Hagai Kraus.PNG -------------------------------------------------------------------------------- /app/static/credits_pictures/Idan Pelled.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/credits_pictures/Idan Pelled.PNG -------------------------------------------------------------------------------- /app/static/credits_pictures/Nadav Pesach.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/credits_pictures/Nadav Pesach.PNG -------------------------------------------------------------------------------- /app/static/credits_pictures/Nir Perelshtein.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/credits_pictures/Nir Perelshtein.PNG -------------------------------------------------------------------------------- /app/static/credits_pictures/Odelia Yechiel.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/credits_pictures/Odelia Yechiel.PNG -------------------------------------------------------------------------------- /app/static/credits_pictures/Ori Hirshfeld.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/credits_pictures/Ori Hirshfeld.PNG -------------------------------------------------------------------------------- /app/static/credits_pictures/RonHuberfeld.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/credits_pictures/RonHuberfeld.PNG -------------------------------------------------------------------------------- /app/static/credits_pictures/Sagi Zaid Or.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/credits_pictures/Sagi Zaid Or.PNG -------------------------------------------------------------------------------- /app/static/credits_pictures/Yaakov Fogel.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/credits_pictures/Yaakov Fogel.PNG -------------------------------------------------------------------------------- /app/static/credits_pictures/Yam Mesicka.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/credits_pictures/Yam Mesicka.PNG -------------------------------------------------------------------------------- /app/static/credits_pictures/YuvalCagan.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/credits_pictures/YuvalCagan.PNG -------------------------------------------------------------------------------- /app/static/credits_pictures/Zohar Yamin.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/credits_pictures/Zohar Yamin.PNG -------------------------------------------------------------------------------- /app/static/credits_pictures/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/credits_pictures/profile.png -------------------------------------------------------------------------------- /app/static/credits_style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin-left: 6.25em; 3 | margin-right: 6.25em; 4 | } 5 | 6 | div.gallery { 7 | margin: 1.875em; 8 | border: 0.0625em solid #ccc; 9 | float: left; 10 | width: 21.875em; 11 | height: 31.25em; 12 | } 13 | 14 | div.gallery:hover { 15 | border: 0.0625em solid #777; 16 | } 17 | 18 | div.gallery img { 19 | width: 100%; 20 | height: 23.75em; 21 | } 22 | 23 | div.credit-details { 24 | padding: 1em; 25 | text-align: center; 26 | } 27 | 28 | div.a.contact { 29 | padding: 1em; 30 | text-align: center; 31 | } 32 | 33 | p { 34 | color: rgb(13, 110, 253); 35 | } 36 | -------------------------------------------------------------------------------- /app/static/currency.css: -------------------------------------------------------------------------------- 1 | div[data-visible='0'] { 2 | display: none; 3 | } 4 | 5 | div[data-visible='1'] { 6 | display: block; 7 | } -------------------------------------------------------------------------------- /app/static/event/eventedit.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | min-width: min-content; 5 | max-width: -moz-available; 6 | max-width: -webkit-fill-available; 7 | max-width: fill-available; 8 | } 9 | 10 | body { 11 | display: flex; 12 | flex-direction: column; 13 | } 14 | 15 | #event_edit_tabs { 16 | flex: 1; 17 | } 18 | 19 | .tab-pane { 20 | height: 100%; 21 | display: flex; 22 | flex-direction: column; 23 | } 24 | 25 | form { 26 | display: flex; 27 | flex-direction: column; 28 | } 29 | 30 | .form_row, 31 | .form_row_start, 32 | .form_row_end { 33 | display: flex 34 | } 35 | 36 | .form_row_start, 37 | .form_row_end { 38 | flex: 1; 39 | } 40 | 41 | .form_row_end { 42 | justify-content: flex-end; 43 | } 44 | 45 | .form_row { 46 | flex: 1; 47 | min-height: 2.25em; 48 | max-height: 3.25em; 49 | } 50 | 51 | .form_row.textarea { 52 | flex: 4; 53 | max-height: 19em; 54 | } 55 | 56 | input[type="text"], 57 | input[type="date"], 58 | textarea, 59 | .form_row_start { 60 | flex: 1; 61 | } 62 | 63 | input[type="submit"] { 64 | width: 100%; 65 | } -------------------------------------------------------------------------------- /app/static/event/eventview.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | min-width: min-content; 5 | max-width: -moz-available; 6 | max-width: -webkit-fill-available; 7 | max-width: fill-available; 8 | } 9 | 10 | body { 11 | display: flex; 12 | flex-direction: column; 13 | } 14 | 15 | .event_view_wrapper { 16 | display: flex; 17 | flex-direction: column; 18 | height: 100%; 19 | } 20 | 21 | #event_view_tabs { 22 | flex: 1; 23 | } 24 | 25 | .tab-pane { 26 | height: 100%; 27 | display: flex; 28 | flex-direction: column; 29 | } 30 | 31 | .event_info_row, 32 | .event_info_row_start, 33 | .event_info_row_end { 34 | display: flex 35 | } 36 | 37 | .event_info_row_start, 38 | .event_info_row_end { 39 | flex: 1; 40 | } 41 | 42 | .event_info_row_end { 43 | justify-content: flex-end; 44 | } 45 | 46 | div.event_info_row, 47 | .event_info_buttons_row { 48 | align-items: center; 49 | margin-block-start: 0.2em; 50 | margin-block-end: 0.2em; 51 | } 52 | 53 | .title { 54 | border-bottom: 4px solid blue; 55 | } 56 | 57 | .title h1 { 58 | white-space: nowrap; 59 | margin-block-start: 0.2em; 60 | margin-block-end: 0.2em; 61 | padding-right: 0.5em; 62 | } 63 | 64 | .icon { 65 | padding-right: 1em; 66 | } 67 | 68 | .event_info_buttons_row { 69 | min-height: 2.25em; 70 | max-height: 3.25em; 71 | } 72 | 73 | button { 74 | height: 100%; 75 | } -------------------------------------------------------------------------------- /app/static/event_flairs/birthday.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/birthday.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/christmas.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/christmas.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/coffee.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/coffee.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/concert.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/concert.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/cycle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/cycle.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/dentist.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/dentist.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/drank.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/drank.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/food.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/food.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/golf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/golf.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/graduate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/graduate.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/gym.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/gym.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/haircut.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/haircut.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/halloween.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/halloween.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/hike.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/hike.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/kayak.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/kayak.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/manicure.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/manicure.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/massage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/massage.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/music.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/music.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/pill.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/pill.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/pingpong.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/pingpong.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/plan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/plan.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/pokemon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/pokemon.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/ran.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/ran.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/read.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/read.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/repair.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/repair.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/sail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/sail.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/santa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/santa.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/ski.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/ski.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/soccer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/soccer.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/swam.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/swam.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/tennis.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/tennis.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/thanksgiving.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/thanksgiving.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/wed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/wed.jpg -------------------------------------------------------------------------------- /app/static/event_flairs/yoga.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/event_flairs/yoga.jpg -------------------------------------------------------------------------------- /app/static/eventdisplay.js: -------------------------------------------------------------------------------- 1 | function scrollTo(element) { 2 | window.scroll({ 3 | behavior: 'smooth', 4 | left: 0, 5 | top: element.offsetTop 6 | }); 7 | } 8 | 9 | document.addEventListener('DOMContentLoaded', () => { 10 | const chosen_button = document.getElementsByClassName('event-btn'); 11 | for (let i = 0; i < chosen_button.length; i++) { 12 | chosen_button[i].addEventListener("click", function(e) { 13 | let clickedElem = e.target.id 14 | fetch('/event/edit') 15 | .then(function(response) { 16 | return response.text(); 17 | }) 18 | .then(function(body) { 19 | document.querySelector('#event_block').innerHTML = body; 20 | scrollTo(document.getElementById("event_block")); 21 | }); 22 | }); 23 | } 24 | }); -------------------------------------------------------------------------------- /app/static/friendview.css: -------------------------------------------------------------------------------- 1 | .friend-view { 2 | display: block; 3 | margin-top: 1em; 4 | margin-left: 1em; 5 | } 6 | 7 | 8 | .event-line { 9 | margin-left: 3em; 10 | margin-right: 3em; 11 | margin-bottom: 1em; 12 | border: 0.2em solid black; 13 | padding: 0.2em; 14 | } 15 | -------------------------------------------------------------------------------- /app/static/global.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Texts */ 3 | --text_xxs: 0.5rem; /* 8px */ 4 | --text_xs: 0.75rem; /* 12px */ 5 | --text_s: 1rem; /* 16px */ 6 | --text_m: 1.2rem; /* 19px */ 7 | --text_l: 1.5rem; /* 24px */ 8 | --text_xl: 1.75rem; /* 28px */ 9 | --text_xxl: 2rem; /* 32px */ 10 | 11 | /* Spaces */ 12 | --space_xxs: 0.25rem; /* 4px */ 13 | --space_xs: 0.5rem; /* 8px */ 14 | --space_s: 0.75rem; /* 12px */ 15 | --space_m: 1.25rem; /* 20px */ 16 | --space_l: 2rem; /* 32px */ 17 | --space_xl: 4rem; /* 64px */ 18 | 19 | /* colors */ 20 | --primary: #24396a; 21 | --primary-variant: #041E51; 22 | --secondary: #fbc44a; 23 | --secondary-variant: #ffa201; 24 | --background: #F7F7F7; 25 | --surface: #e6e6e6; /* borders */ 26 | --surface-variant: #d2d1d1; /*up_surface, borders-variant*/ 27 | --negative: #f24726; 28 | --negative-variant: #e83305; 29 | --positive: #4ca486; 30 | --positive-variant: #008c73; 31 | 32 | --on-primary: #ffffff; 33 | --on-secondary: #000000; 34 | --on-background: #000000; 35 | --on-surface: #000000; 36 | --on-negative: #ffffff; 37 | --on-positive: #ffffff; 38 | } 39 | 40 | * { 41 | margin: 0; 42 | padding: 0; 43 | box-sizing: border-box; 44 | } 45 | 46 | html, 47 | body { 48 | height: 100%; 49 | } 50 | 51 | body { 52 | background-color: #F7F7F7; 53 | color: #222831; 54 | font-family: "Assistant", "Ariel", sans-serif; 55 | font-weight: 400; 56 | line-height: 1.7; 57 | text-rendering: optimizeLegibility; 58 | scroll-behavior: smooth; 59 | width: 100%; 60 | } 61 | 62 | a { 63 | text-decoration: none; 64 | color: inherit; 65 | } -------------------------------------------------------------------------------- /app/static/horoscope.js: -------------------------------------------------------------------------------- 1 | function sendSignDescription(singName) { 2 | const sign = singName.toLowerCase(); 3 | const signData = "https://aztro.sameerkumar.website/?sign=" + sign + 4 | "&day=today"; 5 | const xhr = new XMLHttpRequest(); 6 | xhr.open("POST", signData, true); 7 | xhr.onload = function () { 8 | let jsonObject = JSON.parse(this.responseText); 9 | let element = document.getElementById("daily_horoscope"); 10 | let str = jsonObject.description; 11 | element.innerHTML = str; 12 | }; 13 | xhr.send(); 14 | } 15 | 16 | 17 | function addEventsAfterPageLoaded() { 18 | const elements = document.getElementsByClassName("sign"); 19 | Array.from(elements).forEach((element) => { 20 | let singName = element.name; 21 | element.addEventListener("click", function () { 22 | sendSignDescription(singName); 23 | }, false); 24 | }); 25 | } 26 | 27 | document.addEventListener("DOMContentLoaded", addEventsAfterPageLoaded); -------------------------------------------------------------------------------- /app/static/images/calendar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/static/images/calendar.jpg -------------------------------------------------------------------------------- /app/static/images/icons/calendar-outline.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns='http://www.w3.org/2000/svg' class='ionicon' viewBox='0 0 512 512'><title>Calendar -------------------------------------------------------------------------------- /app/static/images/icons/close_sidebar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/static/images/icons/pencil.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/static/images/icons/plus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/static/images/icons/trash-can.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/static/images/zodiac/Aquarius.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 29 | -------------------------------------------------------------------------------- /app/static/images/zodiac/Aries.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 29 | -------------------------------------------------------------------------------- /app/static/images/zodiac/Cancer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/static/images/zodiac/Capricorn.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/static/images/zodiac/Gemini.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 29 | -------------------------------------------------------------------------------- /app/static/images/zodiac/Leo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/static/images/zodiac/Libra.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 29 | -------------------------------------------------------------------------------- /app/static/images/zodiac/Pisces.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /app/static/images/zodiac/Sagittarius.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/static/images/zodiac/Scorpio.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 29 | -------------------------------------------------------------------------------- /app/static/images/zodiac/Taurus.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 29 | -------------------------------------------------------------------------------- /app/static/images/zodiac/Virgo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 29 | -------------------------------------------------------------------------------- /app/static/popover.js: -------------------------------------------------------------------------------- 1 | // Enable bootstrap popovers 2 | 3 | var popoverList = popoverTriggerList.map(function (popoverTriggerEl) { 4 | return new bootstrap.Popover(popoverTriggerEl, { 5 | container: 'body', 6 | html: true, 7 | sanitize: false 8 | }) 9 | }); 10 | -------------------------------------------------------------------------------- /app/static/share_event.css: -------------------------------------------------------------------------------- 1 | .msg_title{ 2 | font-size: 1.5rem; 3 | } 4 | 5 | .share_event{ 6 | width: 30rem; 7 | } 8 | 9 | .card-subtitle{ 10 | font-weight: 600; 11 | } -------------------------------------------------------------------------------- /app/static/style.css: -------------------------------------------------------------------------------- 1 | .profile-image { 2 | width: 7em; 3 | } 4 | 5 | .card-profile { 6 | border-radius: 10px; 7 | } 8 | 9 | .card-event { 10 | border-radius: 10px; 11 | } 12 | 13 | .card-event:hover { 14 | transform: scale(1.02); 15 | } 16 | 17 | .top-line { 18 | height: 0.25rem; 19 | } 20 | 21 | .bg-gradient2 { 22 | background: linear-gradient( 23 | 135deg, 24 | rgba(0, 97, 215, 1) 0%, 25 | rgba(0, 200, 255, 1) 100% 26 | ); 27 | } 28 | 29 | h2, 30 | p { 31 | display: flex; 32 | align-items: center; 33 | justify-content: space-around; 34 | } 35 | 36 | #inner:hover { 37 | cursor: pointer; 38 | padding: 50px; 39 | background-color: linear-gradient( 40 | 135deg, 41 | rgba(0, 97, 215, 1) 0%, 42 | rgba(0, 200, 255, 1) 100% 43 | ); 44 | } 45 | 46 | #inner { 47 | transition: background 0.2s ease, padding 0.8s linear; 48 | } 49 | 50 | .landing-page-button { 51 | border-radius: 9999px; 52 | justify-content: center; 53 | } 54 | 55 | .event-posted-time { 56 | font-size: 0.7rem; 57 | } 58 | 59 | .no-border { 60 | border: none; 61 | } 62 | 63 | .profile-modal-fadeIn { 64 | -webkit-animation-name: profile-modal-fadeIn; 65 | animation-name: profile-modal-fadeIn; 66 | 67 | -webkit-animation-duration: 1s; 68 | animation-duration: 1s; 69 | -webkit-animation-fill-mode: both; 70 | animation-fill-mode: both; 71 | } 72 | 73 | @keyframes profile-modal-fadeIn { 74 | from { 75 | opacity: 0; 76 | -webkit-transform: translate3d(-100%, 0, 0); 77 | transform: translate3d(-100%, 0, 0); 78 | } 79 | 80 | to { 81 | opacity: 1; 82 | -webkit-transform: translate3d(0, 0, 0); 83 | transform: translate3d(0, 0, 0); 84 | } 85 | } 86 | 87 | .profile-modal-dialog { 88 | margin: 0; 89 | } 90 | 91 | .card-body { 92 | overflow: auto; 93 | } 94 | 95 | .profile-modal-header { 96 | border: none; 97 | background-color: whitesmoke; 98 | } 99 | 100 | .error-message { 101 | line-height: 0; 102 | color: red; 103 | padding-left: 12.5rem; 104 | } 105 | 106 | .subtitle { 107 | font-size: 1.25rem; 108 | } 109 | 110 | .error-message { 111 | line-height: 0; 112 | color: red; 113 | padding-left: 12.5rem; 114 | margin-bottom: 1em; 115 | } 116 | 117 | .input-upload-file { 118 | margin-top: 1em; 119 | } 120 | 121 | .upload-file { 122 | margin: auto 1em auto 0em; 123 | } 124 | 125 | h2.modal-title { 126 | font-size: 1.25rem; 127 | } 128 | -------------------------------------------------------------------------------- /app/static/text_editor.js: -------------------------------------------------------------------------------- 1 | let timerInterval; 2 | const {colorSyntax} = toastui.Editor.plugin; 3 | const editor = new toastui.Editor({ 4 | el: document.querySelector('#editorSection'), 5 | initialEditType: 'wysiwyg', 6 | previewStyle: 'vertical', 7 | height: 'auto', 8 | plugins: [colorSyntax], 9 | useDefaultHTMLSanitizer: true, 10 | toolbarItems: [ 11 | 'heading', 12 | 'bold', 13 | 'italic', 14 | 'strike', 15 | 'divider', 16 | 'hr', 17 | 'quote', 18 | 'divider', 19 | 'ul', 20 | 'ol', 21 | 'task', 22 | 'indent', 23 | 'outdent', 24 | 'divider', 25 | 'table', 26 | 'link', 27 | 'divider', 28 | 'code', 29 | 'codeblock' 30 | ] 31 | 32 | }); 33 | document.getElementById("but").addEventListener("click", function () { 34 | fetch(`${window.origin}`, { 35 | method: "POST", 36 | headers: {'Content-Type': 'application/json'}, 37 | body: JSON.stringify({"datahtml": editor.getHtml(), 'datamd': editor.getMarkdown()}) 38 | }).then(response => { 39 | if (editor.getMarkdown().length >= 1) 40 | Swal.fire({ 41 | title: 'Working on it!', 42 | html: 'Data is being transfered to the server!', 43 | timer: 1000, 44 | timerProgressBar: true, 45 | didOpen: () => { 46 | Swal.showLoading() 47 | timerInterval = setInterval(() => { 48 | const content = Swal.getContent() 49 | }, 1) 50 | }, 51 | willClose: () => { 52 | clearInterval(timerInterval) 53 | } 54 | }) 55 | }); 56 | 57 | 58 | }); -------------------------------------------------------------------------------- /app/static/weekview.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary:#30465D; 3 | --primary-variant:#FFDE4D; 4 | --secondary:#EF5454; 5 | --borders:#E7E7E7; 6 | --borders-variant:#F7F7F7; 7 | } 8 | 9 | .day-weekview { 10 | border-left: 1px solid var(--borders); 11 | width: 100%; 12 | } 13 | 14 | #week-view { 15 | display: grid; 16 | grid-template-rows: 1fr; 17 | grid-template-columns: 2.3em 1fr; 18 | } 19 | 20 | 21 | #week-schedule { 22 | grid-row: 1; 23 | grid-column: 2; 24 | z-index: 10; 25 | } 26 | 27 | #hoursgrid { 28 | grid-row: 1; 29 | grid-column: 1; 30 | flex: 1; 31 | margin-top: 4em; 32 | margin-left: 0.8em; 33 | z-index: 30; 34 | } 35 | 36 | .hour-top { 37 | color: white; 38 | width: 2.4em; 39 | margin-left: -2; 40 | overflow: hidden; 41 | } 42 | -------------------------------------------------------------------------------- /app/telegram/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/telegram/__init__.py -------------------------------------------------------------------------------- /app/telegram/bot.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from app import config 4 | from app.dependencies import get_settings 5 | from .models import Bot 6 | 7 | settings: config.Settings = get_settings() 8 | 9 | BOT_API = settings.bot_api 10 | WEBHOOK_URL = settings.webhook_url 11 | 12 | telegram_bot = Bot(BOT_API, WEBHOOK_URL) 13 | 14 | loop = asyncio.get_event_loop() 15 | asyncio.set_event_loop(loop) 16 | asyncio.ensure_future(telegram_bot.set_webhook()) 17 | -------------------------------------------------------------------------------- /app/telegram/keyboards.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | from typing import Any, Dict, List 4 | 5 | show_events_buttons = [ 6 | [ 7 | {'text': 'Today', 'callback_data': 'Today'}, 8 | {'text': 'This week', 'callback_data': 'This week'} 9 | ] 10 | ] 11 | 12 | new_event_buttons = [ 13 | [ 14 | {'text': 'Create ✅', 'callback_data': 'create'}, 15 | {'text': 'Cancel 🚫', 'callback_data': 'cancel'} 16 | ] 17 | ] 18 | 19 | field_buttons = [ 20 | [ 21 | {'text': 'Restart 🚀', 'callback_data': 'restart'}, 22 | {'text': 'Cancel 🚫', 'callback_data': 'cancel'} 23 | ] 24 | ] 25 | 26 | DATE_FORMAT = '%d %b %Y' 27 | 28 | 29 | def get_this_week_buttons() -> List[List[Any]]: 30 | today = datetime.datetime.today() 31 | buttons = [] 32 | for day in range(1, 7): 33 | day = today + datetime.timedelta(days=day) 34 | buttons.append(day.strftime(DATE_FORMAT)) 35 | 36 | return [ 37 | [ 38 | { 39 | 'text': buttons[0], 40 | 'callback_data': buttons[0], 41 | }, 42 | { 43 | 'text': buttons[1], 44 | 'callback_data': buttons[1], 45 | }, 46 | { 47 | 'text': buttons[2], 48 | 'callback_data': buttons[2], 49 | }, 50 | ], 51 | [ 52 | { 53 | 'text': buttons[3], 54 | 'callback_data': buttons[3], 55 | }, 56 | { 57 | 'text': buttons[4], 58 | 'callback_data': buttons[4], 59 | }, 60 | { 61 | 'text': buttons[5], 62 | 'callback_data': buttons[5], 63 | }, 64 | ], 65 | ] 66 | 67 | 68 | def gen_inline_keyboard(buttons: List[List[Any]]) -> Dict[str, Any]: 69 | return {'reply_markup': json.dumps({'inline_keyboard': buttons})} 70 | 71 | 72 | show_events_kb = gen_inline_keyboard(show_events_buttons) 73 | new_event_kb = gen_inline_keyboard(new_event_buttons) 74 | field_kb = gen_inline_keyboard(field_buttons) 75 | -------------------------------------------------------------------------------- /app/telegram/models.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional 2 | 3 | from httpx import AsyncClient 4 | 5 | 6 | class Chat: 7 | def __init__(self, data: Dict): 8 | self.message = self._get_message_content(data) 9 | self.user_id = self._get_user_id(data) 10 | self.first_name = self._get_first_name(data) 11 | 12 | def _get_message_content(self, data: Dict) -> str: 13 | if 'callback_query' in data: 14 | return data['callback_query']['data'] 15 | return data['message']['text'] 16 | 17 | def _get_user_id(self, data: Dict) -> str: 18 | if 'callback_query' in data: 19 | return data['callback_query']['from']['id'] 20 | return data['message']['from']['id'] 21 | 22 | def _get_first_name(self, data: Dict) -> str: 23 | if 'callback_query' in data: 24 | return data['callback_query']['from']['first_name'] 25 | return data['message']['from']['first_name'] 26 | 27 | 28 | class Bot: 29 | MEMORY = {} 30 | 31 | def __init__(self, bot_api: str, webhook_url: str): 32 | self.base = self._set_base_url(bot_api) 33 | self.webhook_setter_url = self._set_webhook_setter_url(webhook_url) 34 | 35 | def _set_base_url(self, bot_api: str) -> str: 36 | return f'https://api.telegram.org/bot{bot_api}/' 37 | 38 | def _set_webhook_setter_url(self, webhook_url: str) -> str: 39 | return f'{self.base}setWebhook?url={webhook_url}/telegram/' 40 | 41 | async def set_webhook(self): 42 | async with AsyncClient() as ac: 43 | return await ac.get(self.webhook_setter_url) 44 | 45 | async def drop_webhook(self): 46 | async with AsyncClient() as ac: 47 | data = {'drop_pending_updates': True} 48 | return await ac.post(url=f'{self.base}deleteWebhook', data=data) 49 | 50 | async def send_message( 51 | self, chat_id: str, 52 | text: str, 53 | reply_markup: Optional[Dict[str, Any]] = None): 54 | async with AsyncClient(base_url=self.base) as ac: 55 | message = { 56 | 'chat_id': chat_id, 57 | 'text': text} 58 | if reply_markup: 59 | message.update(reply_markup) 60 | return await ac.post('sendMessage', data=message) 61 | -------------------------------------------------------------------------------- /app/templates/calendar/add_week.html: -------------------------------------------------------------------------------- 1 | {% for week in weeks_block %} 2 |
3 | {% for day in week.days %} 4 |
5 |
6 |
{{day}}
7 | 8 |
9 | {% for devent in day.dailyevents %} 10 |
11 |
{{devent[0]}}
12 |
{{devent[1]}}
13 |
14 | {% endfor %} 15 | {% for event in day.events %} 16 |
{{event[0]}} {{event[1]}}
17 | {% endfor %} 18 |
19 | {% endfor %} 20 |
21 | {% endfor %} -------------------------------------------------------------------------------- /app/templates/calendar/calendar.html: -------------------------------------------------------------------------------- 1 | {% extends 'calendar/layout.html' %} 2 | 3 | {% block main %} 4 |
5 |
6 | {% for d in week_days %} 7 | {% if d == day.sday %} 8 |
{{ d.upper() }}
9 | {% else %} 10 |
{{ d.upper() }}
11 | {% endif %} 12 | {% endfor %} 13 |
14 |
15 | {% include 'calendar/add_week.html' %} 16 |
17 |
18 |
19 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/calendar_monthly_view.html: -------------------------------------------------------------------------------- 1 | {% extends "partials/calendar/calendar_base.html" %} 2 | {% block content %} 3 |
4 |
5 |
{{ day.display() }}
6 |
Location 0oc 00:00
7 |
8 | 13 |
14 |
15 | {% include 'partials/calendar/monthly_view/monthly_grid.html' %} 16 |
17 | {% endblock content %} -------------------------------------------------------------------------------- /app/templates/celebrity.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |
6 |
7 |
Celebrities born today ({{ date }}):
8 |
9 |
10 |
11 |
12 | 13 |
14 |
15 |
16 |
17 | 18 | 19 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/credits.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | 6 | {% endblock %} 7 | 8 | {% block content %} 9 |

Say hello to our developers:

10 | {% for credit in credit_list %} 11 | 22 | {% endfor %} 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /app/templates/currency.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | currency 11 | 12 | 13 | 14 |
15 |

16 | 20 |

21 |
22 |
23 |
    24 | 25 |
26 |
27 |
28 |
29 |
30 | 31 | 40 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/templates/demo/home_email.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |
6 |

{{ message }}

7 |
8 | 9 |
10 |
11 | 14 |
15 |
16 |
17 | 18 |
19 | 25 |
26 | 27 |
28 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/eventedit.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 17 |
18 |
19 | {% include "partials/calendar/event/edit_event_details_tab.html" %} 20 |
21 | 22 | 25 |
26 |
27 | 28 |
29 |
30 | 31 | -------------------------------------------------------------------------------- /app/templates/eventview.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 22 | 23 |
24 |
25 | {% include "partials/calendar/event/view_event_details_tab.html" %} 26 |
27 |
28 | {% include 'partials/calendar/event/comments_tab.html' %} 29 |
30 | 31 | 34 |
35 |
36 | -------------------------------------------------------------------------------- /app/templates/four_o_four.j2: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} - 404{% endblock %} 4 | 5 | {% block content %} 6 | 7 |
8 |

9 | Not implemented

10 |
11 | 12 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/friendview.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head %} 3 | {{ super() }} 4 | 5 | {% endblock %} 6 | {% block content %} 7 |
8 |

Friends View

9 |
10 |
11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | {% if events | length == 0 %} 19 |

No mutual events found...

20 | {% else %} 21 |

Mutual events with {{ my_friend }}

22 | {% for event in events %} 23 |
24 |
{{ event.start.strftime("%d/%m/%Y %H:%M") }} - {{ event.end.strftime("%d/%m/%Y %H:%M") }} {{ event.title }}
Invitees: {{event.invitees}}
25 |
26 | {% endfor %} 27 | {% endif %} 28 |
29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /app/templates/hello.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |
6 |

{{ message }}

7 |
8 | 9 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "partials/index/index_base.html" %} 2 | 3 | {% block content %} 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 |
12 | {% if quote %} 13 | {% if not quote.author %} 14 |

"{{ quote.text }}"

15 | {% else %} 16 |

"{{ quote.text }}"   \ {{ quote.author }}

17 | {% endif %} 18 | {% endif %} 19 |
20 | 21 | 22 |
23 | {% if quote %} 24 | {% if not quote.author%} 25 |

"{{ quote.text }}"

26 | {% else %} 27 |

"{{ quote.text }}"   \ {{quote.author}}

28 | {% endif %} 29 | {% endif %} 30 |
31 | 32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /app/templates/import_holidays.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block content %} 5 | 6 |
7 |

Import holidays using ics file

8 | Check this website to export holidays file by country 9 |
10 | 11 | 12 |
13 |
14 | 15 |
16 |
17 | 18 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |
6 |
7 |
8 |

9 | PyLendar 10 |
11 |

12 |

13 | Open Source Calendar built with Python 14 |

15 |
16 |
17 | 18 | {% if quote %} 19 | {% if not quote.author%} 20 |

"{{ quote.text }}"

21 | {% else %} 22 |

"{{ quote.text }}"   \ {{quote.author}} 23 |

24 | {% endif %} 25 | {% endif %} 26 |
27 |
28 |
29 | 36 |
37 |
38 |
39 |
40 | calendar image 41 |
42 |
43 | 44 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/invitations.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block content %} 5 | 6 |
7 |

{{ message }}

8 |
9 | 10 | {% if invitations %} 11 |
12 | {% for i in invitations %} 13 |
14 | {{ i.event.owner.username }} - {{ i.event.title }} ({{ i.event.start }}) ({{ i.status }}) 15 | 16 | 17 |
18 | {% endfor %} 19 |
20 | {% else %} 21 | You don't have any invitations. 22 | {% endif %} 23 | 24 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/invite_mail.html: -------------------------------------------------------------------------------- 1 | {% extends "mail_base.html" %} 2 | 3 | 4 | {% block content %} 5 | 6 |
7 |

Hi, {{ recipient }}!

8 |

{{ sender }} invites you to join {{ site_name }}.

9 |

if you want to join please click on the link

10 |

if you want to get acquainted with our application please click on the link

11 |

This email has been sent to {{ recipient_mail }}.

12 |

If you think it was sent in error, please ignore it

13 |
14 | 15 | 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /app/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 |
4 |

Login

5 | {% if message %} 6 |
{{ message }}
7 | {% endif %} 8 |
9 |
10 | 11 |
12 |
13 |
14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 | 22 |
23 |
24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /app/templates/on_this_day.html: -------------------------------------------------------------------------------- 1 |
2 |

On This Day

3 |
    4 | {% for event in on_this_day_data.events[1::-1] %} 5 |
  • 6 | ({{ event.year }}) {{ event.description }} 7 | > 8 |
  • 9 | {% endfor %} 10 |
11 | Explore More ... 12 |
13 | -------------------------------------------------------------------------------- /app/templates/partials/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block head %} 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 27 | 30 | 31 | {% endblock head %} 32 | {% block title %} 33 | Pylendar{% if self.page_name() %} - {% endif %}{% block page_name %}{% endblock %} 34 | {% endblock %} 35 | 36 | 37 | {% block body %} 38 | {% endblock %} 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/templates/partials/calendar/calendar_base.html: -------------------------------------------------------------------------------- 1 | {% extends "./partials/base.html" %} 2 | {% block head %} 3 | {{super()}} 4 | 5 | 6 | 7 | 8 | {% endblock head %} 9 | {% block page_name %}Month View{% endblock page_name %} 10 | {% block body %} 11 |
12 | {% include 'partials/calendar/navigation.html' %} 13 |
14 | {% include 'partials/calendar/feature_settings/example.html' %} 15 |
16 |
17 | {% block content %} 18 | {% endblock content %} 19 |
20 |
21 | 22 | 23 | 24 | 25 | {% endblock body %} -------------------------------------------------------------------------------- /app/templates/partials/calendar/event/comments_tab.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 | {% for comment in comments %} 7 |
8 |
9 | Profile image 10 |
11 | 12 | 13 | 14 |
15 |

{{ comment['username'] }}

16 |

{{ comment['time'] }}

17 |
18 |
19 | {{ comment['content'] }} 20 |
21 |
22 | {% endfor %} -------------------------------------------------------------------------------- /app/templates/partials/calendar/event/text_editor_partial_body.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /app/templates/partials/calendar/event/text_editor_partial_head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/templates/partials/calendar/event/view_event_details_tab.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ event.title }}

4 |
5 |
6 | 7 | 8 |
9 |
10 |
11 | ICON 12 | 13 | {% if end_format != "" %} 14 | - 15 | 16 | {% endif %} 17 |
{{ 'Busy' if event.availability == True else 'Free' }}
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | ICON 27 |
{{ event.location }}
28 |
VC linkVC URL
29 |
30 | {% if event.invitees %} 31 |
32 |
33 | 34 | 39 | 40 |
41 | {% endif %} 42 |

43 | {{event.owner.username}} 44 |

45 | 46 |
47 | 48 | 49 | 50 |
51 | -------------------------------------------------------------------------------- /app/templates/partials/calendar/feature_settings/example.html: -------------------------------------------------------------------------------- 1 |
FEATURE NAME
-------------------------------------------------------------------------------- /app/templates/partials/calendar/monthly_view/add_week.html: -------------------------------------------------------------------------------- 1 | {% for week in weeks_block %} 2 |
3 | {% for day in week.days %} 4 |
5 |
6 |
{{ day }}
7 | 8 |
9 | {% for devent in day.dailyevents %} 10 |
11 |
{{ devent[0] }}
12 |
{{ devent[1] }}
13 |
14 | {% endfor %} 15 | {% for event in day.events %} 16 |
{{ event[0] }} {{ event[1] }}
17 | {% endfor %} 18 |
19 | {% endfor %} 20 |
21 | {% endfor %} -------------------------------------------------------------------------------- /app/templates/partials/calendar/monthly_view/monthly_grid.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% for d in week_days %} 4 | {% if d == day.sday %} 5 |
{{ d.upper() }}
6 | {% else %} 7 |
{{ d.upper() }}
8 | {% endif %} 9 | {% endfor %} 10 |
11 |
12 | {% include 'partials/calendar/monthly_view/add_week.html' %} 13 |
14 |
15 |
16 |
17 |
18 | 19 | TODAY 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /app/templates/partials/calendar/navigation.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/templates/partials/index/index_base.html: -------------------------------------------------------------------------------- 1 | {% extends "partials/base.html" %} 2 | {% block head %} 3 | {{ super() }} 4 | 5 | 6 | {% endblock head %} 7 | {% block body %} 8 | {% include 'partials/index/navigation.html' %} 9 | 10 | {% block content %} 11 | {% endblock %} 12 | 13 | {% endblock body %} -------------------------------------------------------------------------------- /app/templates/partials/index/navigation.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/templates/salary/month.j2: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |
6 |

{{ category }}

7 |

Monthly Salary

8 |
9 | 10 |
11 |
12 | 13 | 14 |
15 | 16 |
17 | 18 | 19 |
20 | 21 |
22 | 23 | 24 |
25 | 26 |
27 | 28 | 29 |
30 | 31 | 32 |
33 | 34 |
35 |

Need to alter your settings? Edit your settings here

36 |
37 |
38 | {% endblock content %} -------------------------------------------------------------------------------- /app/templates/salary/pick.j2: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

6 | {% if edit %} 7 | Edit Settings 8 | {% else %} 9 | View Salary 10 | {% endif %} 11 |

12 |
13 |
14 | 20 |
21 | 22 | 29 |
30 | 31 |
32 |

Want to create salary settings for a different category? Create settings here

33 |
34 |
35 | {% endblock content %} -------------------------------------------------------------------------------- /app/templates/salary/view.j2: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 |

{{ category }}

8 |

{{ month }} {{ salary.year}}

9 |
10 | 11 |
12 |

Base Salary

13 |

Hourly Wage: {{ wage.wage }}

14 |

Number of shifts: {{ salary.num_of_shifts }}

15 |

Base Salary: {{ salary.base_salary }}

16 | 17 |

Additions

18 | {% if salary.month_weekly_overtime %} 19 |

Weekly Overtime Total: {{ salary.month_weekly_overtime }}

20 | {% endif %} 21 | {% if salary.transport %} 22 |

Transport: {{ salary.transport }}

23 | {% endif %} 24 | {% if salary.bonus %} 25 |

Bonus: {{ salary.bonus }}

26 | {% endif %} 27 | 28 | {% if salary.deduction %} 29 |

Deductions

30 |

Deduction: {{ salary.deduction }}

31 | {% endif %} 32 |
33 | 34 | 38 |
39 | 40 |
41 |

Need to alter your settings? Edit your settings here

42 |
43 |
44 | {% endblock content %} -------------------------------------------------------------------------------- /app/templates/search.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block content %} 5 | 6 |
7 |

Hello, {{ username }}

8 |
9 |
10 |
11 | 12 | 14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 | 22 | {% if message %} 23 |
24 | {{ message }} 25 |
26 | {% endif %} 27 | 28 | 29 | {% if results %} 30 |
31 |
32 | Showing results for '{{ keywords }}': 33 |
34 | 35 |
36 |
37 | {% for result in results %} 38 | 39 |
40 |
41 | 42 | {{ loop.index }}. {{ result.title }} 43 | 44 |
45 |
46 |

47 | {{ result.content }} 48 |

49 |
50 | {{ result.date }} 51 |
52 |
53 | 54 | {% endfor %} 55 |
56 |
57 | {% endif %} 58 | 59 | 60 | 70 | 71 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/share_event.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 40 | 41 | -------------------------------------------------------------------------------- /app/templates/weekview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | weekview 11 | 12 | 13 |
14 |
15 | {% for day, dayview, events_and_attr in week %} 16 |
17 |
{{ day.strftime('%A').upper()[:3] }}
18 | {% set month = day.month %} 19 | {% set day = day.day %} 20 | {% set events = events_and_attr%} 21 | {% include dayview.template %} 22 |
23 | {% endfor %} 24 |
25 |
26 | {% for hour in range(24)%} 27 |
28 |
29 | {% set hour = hour|string() %} 30 | {{hour.zfill(2)}}:00 31 |
32 |
33 | {% endfor %} 34 |
35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /app/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/app/utils/__init__.py -------------------------------------------------------------------------------- /app/utils/extending_openapi.py: -------------------------------------------------------------------------------- 1 | from fastapi.openapi.utils import get_openapi 2 | 3 | 4 | def custom_openapi(app): 5 | if app.openapi_schema: 6 | return app.openapi_schema 7 | url = ('https://forums.pythonic.guru' 8 | '/uploads/default/original/1X/' 9 | '3c7e2ccc77e214fb4e38daa421f1b8878a5677f9.jpeg') 10 | openapi_schema = get_openapi( 11 | title="Pylander API", 12 | version="1.0.0", 13 | description="This is a custom OpenAPI schema for Pylander Developers", 14 | routes=app.routes, 15 | ) 16 | openapi_schema["info"]["x-logo"] = { 17 | # TODO: change logo when we have one 18 | "url": url 19 | } 20 | app.openapi_schema = openapi_schema 21 | app.openapiv = app.openapi_schema 22 | -------------------------------------------------------------------------------- /git: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/git -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | # Mypy configuration 2 | [mypy] 3 | 4 | # The following is needed to disable Mypy reporting the following error: 5 | # Skipping analyzing X: found module but no type hints or library stubs 6 | [mypy-arrow.*] 7 | ignore_missing_imports = True 8 | 9 | [mypy-email_validator.*] 10 | ignore_missing_imports = True 11 | 12 | [mypy-fastapi_mail.*] 13 | ignore_missing_imports = True 14 | 15 | [mypy-icalendar.*] 16 | ignore_missing_imports = True 17 | 18 | [mypy-iso639.*] 19 | ignore_missing_imports = True 20 | 21 | [mypy-nltk.*] 22 | ignore_missing_imports = True 23 | 24 | [mypy-passlib.*] 25 | ignore_missing_imports = True 26 | 27 | [mypy-PIL.*] 28 | ignore_missing_imports = True 29 | 30 | [mypy-textblob.*] 31 | ignore_missing_imports = True 32 | 33 | [mypy-responses.*] 34 | ignore_missing_imports = True 35 | 36 | [mypy-sqlalchemy.*] 37 | ignore_missing_imports = True 38 | 39 | [mypy-word_forms.*] 40 | ignore_missing_imports = True -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 79 3 | -------------------------------------------------------------------------------- /schema.md: -------------------------------------------------------------------------------- 1 | . 2 | ├── app 3 | │ ├── __init__.py 4 | │ ├── dependencies.py 5 | │ ├── main.py 6 | │ ├── database 7 | │ ├── __init__.py 8 | │ ├── database.py 9 | │ ├── models.py 10 | │ ├── schemas.py 11 | │ ├── internal 12 | │ ├── __init__.py 13 | │ ├── admin.py 14 | │ ├── agenda_events.py 15 | │ ├── email.py 16 | │ ├── media 17 | │ ├── example.png 18 | │ ├── fake_user.png 19 | │ ├── profile.png 20 | │ ├── routers 21 | │ ├── __init__.py 22 | │ ├── agenda.py 23 | │ ├── categories.py 24 | │ ├── email.py 25 | │ ├── event.py 26 | │ ├── profile.py 27 | │ ├── static 28 | │ ├── event 29 | │ ├── eventedit.css 30 | │ ├── eventview.css 31 | │ ├── agenda_style.css 32 | │ ├── popover.js 33 | │ ├── style.css 34 | │ ├── templates 35 | │ ├── base.html 36 | │ ├── home.html 37 | │ ├── profile.html 38 | ├── LICENSE 39 | ├── requirements.txt 40 | ├── schema.md 41 | └── tests 42 | ├── __init__.py 43 | ├── conftest.py 44 | ├── test_agenda_internal.py 45 | ├── test_agenda_route.py 46 | ├── test_app.py 47 | ├── test_categories.py 48 | ├── test_email.py 49 | ├── test_event.py 50 | └── test_profile.py -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/tests/__init__.py -------------------------------------------------------------------------------- /tests/association_fixture.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy.orm import Session 3 | 4 | from app.database.models import Event, UserEvent 5 | 6 | 7 | @pytest.fixture 8 | def association(event: Event, session: Session) -> UserEvent: 9 | return ( 10 | session.query(UserEvent) 11 | .filter(UserEvent.event_id == event.id) 12 | ).first() 13 | -------------------------------------------------------------------------------- /tests/asyncio_fixture.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | from httpx import AsyncClient 4 | import pytest 5 | 6 | from app.database.models import Base 7 | from app.main import app 8 | from app.routers import telegram 9 | from app.routers.event import create_event 10 | from tests.client_fixture import get_test_placeholder_user 11 | from tests.conftest import get_test_db, test_engine 12 | 13 | 14 | @pytest.fixture 15 | async def telegram_client(): 16 | Base.metadata.create_all(bind=test_engine) 17 | app.dependency_overrides[telegram.get_db] = get_test_db 18 | async with AsyncClient(app=app, base_url="http://test") as ac: 19 | yield ac 20 | app.dependency_overrides = {} 21 | Base.metadata.drop_all(bind=test_engine) 22 | 23 | 24 | today_date = datetime.today().replace(hour=0, minute=0, second=0) 25 | 26 | 27 | @pytest.fixture 28 | def fake_user_events(session): 29 | Base.metadata.create_all(bind=test_engine) 30 | user = get_test_placeholder_user() 31 | session.add(user) 32 | session.commit() 33 | create_event( 34 | db=session, 35 | title='Cool today event', 36 | start=today_date, 37 | end=today_date + timedelta(days=2), 38 | all_day=False, 39 | content='test event', 40 | owner_id=user.id, 41 | location="Here", 42 | is_google_event=False, 43 | ) 44 | create_event( 45 | db=session, 46 | title='Cool (somewhen in two days) event', 47 | start=today_date + timedelta(days=1), 48 | end=today_date + timedelta(days=3), 49 | all_day=False, 50 | content='this week test event', 51 | owner_id=user.id, 52 | location="Here", 53 | is_google_event=False, 54 | ) 55 | yield user 56 | Base.metadata.drop_all(bind=test_engine) 57 | -------------------------------------------------------------------------------- /tests/calendar-linux.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "calendar#events", 3 | "etag": "etag", 4 | "summary": "string", 5 | "description": "string", 6 | "updated": "2021-01-13T09:10:02.000Z", 7 | "timeZone": "string", 8 | "accessRole": "string", 9 | "defaultReminders": [ 10 | { 11 | "method": "string", 12 | "minutes": 5 13 | } 14 | ], 15 | "nextPageToken": "string", 16 | "nextSyncToken": "string", 17 | "items": [ 18 | { 19 | "kind": "calendar#event", 20 | "etag": "somecode", 21 | "id": "somecode", 22 | "status": "confirmed", 23 | "htmlLink": "https://www.google.com/calendar/event?eid=somecode", 24 | "created": "2021-01-13T09:10:02.000Z", 25 | "updated": "2021-01-13T09:10:02.388Z", 26 | "summary": "some title", 27 | "creator": { 28 | "email": "someemail", 29 | "self": true 30 | }, 31 | "organizer": { 32 | "email": "someemail", 33 | "self": true 34 | }, 35 | "start": { 36 | "dateTime": "2021-02-25T13:00:00+02:00" 37 | }, 38 | "end": { 39 | "dateTime": "2021-02-25T14:00:00+02:00" 40 | }, 41 | "iCalUID": "somecode", 42 | "sequence": 0, 43 | "reminders": { 44 | "useDefault": true 45 | } 46 | }, 47 | { 48 | "kind": "calendar#event", 49 | "etag": "somecode", 50 | "id": "somecode", 51 | "status": "confirmed", 52 | "htmlLink": "https://www.google.com/calendar/event?eid=somecode", 53 | "created": "2021-01-13T09:10:02.000Z", 54 | "updated": "2021-01-13T09:10:02.388Z", 55 | "summary": "some title to all day event", 56 | "creator": { 57 | "email": "someemail", 58 | "self": true 59 | }, 60 | "organizer": { 61 | "email": "someemail", 62 | "self": true 63 | }, 64 | "start": { 65 | "date": "2021-02-25" 66 | }, 67 | "end": { 68 | "date": "2021-02-25" 69 | }, 70 | "iCalUID": "somecode", 71 | "sequence": 0, 72 | "location": "somelocation", 73 | "reminders": { 74 | "useDefault": true 75 | } 76 | } 77 | ] 78 | } -------------------------------------------------------------------------------- /tests/category_fixture.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy.orm import Session 3 | 4 | from app.database.models import Category, User 5 | 6 | 7 | @pytest.fixture 8 | def category(session: Session, sender: User) -> Category: 9 | category = Category.create(session, name="Guitar Lesson", color="121212", 10 | user_id=sender.id) 11 | yield category 12 | session.delete(category) 13 | session.commit() 14 | -------------------------------------------------------------------------------- /tests/comment_fixture.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Iterator 3 | 4 | import pytest 5 | from sqlalchemy.orm.session import Session 6 | 7 | from app.database.models import Comment, Event, User 8 | from app.internal.utils import create_model, delete_instance 9 | 10 | 11 | @pytest.fixture 12 | def comment(session: Session, event: Event, user: User) -> Iterator[Comment]: 13 | data = { 14 | 'user': user, 15 | 'event': event, 16 | 'content': 'test comment', 17 | 'time': datetime(2021, 1, 1, 0, 1), 18 | } 19 | create_model(session, Comment, **data) 20 | comment = session.query(Comment).first() 21 | yield comment 22 | delete_instance(session, comment) 23 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import calendar 2 | 3 | import pytest 4 | from sqlalchemy import create_engine 5 | from sqlalchemy.orm import sessionmaker 6 | 7 | from app.config import PSQL_ENVIRONMENT 8 | from app.database.models import Base 9 | 10 | pytest_plugins = [ 11 | 'tests.user_fixture', 12 | 'tests.event_fixture', 13 | 'tests.dayview_fixture', 14 | 'tests.invitation_fixture', 15 | 'tests.association_fixture', 16 | 'tests.client_fixture', 17 | 'tests.asyncio_fixture', 18 | 'tests.logger_fixture', 19 | 'tests.category_fixture', 20 | 'smtpdfix', 21 | 'tests.quotes_fixture', 22 | 'tests.zodiac_fixture', 23 | 'tests.comment_fixture', 24 | ] 25 | 26 | # When testing in a PostgreSQL environment please make sure that: 27 | # - Base string is a PSQL string 28 | # - app.config.PSQL_ENVIRONMENT is set to True 29 | 30 | if PSQL_ENVIRONMENT: 31 | SQLALCHEMY_TEST_DATABASE_URL = ( 32 | "postgresql://postgres:1234" 33 | "@localhost/postgres" 34 | ) 35 | test_engine = create_engine( 36 | SQLALCHEMY_TEST_DATABASE_URL 37 | ) 38 | 39 | else: 40 | SQLALCHEMY_TEST_DATABASE_URL = "sqlite:///./test.db" 41 | test_engine = create_engine( 42 | SQLALCHEMY_TEST_DATABASE_URL, connect_args={"check_same_thread": False} 43 | ) 44 | 45 | TestingSessionLocal = sessionmaker( 46 | autocommit=False, autoflush=False, bind=test_engine) 47 | 48 | 49 | def get_test_db(): 50 | return TestingSessionLocal() 51 | 52 | 53 | @pytest.fixture 54 | def session(): 55 | Base.metadata.create_all(bind=test_engine) 56 | session = get_test_db() 57 | yield session 58 | session.rollback() 59 | session.close() 60 | Base.metadata.drop_all(bind=test_engine) 61 | 62 | 63 | @pytest.fixture 64 | def sqlite_engine(): 65 | SQLALCHEMY_TEST_DATABASE_URL = "sqlite:///./test.db" 66 | sqlite_test_engine = create_engine( 67 | SQLALCHEMY_TEST_DATABASE_URL, connect_args={"check_same_thread": False} 68 | ) 69 | 70 | TestingSession = sessionmaker( 71 | autocommit=False, autoflush=False, bind=sqlite_test_engine) 72 | 73 | yield sqlite_test_engine 74 | session = TestingSession() 75 | session.close() 76 | Base.metadata.drop_all(bind=sqlite_test_engine) 77 | 78 | 79 | @pytest.fixture 80 | def Calendar(): 81 | return calendar.Calendar(0) 82 | -------------------------------------------------------------------------------- /tests/dayview_fixture.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import pytest 4 | 5 | from app.database.models import Event 6 | 7 | 8 | @pytest.fixture 9 | def event1(): 10 | start = datetime(year=2021, month=2, day=1, hour=7, minute=5) 11 | end = datetime(year=2021, month=2, day=1, hour=9, minute=15) 12 | return Event(title='test1', content='test', 13 | start=start, end=end, owner_id=1) 14 | 15 | 16 | @pytest.fixture 17 | def event2(): 18 | start = datetime(year=2021, month=2, day=1, hour=13, minute=13) 19 | end = datetime(year=2021, month=2, day=1, hour=15, minute=46) 20 | return Event(title='test2', content='test', 21 | start=start, end=end, owner_id=1, color='blue') 22 | 23 | 24 | @pytest.fixture 25 | def event3(): 26 | start = datetime(year=2021, month=2, day=3, hour=7, minute=5) 27 | end = datetime(year=2021, month=2, day=3, hour=9, minute=15) 28 | return Event(title='test3', content='test', 29 | start=start, end=end, owner_id=1) 30 | 31 | 32 | @pytest.fixture 33 | def all_day_event1(): 34 | start = datetime(year=2021, month=2, day=3, hour=7, minute=5) 35 | end = datetime(year=2021, month=2, day=3, hour=9, minute=15) 36 | return Event(title='test3', content='test', all_day=True, 37 | start=start, end=end, owner_id=1) 38 | 39 | 40 | @pytest.fixture 41 | def small_event(): 42 | start = datetime(year=2021, month=2, day=3, hour=7) 43 | end = datetime(year=2021, month=2, day=3, hour=8, minute=30) 44 | return Event(title='test3', content='test', 45 | start=start, end=end, owner_id=1) 46 | 47 | 48 | @pytest.fixture 49 | def event_with_no_minutes_modified(): 50 | start = datetime(year=2021, month=2, day=3, hour=7) 51 | end = datetime(year=2021, month=2, day=3, hour=8) 52 | return Event(title='test_no_modify', content='test', 53 | start=start, end=end, owner_id=1) 54 | 55 | 56 | @pytest.fixture 57 | def multiday_event(): 58 | start = datetime(year=2021, month=2, day=1, hour=13) 59 | end = datetime(year=2021, month=2, day=3, hour=13) 60 | return Event(title='test_multiday', content='test', 61 | start=start, end=end, owner_id=1, color='blue') 62 | 63 | 64 | @pytest.fixture 65 | def weekdays(): 66 | return [ 67 | 'Sunday', 'Monday', 'Tuesday', 68 | 'Wednesday', 'Thursday', 'Friday', 'Saturday', 69 | ] 70 | 71 | 72 | @pytest.fixture 73 | def sunday(): 74 | return datetime(day=3, month=1, year=2021) 75 | -------------------------------------------------------------------------------- /tests/files_for_import_file_tests/sample.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | CALSCALE:GREGORIAN 4 | BEGIN:VEVENT 5 | SUMMARY:HeadA 6 | DTSTART;TZID=America/New_York:20190802T103400 7 | DTEND;TZID=America/New_York:20190802T110400 8 | LOCATION:Tel-Aviv 9 | DESCRIPTION:Content1 10 | STATUS:CONFIRMED 11 | SEQUENCE:3 12 | BEGIN:VALARM 13 | TRIGGER:-PT10M 14 | DESCRIPTION:desc_1 15 | ACTION:DISPLAY 16 | END:VALARM 17 | END:VEVENT 18 | BEGIN:VEVENT 19 | SUMMARY:HeadB 20 | DTSTART;TZID=America/New_York:20190802T200000 21 | DTEND;TZID=America/New_York:20190802T203000 22 | LOCATION:Tel-Aviv 23 | DESCRIPTION:Content2 24 | STATUS:CONFIRMED 25 | SEQUENCE:3 26 | BEGIN:VALARM 27 | TRIGGER:-PT10M 28 | DESCRIPTION:desc_2 29 | ACTION:DISPLAY 30 | END:VALARM 31 | END:VEVENT 32 | END:VCALENDAR -------------------------------------------------------------------------------- /tests/files_for_import_file_tests/sample2.blabla: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonFreeCourse/calendar/23a33703a0038d0eae8ce7299a93ad172c8f68e9/tests/files_for_import_file_tests/sample2.blabla -------------------------------------------------------------------------------- /tests/files_for_import_file_tests/sample2.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | CALSCALE:GREGORIAN 4 | 5 | SUMMARY:HeadA 6 | DTSTART;TZID=America/New_York:20190802T103400 7 | DTEND;TZID=America/New_York:20190802T110400 8 | LOCATION:Tel-Aviv 9 | DESCRIPTION:Content1 10 | STATUS:CONFIRMED 11 | SEQUENCE:3 12 | BEGIN:VALARM 13 | TRIGGER:-PT10M 14 | DESCRIPTION:desc_1 15 | ACTION:DISPLAY 16 | END:VALARM 17 | END:VEVENT 18 | BEGIN:VEVENT 19 | SUMMARY:HeadB 20 | DTSTART;TZID=America/New_York:20190802T200000 21 | DTEND;TZID=America/New_York:20190802T203000 22 | LOCATION:Tel-Aviv 23 | DESCRIPTION:Content2 24 | STATUS:CONFIRMED 25 | SEQUENCE:3 26 | BEGIN:VALARM 27 | TRIGGER:-PT10M 28 | DESCRIPTION:desc_2 29 | ACTION:DISPLAY 30 | END:VALARM 31 | END:VEVENT 32 | END:VCALENDAR -------------------------------------------------------------------------------- /tests/files_for_import_file_tests/sample3.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | CALSCALE:GREGORIAN 4 | BEGIN:VEVENT 5 | 6 | 7 | 8 | LOCATION:Tel-Aviv 9 | DESCRIPTION:Content1 10 | STATUS:CONFIRMED 11 | SEQUENCE:3 12 | BEGIN:VALARM 13 | TRIGGER:-PT10M 14 | DESCRIPTION:desc_1 15 | ACTION:DISPLAY 16 | END:VALARM 17 | END:VEVENT 18 | BEGIN:VEVENT 19 | SUMMARY:HeadB 20 | DTSTART;TZID=America/New_York:20190802T200000 21 | DTEND;TZID=America/New_York:20190802T203000 22 | LOCATION:Tel-Aviv 23 | DESCRIPTION:Content2 24 | STATUS:CONFIRMED 25 | SEQUENCE:3 26 | BEGIN:VALARM 27 | TRIGGER:-PT10M 28 | DESCRIPTION:desc_2 29 | ACTION:DISPLAY 30 | END:VALARM 31 | END:VEVENT 32 | END:VCALENDAR -------------------------------------------------------------------------------- /tests/files_for_import_file_tests/sample_calendar_data.csv: -------------------------------------------------------------------------------- 1 | Head1, Content1, 05-21-2019, 05-21-2019 2 | Head2, Content2, 01-11-2010, 01-11-2010 3 | Head3, Content3, 02-02-2022, 02-02-2022 -------------------------------------------------------------------------------- /tests/files_for_import_file_tests/sample_calendar_data.txt: -------------------------------------------------------------------------------- 1 | Head1, Content1, 05-21-2019, 05-21-2019 2 | Head2, Content2, 01-11-2010, 01-11-2010 3 | Head3, Content3, 02-02-2022, 02-02-2022 -------------------------------------------------------------------------------- /tests/files_for_import_file_tests/sample_data_invalid.txt: -------------------------------------------------------------------------------- 1 | Head1, Content1, 05-21-2019, 05-21-2019 2 | , Content2, 01-11-2010, 01-11-2010 3 | Head3, Content3, 02-02-2022, 02-02-2022 -------------------------------------------------------------------------------- /tests/files_for_import_file_tests/sample_date2_ver.txt: -------------------------------------------------------------------------------- 1 | Option1, Content1, 05-21-2019 10:30, 05-21-2019 11:30 2 | Option2, Content2, 01-11-2010 11:30, 01-11-2010 12:30 3 | Option3, Content3, 02-02-2022 13:00, 02-02-2022 13:05 -------------------------------------------------------------------------------- /tests/files_for_import_file_tests/sample_date_mix.txt: -------------------------------------------------------------------------------- 1 | Option1, Content1, 05-21-2019, 05-21-2019 11:30 2 | Option2, Content2, 01-11-2010 11:30, 01-11-2010 12:30 3 | Option3, Content3, 02-02-2022 13:00, 02-02-2022 13:05 -------------------------------------------------------------------------------- /tests/files_for_import_file_tests/sample_rng_invalid.txt: -------------------------------------------------------------------------------- 1 | Head1, Content1, 05-21-1990, 05-21-1990 2 | Head2, Content2, 01-11-2010, 01-11-2010 3 | Head3, Content3, 02-02-2022, 02-02-2022 -------------------------------------------------------------------------------- /tests/files_for_import_file_tests/‏‏sample_loc_ver.txt: -------------------------------------------------------------------------------- 1 | Option1, Content1, 05-21-2019 10:30, 05-21-2019 11:30, aaa 2 | Option2, Content2, 01-11-2010 11:30, 01-11-2010 12:30, bbb 3 | Option3, Content3, 02-02-2022 13:00, 02-02-2022 13:05, ccc -------------------------------------------------------------------------------- /tests/invitation_fixture.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Generator 3 | 4 | import pytest 5 | from sqlalchemy.orm import Session 6 | 7 | from app.database.models import Event, Invitation, User 8 | from app.internal.utils import create_model, delete_instance 9 | 10 | 11 | @pytest.fixture 12 | def invitation( 13 | event: Event, user: User, session: Session 14 | ) -> Generator[Invitation, None, None]: 15 | """Returns an Invitation object after being created in the database. 16 | 17 | Args: 18 | event: An Event instance. 19 | user: A user instance. 20 | session: A database connection. 21 | 22 | Returns: 23 | An Invitation object. 24 | """ 25 | invitation = create_model( 26 | session, Invitation, 27 | creation=datetime.now(), 28 | recipient=user, 29 | event=event, 30 | event_id=event.id, 31 | recipient_id=user.id, 32 | ) 33 | yield invitation 34 | delete_instance(session, invitation) 35 | -------------------------------------------------------------------------------- /tests/logger_fixture.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from _pytest.logging import caplog as _caplog # noqa: F401 4 | from loguru import logger 5 | import pytest 6 | 7 | from app import config 8 | from app.internal.logger_customizer import LoggerCustomizer 9 | 10 | 11 | @pytest.fixture(scope='module') 12 | def logger_instance(): 13 | _logger = LoggerCustomizer.make_logger(config.LOG_PATH, 14 | config.LOG_FILENAME, 15 | config.LOG_LEVEL, 16 | config.LOG_ROTATION_INTERVAL, 17 | config.LOG_RETENTION_INTERVAL, 18 | config.LOG_FORMAT) 19 | 20 | return _logger 21 | 22 | 23 | @pytest.fixture 24 | def caplog(_caplog): # noqa: F811 25 | class PropagateHandler(logging.Handler): 26 | def emit(self, record): 27 | logging.getLogger(record.name).handle(record) 28 | 29 | handler_id = logger.add(PropagateHandler(), format="{message} {extra}") 30 | yield _caplog 31 | logger.remove(handler_id) 32 | -------------------------------------------------------------------------------- /tests/quotes_fixture.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy.orm import Session 3 | 4 | from app.database.models import Quote 5 | from app.internal.utils import create_model, delete_instance 6 | 7 | 8 | def add_quote( 9 | session: Session, id_quote: int, text: str, author: str 10 | ) -> Quote: 11 | quote = create_model( 12 | session, 13 | Quote, 14 | id=id_quote, 15 | text=text, 16 | author=author, 17 | ) 18 | yield quote 19 | delete_instance(session, quote) 20 | 21 | 22 | @pytest.fixture 23 | def quote1(session: Session) -> Quote: 24 | yield from add_quote( 25 | session=session, 26 | id_quote=1, 27 | text='You have to believe in yourself.', 28 | author='Sun Tzu', 29 | ) 30 | 31 | 32 | @pytest.fixture 33 | def quote2(session: Session) -> Quote: 34 | yield from add_quote( 35 | session=session, 36 | id_quote=2, 37 | text='Wisdom begins in wonder.', 38 | author='Socrates', 39 | ) 40 | -------------------------------------------------------------------------------- /tests/resources/ics_example.txt: -------------------------------------------------------------------------------- 1 | STATUS:CONFIRMED 2 | DTSTAMP:20210225T000000 3 | DTSTART;VALUE=DATE:20210225 4 | UID:feiertag2021-0323-3542-fcal.ch 5 | CATEGORIES:Public holidays 6 | END:VEVENT 7 | BEGIN:VEVENT 8 | SUMMARY:Purim 9 | DESCRIPTION:Feiertagskalender.ch - free data for private /internal use only. 10 | STATUS:CONFIRMED 11 | DTSTAMP:20210226T000000 12 | DTSTART;VALUE=DATE:20210226 13 | UID:feiertag2021-0324-3542-fcal.ch 14 | CATEGORIES:Public holidays 15 | END:VEVENT 16 | BEGIN:VEVENT 17 | SUMMARY:Pesach 18 | DESCRIPTION:Feiertagskalender.ch - free data for private /internal use only. 19 | STATUS:CONFIRMED 20 | DTSTAMP:20210328T000000 21 | DTSTART;VALUE=DATE:20210328 22 | UID:feiertag2021-0336-3542-fcal.ch 23 | CATEGORIES:Public holidays 24 | END:VEVENT 25 | BEGIN:VEVENT 26 | SUMMARY:Pesach 27 | DESCRIPTION:Feiertagskalender.ch - free data for private /internal use only. 28 | STATUS:CONFIRMED 29 | DTSTAMP:20210329T010000 30 | DTSTART;VALUE=DATE:20210329 31 | UID:feiertag2021-0336-3542-fcal.ch 32 | CATEGORIES:Public holidays 33 | END:VEVENT 34 | BEGIN:VEVENT 35 | SUMMARY:One More Holiday For Test 36 | DTSTAMP:20210101T010000 37 | -------------------------------------------------------------------------------- /tests/resources/wrong_ics_example.txt: -------------------------------------------------------------------------------- 1 | STATUS:CONFIRMED 2 | DTSTAMP:20210225T000000 3 | DTSTART;VALUE=DATE:20210225 4 | UID:feiertag2021-0323-3542-fcal.ch 5 | CATEGORIES:Public holidays 6 | END:VEVENT 7 | BEGIN:VEVENT 8 | DESCRIPTION:Feiertagskalender.ch - free data for private /internal use only. 9 | STATUS:CONFIRMED 10 | DTSTAMP:20210226T000000 11 | DTSTART;VALUE=DATE:20210226 12 | UID:feiertag2021-0324-3542-fcal.ch 13 | CATEGORIES:Public holidays 14 | END:VEVENT 15 | BEGIN:VEVENT 16 | SUMMARY:Pesach 17 | DESCRIPTION:Feiertagskalender.ch - free data for private /internal use only. 18 | STATUS:CONFIRMED 19 | DTSTART;VALUE=DATE:20210328 20 | UID:feiertag2021-0336-3542-fcal.ch 21 | CATEGORIES:Public holidays 22 | END:VEVENT 23 | BEGIN:VEVENT 24 | SUMMARY:Pesach 25 | DESCRIPTION:Feiertagskalender.ch - free data for private /internal use only. 26 | -------------------------------------------------------------------------------- /tests/security_testing_routes.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, Request 2 | 3 | from app.internal.security.dependancies import ( 4 | current_user, current_user_from_db, 5 | is_logged_in, is_manager, User 6 | ) 7 | 8 | 9 | """ 10 | These routes are for security testing. 11 | They represent an example for how to use 12 | security dependencies in other routes. 13 | """ 14 | router = APIRouter( 15 | prefix="", 16 | tags=["/security"], 17 | responses={404: {"description": "Not found"}}, 18 | ) 19 | 20 | 21 | @router.get('/is_logged_in') 22 | async def is_logged_in( 23 | request: Request, user: bool = Depends(is_logged_in)): 24 | """This is how to protect route for logged in user only. 25 | Dependency will return True. 26 | if user not looged-in, will be redirected to login route. 27 | """ 28 | return {"user": user} 29 | 30 | 31 | @router.get('/is_manager') 32 | async def is_manager( 33 | request: Request, user: bool = Depends(is_manager)): 34 | """This is how to protect route for logged in manager only. 35 | Dependency will return True. 36 | if user not looged-in, or have no manager permission, 37 | will be redirected to login route. 38 | """ 39 | return {"manager": user} 40 | 41 | 42 | @router.get('/current_user_from_db') 43 | async def current_user_from_db( 44 | request: Request, user: User = Depends(current_user_from_db)): 45 | """This is how to protect route for logged in user only. 46 | Dependency will return User object. 47 | if user not looged-in, will be redirected to login route. 48 | """ 49 | return {"user": user.username} 50 | 51 | 52 | @router.get('/current_user') 53 | async def current_user( 54 | request: Request, user: User = Depends(current_user)): 55 | """This is how to protect route for logged in user only. 56 | Dependency will return schema.CurrentUser object, 57 | contains user_id and username. 58 | if user not looged-in, will be redirected to login route. 59 | """ 60 | return {"user": user.username} 61 | -------------------------------------------------------------------------------- /tests/test_about.py: -------------------------------------------------------------------------------- 1 | def test_about_page(client): 2 | response = client.get("/about") 3 | assert response.ok 4 | assert b"Yam" in response.content 5 | -------------------------------------------------------------------------------- /tests/test_agenda_internal.py: -------------------------------------------------------------------------------- 1 | from datetime import date, datetime 2 | 3 | import pytest 4 | 5 | from app.internal import agenda_events 6 | from app.internal.agenda_events import get_events_per_dates 7 | 8 | 9 | class TestAgenda: 10 | START = datetime(2021, 11, 1, 8, 00, 00) 11 | dates = [ 12 | (START, datetime(2021, 11, 3, 8, 00, 0), 13 | '2 days'), 14 | (START, datetime(2021, 11, 3, 10, 30, 0), 15 | '2 days 2 hours and 30 minutes'), 16 | (START, datetime(2021, 11, 1, 8, 30, 0), 17 | '30 minutes'), 18 | (START, datetime(2021, 11, 1, 10, 00, 0), 19 | '2 hours'), 20 | (START, datetime(2021, 11, 1, 10, 30, 0), 21 | '2 hours and 30 minutes'), 22 | (START, datetime(2021, 11, 2, 10, 00, 0), 23 | 'a day and 2 hours'), 24 | ] 25 | 26 | @pytest.mark.parametrize('start, end, diff', dates) 27 | def test_get_time_delta_string(self, start, end, diff): 28 | assert agenda_events.get_time_delta_string(start, end) == diff 29 | 30 | def test_get_events_per_dates_success(self, today_event, session): 31 | events = get_events_per_dates( 32 | session=session, 33 | user_id=today_event.owner_id, 34 | start=today_event.start.date(), 35 | end=today_event.end.date(), 36 | ) 37 | assert list(events) == [today_event] 38 | 39 | def test_get_events_per_dates_failure(self, yesterday_event, session): 40 | events = get_events_per_dates( 41 | session=session, 42 | user_id=yesterday_event.owner_id, 43 | start=date.today(), 44 | end=date.today(), 45 | ) 46 | assert list(events) == [] 47 | -------------------------------------------------------------------------------- /tests/test_app.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | from app.dependencies import get_db 4 | 5 | 6 | class TestApp: 7 | 8 | @staticmethod 9 | def test_get_db(): 10 | assert isinstance(next(get_db()), Session) 11 | -------------------------------------------------------------------------------- /tests/test_association.py: -------------------------------------------------------------------------------- 1 | class TestAssociation: 2 | def test_association_data(self, association, event): 3 | assert association.events == event 4 | 5 | def test_repr(self, association): 6 | assert ( 7 | association.__repr__() 8 | == f'') 10 | -------------------------------------------------------------------------------- /tests/test_calendar_privacy.py: -------------------------------------------------------------------------------- 1 | from app.internal.calendar_privacy import can_show_calendar 2 | # TODO after user system is merged: 3 | # from app.internal.security.dependancies import CurrentUser 4 | from app.routers.user import create_user 5 | 6 | 7 | def test_can_show_calendar_public(session, user): 8 | user.privacy = "Public" 9 | # TODO to be replaced after user system is merged: 10 | # current_user = CurrentUser(**user.__dict__) 11 | current_user = user 12 | result = can_show_calendar( 13 | requested_user_username='test_username', 14 | db=session, current_user=current_user 15 | ) 16 | assert result is True 17 | session.commit() 18 | 19 | 20 | def test_can_show_calendar_private(session, user): 21 | another_user = create_user( 22 | session=session, 23 | username='new_test_username2', 24 | email='new_test.email2@gmail.com', 25 | password='passpar_2', 26 | language_id=1 27 | ) 28 | current_user = user 29 | # TODO to be replaced after user system is merged: 30 | # current_user = CurrentUser(**user.__dict__) 31 | 32 | result_a = can_show_calendar( 33 | requested_user_username='new_test_username2', 34 | db=session, current_user=current_user 35 | ) 36 | result_b = can_show_calendar( 37 | requested_user_username='test_username', 38 | db=session, current_user=current_user 39 | ) 40 | assert result_a is False 41 | assert result_b is True 42 | session.delete(another_user) 43 | session.commit() 44 | -------------------------------------------------------------------------------- /tests/test_celebrity.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import pytest 4 | 5 | from app.internal.celebrity import get_today_month_and_day 6 | 7 | CELEBRITY_ROUTE = "/celebrity" 8 | FAKE_TIME = datetime.date(2018, 9, 18) 9 | 10 | BAD_DATES = [ 11 | datetime.date(2021, 1, 1), 12 | datetime.date(1789, 7, 14), 13 | datetime.date(1776, 7, 4), 14 | datetime.date(1945, 1, 27), 15 | datetime.date(2000, 10, 16), 16 | ] 17 | 18 | GOOD_DATES = [ 19 | datetime.date(2020, 9, 18), 20 | datetime.date(2019, 9, 18), 21 | datetime.date(2016, 9, 18), 22 | ] 23 | 24 | 25 | @pytest.fixture 26 | def datetime_mock(monkeypatch): 27 | class MockDateTime: 28 | 29 | @staticmethod 30 | def today(): 31 | return FAKE_TIME 32 | 33 | monkeypatch.setattr(datetime, 'date', MockDateTime) 34 | 35 | 36 | @pytest.mark.parametrize('date', BAD_DATES) 37 | def test_get_today_month_and_day_bad(date, datetime_mock): 38 | assert get_today_month_and_day() != date.strftime("%m-%d") 39 | 40 | 41 | @pytest.mark.parametrize('date', GOOD_DATES) 42 | def test_get_today_month_and_day_good(date, datetime_mock): 43 | assert get_today_month_and_day() == date.strftime("%m-%d") 44 | 45 | 46 | def test_celebrity_page_exists(client): 47 | response = client.get(CELEBRITY_ROUTE) 48 | assert response.ok 49 | assert b'born today' in response.content 50 | -------------------------------------------------------------------------------- /tests/test_comment.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from sqlalchemy.orm.session import Session 4 | 5 | from app.database.models import Comment, Event, User 6 | from app.internal import comment as cmt 7 | from app.internal.utils import delete_instance 8 | 9 | 10 | def test_create_comment(session: Session, event: Event, user: User) -> None: 11 | assert session.query(Comment).first() is None 12 | cmt.create_comment(session, event, 'test content') 13 | comment = session.query(Comment).first() 14 | assert comment 15 | delete_instance(session, comment) 16 | 17 | 18 | def test_parse_comment(session: Session, comment: Comment) -> None: 19 | data = { 20 | 'id': 1, 21 | 'avatar': 'profile.png', 22 | 'username': 'test_username', 23 | 'time': '01/01/2021 00:01', 24 | 'content': 'test comment', 25 | } 26 | assert cmt.parse_comment(session, comment) == data 27 | 28 | 29 | def test_display_comment(session: Session, event: Event, 30 | comment: Comment) -> None: 31 | comments = json.loads(cmt.display_comments(session, event)) 32 | assert len(comments) == 1 33 | 34 | 35 | def test_display_comment_empty(session: Session, event: Event) -> None: 36 | comments = json.loads(cmt.display_comments(session, event)) 37 | assert comments == [] 38 | 39 | 40 | def test_delete_comment(session: Session, comment: Comment) -> None: 41 | assert session.query(Comment).first() 42 | assert cmt.delete_comment(session, comment.id) 43 | assert session.query(Comment).first() is None 44 | assert not cmt.delete_comment(session, comment.id) 45 | -------------------------------------------------------------------------------- /tests/test_credits.py: -------------------------------------------------------------------------------- 1 | class TestCredits: 2 | CREDITS_OPENING = b"Say hello to our developers" 3 | 4 | @staticmethod 5 | def test_get_credits_ok_request(client): 6 | response = client.get("/credits") 7 | assert response.ok 8 | assert TestCredits.CREDITS_OPENING in response.content 9 | -------------------------------------------------------------------------------- /tests/test_currency.py: -------------------------------------------------------------------------------- 1 | CURRENCY = '/currency' 2 | CUSTOM_DATE = "/2021-1-3" 3 | 4 | 5 | def test_router_good(client): 6 | resp = client.get(CURRENCY) 7 | assert resp.ok 8 | resp = client.get(CURRENCY + CUSTOM_DATE) 9 | assert resp.ok 10 | assert b'Currency' in resp.content 11 | -------------------------------------------------------------------------------- /tests/test_friendview.py: -------------------------------------------------------------------------------- 1 | from fastapi import status 2 | 3 | 4 | class TestFriendview: 5 | FRIENDVIEW = "/friendview" 6 | NO_EVENTS = b"No mutual events found..." 7 | 8 | @staticmethod 9 | def test_friendview_page_no_arguments_when_no_today_events( 10 | friendview_test_client, 11 | session, 12 | ): 13 | resp = friendview_test_client.get(TestFriendview.FRIENDVIEW) 14 | assert resp.ok 15 | assert TestFriendview.NO_EVENTS in resp.content 16 | 17 | @staticmethod 18 | def test_no_show_events_user_2(friendview_test_client, today_event): 19 | resp = friendview_test_client.get(TestFriendview.FRIENDVIEW) 20 | assert resp.status_code == status.HTTP_200_OK 21 | assert b"event 7" not in resp.content 22 | -------------------------------------------------------------------------------- /tests/test_holidays.py: -------------------------------------------------------------------------------- 1 | import os 2 | from app.database.models import Event, User 3 | from app.routers import profile 4 | from sqlalchemy.orm import Session 5 | 6 | 7 | class TestHolidaysImport: 8 | HOLIDAYS = '/profile/holidays/import' 9 | 10 | @staticmethod 11 | def test_import_holidays_page_exists(client): 12 | resp = client.get(TestHolidaysImport.HOLIDAYS) 13 | assert resp.ok 14 | assert b'Import holidays using ics file' in resp.content 15 | 16 | def test_get_holidays(self, session: Session, user: User): 17 | current_folder = os.path.dirname(os.path.realpath(__file__)) 18 | resource_folder = os.path.join(current_folder, 'resources') 19 | test_file = os.path.join(resource_folder, 'ics_example.txt') 20 | with open(test_file) as file: 21 | ics_content = file.read() 22 | holidays = profile.get_holidays_from_file(ics_content, session) 23 | profile.save_holidays_to_db(holidays, session) 24 | assert len(session.query(Event).all()) == 4 25 | 26 | def test_wrong_file_get_holidays(self, session: Session, user: User): 27 | current_folder = os.path.dirname(os.path.realpath(__file__)) 28 | resource_folder = os.path.join(current_folder, 'resources') 29 | test_file = os.path.join(resource_folder, 'wrong_ics_example.txt') 30 | with open(test_file) as file: 31 | ics_content = file.read() 32 | holidays = profile.get_holidays_from_file(ics_content, session) 33 | profile.save_holidays_to_db(holidays, session) 34 | assert len(session.query(Event).all()) == 0 35 | -------------------------------------------------------------------------------- /tests/test_home.py: -------------------------------------------------------------------------------- 1 | class TestHome: 2 | URL = "/" 3 | 4 | @staticmethod 5 | def test_get_page(client): 6 | response = client.get(TestHome.URL) 7 | assert response.ok 8 | -------------------------------------------------------------------------------- /tests/test_invitation.py: -------------------------------------------------------------------------------- 1 | from fastapi import status 2 | 3 | from app.routers.invitation import get_all_invitations, get_invitation_by_id 4 | 5 | 6 | class TestInvitations: 7 | NO_INVITATIONS = b"You don't have any invitations." 8 | URL = "/invitations/" 9 | 10 | @staticmethod 11 | def test_view_no_invitations(invitation_test_client): 12 | response = invitation_test_client.get(TestInvitations.URL) 13 | assert response.ok 14 | assert TestInvitations.NO_INVITATIONS in response.content 15 | 16 | @staticmethod 17 | def test_accept_invitations(user, invitation, invitation_test_client): 18 | invitation = {"invite_id ": invitation.id} 19 | resp = invitation_test_client.post( 20 | TestInvitations.URL, data=invitation) 21 | assert resp.status_code == status.HTTP_302_FOUND 22 | 23 | @staticmethod 24 | def test_get_all_invitations_success(invitation, event, user, session): 25 | invitations = get_all_invitations(event=event, db=session) 26 | assert invitations == [invitation] 27 | invitations = get_all_invitations(recipient=user, db=session) 28 | assert invitations == [invitation] 29 | 30 | @staticmethod 31 | def test_get_all_invitations_failure(user, session): 32 | invitations = get_all_invitations(unknown_parameter=user, db=session) 33 | assert invitations == [] 34 | 35 | invitations = get_all_invitations(recipient=None, db=session) 36 | assert invitations == [] 37 | 38 | @staticmethod 39 | def test_get_invitation_by_id(invitation, session): 40 | get_invitation = get_invitation_by_id(invitation.id, db=session) 41 | assert get_invitation == invitation 42 | 43 | @staticmethod 44 | def test_repr(invitation): 45 | invitation_repr = ( 46 | f'' 49 | ) 50 | assert invitation.__repr__() == invitation_repr 51 | -------------------------------------------------------------------------------- /tests/test_json_data_loader.py: -------------------------------------------------------------------------------- 1 | from app.database.models import Quote, Zodiac 2 | from app.internal import json_data_loader 3 | 4 | 5 | def get_objects_amount(session, table): 6 | return session.query(table).count() 7 | 8 | 9 | def test_load_daily_quotes(session): 10 | json_data_loader.load_to_database(session) 11 | assert get_objects_amount(session, Quote) > 0 12 | 13 | 14 | def test_load_zodiacs(session): 15 | json_data_loader.load_to_database(session) 16 | assert get_objects_amount(session, Zodiac) > 0 17 | 18 | 19 | # tests for basic functionality of the json data loader 20 | def test_load_data_with_json_value_error(mocker, session): 21 | mocker.patch('json.load', side_effect=ValueError) 22 | json_data_loader.load_to_database(session) 23 | assert get_objects_amount(session, Quote) == 0 24 | 25 | 26 | def test_data_not_load_twice_to_db(session): 27 | json_data_loader.load_to_database(session) 28 | first_quotes_amount = get_objects_amount(session, Quote) 29 | json_data_loader.load_to_database(session) 30 | assert first_quotes_amount == get_objects_amount(session, Quote) 31 | -------------------------------------------------------------------------------- /tests/test_language.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | from app.dependencies import templates 6 | from app.internal import languages 7 | 8 | 9 | class TestLanguage: 10 | # Empty, invalid, or valid, but unsupported language codes, 11 | # (currently 'en' and 'he') are set to the default language setting 12 | # at config.WEBSITE_LANGUAGE, which is currently set to 'en' (English). 13 | LANGUAGE_TESTS = [ 14 | ('en', 'test python translation', True), 15 | ('he', 'בדיקת תרגום בפייתון', True), 16 | (None, 'test python translation', False), 17 | ('', 'test python translation', False), 18 | ('de', 'test python translation', False), 19 | (["en"], 'test python translation', False), 20 | (3, 'test python translation', False), 21 | ] 22 | 23 | NUMBER_OF_LANGUAGES = 2 24 | 25 | @staticmethod 26 | @pytest.mark.parametrize( 27 | "language_code, translation, is_valid", LANGUAGE_TESTS) 28 | def test_gettext_python(language_code, translation, is_valid): 29 | languages.set_ui_language(language_code) 30 | 31 | # i18n: String used in testing. Do not change. 32 | gettext_translation = _("test python translation") 33 | assert ((is_valid and gettext_translation == translation) 34 | or (not is_valid and gettext_translation == translation)) 35 | 36 | @staticmethod 37 | @pytest.mark.parametrize( 38 | "language_code, translation, is_valid", LANGUAGE_TESTS) 39 | def test_gettext_html(language_code, translation, is_valid): 40 | languages.set_ui_language(language_code) 41 | 42 | template = templates.env.from_string( 43 | '{{ gettext("test python translation") }}') 44 | text = template.render() 45 | assert ((is_valid and translation in text) 46 | or (not is_valid and translation in text)) 47 | 48 | @staticmethod 49 | def test_get_supported_languages(): 50 | number_of_languages = len(list(languages._get_supported_languages())) 51 | assert number_of_languages == TestLanguage.NUMBER_OF_LANGUAGES 52 | 53 | @staticmethod 54 | def test_get_language_directory(): 55 | pytest.MonkeyPatch().setattr(Path, 'is_dir', lambda x: True) 56 | assert languages._get_language_directory() 57 | 58 | @staticmethod 59 | def test_get_display_language(): 60 | # TODO: Waiting for user registration. 61 | # Test: no user, user not logged in and user with non-english set. 62 | pass 63 | -------------------------------------------------------------------------------- /tests/test_logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pytest 4 | 5 | from app import config 6 | from app.internal.logger_customizer import LoggerConfigError, LoggerCustomizer 7 | 8 | 9 | class TestLogger: 10 | @staticmethod 11 | def test_log_debug(caplog, logger_instance): 12 | with caplog.at_level(logging.DEBUG): 13 | logger_instance.debug('Is it debugging now?') 14 | assert 'Is it debugging now?' in caplog.text 15 | 16 | @staticmethod 17 | def test_log_info(caplog, logger_instance): 18 | with caplog.at_level(logging.INFO): 19 | logger_instance.info('App started') 20 | assert 'App started' in caplog.text 21 | 22 | @staticmethod 23 | def test_log_error(caplog, logger_instance): 24 | with caplog.at_level(logging.ERROR): 25 | logger_instance.error('Something bad happened!') 26 | assert 'Something bad happened!' in caplog.text 27 | 28 | @staticmethod 29 | def test_log_critical(caplog, logger_instance): 30 | with caplog.at_level(logging.CRITICAL): 31 | logger_instance.critical("WE'RE DOOMED!") 32 | assert "WE'RE DOOMED!" in caplog.text 33 | 34 | @staticmethod 35 | def test_bad_configuration(): 36 | with pytest.raises(LoggerConfigError): 37 | LoggerCustomizer.make_logger(config.LOG_PATH, 38 | config.LOG_FILENAME, 39 | 'eror', 40 | config.LOG_ROTATION_INTERVAL, 41 | config.LOG_RETENTION_INTERVAL, 42 | config.LOG_FORMAT) 43 | -------------------------------------------------------------------------------- /tests/test_on_this_day_events.py: -------------------------------------------------------------------------------- 1 | from app.database.models import WikipediaEvents 2 | from app.internal.on_this_day_events import (get_on_this_day_events, 3 | insert_on_this_day_data) 4 | 5 | 6 | def test_insert_on_this_day_data(session): 7 | is_exists_data = session.query(WikipediaEvents).all() 8 | assert not is_exists_data 9 | insert_on_this_day_data(session) 10 | is_exists_data = session.query(WikipediaEvents).all() 11 | assert is_exists_data is not None 12 | 13 | 14 | def test_get_on_this_day_events(session): 15 | data = get_on_this_day_events(session) 16 | assert isinstance(data, dict) 17 | assert isinstance(data.get('events'), list) 18 | assert isinstance(data.get('wikipedia'), str) 19 | 20 | 21 | def test_get_on_this_day_events_exists(session): 22 | fake_object = WikipediaEvents( 23 | events=['fake'], wikipedia="www.fake.com", date_="not a date string") 24 | session.add(fake_object) 25 | session.commit() 26 | fake_data = get_on_this_day_events(session) 27 | assert fake_data.events[0] == 'fake' 28 | assert fake_data.wikipedia == 'www.fake.com' 29 | assert fake_data.date_ == 'not a date string' 30 | -------------------------------------------------------------------------------- /tests/test_psql_environment.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from app.database import create_env_engine 4 | from app.database.models import PSQLEnvironmentError 5 | from app.main import create_tables 6 | 7 | 8 | def test_main_create_tables_error(sqlite_engine): 9 | raised_error = False 10 | with pytest.raises(PSQLEnvironmentError): 11 | create_tables(sqlite_engine, True) 12 | raised_error = True 13 | assert raised_error 14 | 15 | 16 | def test_database_create_engine(): 17 | sqlalchemy_database_url = "postgresql://postgres:1234@localhost/postgres" 18 | engine = create_env_engine(True, sqlalchemy_database_url) 19 | assert 'postgres' in str(engine.url) 20 | sqlalchemy_database_url = "sqlite:///./test1.db" 21 | engine = create_env_engine(False, sqlalchemy_database_url) 22 | assert 'sqlite' in str(engine.url) 23 | -------------------------------------------------------------------------------- /tests/test_quotes.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from app.internal import daily_quotes 4 | 5 | DATE = date(2021, 1, 1) 6 | DATE2 = date(2021, 1, 2) 7 | 8 | 9 | def test_get_quote(): 10 | quotes_fields = { 11 | 'text': 'some_quote', 12 | 'author': 'Freud', 13 | } 14 | result = daily_quotes.get_quote(quotes_fields) 15 | assert result.text == 'some_quote' 16 | assert result.author == 'Freud' 17 | 18 | 19 | def test_get_quote_of_day_no_quotes(session): 20 | assert daily_quotes.get_quote_of_day(session, DATE) is None 21 | 22 | 23 | def test_get_quote_of_day_get_first_quote(session, quote1, quote2): 24 | assert daily_quotes.get_quote_of_day( 25 | session, DATE).text == quote1.text 26 | 27 | 28 | def test_get_quote_of_day_get_second_quote(session, quote1, quote2): 29 | assert daily_quotes.get_quote_of_day( 30 | session, DATE2).text == quote2.text 31 | -------------------------------------------------------------------------------- /tests/test_share_event.py: -------------------------------------------------------------------------------- 1 | from app.routers.invitation import get_all_invitations 2 | from app.routers.share import (accept, send_email_invitation, 3 | send_in_app_invitation, share, sort_emails) 4 | 5 | 6 | class TestShareEvent: 7 | 8 | def test_share_success(self, user, event, session): 9 | participants = [user.email] 10 | share(event, participants, session) 11 | invitations = get_all_invitations(db=session, recipient_id=user.id) 12 | assert invitations != [] 13 | 14 | def test_share_failure(self, event, session): 15 | participants = [event.owner.email] 16 | share(event, participants, session) 17 | invitations = get_all_invitations( 18 | db=session, recipient_id=event.owner.id) 19 | assert invitations == [] 20 | 21 | def test_sort_emails(self, user, session): 22 | # the user is being imported 23 | # so he will be created 24 | data = [ 25 | 'test.email@gmail.com', # registered user 26 | 'not_logged_in@gmail.com', # unregistered user 27 | ] 28 | sorted_data = sort_emails(data, session=session) 29 | assert sorted_data == { 30 | 'registered': ['test.email@gmail.com'], 31 | 'unregistered': ['not_logged_in@gmail.com'] 32 | } 33 | 34 | def test_send_in_app_invitation_success( 35 | self, user, sender, event, session 36 | ): 37 | assert send_in_app_invitation([user.email], event, session=session) 38 | invitation = get_all_invitations(db=session, recipient=user)[0] 39 | assert invitation.event.owner == sender 40 | assert invitation.recipient == user 41 | session.delete(invitation) 42 | 43 | def test_send_in_app_invitation_failure( 44 | self, user, sender, event, session): 45 | assert (send_in_app_invitation( 46 | [sender.email], event, session=session) is False) 47 | 48 | def test_send_email_invitation(self, user, event): 49 | send_email_invitation([user.email], event) 50 | # TODO add email tests 51 | assert True 52 | 53 | def test_accept(self, invitation, session): 54 | accept(invitation, session=session) 55 | assert invitation.status == 'accepted' 56 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | from app.database.models import User 4 | from app.internal import utils 5 | 6 | 7 | class TestUtils: 8 | 9 | def test_save_success(self, user: User, session: Session) -> None: 10 | user.username = 'edit_username' 11 | assert utils.save(session, user) 12 | 13 | def test_save_failure(self, session: Session) -> None: 14 | user = 'not a user instance' 15 | assert not utils.save(session, user) 16 | 17 | def test_create_model(self, session: Session) -> None: 18 | assert session.query(User).first() is None 19 | info = { 20 | 'username': 'test', 21 | 'email': 'test@test.com', 22 | 'password': 'test1234' 23 | } 24 | utils.create_model(session, User, **info) 25 | assert session.query(User).first() 26 | 27 | def test_delete_instance(self, session: Session, user: User): 28 | assert session.query(User).first() 29 | utils.delete_instance(session, user) 30 | assert session.query(User).first() is None 31 | 32 | def test_get_current_user(self, session: Session) -> None: 33 | # Code revision required after user login feature is added 34 | assert session.query(User).filter_by(id=1).first() is None 35 | utils.get_current_user(session) 36 | assert session.query(User).filter_by(id=1).first() 37 | 38 | def test_get_user(self, user: User, session: Session) -> None: 39 | assert utils.get_user(session, user.id) == user 40 | assert utils.get_user(session, 2) is None 41 | -------------------------------------------------------------------------------- /tests/test_weekview.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | import pytest 3 | 4 | from app.routers.event import create_event 5 | from app.routers.weekview import get_week_dates 6 | 7 | 8 | def create_weekview_event(events, session, user): 9 | for event in events: 10 | create_event( 11 | db=session, 12 | title='test', 13 | start=event.start, 14 | end=event.end, 15 | owner_id=user.id, 16 | color=event.color 17 | ) 18 | 19 | 20 | def test_get_week_dates(weekdays, sunday): 21 | week_dates = list(get_week_dates(sunday)) 22 | for i in range(6): 23 | assert week_dates[i].strftime('%A') == weekdays[i] 24 | 25 | 26 | def test_weekview_day_names(session, user, client, weekdays): 27 | response = client.get("/week/2021-1-3") 28 | soup = BeautifulSoup(response.content, 'html.parser') 29 | day_divs = soup.find_all("div", {"class": 'day-name'}) 30 | for i in range(6): 31 | assert weekdays[i][:3].upper() in str(day_divs[i]) 32 | 33 | 34 | def test_weekview_day_dates(session, user, client, sunday): 35 | response = client.get("/week/2021-1-3") 36 | soup = BeautifulSoup(response.content, 'html.parser') 37 | day_divs = soup.find_all("span", {"class": 'date-nums'}) 38 | week_dates = list(get_week_dates(sunday)) 39 | for i in range(6): 40 | time_str = f'{week_dates[i].day} / {week_dates[i].month}' 41 | assert time_str in day_divs[i] 42 | 43 | 44 | @pytest.mark.parametrize( 45 | "date,event", 46 | [("2021-1-31", 'event1'), 47 | ("2021-1-31", 'event2'), 48 | ("2021-2-3", 'event3')] 49 | ) 50 | def test_weekview_html_events( 51 | event1, event2, event3, session, user, client, date, event 52 | ): 53 | create_weekview_event([event1, event2, event3], session=session, user=user) 54 | response = client.get(f"/week/{date}") 55 | soup = BeautifulSoup(response.content, 'html.parser') 56 | assert event in str(soup.find("div", {"id": event})) 57 | -------------------------------------------------------------------------------- /tests/test_whatsapp.py: -------------------------------------------------------------------------------- 1 | from app.routers import whatsapp 2 | 3 | 4 | def test_whatsapp_send(): 5 | """Test with a valid phone number and text. 6 | 7 | Redirects directly to the specified contact and the message will 8 | already be there (or to WhatsApp Web if the call is from the web). 9 | 10 | """ 11 | phone_number = "972536106106" 12 | message = 'Hello hello' 13 | expected = ("https://api.whatsapp.com/send?phone=972536106106&text=" 14 | "Hello+hello") 15 | assert whatsapp.make_link(phone_number, message) == expected 16 | 17 | 18 | def test_wrong_phone_number(): 19 | """Text with invalid phone number and valid text. 20 | 21 | Redirects you to a popup: The phone number shared via a link is incorrect. 22 | 23 | """ 24 | phone_number = "999999" 25 | message = 'Hello hello' 26 | expected = "https://api.whatsapp.com/send?phone=999999&text=Hello+hello" 27 | assert whatsapp.make_link(phone_number, message) == expected 28 | 29 | 30 | def test_no_message(): 31 | """Test with valid phone number and no text. 32 | 33 | Redirects to WhatsApp of the specified number. Write your own message. 34 | 35 | """ 36 | phone_number = "972536106106" 37 | message = '' 38 | expected = "https://api.whatsapp.com/send?phone=972536106106&text=" 39 | assert whatsapp.make_link(phone_number, message) == expected 40 | 41 | 42 | def test_no_number(): 43 | """Test with no phone number and valid text. 44 | 45 | Redirects to WhatsApp window. Choose someone from your own contact list. 46 | 47 | """ 48 | phone_number = "" 49 | message = 'Hello hello' 50 | expected = "https://api.whatsapp.com/send?phone=&text=Hello+hello" 51 | assert whatsapp.make_link(phone_number, message) == expected 52 | 53 | 54 | def test_end_to_end_testing(client): 55 | resp = client.get('/whatsapp?phone_number=972536106106&message=testing') 56 | assert resp.ok 57 | assert resp.json 58 | -------------------------------------------------------------------------------- /tests/test_zodiac.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from app.internal import zodiac 4 | 5 | DATE = date(2021, 3, 22) 6 | DATE2 = date(2021, 4, 10) 7 | 8 | 9 | def test_create_zodiac_object(): 10 | zodiac_fields = { 11 | 'name': 'aries', 12 | 'start_month': 3, 13 | 'start_day_in_month': 20, 14 | 'end_month': 4, 15 | 'end_day_in_month': 19, 16 | } 17 | 18 | result = zodiac.get_zodiac(zodiac_fields) 19 | assert result.name == 'aries' 20 | assert result.start_month == 3 21 | assert str(result) == "" 22 | 23 | 24 | def test_get_correct_zodiac_first_half_month(session, zodiac_sign): 25 | result = zodiac.get_zodiac_of_day(session, DATE) 26 | assert result.name == zodiac_sign.name 27 | 28 | 29 | def test_get_correct_zodiac_second_half_month(session, zodiac_sign): 30 | result = zodiac.get_zodiac_of_day(session, DATE2) 31 | assert result.name == zodiac_sign.name 32 | 33 | 34 | def test_get_correct_month_zodiac(session, zodiac_sign): 35 | result = zodiac.get_zodiac_of_month(session, DATE2) 36 | assert result.name == zodiac_sign.name 37 | 38 | 39 | def test_no_zodiac(session): 40 | result = zodiac.get_zodiac_of_month(session, DATE) 41 | assert result is None 42 | -------------------------------------------------------------------------------- /tests/user_fixture.py: -------------------------------------------------------------------------------- 1 | from collections import Generator 2 | 3 | import pytest 4 | from sqlalchemy.orm import Session 5 | 6 | from app.database.models import User 7 | from app.internal.utils import create_model, delete_instance 8 | 9 | 10 | @pytest.fixture 11 | def user(session: Session) -> Generator[User, None, None]: 12 | mock_user = create_model( 13 | session, User, 14 | username='test_username', 15 | password='test_password', 16 | email='test.email@gmail.com', 17 | language_id=1, 18 | ) 19 | yield mock_user 20 | delete_instance(session, mock_user) 21 | 22 | 23 | @pytest.fixture 24 | def sender(session: Session) -> Generator[User, None, None]: 25 | mock_user = create_model( 26 | session, User, 27 | username='sender_username', 28 | password='sender_password', 29 | email='sender.email@gmail.com', 30 | language_id=1, 31 | ) 32 | yield mock_user 33 | delete_instance(session, mock_user) 34 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | 4 | def create_model(session: Session, model_class, **kw): 5 | instance = model_class(**kw) 6 | session.add(instance) 7 | session.commit() 8 | return instance 9 | 10 | 11 | def delete_instance(session: Session, instance): 12 | session.delete(instance) 13 | session.commit() 14 | -------------------------------------------------------------------------------- /tests/zodiac_fixture.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy.orm import Session 3 | 4 | from app.database.models import Zodiac 5 | from app.internal.utils import create_model, delete_instance 6 | 7 | 8 | @pytest.fixture 9 | def zodiac_sign(session: Session) -> Zodiac: 10 | zodiac = create_model( 11 | session, Zodiac, 12 | name="aries", 13 | start_month=3, 14 | start_day_in_month=20, 15 | end_month=4, 16 | end_day_in_month=19, 17 | ) 18 | yield zodiac 19 | delete_instance(session, zodiac) 20 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist=True 3 | envlist = cov, flake8 4 | 5 | [testenv] 6 | basepython = python3 7 | deps = -rrequirements.txt 8 | 9 | 10 | [testenv:rep] 11 | commands = 12 | pytest --cov=app --cov-report=html 13 | 14 | 15 | [testenv:cov] 16 | commands = 17 | pytest --cov=app 18 | 19 | 20 | [testenv:flake8] 21 | deps = flake8 22 | commands = 23 | flake8 app 24 | flake8 tests 25 | 26 | 27 | # pytest Configuration 28 | [pytest] 29 | junit_family = xunit2 30 | testpaths = tests 31 | filterwarnings = 32 | ignore:.*'collections'.*'collections.abc'.*:DeprecationWarning 33 | ignore:Task.all_tasks() is deprecated, use asyncio.all_tasks().*:PendingDeprecationWarning 34 | 35 | # Flake8 Configuration 36 | [flake8] 37 | # gettext() adds _() to the global namespace. This lets flake recognize it. 38 | builtins = 39 | _, 40 | --------------------------------------------------------------------------------