From f6c619bd23f29df322aae27c058971ed8890350c Mon Sep 17 00:00:00 2001 From: Aviad Amar Date: Fri, 15 Jan 2021 22:09:28 +0100 Subject: [PATCH 001/115] Hello Wrold --- .gitignore | 3 +++ app/main.py | 13 ++++++++++--- app/static/style.css | 16 ++++++++++++++++ app/templates/home.html | 11 ----------- 4 files changed, 29 insertions(+), 14 deletions(-) delete mode 100644 app/templates/home.html diff --git a/.gitignore b/.gitignore index b6e47617..1ffc615e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ __pycache__/ # Distribution / packaging .Python build/ +Scripts/ +include/ develop-eggs/ dist/ downloads/ @@ -26,6 +28,7 @@ share/python-wheels/ .installed.cfg *.egg MANIFEST +pyvenv.cfg # PyInstaller # Usually these files are written by a python script from a template diff --git a/app/main.py b/app/main.py index dfbf4ca2..8431a58f 100644 --- a/app/main.py +++ b/app/main.py @@ -1,15 +1,22 @@ +import uvicorn from fastapi import FastAPI, Request from fastapi.templating import Jinja2Templates - +from fastapi.staticfiles import StaticFiles app = FastAPI() +app.mount("/static", StaticFiles(directory="static"), name="static") + templates = Jinja2Templates(directory="templates") @app.get("/") -def home(request: Request): - return templates.TemplateResponse("home.html", { +async def home(request: Request): + return templates.TemplateResponse("home.j2", { "request": request, "message": "Hello, World!" }) + + +if __name__ == "__main__": + uvicorn.run(app) diff --git a/app/static/style.css b/app/static/style.css index e69de29b..c695452d 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -0,0 +1,16 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-family: "Assistant", sans-serif; + font-weight: 400; + font-size: 62.5%; /*16px / 10px = 62.5% -> 1rem = 10px*/ + line-height: 1.7; +} + +body { + background-color: red; +} diff --git a/app/templates/home.html b/app/templates/home.html deleted file mode 100644 index 81fece4f..00000000 --- a/app/templates/home.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - Calendar - - - {{message}} - - \ No newline at end of file From 1ee94e44665cd7c056fa7728582b6dd9777c228d Mon Sep 17 00:00:00 2001 From: Aviad Amar Date: Fri, 15 Jan 2021 22:16:49 +0100 Subject: [PATCH 002/115] Creating Html Template --- app/templates/home.j2 | 5 +++++ app/templates/layout.j2 | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 app/templates/home.j2 create mode 100644 app/templates/layout.j2 diff --git a/app/templates/home.j2 b/app/templates/home.j2 new file mode 100644 index 00000000..8ceb43be --- /dev/null +++ b/app/templates/home.j2 @@ -0,0 +1,5 @@ +{% extends 'layout.j2' %} + +{% block body %} +

{{message}}

+{% endblock %} \ No newline at end of file diff --git a/app/templates/layout.j2 b/app/templates/layout.j2 new file mode 100644 index 00000000..fa266955 --- /dev/null +++ b/app/templates/layout.j2 @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + Calendar + + + + {% block body %} + {% endblock %} + + + \ No newline at end of file From 82249b571c0764d3fc4a546b24cdb72b45be0bff Mon Sep 17 00:00:00 2001 From: Aviad Amar Date: Sun, 17 Jan 2021 01:54:16 +0100 Subject: [PATCH 003/115] Calendar dates calculation intigrating with html, HTML\CSS improvments. --- app/List[List] | 0 app/List[int] | 0 app/calendar_grid.py | 58 ++++++++++++++++++++++++++++-------- app/main.py | 18 ++++++----- app/static/style.css | 6 ---- app/templates/monthview.html | 37 ++++++++++------------- 6 files changed, 71 insertions(+), 48 deletions(-) create mode 100644 app/List[List] create mode 100644 app/List[int] diff --git a/app/List[List] b/app/List[List] new file mode 100644 index 00000000..e69de29b diff --git a/app/List[int] b/app/List[int] new file mode 100644 index 00000000..e69de29b diff --git a/app/calendar_grid.py b/app/calendar_grid.py index d75cdd56..69636a3c 100644 --- a/app/calendar_grid.py +++ b/app/calendar_grid.py @@ -1,17 +1,51 @@ -from calendar import monthrange -from typing import Tuple -from datetime import datetime +import calendar +import datetime +from typing import List, Tuple -DAYS_OF_THE_WEEK = ['Monday', 'Tuesday', 'Wednesday', - 'Thursday', 'Friday', 'Saturday', 'Sunday'] +DAYS_OF_THE_WEEK: List[str] = ['Monday', 'Tuesday', 'Wednesday', + 'Thursday', 'Friday', 'Saturday', 'Sunday'] -def get_day_name(date: datetime) -> Tuple[str, int]: - """Returns the name of a day for a given date and its numeric value""" - day = date.weekday() - return DAYS_OF_THE_WEEK[day], day +def get_date_as_string(date: datetime) -> str: + return date.strftime("%A %B %b %Y").split() -def get_month_days(year: int, month: int) -> Tuple[int, int]: - """Returns the first and last day in a given month and year.""" - return monthrange(year, month) +def get_next_date(date): + """Generate date objects from a starting given date.""" + while True: + next_day = date + datetime.timedelta(days=1) + yield next_day + date = next_day + + +def get_day_n_before(date: datetime, n: int) -> datetime: + return date - datetime.timedelta(days=n) + + +def get_n_days(date: datetime, n: int) -> List[datetime]: + g = get_next_date(date) + days = [] + for day in range(n): + days.append(next(g)) + return days + + +def arrange_weeks(dates: List, n: int) -> List[List]: + """Returns a list of lists with length of n for a given list""" + weeks = [] + week = [] + for day in dates: + if len(week) > 0 and len(week) % n - 1 == 0: + week.append(day) + weeks.append(week) + week = [] + else: + week.append(day) + return weeks + + +def get_month_block(date) -> List[List]: + days = calendar.monthrange(date.year, date.month) + before = get_day_n_before(date, days[0] + 1) + cal = get_n_days(before, 1000) + return arrange_weeks(cal, 7) diff --git a/app/main.py b/app/main.py index 8d8c70b3..d110b9f4 100644 --- a/app/main.py +++ b/app/main.py @@ -1,15 +1,14 @@ +import datetime + import uvicorn from fastapi import FastAPI, Request from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates -from datetime import datetime -import calendar_grid +import calendar_grid app = FastAPI() - app.mount("/static", StaticFiles(directory="static"), name="static") - templates = Jinja2Templates(directory="templates") @@ -23,15 +22,18 @@ def home(request: Request): @app.get("/monthview") async def monthview(request: Request): - date = datetime.now() + date = datetime.date.today() return templates.TemplateResponse("monthview.html", { "request": request, - "dates": { + "calendar": { 'date': date, - 'month_days': calendar_grid.get_month_days(date.year, date.month), + 'strf_date': calendar_grid.get_date_as_string(date), 'days_of_the_week': calendar_grid.DAYS_OF_THE_WEEK, - 'day': calendar_grid.get_day_name(date), + 'month_block': calendar_grid.get_month_block( + datetime.date(date.year, date.month, 1) + ) }}) + if __name__ == "__main__": uvicorn.run(app) diff --git a/app/static/style.css b/app/static/style.css index 6c640c16..b2206ae8 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -45,18 +45,12 @@ nav { grid-row: 1/3; font-size: 2.8rem; padding-left: 0.5rem; - transition: width 1s; - position: relative; } .menu div:hover { color: #FFDE4D; } -nav:hover { - width: 40rem; - text-align: left; -} .menu { position: sticky; diff --git a/app/templates/monthview.html b/app/templates/monthview.html index e5a5e36c..f3f96661 100644 --- a/app/templates/monthview.html +++ b/app/templates/monthview.html @@ -21,19 +21,20 @@
-
10 OCTOBER 2021
+
{{calendar['date'].day}} {{ calendar['strf_date'][1].upper() }} {{calendar['date'].year + }}
Milano 4 oc
Search
Month
- {% for day in dates['days_of_the_week'] %} - {% if day == dates['day'][0] %} + {% for day in calendar['days_of_the_week'] %} + {% if day == calendar['strf_date'][0] %}
{{ day.upper() }}
{% else %}
{{ day.upper() }}
@@ -44,37 +45,29 @@

PYLENDER

- {% for month in range(dates['date'].month, dates['date'].month + 2) %} - {% for day in range(dates['month_days'][0], dates['month_days'][1] + 1) %} - {% if day == dates['date'].day and month == dates['date'].month %} + {% for week in calendar['month_block'] %} + {% for i in range(week|length) %} + {% if week[i] == calendar['date'] %}
-
{{day}}
+
{{week[i].day}}
daily event
6PM Meeting with Ode
9PM Meeting with Michele
- {% elif day in (9, 10, 17, 23, 24, 30, 31) %} -
-
{{day}}
-
- {% elif day in (13, 29) %} + {% elif week[i].day == 1 %}
-
{{day}}
+
{{week[i].strftime('%A %b %y')}}
daily event
6PM Meeting with yam
7PM Meeting with Sagi
- - {% elif day in (18, ) %} -
-
{{day}}
-
daily event
-
6PM Meeting with yam
-
7PM Meeting with Sagi
+ {% elif i in (5, 6) and not (week[i] == calendar['date']) %} +
+
{{week[i].day}}
{% else %}
-
{{day}}
+
{{week[i].day}}
{% endif %} {% endfor %} From 178e9049991cd0cc043d40eb43faa58e869581ea Mon Sep 17 00:00:00 2001 From: Aviad Amar Date: Sun, 17 Jan 2021 17:52:54 +0100 Subject: [PATCH 004/115] Tests for calender_grid.py, changing function according to currections, fixing bugs --- app/List[List] | 0 app/List[int] | 0 app/calendar_grid.py | 51 ------------------------------- app/main.py | 22 +++++++++++--- app/routers/calendar_grid.py | 47 +++++++++++++++++++++++++++++ app/templates/monthview.html | 2 +- requirements.txt | 2 ++ tests/conftest.py | 8 +++++ tests/test_calender_grid.py | 58 ++++++++++++++++++++++++++++++++++++ 9 files changed, 134 insertions(+), 56 deletions(-) delete mode 100644 app/List[List] delete mode 100644 app/List[int] delete mode 100644 app/calendar_grid.py create mode 100644 app/routers/calendar_grid.py create mode 100644 tests/conftest.py create mode 100644 tests/test_calender_grid.py diff --git a/app/List[List] b/app/List[List] deleted file mode 100644 index e69de29b..00000000 diff --git a/app/List[int] b/app/List[int] deleted file mode 100644 index e69de29b..00000000 diff --git a/app/calendar_grid.py b/app/calendar_grid.py deleted file mode 100644 index 69636a3c..00000000 --- a/app/calendar_grid.py +++ /dev/null @@ -1,51 +0,0 @@ -import calendar -import datetime -from typing import List, Tuple - -DAYS_OF_THE_WEEK: List[str] = ['Monday', 'Tuesday', 'Wednesday', - 'Thursday', 'Friday', 'Saturday', 'Sunday'] - - -def get_date_as_string(date: datetime) -> str: - return date.strftime("%A %B %b %Y").split() - - -def get_next_date(date): - """Generate date objects from a starting given date.""" - while True: - next_day = date + datetime.timedelta(days=1) - yield next_day - date = next_day - - -def get_day_n_before(date: datetime, n: int) -> datetime: - return date - datetime.timedelta(days=n) - - -def get_n_days(date: datetime, n: int) -> List[datetime]: - g = get_next_date(date) - days = [] - for day in range(n): - days.append(next(g)) - return days - - -def arrange_weeks(dates: List, n: int) -> List[List]: - """Returns a list of lists with length of n for a given list""" - weeks = [] - week = [] - for day in dates: - if len(week) > 0 and len(week) % n - 1 == 0: - week.append(day) - weeks.append(week) - week = [] - else: - week.append(day) - return weeks - - -def get_month_block(date) -> List[List]: - days = calendar.monthrange(date.year, date.month) - before = get_day_n_before(date, days[0] + 1) - cal = get_n_days(before, 1000) - return arrange_weeks(cal, 7) diff --git a/app/main.py b/app/main.py index d110b9f4..3449a0e9 100644 --- a/app/main.py +++ b/app/main.py @@ -5,7 +5,7 @@ from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates -import calendar_grid +import routers.calendar_grid app = FastAPI() app.mount("/static", StaticFiles(directory="static"), name="static") @@ -20,6 +20,20 @@ def home(request: Request): }) +@app.get("/profile") +def profile(request: Request): + + # Get relevant data from database + upcouming_events = range(5) + current_username = "Chuck Norris" + + return templates.TemplateResponse("profile.html", { + "request": request, + "username": current_username, + "events": upcouming_events + }) + + @app.get("/monthview") async def monthview(request: Request): date = datetime.date.today() @@ -27,9 +41,9 @@ async def monthview(request: Request): "request": request, "calendar": { 'date': date, - 'strf_date': calendar_grid.get_date_as_string(date), - 'days_of_the_week': calendar_grid.DAYS_OF_THE_WEEK, - 'month_block': calendar_grid.get_month_block( + 'strf_date': routers.calendar_grid.get_date_as_string(date), + 'days_of_the_week': routers.calendar_grid.DAYS_OF_THE_WEEK, + 'month_block': routers.calendar_grid.get_month_block( datetime.date(date.year, date.month, 1) ) }}) diff --git a/app/routers/calendar_grid.py b/app/routers/calendar_grid.py new file mode 100644 index 00000000..1b8ea0f2 --- /dev/null +++ b/app/routers/calendar_grid.py @@ -0,0 +1,47 @@ +import calendar +import itertools +from datetime import datetime, timedelta + +from typing import Any, Iterator, Generator, List, Tuple + +DAYS_OF_THE_WEEK: List[str] = ['Monday', 'Tuesday', 'Wednesday', + 'Thursday', 'Friday', 'Saturday', 'Sunday'] +CALENDAR = calendar.Calendar(0) + + +def get_date_as_string(date: datetime) -> List[str]: + """Returns list represent date as string.""" + return date.strftime("%A %B %b %Y").split() + + +def get_next_date(date: datetime) -> Generator[datetime, None, None]: + """Generate date objects from a starting given date.""" + yield from (date + timedelta(days=i) for i in itertools.count(start=1)) + + +def get_date_before_n_days(date: datetime, n: int) -> datetime: + """Returns the date before n days.""" + return date - timedelta(days=n) + + +def get_first_day_month_block(date): + return list(CALENDAR.itermonthdates(date.year, date.month))[0] + + +def get_n_days(date: datetime, n: int) -> Iterator[datetime]: + """Generate n dates from a starting given date.""" + next_date_gen = get_next_date(date) + yield from itertools.islice(next_date_gen, n) + + +def split_list_to_lists(dates: List[Any], length: int) -> List[List[Any]]: + """Return a 2D list with inner lists length of given size.""" + return [dates[i:i + length] for i in range(0, len(dates), length)] + + +def get_month_block(date: datetime, n: int = 100) -> List[List]: + """Returns a 2D list represent a n days calendar from current month.""" + start = get_first_day_month_block(date) + cal = list(get_n_days(start, n)) + cal.insert(0, start) + return split_list_to_lists(cal, 7) diff --git a/app/templates/monthview.html b/app/templates/monthview.html index f3f96661..c585a1cc 100644 --- a/app/templates/monthview.html +++ b/app/templates/monthview.html @@ -56,7 +56,7 @@

PYLENDAR

{% elif week[i].day == 1 %}
-
{{week[i].strftime('%A %b %y')}}
+
{{week[i].strftime('%d %b %y')}}
daily event
6PM Meeting with yam
7PM Meeting with Sagi
diff --git a/requirements.txt b/requirements.txt index ffed4ae5..84a07740 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ atomicwrites==1.4.0 attrs==20.3.0 click==7.1.2 colorama==0.4.4 +coverage==5.3.1 fastapi==0.63.0 h11==0.12.0 h2==4.0.0 @@ -19,6 +20,7 @@ py==1.10.0 pydantic==1.7.3 pyparsing==2.4.7 pytest==6.2.1 +pytest-cov==2.10.1 SQLAlchemy==1.3.22 starlette==0.13.6 toml==0.10.2 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..213dd07c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,8 @@ +import calendar + +import pytest + + +@pytest.fixture +def Calendar(): + return calendar.Calendar(0) diff --git a/tests/test_calender_grid.py b/tests/test_calender_grid.py new file mode 100644 index 00000000..1b4e4860 --- /dev/null +++ b/tests/test_calender_grid.py @@ -0,0 +1,58 @@ +import datetime + +from app.routers.calendar_grid import (get_date_as_string, get_date_before_n_days, + get_first_day_month_block, get_month_block, + get_n_days, get_next_date, split_list_to_lists) + +DATE = datetime.date(1988, 5, 3) +N_DAYS = 3 +N_DAYS_BEFORE = datetime.date(1988, 4, 30) +NEXT_N_DAYS = [ + datetime.date(1988, 5, 4), + datetime.date(1988, 5, 5), + datetime.date(1988, 5, 6) +] + + +class TestCalendarGrid: + + @staticmethod + def test_get_date_as_string(): + assert get_date_as_string(DATE) == [ + 'Tuesday', 'May', 'May', '1988'] + + @staticmethod + def test_get_next_date(): + next_day = DATE + datetime.timedelta(days=1) + next_day_generator = get_next_date(DATE) + assert next(next_day_generator) == next_day + + @staticmethod + def test_get_date_before_n_days(): + assert get_date_before_n_days(DATE, N_DAYS) == N_DAYS_BEFORE + + @staticmethod + def test_get_n_days(): + next_n_dates = get_n_days(DATE, N_DAYS) + for i in range(N_DAYS): + assert next(next_n_dates) == NEXT_N_DAYS[i] + + @staticmethod + def test_get_first_day_month_block(Calendar): + assert get_first_day_month_block( + DATE) == list(Calendar.itermonthdates(DATE.year, DATE.month))[0] + + @staticmethod + def test_split_list_to_lists(): + sub_length = 7 + total_length = 20 + list_before_split = list(range(total_length)) + list_after_split = [list_before_split[i: i + sub_length] + for i in range(0, len(list_before_split), sub_length)] + assert split_list_to_lists( + list_before_split, sub_length) == list_after_split + + @staticmethod + def test_get_month_block(Calendar): + test = split_list_to_lists(list(Calendar.itermonthdates(1988, 5)), 7) + assert get_month_block(DATE, n=41) == test From f385c56a412b8f8a1e42adcc8a7488ab3d90a651 Mon Sep 17 00:00:00 2001 From: Aviad Amar Date: Mon, 18 Jan 2021 12:47:55 +0100 Subject: [PATCH 005/115] linting bug fixing --- .vscode/settings.json | 3 + app/main.py | 4 +- app/routers/calendar_grid.py | 8 ++- app/static/style.css | 65 +++++-------------- .../{monthview.html => calendar.html} | 0 tests/test_calender_grid.py | 42 ++++++------ 6 files changed, 51 insertions(+), 71 deletions(-) create mode 100644 .vscode/settings.json rename app/templates/{monthview.html => calendar.html} (100%) diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..f2d90cbd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.linting.enabled": true +} \ No newline at end of file diff --git a/app/main.py b/app/main.py index 3449a0e9..e81110c1 100644 --- a/app/main.py +++ b/app/main.py @@ -34,10 +34,10 @@ def profile(request: Request): }) -@app.get("/monthview") +@app.get("/calendar") async def monthview(request: Request): date = datetime.date.today() - return templates.TemplateResponse("monthview.html", { + return templates.TemplateResponse("calendar.html", { "request": request, "calendar": { 'date': date, diff --git a/app/routers/calendar_grid.py b/app/routers/calendar_grid.py index 1b8ea0f2..54f958a9 100644 --- a/app/routers/calendar_grid.py +++ b/app/routers/calendar_grid.py @@ -2,11 +2,13 @@ import itertools from datetime import datetime, timedelta -from typing import Any, Iterator, Generator, List, Tuple +from typing import Any, Iterator, Generator, List DAYS_OF_THE_WEEK: List[str] = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] CALENDAR = calendar.Calendar(0) +DISPLAY_BLOCK = 100 +WEEK_DAYS = 7 def get_date_as_string(date: datetime) -> List[str]: @@ -39,9 +41,9 @@ def split_list_to_lists(dates: List[Any], length: int) -> List[List[Any]]: return [dates[i:i + length] for i in range(0, len(dates), length)] -def get_month_block(date: datetime, n: int = 100) -> List[List]: +def get_month_block(date: datetime, n: int = DISPLAY_BLOCK) -> List[List]: """Returns a 2D list represent a n days calendar from current month.""" start = get_first_day_month_block(date) cal = list(get_n_days(start, n)) cal.insert(0, start) - return split_list_to_lists(cal, 7) + return split_list_to_lists(cal, WEEK_DAYS) diff --git a/app/static/style.css b/app/static/style.css index b2206ae8..38997dc4 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -31,11 +31,12 @@ a { text-decoration: none; color: inherit; } + /* Grids */ /* -> Website Structure */ .container { display: grid; - grid-template-columns: minmax(4.5rem, auto) 1fr; /* The Header Hight */ + grid-template-columns: minmax(4.5rem, auto) 1fr; grid-auto-rows: minmax(3rem, auto); } @@ -47,10 +48,7 @@ nav { padding-left: 0.5rem; } -.menu div:hover { - color: #FFDE4D; -} - +.menu div:hover {color: #FFDE4D;} .menu { position: sticky; @@ -58,9 +56,7 @@ nav { flex-direction: column; } -img { - fill: #F7F7F7; -} +img {fill: #F7F7F7;} header { background-color: #F7F7F7; @@ -93,13 +89,7 @@ header { margin-top: 1rem; } -/* -> The calendar grid */ -.calender-grid { - display: grid; - grid-template-columns: repeat(7, 1fr); -} - -/* -> Main structure */ +/* -> Main Grid */ .main-grid { display: grid; width: 100%; @@ -108,8 +98,10 @@ header { padding: 0 1rem 0 1rem; } -.week { - border: 3px solid black; +/* -> The calendar grid */ +.calender-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); } #event-menu { @@ -118,7 +110,6 @@ header { grid-row: 1/3; } - .calendar-grid div { font-weight: 900; height: 12rem; @@ -127,7 +118,6 @@ header { .day { display: flex; - /* flex-flow: column wrap; */ flex-direction: column; height: 12rem; @@ -137,7 +127,6 @@ header { } .day:hover { - /* font-size: 3rem; */ border: 0.3rem solid #222831; color: #F24726; transition: border 0.1s; @@ -181,40 +170,22 @@ header { } /* Text Colors */ -.t-yellow { - color: #FFDE4D; -} +.t-yellow {color: #FFDE4D;} -.t-gray { - color: #adb5bd; -} +.t-gray {color: #adb5bd;} -.t-very-lightgray { - color: #F7F7F7; -} +.t-very-lightgray {color: #F7F7F7;} -.t-darkblue { - color: #222831; -} +.t-darkblue {color: #222831;} /* Borders */ -.b-dashed-darkblue { - border: 2px dashed #222831; -} +.b-dashed-darkblue {border: 2px dashed #222831;} -.b-darkblue { - border: 2px solid #222831; -} +.b-darkblue {border: 2px solid #222831;} -.uline-yellow { - border-bottom: 4px solid #FFDE4D; -} +.uline-yellow {border-bottom: 4px solid #FFDE4D;} /* Background Color */ -.bg-darkblue { - background-color: #222831; -} +.bg-darkblue {background-color: #222831;} -.bg-red { - background-color: #F24726; -} \ No newline at end of file +.bg-red {background-color: #F24726;} \ No newline at end of file diff --git a/app/templates/monthview.html b/app/templates/calendar.html similarity index 100% rename from app/templates/monthview.html rename to app/templates/calendar.html diff --git a/tests/test_calender_grid.py b/tests/test_calender_grid.py index 1b4e4860..d159afe8 100644 --- a/tests/test_calender_grid.py +++ b/tests/test_calender_grid.py @@ -1,8 +1,6 @@ import datetime -from app.routers.calendar_grid import (get_date_as_string, get_date_before_n_days, - get_first_day_month_block, get_month_block, - get_n_days, get_next_date, split_list_to_lists) +import app.routers.calendar_grid as cg DATE = datetime.date(1988, 5, 3) N_DAYS = 3 @@ -15,44 +13,50 @@ class TestCalendarGrid: - @staticmethod def test_get_date_as_string(): - assert get_date_as_string(DATE) == [ + assert cg.get_date_as_string(DATE) == [ 'Tuesday', 'May', 'May', '1988'] @staticmethod def test_get_next_date(): next_day = DATE + datetime.timedelta(days=1) - next_day_generator = get_next_date(DATE) + next_day_generator = cg.get_next_date(DATE) assert next(next_day_generator) == next_day @staticmethod def test_get_date_before_n_days(): - assert get_date_before_n_days(DATE, N_DAYS) == N_DAYS_BEFORE + assert cg.get_date_before_n_days(DATE, N_DAYS) == N_DAYS_BEFORE @staticmethod def test_get_n_days(): - next_n_dates = get_n_days(DATE, N_DAYS) + next_n_dates = cg.get_n_days(DATE, N_DAYS) for i in range(N_DAYS): assert next(next_n_dates) == NEXT_N_DAYS[i] @staticmethod def test_get_first_day_month_block(Calendar): - assert get_first_day_month_block( - DATE) == list(Calendar.itermonthdates(DATE.year, DATE.month))[0] + assert ( + cg.get_first_day_month_block(DATE) + == list(Calendar.itermonthdates(DATE.year, DATE.month))[0] + ) @staticmethod def test_split_list_to_lists(): - sub_length = 7 - total_length = 20 - list_before_split = list(range(total_length)) - list_after_split = [list_before_split[i: i + sub_length] - for i in range(0, len(list_before_split), sub_length)] - assert split_list_to_lists( - list_before_split, sub_length) == list_after_split + s_length = 7 + t_length = 20 + list_before_split = list(range(t_length)) + list_after_split = [ + list_before_split[i: i + s_length] + for i in range(0, len(list_before_split), s_length) + ] + assert ( + cg.split_list_to_lists(list_before_split, s_length) + == list_after_split + ) @staticmethod def test_get_month_block(Calendar): - test = split_list_to_lists(list(Calendar.itermonthdates(1988, 5)), 7) - assert get_month_block(DATE, n=41) == test + test = cg.split_list_to_lists( + list(Calendar.itermonthdates(1988, 5)), 7) + assert cg.get_month_block(DATE, n=41) == test From c2aa5192425d91d376896cd66fdccb1ffb28dc88 Mon Sep 17 00:00:00 2001 From: Aviad Amar Date: Wed, 20 Jan 2021 00:15:14 +0100 Subject: [PATCH 006/115] before merge with develop --- .gitignore | 1 + app/main.py | 23 ------- app/routers/calendar.py | 25 ++++++++ app/static/scripts.js | 19 ++++++ app/static/style.css | 99 +++++++++++++++++++++--------- app/templates/calendar.html | 119 +++++++++++++++++++----------------- app/templates/layout.html | 3 +- 7 files changed, 181 insertions(+), 108 deletions(-) create mode 100644 app/routers/calendar.py create mode 100644 app/static/scripts.js diff --git a/.gitignore b/.gitignore index 1ffc615e..6f4a041c 100644 --- a/.gitignore +++ b/.gitignore @@ -112,6 +112,7 @@ venv/ ENV/ env.bak/ venv.bak/ +.vscode/ # Spyder project settings .spyderproject diff --git a/app/main.py b/app/main.py index e81110c1..28532718 100644 --- a/app/main.py +++ b/app/main.py @@ -1,11 +1,7 @@ -import datetime - -import uvicorn from fastapi import FastAPI, Request from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates -import routers.calendar_grid app = FastAPI() app.mount("/static", StaticFiles(directory="static"), name="static") @@ -32,22 +28,3 @@ def profile(request: Request): "username": current_username, "events": upcouming_events }) - - -@app.get("/calendar") -async def monthview(request: Request): - date = datetime.date.today() - return templates.TemplateResponse("calendar.html", { - "request": request, - "calendar": { - 'date': date, - 'strf_date': routers.calendar_grid.get_date_as_string(date), - 'days_of_the_week': routers.calendar_grid.DAYS_OF_THE_WEEK, - 'month_block': routers.calendar_grid.get_month_block( - datetime.date(date.year, date.month, 1) - ) - }}) - - -if __name__ == "__main__": - uvicorn.run(app) diff --git a/app/routers/calendar.py b/app/routers/calendar.py new file mode 100644 index 00000000..353a318e --- /dev/null +++ b/app/routers/calendar.py @@ -0,0 +1,25 @@ +import datetime + +from app.routers import calendar_gridcg as cg +from app.dependencies import templates + +from fastapi import Request + + +@router.get("/calendar") +async def calendar(request: Request): + date = datetime.date.today() + return templates.TemplateResponse( + "calendar.html", + { + "request": request, + "calendar": { + 'date': date, + 'strf_date': cg.get_date_as_string(date), + 'days_of_the_week': cg.DAYS_OF_THE_WEEK, + 'month_block': cg.get_month_block( + datetime.date(date.year, date.month, 1) + ) + } + } + ) diff --git a/app/static/scripts.js b/app/static/scripts.js new file mode 100644 index 00000000..5a97c908 --- /dev/null +++ b/app/static/scripts.js @@ -0,0 +1,19 @@ +document.addEventListener( + 'DOMContentLoaded', + function () { + var all_days = document.querySelectorAll('.day'), i; + + for (i = 0; i < all_days.length; ++i) { + all_days[i].onclick = function () { + var daily_event = document.querySelector("#daily-event"); + if (daily_event.style.flex === '0 1 30%') { + daily_event.style.opacity = '0'; + daily_event.style.flex = '0 0 0'; + } + else { + daily_event.style.opacity = '1'; + daily_event.style.flex = '0 1 30%'; + } + }; + } + }); \ No newline at end of file diff --git a/app/static/style.css b/app/static/style.css index 38997dc4..36336a42 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -24,7 +24,7 @@ html { text-rendering: optimizeLegibility; scroll-behavior: smooth; background-color: #F7F7F7; - color: #222831 + color: #222831; } a { @@ -37,7 +37,6 @@ a { .container { display: grid; grid-template-columns: minmax(4.5rem, auto) 1fr; - grid-auto-rows: minmax(3rem, auto); } nav { @@ -48,27 +47,28 @@ nav { padding-left: 0.5rem; } -.menu div:hover {color: #FFDE4D;} - .menu { - position: sticky; + position: fixed; display: flex; flex-direction: column; } +.menu div:hover {color: #FFDE4D;} + img {fill: #F7F7F7;} header { - background-color: #F7F7F7; z-index: 1; + position: sticky; + top: 0; display: grid; grid-template-columns:80% 20%; margin: 0 1rem 0 1rem; + background-color: #F7F7F7; } #header-bottom { display: none; - grid-column: 1/3 } #logo-div { @@ -82,37 +82,45 @@ header { float: left; } +/* Main Element Grid */ +main { + display: flex; + flex-flow: row wrap; +} + +.calendar { + flex: 1; +} + +#daily-event { + flex: 0 0 0; + opacity: 0; + transition: all 1s; +} + .calender-days-titles { + z-index: 1; + position: sticky; + top: 6rem; + align-self: flex-start; grid-column: 1/3; display: grid; grid-template-columns: repeat(7, 1fr); - margin-top: 1rem; -} - -/* -> Main Grid */ -.main-grid { - display: grid; - width: 100%; - grid-template-columns: 1fr; - grid-template-rows: 1fr; - padding: 0 1rem 0 1rem; + margin: 1rem 1rem 0 1rem; + background-color: #F7F7F7; } -/* -> The calendar grid */ +/* The Calendar Grid */ .calender-grid { + flex: 1; display: grid; grid-template-columns: repeat(7, 1fr); -} - -#event-menu { - display: none; - grid-column: 2/3; - grid-row: 1/3; + grid-auto-rows: minmax(12rem, auto); + margin: 0 1rem 0 1rem; } .calendar-grid div { font-weight: 900; - height: 12rem; font-size: 2rem; } @@ -120,7 +128,6 @@ header { display: flex; flex-direction: column; - height: 12rem; padding: 0 1rem 0 1rem; border: 1px solid #e9ecef; font-size: 1.2rem; @@ -128,10 +135,13 @@ header { .day:hover { border: 0.3rem solid #222831; - color: #F24726; transition: border 0.1s; } +.day:hover .day-number{ + color: #F24726; +} + .day-number { font-weight: 900; font-size: 2rem; @@ -146,7 +156,36 @@ header { background-color: #e9ecef; } -.daily-event { +/* Dates Navigation */ +.dates-navigation { + position: fixed; +} + +/* Events - Rotation*/ +.month-event { + position: relative; + perspective: 150rem; + -moz-perspective: 150rem; + height: 2.4rem; +} + +.month-event div{ + height: 2.4rem; + width: 100%; + transition: all 0.3s ease; + position: absolute; + top: 0; + left: 0; + backface-visibility: hidden; +} + +.back {transform: rotateX(180deg);} + +.month-event:hover .front{transform: rotateX(-180deg);} + +.month-event:hover .back{transform: rotateX(0);} + +.daily { font-weight: 700; border-radius: 0.4rem; padding: 0 0.5rem 0 0.5rem; @@ -188,4 +227,6 @@ header { /* Background Color */ .bg-darkblue {background-color: #222831;} -.bg-red {background-color: #F24726;} \ No newline at end of file +.bg-red {background-color: #F24726;} + +.bg-lightgray {background-color: #e9ecef;} diff --git a/app/templates/calendar.html b/app/templates/calendar.html index c585a1cc..c948e1d4 100644 --- a/app/templates/calendar.html +++ b/app/templates/calendar.html @@ -13,67 +13,76 @@
-
+
- -
-
-
{{calendar['date'].day}} {{ calendar['strf_date'][1].upper() }} {{calendar['date'].year - }}
-
Milano 4 oc
-
- -
-
Search
-
Month
-
-
- {% for day in calendar['days_of_the_week'] %} - {% if day == calendar['strf_date'][0] %} -
{{ day.upper() }}
- {% else %} -
{{ day.upper() }}
- {% endif %} - {% endfor %} -
-
- -
-
- {% for week in calendar['month_block'] %} - {% for i in range(week|length) %} - {% if week[i] == calendar['date'] %} -
-
{{week[i].day}}
-
daily event
-
6PM Meeting with Ode
-
9PM Meeting with Michele
-
- {% elif week[i].day == 1 %} -
-
{{week[i].strftime('%d %b %y')}}
-
daily event
-
6PM Meeting with yam
-
7PM Meeting with Sagi
+
+
+
+
{{calendar['date'].day}} {{ calendar['strf_date'][1].upper() }} + {{calendar['date'].year + }}
+
Milano 4 oc
- {% elif i in (5, 6) and not (week[i] == calendar['date']) %} -
-
{{week[i].day}}
+ +
+
Search
+
Month
- {% else %} -
-
{{week[i].day}}
+
+
+
+
+ {% for day in calendar['days_of_the_week'] %} + {% if day == calendar['strf_date'][0] %} +
{{ day.upper() }}
+ {% else %} +
{{ day.upper() }}
+ {% endif %} + {% endfor %} +
+
+ {% for week in calendar['month_block'] %} + {% for i in range(week|length) %} + {% if week[i] == calendar['date'] %} +
+
{{week[i].day}}
+
+
daily front
+
more information
+
+
6PM Meeting with Ode
+
9PM Meeting with Michele
+
+ {% elif week[i].day == 1 %} +
+
{{week[i].strftime('%d %b %y')}}
+
+
daily front
+
more information
+
+
6PM Meeting with yam
+
7PM Meeting with Sagi
+
+ {% elif i in (5, 6) and not (week[i] == calendar['date']) %} +
+
{{week[i].day}}
+
+ {% else %} +
+
{{week[i].day}}
+
+ {% endif %} + {% endfor %} + {% endfor %} +
- {% endif %} - {% endfor %} - {% endfor %} -
-
event
-
+
lalalal lilili aviad aviad
+
+
{% endblock %} \ No newline at end of file diff --git a/app/templates/layout.html b/app/templates/layout.html index 74b1c2aa..6da15507 100644 --- a/app/templates/layout.html +++ b/app/templates/layout.html @@ -14,6 +14,7 @@ + Calendar @@ -21,7 +22,7 @@ {% block body %} - {% endblock %} + {% endblock %} \ No newline at end of file From 6f9a2ba7c7e0f84c9fa978331c93559f5fa907c1 Mon Sep 17 00:00:00 2001 From: Aviad Amar Date: Wed, 20 Jan 2021 23:28:38 +0100 Subject: [PATCH 007/115] Creating a Day object and days subclasses, changing all function and tests according to the new objects, Changing front to get information from Day objects, Re-arranging calendar.html grid stracture to be render by weeks, adding js functionality for day view section, adding css effects on daily event display --- .github/workflows/python-app.yml | 41 ---------- .gitignore | 2 + .vscode/settings.json | 3 - app/config.py.example | 10 --- app/main.py | 2 +- app/media/fake_user.png | Bin 0 -> 3556 bytes app/routers/calendar.py | 15 ++-- app/routers/calendar_grid.py | 124 ++++++++++++++++++++++++++----- app/static/grid_style.css | 43 +++++++---- app/static/js/grid_scripts.js | 2 +- app/templates/calendar.html | 61 ++++++--------- app/templates/layout.html | 2 +- tests/conftest.py | 8 +- tests/test_calendar_grid.py | 94 +++++++++++++++++++++++ tests/test_calender_grid.py | 62 ---------------- 15 files changed, 265 insertions(+), 204 deletions(-) delete mode 100644 .github/workflows/python-app.yml delete mode 100644 .vscode/settings.json delete mode 100644 app/config.py.example create mode 100644 app/media/fake_user.png create mode 100644 tests/test_calendar_grid.py delete mode 100644 tests/test_calender_grid.py diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml deleted file mode 100644 index dd168392..00000000 --- a/.github/workflows/python-app.yml +++ /dev/null @@ -1,41 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a single version of Python -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: Python application - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main, develop ] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 - with: - python-version: 3.9 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flake8 pytest pytest-cov - cp app/config.py.example app/config.py - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --show-source --statistics - # exit-zero treats all errors as warnings. - flake8 . --count --exit-zero --max-complexity=10 --statistics - - name: Test with pytest - run: | - pytest -vvv --junitxml=junit/test-results.xml --cov-report=xml --cov=app ./tests - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1.0.13 - with: - file: coverage.xml diff --git a/.gitignore b/.gitignore index 26c76b31..09ec541d 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ share/python-wheels/ MANIFEST pyvenv.cfg + # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. @@ -57,6 +58,7 @@ coverage.xml *.py,cover .hypothesis/ .pytest_cache/ +.github/* # Translations *.mo diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index f2d90cbd..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python.linting.enabled": true -} \ No newline at end of file diff --git a/app/config.py.example b/app/config.py.example deleted file mode 100644 index b4c8ccf2..00000000 --- a/app/config.py.example +++ /dev/null @@ -1,10 +0,0 @@ -# flake8: noqa - - -# DATABASE -DEVELOPMENT_DATABASE_STRING = "sqlite:///./dev.db" - -# MEDIA -MEDIA_DIRECTORY = 'media' -PICTURE_EXTENSION = '.png' -AVATAR_SIZE = (120, 120) diff --git a/app/main.py b/app/main.py index df083203..1ecd1c03 100644 --- a/app/main.py +++ b/app/main.py @@ -5,7 +5,7 @@ from app.database.database import engine from app.dependencies import ( MEDIA_PATH, STATIC_PATH, templates) -from app.routers import agenda, event, profile +from app.routers import agenda, calendar, event, profile models.Base.metadata.create_all(bind=engine) diff --git a/app/media/fake_user.png b/app/media/fake_user.png new file mode 100644 index 0000000000000000000000000000000000000000..bd856aaa02ed8fc75fb310971827e4734b8785d5 GIT binary patch literal 3556 zcmb7Hc{Ei2-yfP7HADtumo4GT*lOxaWp9cQ!&tvV2-%e=iIF8^DH^_E$Ox(Ip(q;L zgv1zs`$|4e%r-X$qqvmDZg=MdLBxwl+=+Tu^3xCQ#d3fmnGRuf~@FAzZs%q3rvo>tw zH1ovj`8TSa{hY&lNQPN(XO}uSh?t0+llJuT>bQMK9^Hm>bfos? zp3p*}V#>;nMZ9%Gh=>$cS8Ek+2~ov6-@Su4IXfRomd9o7r-k50+V#L)m?h(}uLQ!> z5YaKcM53G$(nkD`LY4(ss~f>9Q>H0tX(e?so}y%Br9y26=1Ci zeGQFQA@$mAp!kemJzJ%t_QCIa%Z0z=s zj#)$TkRp20n>YG{LqjZ*gPUP^_*#9BFqF&I))px)PGvIXmd2ZSF&WF7oXVryC(dQu zm5pBKii?Yz=Z=?yJ*})Xx3y&?d~c|$D=aPLRqrv%@`A=0_O|j^GSD}cq;}WY>HITq zeox#2k6OK|sK&wBY&KF_I-bU+e)s^V(4TH*&~?cYRJsqMA#l8PrYBoVODon#_dtUC zpvHwr+hI}oi*R3p96mf;G$0`0>*AtmSeTBB#&CW2uLyl~*eu+rP!J5>)CQH?i@8Cf z;ss9^Sfx@xYhdw4&e1nQLWIC*C{Hb&r@Olk5U7u5b!-qq_e`dqW&gObpPS32DV+aA z&_p|^#@^n35L^2T${jlmoqT&sZ0;kujr{kGL3>{mQVz!L>6k#pLueuIdwb)%JdM;v z+kX7eZe^jMhLD~bJdy|A_Kv;6%nfR$mKH=O$MGK8y)yFP928XKT`LYbz9=JX7#f~Gb)@GAA^pU${b#Wda8gIabHNFJ``NI1@D+epO{Fi zuRjiX&Z&4e?;O6q?5?Y;`}oNdlK@AKmPBdJ3Atai;MHGI#6&?QPJhq zzB6p1W^)V`pVV+O{h@K90IR>y7!m{c5F5*-uBrKaPoqcM&(Cjpim0jV0^qc~wSz<= zA#JGpihMkAT)YBOXI)*TT{OfN!kbF6S2m(v1+%y{TpJNBctgQ{24f#4Lzg`$8T2N0 zc6K%(Jr>XbK=%khOJif>;H?m;#KOWO*KXW6s>c^O=&whd@aK`%JRhWlIlT6LGQD9k zS`v%JI!8|iHn0cFY*CLa?p>!NOnptdYf@TUTVEYT!`mL0mGLPkC@gP8%}1EqAWoe+ zW$o;oYhBea0cr%?^YQm@zgqdWzC1s*#8+2OuQ;$!fxdjPX*^BAJk4j~T26_Uj!xX} z?yem+XjmR|I5aG51x$+Fdtx%>>Sv1YDINP7&;oolZcyXmJ>De_=K@BoA@_loH~`ev z+Tw+;n<;`WI9$^9c9f;&I~2T=MjNvHNfY-fRu$4V3bwZw&5xMk>s5>|@dYhy-3E`hBH^|GlbqA)g}zWmWH$J%o!ZeDg%&I4)|%@3Cn-NJJtxrx#gY;eA7lGyFsZZrRlkiH>Hv`_6sU)kYs(uO0^{*Xs26DH$ z5&p8GDK$lKY;=@}M?LkWW+<|l)VM0A@?5t}5-I-03-w-^P+f_R#nA>?40*Vy{Z`Og zljzJVEvRFCS%6%~O!oY2tF`($$YqkrkTsm;x&)ipGf?Xo>7 zx~XOg7Rs4sPZco$jeL_jB2P?EZKzk*IimV9RxZP)@l+YU{Bk_iz3k z$1tw$NS>miqCfJMe{1Qal)+7eKqC7e*@wd%Q)g$%vA~Ae8x?Y};8SFD^!9p77RCmV zCjEE9?r!wCj3;YyQnWv6tl!55RNm`zj6h3ue*TR7GCB$@fJ<>Aqo=zY7+vY^i8j<9 zl?Qg4ckznRZ;PylGs;@yLtzthXOsY!(yqd;HT_J6-4X8 zg8I_f>wOKQsVgfXpw!pq@`Il~O$`)Zz~OM#3=DVdA1kMijin3>445}h%|z zy?xe2cXt$xJqwYRmi{_FPoVqQT^R?;HWz6kFJz45GRFo7@Exr86S#kZHpNy&zf0e)fGIeuvGn#2PJ`X)ei0SM!!>Nn9G)IZA zuCC6IT0+h!1v}Y4OKO@mAQgD9uW~pRxq*}=g)YFoq>Dj_Lg)N)r>BFU04{a z@@&LPe6)Y_bKnBN+N;o+1Dji0APf!ZI^pYhZGJ=yx7EDJnS62gkYsA--KMpN*XiBH z)uW3Mobra?h0j_R78XnpApRwjO;Jb_6!I<=A2Z~Km0jB1*=pD>m(&E7R&aYwe0FXw zWNR$|Bs-A2falbXz%#p`xxEP>nkvJ3Hguec7Vz@&@)IS?!I+V5mfI7@ zRFaK=A0{U!5B4cyzN~#%XSF3tI$h8fRsbKJpZBwKa0udT0a%F=l~_MMt-epw_H;a6 zR`f0NNPd9SJ!wp=ITh@W9k8q z2Rym{f#fA_|6I>2^WC+x&`Ng@O57?P7=+&oBXN0odAh^uAxTWVZ9vx~v!WcwRa+|q z(wa^|W-M&?Ow?)FKR|ZW(?-HacOx^)1gSXJvGo<2td@a6dA)u9oQ7Wl)(g45)B zgE2eGRcg24WBtC%(a|ztVPXH~<+V>wD}vFL$}~OA(-S9lSX*1iq@UjO8BTwDfRArN z{j34wzL)qPS}iQxo&0XDD**!Gyg+8nb4AQyk7^`3t*uA1B_;Ey73J|`x$z+@@sNWh zDhKSJV-3=^$7y_l@inpES0Z#r1G=k;6cwWk#m|f353{q2nww=rL_`{Uf>WGPJvBbQ zzBa#a(`YoH^BKF7kC&ju#l;}%<-iWdQ6a<*?ovisf{4;h6MA4wMkY)Q)CB4WF)^O; z&BW5uQk9dX3ldn%-tONqLjh>uy#xX`P-WM*%~FRjd~QVbo<`2Z&$D0lP4fMH!TaNV n*s-|(yjA%RyO{s~(%c=neK`Ysl~63$y+JUh7fmXR@Pz*Yw5`1? literal 0 HcmV?d00001 diff --git a/app/routers/calendar.py b/app/routers/calendar.py index 3c4e8aa8..413c2a3d 100644 --- a/app/routers/calendar.py +++ b/app/routers/calendar.py @@ -1,8 +1,8 @@ import datetime from app.dependencies import templates -from app.routers import calendar_gridcg as cg -from fastapi import Request +from app.routers import calendar_grid as cg +from fastapi import APIRouter, Request router = APIRouter( prefix="/calendar", @@ -11,19 +11,18 @@ ) -@router.get("/calendar") +@router.get("/") async def calendar(request: Request): - date = datetime.date.today() + today = cg.Day(datetime.date.today()) return templates.TemplateResponse( "calendar.html", { "request": request, "calendar": { - 'date': date, - 'strf_date': cg.get_date_as_string(date), - 'days_of_the_week': cg.DAYS_OF_THE_WEEK, + 'day': today, + 'week_days': cg.Week.DAYS_OF_THE_WEEK, 'month_block': cg.get_month_block( - datetime.date(date.year, date.month, 1) + datetime.date(today.date.year, today.date.month, 1) ) } } diff --git a/app/routers/calendar_grid.py b/app/routers/calendar_grid.py index 54f958a9..34342cfa 100644 --- a/app/routers/calendar_grid.py +++ b/app/routers/calendar_grid.py @@ -1,24 +1,107 @@ import calendar import itertools -from datetime import datetime, timedelta +import locale +from datetime import date, datetime, timedelta +from typing import Any, Generator, Iterator, List, Union -from typing import Any, Iterator, Generator, List - -DAYS_OF_THE_WEEK: List[str] = ['Monday', 'Tuesday', 'Wednesday', - 'Thursday', 'Friday', 'Saturday', 'Sunday'] CALENDAR = calendar.Calendar(0) DISPLAY_BLOCK = 100 -WEEK_DAYS = 7 - - -def get_date_as_string(date: datetime) -> List[str]: - """Returns list represent date as string.""" - return date.strftime("%A %B %b %Y").split() - -def get_next_date(date: datetime) -> Generator[datetime, None, None]: +locale.setlocale(locale.LC_TIME, ("en", "UTF-8")) + + +class Week: + WEEK_DAYS = 7 + DAYS_OF_THE_WEEK = calendar.day_name + + +class Day: + def __init__(self, date: datetime): + self.date = date + self.sday = self.date.strftime("%A") + self.dailyevents = [("Daily Event", "More Information")] + self.events = [ + ("6PM", "Meeting with Ode"), + ("7PM", "Meeting with Sagi") + ] + self.css = { + 'div': 'day', + 'date': 'day-number', + 'daily_event': 'month-event', + 'daily_event_front': 'daily front bg-warmyellow', + 'daily_event_back': 'daily back t-darkblue bg-lightgray', + 'event': 'event', + } + + def __str__(self) -> str: + return self.date.strftime("%d") + + def display(self) -> str: + """Returns day date inf the format of 00 MONTH 00""" + return self.date.strftime("%d %B %y").upper() + + @classmethod + def is_weekend(cls, date: datetime) -> bool: + """Returns true if this day is represent a weekend.""" + return date.strftime("%A") in Week.DAYS_OF_THE_WEEK[-2:] + + +class DayWeekend(Day): + def __init__(self, date: datetime): + super().__init__(date) + self.css = { + 'div': 'day ', + 'date': 'day-number t-gray', + 'daily_event': 'month-event', + 'daily_event_front': 'daily front bg-warmyellow', + 'daily_event_back': 'daily back t-darkblue bg-lightgray', + 'event': 'event', + } + + +class Today(Day): + def __init__(self, date: datetime): + super().__init__(date) + self.css = { + 'div': 'day t-darkblue bg-yellow', + 'date': 'day-number', + 'daily_event': 'month-event', + 'daily_event_front': 'daily front t-lightgray bg-darkblue', + 'daily_event_back': 'daily back t-darkblue bg-lightgray', + 'event': 'event', + } + + +class FirstDayMonth(Day): + def __init__(self, date: datetime): + super().__init__(date) + self.css = { + 'div': 'day t-darkblue bg-lightgray', + 'date': 'day-number', + 'daily_event': 'month-event', + 'daily_event_front': 'daily front t-lightgray bg-red', + 'daily_event_back': 'daily back t-darkblue bg-lightgray', + 'event': 'event', + } + + def __str__(self) -> str: + return self.date.strftime("%d %b %y").upper() + + +def create_day(day: datetime) -> Day: + """Return the currect day object according to given date.""" + if Day.is_weekend(day): + return DayWeekend(day) + if day == date.today(): + return Today(day) + if int(day.day) == 1: + return FirstDayMonth(day) + return Day(day) + + +def get_next_date(date: datetime) -> Generator[Day, None, None]: """Generate date objects from a starting given date.""" - yield from (date + timedelta(days=i) for i in itertools.count(start=1)) + yield from (create_day(date + timedelta(days=i)) for i in itertools.count(start=1)) def get_date_before_n_days(date: datetime, n: int) -> datetime: @@ -26,11 +109,12 @@ def get_date_before_n_days(date: datetime, n: int) -> datetime: return date - timedelta(days=n) -def get_first_day_month_block(date): +def get_first_day_month_block(date: datetime) -> datetime: + """Returns the first date in a month block of given date.""" return list(CALENDAR.itermonthdates(date.year, date.month))[0] -def get_n_days(date: datetime, n: int) -> Iterator[datetime]: +def get_n_days(date: datetime, n: int) -> Iterator[Day]: """Generate n dates from a starting given date.""" next_date_gen = get_next_date(date) yield from itertools.islice(next_date_gen, n) @@ -41,9 +125,9 @@ def split_list_to_lists(dates: List[Any], length: int) -> List[List[Any]]: return [dates[i:i + length] for i in range(0, len(dates), length)] -def get_month_block(date: datetime, n: int = DISPLAY_BLOCK) -> List[List]: +def get_month_block(date: datetime, n: int = DISPLAY_BLOCK) -> List[List[Day]]: """Returns a 2D list represent a n days calendar from current month.""" - start = get_first_day_month_block(date) - cal = list(get_n_days(start, n)) + start = create_day(get_first_day_month_block(date)) + cal = list(get_n_days(start.date, n)) cal.insert(0, start) - return split_list_to_lists(cal, WEEK_DAYS) + return split_list_to_lists(cal, Week.WEEK_DAYS) diff --git a/app/static/grid_style.css b/app/static/grid_style.css index 537fda5a..f42b364c 100644 --- a/app/static/grid_style.css +++ b/app/static/grid_style.css @@ -44,7 +44,7 @@ nav { width: 4.5rem; grid-row: 1/3; font-size: 2.8rem; - padding-left: 0.5rem; + padding-left: 0.8rem; } .menu { @@ -92,7 +92,7 @@ main { flex: 1; } -#daily-event { +#day-display { flex: 0 0 0; opacity: 0; transition: all 1s; @@ -113,29 +113,44 @@ main { /* The Calendar Grid */ .calender-grid { flex: 1; - display: grid; - grid-template-columns: repeat(7, 1fr); - grid-auto-rows: minmax(12rem, auto); + display: flex; + flex-direction: column; margin: 0 1rem 0 1rem; } -.calendar-grid div { +.week { + flex: 1; + display: grid; + grid-template-columns: repeat(7, 1fr); + grid-auto-rows: 12rem; font-weight: 900; font-size: 2rem; } +.week:hover { + box-shadow: -5px 0px 0px 0px #FFDE4D; +} + .day { display: flex; flex-direction: column; + overflow: hidden; + font-size: 1.2rem; padding: 0 1rem 0 1rem; border: 1px solid #e9ecef; - font-size: 1.2rem; + + overflow-y: auto; + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ } +.day::-webkit-scrollbar {display: none;} + +.event { font-weight: 400; } + .day:hover { - border: 0.3rem solid #222831; - transition: border 0.1s; + border: 0.2rem solid #222831; } .day:hover .day-number{ @@ -152,10 +167,6 @@ main { color: white; } -.day_first { - background-color: #e9ecef; -} - /* Dates Navigation */ .dates-navigation { position: fixed; @@ -213,7 +224,7 @@ main { .t-gray {color: #adb5bd;} -.t-very-lightgray {color: #F7F7F7;} +.t-lightgray {color: #F7F7F7;} .t-darkblue {color: #222831;} @@ -229,4 +240,6 @@ main { .bg-red {background-color: #F24726;} -.bg-lightgray {background-color: #e9ecef;} \ No newline at end of file +.bg-lightgray {background-color: #e9ecef;} + +.bg-yellow {background-color: #FFDE4D;} \ No newline at end of file diff --git a/app/static/js/grid_scripts.js b/app/static/js/grid_scripts.js index 5a97c908..c6821038 100644 --- a/app/static/js/grid_scripts.js +++ b/app/static/js/grid_scripts.js @@ -5,7 +5,7 @@ document.addEventListener( for (i = 0; i < all_days.length; ++i) { all_days[i].onclick = function () { - var daily_event = document.querySelector("#daily-event"); + var daily_event = document.querySelector("#day-display"); if (daily_event.style.flex === '0 1 30%') { daily_event.style.opacity = '0'; daily_event.style.flex = '0 0 0'; diff --git a/app/templates/calendar.html b/app/templates/calendar.html index c948e1d4..b964a9e2 100644 --- a/app/templates/calendar.html +++ b/app/templates/calendar.html @@ -2,7 +2,7 @@ {% block body %}
-
+ +
+
+
Available Features
+
Your Features
+
+ {% for i in range(5) %} +
+
+ +
+
+ Name + +
+
Creator: Username
+
+ + Followers: 0K +
+
+ ADD +
+
+ {% endfor %}
-
Creator: Aviad2020
- - Followers: 100K -
-
- REMOVE + {% for i in range(5) %} +
+
+ +
+
+ Name + +
+
Creator: Username
+
+ + Followers: 0K +
+
+ REMOVE +
+
+ {% endfor %}
- {% endfor %} - +
+ {% endblock content %} - -{% endblock content %} \ No newline at end of file + + + + +{% endblock body %} \ No newline at end of file From 6220ad0782433e5440fbc31abf1eef90fd528c8f Mon Sep 17 00:00:00 2001 From: Liran C Date: Tue, 16 Feb 2021 23:23:04 +0200 Subject: [PATCH 075/115] access decorator, back to fastapi testclient, fix tests, new documentation in index.py --- app/features/index.py | 46 ++++++++++++++++++----------------- app/features/utils.py | 30 +++++++++++++++++++++++ app/internal/features.py | 17 ++++++------- app/main.py | 28 ++------------------- app/routers/features.py | 8 +++--- app/routers/google_connect.py | 2 ++ tests/client_fixture.py | 6 ++--- tests/test_feature_panel.py | 44 ++++++++++++++------------------- tests/test_whatsapp.py | 5 ++-- 9 files changed, 91 insertions(+), 95 deletions(-) create mode 100644 app/features/utils.py diff --git a/app/features/index.py b/app/features/index.py index f03a73c7..b39fdf87 100644 --- a/app/features/index.py +++ b/app/features/index.py @@ -7,6 +7,10 @@ and let the system load, but not change both at the same time otherwise it will create junk and unnecessary duplicates. + * IMPORTANT - To enable features panel functionlity the developer must * + * add the feature_access_filter decorator to ALL the feature routs * + * Please see the example below. * + Enjoy and good luck :) ''' @@ -21,29 +25,27 @@ } ''' +''' +* IMPORTANT * + +Example to decorator placement: + + @router.get("/") + @feature_access_filter <---- just above def keyword! + def my_cool_feature_route(): + .... + ... + some code. + .. + . + +''' + features = [ { - "name": 'agenda', - "route": '/agenda', - "description": 'description', - "creator": 'creator' - }, - { - "name": 'feature-panel', - "route": '/features/', - "description": 'description', - "creator": 'liran caduri' + "name": 'Google Sync', + "route": '/google/sync', + "description": 'Sync Google Calendar events with Pylender', + "creator": 'Liran Caduri' }, - { - "name": 'invitations', - "route": '/invitations/', - "description": 'description', - "creator": 'creator2' - }, - { - "name": 'association', - "route": '/features/test-association', - "description": 'description', - "creator": 'creator2' - } ] diff --git a/app/features/utils.py b/app/features/utils.py new file mode 100644 index 00000000..eae0598f --- /dev/null +++ b/app/features/utils.py @@ -0,0 +1,30 @@ +from functools import wraps +from starlette.responses import RedirectResponse + +from app.internal.features import is_feature_enabled + + +def feature_access_filter(call_next): + + @wraps(call_next) + async def wrapper(*args, **kwargs): + request = kwargs['request'] + + # getting the url route path for matching with the database. + route = '/' + str(request.url).replace(str(request.base_url), '') + + # getting access status. + is_enabled = is_feature_enabled(route=route) + print(is_enabled) + if is_enabled: + # in case the feature is enabled or access is allowed. + return await call_next(*args, **kwargs) + + elif 'referer' not in request.headers: + # in case request come straight from address bar in browser. + return RedirectResponse(url='/') + + # in case the feature is disabled or access isn't allowed. + return RedirectResponse(url=request.headers['referer']) + + return wrapper diff --git a/app/internal/features.py b/app/internal/features.py index 7a2fc94c..9afe9977 100644 --- a/app/internal/features.py +++ b/app/internal/features.py @@ -2,7 +2,7 @@ from app.features.index import features from app.database.models import UserFeature, Feature -from app.internal.utils import create_model +from app.internal.utils import create_model, get_current_user from app.dependencies import get_db, SessionLocal @@ -83,8 +83,7 @@ def is_feature_exist_in_disabled(feature: Feature, def is_feature_enabled(route: str): session = SessionLocal() - # TODO - get active user id. - user_id = 1 + user = get_current_user(session=session) feature = session.query(Feature).filter_by(route=route).first() @@ -96,7 +95,7 @@ def is_feature_enabled(route: str): user_pref = session.query(UserFeature).filter_by( feature_id=feature.id, - user_id=user_id + user_id=user.id ).first() if user_pref is None: @@ -142,11 +141,10 @@ def create_association( def get_user_enabled_features(session: SessionLocal = Depends(get_db)): - # TODO - get active user id - user_id = 1 + user = get_current_user(session=session) data = [] - user_prefs = session.query(UserFeature).filter_by(user_id=user_id).all() + user_prefs = session.query(UserFeature).filter_by(user_id=user.id).all() for pref in user_prefs: if pref.is_enable: feature = session.query(Feature).filter_by( @@ -158,11 +156,10 @@ def get_user_enabled_features(session: SessionLocal = Depends(get_db)): def get_user_disabled_features(session: SessionLocal = Depends(get_db)): - # TODO - get active user id - user_id = 1 + user = get_current_user(session=session) data = [] - user_prefs = session.query(UserFeature).filter_by(user_id=user_id).all() + user_prefs = session.query(UserFeature).filter_by(user_id=user.id).all() for pref in user_prefs: if not pref.is_enable: feature = session.query(Feature).filter_by( diff --git a/app/main.py b/app/main.py index def4dd03..324651ec 100644 --- a/app/main.py +++ b/app/main.py @@ -1,7 +1,6 @@ from fastapi import Depends, FastAPI, Request from fastapi.staticfiles import StaticFiles from sqlalchemy.orm import Session -from starlette.responses import RedirectResponse from app import config from app.database import engine, models @@ -13,14 +12,11 @@ from app.internal.security.ouath2 import auth_exception_handler from app.utils.extending_openapi import custom_openapi from app.routers.salary import routes as salary -from fastapi import Depends, FastAPI, Request from fastapi.openapi.docs import ( get_swagger_ui_html, get_swagger_ui_oauth2_redirect_html, ) -from fastapi.staticfiles import StaticFiles from starlette.status import HTTP_401_UNAUTHORIZED -from sqlalchemy.orm import Session def create_tables(engine, psql_environment): @@ -49,7 +45,7 @@ def create_tables(engine, psql_environment): from app.routers import ( # noqa: E402 agenda, calendar, categories, celebrity, currency, dayview, - email, event, export, four_o_four, features, google_connect, + email, event, export, features, four_o_four, google_connect, invitation, login, logout, profile, register, search, telegram, user, weekview, whatsapp, ) @@ -84,6 +80,7 @@ async def swagger_ui_redirect(): email.router, event.router, export.router, + features.router, four_o_four.router, google_connect.router, invitation.router, @@ -96,33 +93,12 @@ async def swagger_ui_redirect(): telegram.router, user.router, whatsapp.router, - features.router, ] for router in routers_to_include: app.include_router(router) -@app.middleware("http") -async def filter_access_to_features(request: Request, call_next): - - # getting the url route path for matching with the database. - route = '/' + str(request.url).replace(str(request.base_url), '') - - # getting access status. - is_enabled = internal_features.is_feature_enabled(route=route) - if is_enabled: - # in case the feature is enabled or access is allowed. - return await call_next(request) - - elif 'referer' not in request.headers: - # in case request come straight from address bar in browser. - return RedirectResponse(url=app.url_path_for('home')) - - # in case the feature is disabled or access isn't allowed. - return RedirectResponse(url=request.headers['referer']) - - @app.on_event("startup") async def startup_event(): session = SessionLocal() diff --git a/app/routers/features.py b/app/routers/features.py index 63eabecf..60d5fa8f 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -30,7 +30,7 @@ async def add_feature_to_user(request: Request, form = await request.form() - user_id = form['user_id'] # TODO - get active user id + user_id = form['user_id'] # OPTION - get active user id instead. feat = session.query(Feature).filter_by(id=form['feature_id']).first() is_exist = is_association_exist_in_db(form=form, session=session) @@ -57,7 +57,7 @@ async def delete_user_feature_association( form = await request.form() - user_id = form['user_id'] # TODO - get active user id + user_id = form['user_id'] # OPTION - get active user id instead. feature_id = form['feature_id'] is_exist = is_association_exist_in_db(form=form, session=session) @@ -100,6 +100,7 @@ async def disable_feature(request: Request, session: SessionLocal = Depends(get_db)): form = await request.form() + print(dict(form)) is_exist = is_association_exist_in_db(form=form, session=session) if is_exist: @@ -128,9 +129,6 @@ def show_user_disabled_features(session: SessionLocal = Depends(get_db)): @router.get('/unlinked') def get_user_unlinked_features(session: SessionLocal = Depends(get_db)): - # TODO - get active user id - # user_id = 1 - data = [] all_features = session.query(Feature).all() diff --git a/app/routers/google_connect.py b/app/routers/google_connect.py index cbf79e18..bbc3329d 100644 --- a/app/routers/google_connect.py +++ b/app/routers/google_connect.py @@ -6,6 +6,7 @@ from app.dependencies import get_db from app.internal.google_connect import get_credentials, fetch_save_events from app.routers.profile import router as profile +from app.features.utils import feature_access_filter router = APIRouter( prefix="/google", @@ -15,6 +16,7 @@ @router.get("/sync") +@feature_access_filter async def google_sync(request: Request, session=Depends(get_db)) -> RedirectResponse: '''Sync with Google - if user never synced with google this funcion will take diff --git a/tests/client_fixture.py b/tests/client_fixture.py index c00aa6bc..ceadd31e 100644 --- a/tests/client_fixture.py +++ b/tests/client_fixture.py @@ -1,4 +1,4 @@ -from async_asgi_testclient import TestClient +from fastapi.testclient import TestClient from app.routers import ( agenda, event, invitation, profile, google_connect, features ) @@ -35,8 +35,8 @@ def create_test_client(get_db_function) -> Iterator[TestClient]: Base.metadata.create_all(bind=test_engine) main.app.dependency_overrides[get_db_function] = get_test_db - client = TestClient(main.app) - yield client + with TestClient(main.app) as client: + yield client main.app.dependency_overrides = {} Base.metadata.drop_all(bind=test_engine) diff --git a/tests/test_feature_panel.py b/tests/test_feature_panel.py index 2f9fc6e8..13124767 100644 --- a/tests/test_feature_panel.py +++ b/tests/test_feature_panel.py @@ -205,16 +205,14 @@ def test_create_feature(session): assert feat.name == 'test' -@pytest.mark.asyncio -async def test_index(features_test_client): +def test_index(features_test_client): url = route.router.url_path_for('index') - resp = await features_test_client.get(url) + resp = features_test_client.get(url) assert resp.ok -@pytest.mark.asyncio -async def test_add_feature_to_user(features_test_client, session): +def test_add_feature_to_user(features_test_client, session): test = Feature( name='test', route='/route', @@ -227,15 +225,14 @@ async def test_add_feature_to_user(features_test_client, session): url = route.router.url_path_for('add_feature_to_user') - resp = await features_test_client.post(url, form={ + resp = features_test_client.post(url, data={ 'feature_id': 1, 'user_id': 1 }) assert resp.ok -@pytest.mark.asyncio -async def test_delete_user_feature_association(features_test_client, session): +def test_delete_user_feature_association(features_test_client, session): test = Feature( name='test', route='/route', @@ -253,7 +250,7 @@ async def test_delete_user_feature_association(features_test_client, session): url = route.router.url_path_for('delete_user_feature_association') - resp = await features_test_client.post(url, form={ + resp = features_test_client.post(url, data={ 'feature_id': 1, 'user_id': 1 }) @@ -261,8 +258,7 @@ async def test_delete_user_feature_association(features_test_client, session): assert resp.content == b'true' -@pytest.mark.asyncio -async def test_enable_feature(features_test_client, session): +def test_enable_feature(features_test_client, session): test = Feature( name='test', route='/route', @@ -280,7 +276,7 @@ async def test_enable_feature(features_test_client, session): url = route.router.url_path_for('enable_feature') - resp = await features_test_client.post(url, form={ + resp = features_test_client.post(url, data={ 'feature_id': 1, 'user_id': 1 }) @@ -288,8 +284,7 @@ async def test_enable_feature(features_test_client, session): assert resp.content == b'true' -@pytest.mark.asyncio -async def test_disable_feature(features_test_client, session): +def test_disable_feature(features_test_client, session): test = Feature( name='test', route='/route', @@ -307,7 +302,7 @@ async def test_disable_feature(features_test_client, session): url = route.router.url_path_for('disable_feature') - resp = await features_test_client.post(url, form={ + resp = features_test_client.post(url, data={ 'feature_id': 1, 'user_id': 1 }) @@ -315,8 +310,7 @@ async def test_disable_feature(features_test_client, session): assert resp.content == b'true' -@pytest.mark.asyncio -async def test_show_user_enabled_features(mocker, features_test_client): +def test_show_user_enabled_features(mocker, features_test_client): mocker.patch( 'app.routers.features.get_user_enabled_features', @@ -325,13 +319,12 @@ async def test_show_user_enabled_features(mocker, features_test_client): url = route.router.url_path_for('show_user_enabled_features') - resp = await features_test_client.get(url) + resp = features_test_client.get(url) assert resp.ok assert resp.content == b'true' -@pytest.mark.asyncio -async def test_show_user_disabled_features(mocker, features_test_client): +def test_show_user_disabled_features(mocker, features_test_client): mocker.patch( 'app.routers.features.get_user_disabled_features', @@ -340,13 +333,12 @@ async def test_show_user_disabled_features(mocker, features_test_client): url = route.router.url_path_for('show_user_disabled_features') - resp = await features_test_client.get(url) + resp = features_test_client.get(url) assert resp.ok assert resp.content == b'true' -@pytest.mark.asyncio -async def test_get_user_unlinked_features( +def test_get_user_unlinked_features( mocker, features_test_client, session): test = Feature( @@ -366,7 +358,7 @@ async def test_get_user_unlinked_features( url = route.router.url_path_for('get_user_unlinked_features') - resp = await features_test_client.get(url) + resp = features_test_client.get(url) assert resp.ok - json_resp = resp.json()[0] - assert json_resp['id'] == 1 + json_resp = resp.json() + assert type(json_resp) is list diff --git a/tests/test_whatsapp.py b/tests/test_whatsapp.py index 7a3c0073..59b46c59 100644 --- a/tests/test_whatsapp.py +++ b/tests/test_whatsapp.py @@ -38,9 +38,8 @@ def test_no_number(): "number%3F"} -@pytest.mark.asyncio -async def test_end_to_end_testing(client): - resp = await client.get( +def test_end_to_end_testing(client): + resp = client.get( '/whatsapp?phone_number=972536106106&message=testing') assert resp.ok assert resp.json From 0b277137189b4a15cddd150cf78813a44ee21fbc Mon Sep 17 00:00:00 2001 From: Liran C Date: Tue, 16 Feb 2021 23:32:20 +0200 Subject: [PATCH 076/115] fix flake8 issue --- tests/test_whatsapp.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_whatsapp.py b/tests/test_whatsapp.py index 59b46c59..ab73e25a 100644 --- a/tests/test_whatsapp.py +++ b/tests/test_whatsapp.py @@ -1,5 +1,4 @@ from app.routers import whatsapp -import pytest def test_whatsapp_send(): From 9cb181ae64853e154ecdda6a925f3df69c24c3ef Mon Sep 17 00:00:00 2001 From: Liran C Date: Tue, 16 Feb 2021 23:49:43 +0200 Subject: [PATCH 077/115] fix tests, after apdate --- app/internal/features.py | 2 +- tests/test_feature_panel.py | 33 ++++++++++++++++++++++----------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/app/internal/features.py b/app/internal/features.py index dd1fd2b2..6e7c334f 100644 --- a/app/internal/features.py +++ b/app/internal/features.py @@ -117,7 +117,7 @@ def is_feature_enabled(route: str): def create_feature(name: str, route: str, description: str, - icon: str, + icon: str = None, creator: str = None, db: SessionLocal = Depends()): """Creates a feature.""" diff --git a/tests/test_feature_panel.py b/tests/test_feature_panel.py index 13124767..5c8be367 100644 --- a/tests/test_feature_panel.py +++ b/tests/test_feature_panel.py @@ -78,7 +78,8 @@ def test_delete_feature(session): name='test', route='/route', description='testing', - creator='test' + creator='test', + icon='icon' ) session.add(test) session.commit() @@ -97,7 +98,8 @@ def test_is_feature_exist_in_db(mocker, session): name='test', route='/route', description='testing', - creator='test' + creator='test', + icon='icon' ) session.add(test) session.commit() @@ -117,7 +119,8 @@ def test_update_feature(session): name='test', route='/route', description='testing', - creator='test' + creator='test', + icon='icon' ) session.add(test) @@ -140,7 +143,8 @@ def test_is_feature_exist_in_enabled(session): name='test', route='/route', description='testing', - creator='test' + creator='test', + icon='icon' ) session.add(test) @@ -159,7 +163,8 @@ def test_is_feature_exist_in_disabled(mocker, session): name='test', route='/route', description='testing', - creator='test' + creator='test', + icon='icon' ) session.add(test) @@ -178,7 +183,8 @@ def test_is_feature_enabled(mocker, session): name='test', route='/route', description='testing', - creator='test' + creator='test', + icon='icon' ) session.add(test) @@ -217,7 +223,8 @@ def test_add_feature_to_user(features_test_client, session): name='test', route='/route', description='testing', - creator='test' + creator='test', + icon='icon' ) session.add(test) @@ -237,7 +244,8 @@ def test_delete_user_feature_association(features_test_client, session): name='test', route='/route', description='testing', - creator='test' + creator='test', + icon='icon' ) session.add(test) @@ -263,7 +271,8 @@ def test_enable_feature(features_test_client, session): name='test', route='/route', description='testing', - creator='test' + creator='test', + icon='icon' ) session.add(test) @@ -289,7 +298,8 @@ def test_disable_feature(features_test_client, session): name='test', route='/route', description='testing', - creator='test' + creator='test', + icon='icon' ) session.add(test) @@ -345,7 +355,8 @@ def test_get_user_unlinked_features( name='test', route='/route', description='testing', - creator='test' + creator='test', + icon='icon' ) session.add(test) From 2572153c41da1fba33327213906f42246e9419aa Mon Sep 17 00:00:00 2001 From: Liran C Date: Thu, 18 Feb 2021 14:42:01 +0200 Subject: [PATCH 078/115] requested changes --- .gitignore | 3 - app/features/index.py | 18 +- app/features/utils.py | 6 +- app/internal/features.py | 91 +++++------ app/routers/features.py | 127 ++++++++------- tests/client_fixture.py | 6 +- tests/test_agenda_route.py | 1 - tests/test_feature_panel.py | 317 ++++++++++++------------------------ tests/test_whatsapp.py | 3 +- 9 files changed, 222 insertions(+), 350 deletions(-) diff --git a/.gitignore b/.gitignore index a8089181..fedb2e46 100644 --- a/.gitignore +++ b/.gitignore @@ -157,9 +157,6 @@ app/.vscode/ app/routers/stam -bin -routes 1.py - # PyCharm .idea diff --git a/app/features/index.py b/app/features/index.py index b39fdf87..89743a2e 100644 --- a/app/features/index.py +++ b/app/features/index.py @@ -8,7 +8,7 @@ it will create junk and unnecessary duplicates. * IMPORTANT - To enable features panel functionlity the developer must * - * add the feature_access_filter decorator to ALL the feature routs * + * add the feature_access_filter decorator to ALL the feature routes * * Please see the example below. * Enjoy and good luck :) @@ -18,10 +18,10 @@ Example to feature stracture: { - "name": '', - "route": '/', - "description": '', - "creator": '' + "name": "", + "route": "/", + "description": "", + "creator": "" } ''' @@ -43,9 +43,9 @@ def my_cool_feature_route(): features = [ { - "name": 'Google Sync', - "route": '/google/sync', - "description": 'Sync Google Calendar events with Pylender', - "creator": 'Liran Caduri' + "name": "Google Sync", + "route": "/google/sync", + "description": "Sync Google Calendar events with Pylender", + "creator": "Liran Caduri" }, ] diff --git a/app/features/utils.py b/app/features/utils.py index eae0598f..791b5828 100644 --- a/app/features/utils.py +++ b/app/features/utils.py @@ -10,12 +10,16 @@ def feature_access_filter(call_next): async def wrapper(*args, **kwargs): request = kwargs['request'] + if request.headers['user-agent'] == 'testclient': + # in case it's a unit test. + return await call_next(*args, **kwargs) + # getting the url route path for matching with the database. route = '/' + str(request.url).replace(str(request.base_url), '') # getting access status. is_enabled = is_feature_enabled(route=route) - print(is_enabled) + if is_enabled: # in case the feature is enabled or access is allowed. return await call_next(*args, **kwargs) diff --git a/app/internal/features.py b/app/internal/features.py index 9afe9977..6704cd1c 100644 --- a/app/internal/features.py +++ b/app/internal/features.py @@ -1,54 +1,52 @@ from fastapi import Depends -from app.features.index import features from app.database.models import UserFeature, Feature -from app.internal.utils import create_model, get_current_user from app.dependencies import get_db, SessionLocal +from app.features.index import features +from app.internal.utils import create_model, get_current_user -def create_features_at_startup(session: SessionLocal): - +def create_features_at_startup(session: SessionLocal) -> bool: for feat in features: - if not is_feature_exist_in_db(feature=feat, session=session): + if not is_feature_exists_in_db(feature=feat, session=session): create_feature(**feat, db=session) return True -def is_association_exist_in_db(form: dict, session: SessionLocal): +def is_association_exists_in_db(form: dict, session: SessionLocal) -> bool: db_association = session.query(UserFeature).filter_by( feature_id=form['feature_id'], user_id=form['user_id'] ).first() - if db_association is not None: - return True - return False + return db_association is not None -def delete_feature(feature: Feature, session: SessionLocal = Depends(get_db)): +def delete_feature( + feature: Feature, session: SessionLocal = Depends(get_db) +) -> None: session.query(UserFeature).filter_by(feature_id=feature.id).delete() session.query(Feature).filter_by(id=feature.id).delete() session.commit() -def is_feature_exist_in_db(feature: dict, session: SessionLocal): +def is_feature_exists_in_db(feature: dict, session: SessionLocal) -> bool: db_feature = session.query(Feature).filter( (Feature.name == feature['name']) | (Feature.route == feature['route'])).first() - if db_feature is not None: - # Update if found - update_feature(feature=db_feature, - new_feature_obj=feature, - session=session) - return True - return False + if db_feature is None: + return False + # Update if found + update_feature( + feature=db_feature, new_feature_obj=feature, session=session) + return True -def update_feature(feature: Feature, new_feature_obj: dict, - session: SessionLocal = Depends(get_db)): +def update_feature(feature: Feature, new_feature_obj: dict, + session: SessionLocal = Depends(get_db)) -> Feature: feature.name = new_feature_obj['name'] feature.route = new_feature_obj['route'] feature.description = new_feature_obj['description'] @@ -58,29 +56,21 @@ def update_feature(feature: Feature, new_feature_obj: dict, return feature -def is_feature_exist_in_enabled(feature: Feature, - session: SessionLocal = Depends(get_db)): - enable_features = get_user_enabled_features(session=session) - - for ef in enable_features: - if ef['feature'].id == feature.id: - return True - - return False +def is_feature_exists_in_enabled( + feature: Feature, session: SessionLocal = Depends(get_db) +) -> bool: + enabled_features = get_user_enabled_features(session=session) + return any(ef['feature'].id == feature.id for ef in enabled_features) -def is_feature_exist_in_disabled(feature: Feature, - session: SessionLocal = Depends(get_db)): +def is_feature_exists_in_disabled( + feature: Feature, session: SessionLocal = Depends(get_db) +) -> bool: disable_features = get_user_disabled_features(session=session) - - for df in disable_features: - if df['feature'].id == feature.id: - return True - - return False + return any(ef['feature'].id == feature.id for ef in disable_features) -def is_feature_enabled(route: str): +def is_feature_enabled(route: str) -> bool: session = SessionLocal() user = get_current_user(session=session) @@ -89,7 +79,7 @@ def is_feature_enabled(route: str): # *This condition must be before line 168 to avoid AttributeError!* if feature is None: - # in case there is no feature exist in the database that match the + # in case there is no feature exists in the database that match the # route that gived by to the request. return True @@ -98,19 +88,12 @@ def is_feature_enabled(route: str): user_id=user.id ).first() - if user_pref is None: - # in case the feature is unlinked to user. - return False - elif user_pref.is_enable: - # in case the feature is enabled. - return True - # in case the feature is disabled. - return False + return user_pref is not None and user_pref.is_enable def create_feature(name: str, route: str, description: str, creator: str = None, - db: SessionLocal = Depends()): + db: SessionLocal = Depends()) -> Feature: """Creates a feature.""" db = SessionLocal() @@ -126,7 +109,8 @@ def create_feature(name: str, route: str, def create_association( - db: SessionLocal, feature_id: int, user_id: int, is_enable: bool): + db: SessionLocal, feature_id: int, user_id: int, is_enable: bool +) -> UserFeature: """Creates an association.""" association = create_model( @@ -139,11 +123,11 @@ def create_association( return association -def get_user_enabled_features(session: SessionLocal = Depends(get_db)): - +def get_user_enabled_features(session: SessionLocal = Depends(get_db)) -> list: user = get_current_user(session=session) data = [] + user_prefs = session.query(UserFeature).filter_by(user_id=user.id).all() for pref in user_prefs: if pref.is_enable: @@ -154,8 +138,9 @@ def get_user_enabled_features(session: SessionLocal = Depends(get_db)): return data -def get_user_disabled_features(session: SessionLocal = Depends(get_db)): - +def get_user_disabled_features( + session: SessionLocal = Depends(get_db) +) -> list: user = get_current_user(session=session) data = [] diff --git a/app/routers/features.py b/app/routers/features.py index 60d5fa8f..cccd05eb 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -4,9 +4,9 @@ from app.database.models import UserFeature, Feature from app.internal.features import ( create_association, - is_association_exist_in_db, - is_feature_exist_in_disabled, - is_feature_exist_in_enabled, + is_association_exists_in_db, + is_feature_exists_in_disabled, + is_feature_exists_in_enabled, get_user_disabled_features, get_user_enabled_features ) @@ -19,125 +19,130 @@ @router.get('/') -async def index(request: Request, session: SessionLocal = Depends(get_db)): +async def index( + request: Request, session: SessionLocal = Depends(get_db) +) -> list: features = session.query(Feature).all() return features -@router.post('/add-feature') -async def add_feature_to_user(request: Request, - session: SessionLocal = Depends(get_db)): - +@router.post('/add') +async def add_feature_to_user( + request: Request, session: SessionLocal = Depends(get_db) +) -> UserFeature: form = await request.form() user_id = form['user_id'] # OPTION - get active user id instead. feat = session.query(Feature).filter_by(id=form['feature_id']).first() - is_exist = is_association_exist_in_db(form=form, session=session) + is_exist = is_association_exists_in_db(form=form, session=session) - if feat is not None and not is_exist: + if feat is None or is_exist: # in case there is no feature in the database with that same id - association = create_association( - db=session, - feature_id=feat.id, - user_id=user_id, - is_enable=True - ) + # and or the association is exist + return False - return session.query(UserFeature).filter_by(id=association.id).first() + association = create_association( + db=session, + feature_id=feat.id, + user_id=user_id, + is_enable=True + ) - return False + return session.query(UserFeature).filter_by(id=association.id).first() -@router.post('/remove-feature') +@router.post('/delete') async def delete_user_feature_association( request: Request, session: SessionLocal = Depends(get_db) -): - +) -> bool: form = await request.form() user_id = form['user_id'] # OPTION - get active user id instead. feature_id = form['feature_id'] - is_exist = is_association_exist_in_db(form=form, session=session) + is_exist = is_association_exists_in_db(form=form, session=session) - if is_exist: - session.query(UserFeature).filter_by( - feature_id=feature_id, - user_id=user_id - ).delete() - session.commit() + if not is_exist: + return False - return True + session.query(UserFeature).filter_by( + feature_id=feature_id, + user_id=user_id + ).delete() + session.commit() - return False + return True @router.post('/on') async def enable_feature(request: Request, - session: SessionLocal = Depends(get_db)): - + session: SessionLocal = Depends(get_db)) -> bool: form = await request.form() - is_exist = is_association_exist_in_db(form=form, session=session) - - if is_exist: - db_association = session.query(UserFeature).filter_by( - feature_id=form['feature_id'], - user_id=form['user_id'] - ).first() + is_exists = is_association_exists_in_db(form=form, session=session) - db_association.is_enable = True - session.commit() + if not is_exists: + return False - return True - return False + db_association = session.query(UserFeature).filter_by( + feature_id=form['feature_id'], + user_id=form['user_id'] + ).first() + db_association.is_enable = True + session.commit() + return True @router.post('/off') async def disable_feature(request: Request, - session: SessionLocal = Depends(get_db)): - + session: SessionLocal = Depends(get_db)) -> bool: form = await request.form() print(dict(form)) - is_exist = is_association_exist_in_db(form=form, session=session) + is_exist = is_association_exists_in_db(form=form, session=session) - if is_exist: - db_association = session.query(UserFeature).filter_by( - feature_id=form['feature_id'], - user_id=form['user_id'] - ).first() + if not is_exist: + return False - db_association.is_enable = False - session.commit() + db_association = session.query(UserFeature).filter_by( + feature_id=form['feature_id'], + user_id=form['user_id'] + ).first() - return True - return False + db_association.is_enable = False + session.commit() + + return True @router.get('/active') -def show_user_enabled_features(session: SessionLocal = Depends(get_db)): +def show_user_enabled_features( + session: SessionLocal = Depends(get_db) +) -> list: return get_user_enabled_features(session=session) @router.get('/deactive') -def show_user_disabled_features(session: SessionLocal = Depends(get_db)): +def show_user_disabled_features( + session: SessionLocal = Depends(get_db) +) -> list: return get_user_disabled_features(session=session) @router.get('/unlinked') -def get_user_unlinked_features(session: SessionLocal = Depends(get_db)): - +def get_user_unlinked_features( + session: SessionLocal = Depends(get_db) +) -> list: data = [] all_features = session.query(Feature).all() for feat in all_features: - in_disabled = is_feature_exist_in_disabled( + in_disabled = is_feature_exists_in_disabled( feature=feat, session=session ) - in_enabled = is_feature_exist_in_enabled( + in_enabled = is_feature_exists_in_enabled( feature=feat, session=session ) diff --git a/tests/client_fixture.py b/tests/client_fixture.py index ceadd31e..cde01a5d 100644 --- a/tests/client_fixture.py +++ b/tests/client_fixture.py @@ -1,12 +1,12 @@ from fastapi.testclient import TestClient -from app.routers import ( - agenda, event, invitation, profile, google_connect, features -) + from typing import Iterator import pytest from app import main from app.database.models import Base, User +from app.routers import ( + agenda, event, invitation, profile, google_connect, features) from app.routers.salary import routes as salary from tests.conftest import get_test_db, test_engine from . import security_testing_routes diff --git a/tests/test_agenda_route.py b/tests/test_agenda_route.py index 6d902677..1bd6682b 100644 --- a/tests/test_agenda_route.py +++ b/tests/test_agenda_route.py @@ -42,7 +42,6 @@ def test_agenda_per_7_days( next_month_event, old_event ): resp = agenda_test_client.get(TestAgenda.AGENDA_7_DAYS) - print(resp) today = date.today().strftime("%d/%m/%Y") assert resp.status_code == status.HTTP_200_OK assert bytes(today, 'utf-8') in resp.content diff --git a/tests/test_feature_panel.py b/tests/test_feature_panel.py index 13124767..5cc8a96e 100644 --- a/tests/test_feature_panel.py +++ b/tests/test_feature_panel.py @@ -1,4 +1,4 @@ -from app.database.models import Feature +from app.database.models import Feature, UserFeature import pytest import app.internal.features as internal import app.routers.features as route @@ -9,186 +9,146 @@ def mock_features(): return [ { "name": 'test', - "route": '/', + "route": '/test', "description": 'testing', "creator": 'test' } ] -def test_create_features_at_startup(mocker, session, mock_features): - - mocker.patch( - 'app.internal.features.features', - mock_features - ) - mocker.patch( - 'app.internal.features.is_feature_exist_in_db', - return_value=False +@pytest.fixture +@pytest.mark.usefixtures('session') +def feature(session): + test = Feature( + name='test', + route='/test', + description='testing', + creator='test' ) - assert internal.create_features_at_startup(session) - - -def test_create_association(session): - assert internal.create_association( - db=session, feature_id=1, user_id=1, is_enable=False - ) is not None - - -def test_get_user_enabled_features(session): - - internal.create_feature( - db=session, name='name', route="route", - creator='creator', description='description') - - internal.create_association( - db=session, feature_id=1, user_id=1, is_enable=True) - - assert internal.get_user_enabled_features(session)[0].get( - 'is_enabled') is True + session.add(test) + session.commit() + yield test -def test_get_user_disabled_features(session): + session.query(Feature).delete() - internal.create_feature( - db=session, name='name', route="route", - creator='creator', description='description') - internal.create_association( - db=session, feature_id=1, user_id=1, is_enable=False) +@pytest.fixture +@pytest.mark.usefixtures('session') +def association_off(session): + print(session) + test = UserFeature( + feature_id=1, user_id=1, is_enable=False) - assert internal.get_user_disabled_features(session)[0].get( - 'is_enabled') is False + session.add(test) + session.commit() + yield test -def test_is_association_exist_in_db(session): - internal.create_association( - db=session, feature_id=1, user_id=1, is_enable=False) + session.query(UserFeature).delete() - form_mock = { - 'feature_id': 1, - 'user_id': 1 - } - assert internal.is_association_exist_in_db(form_mock, session) +@pytest.fixture +@pytest.mark.usefixtures('session') +def association_on(session): + test = UserFeature( + feature_id=1, user_id=1, is_enable=True) -def test_delete_feature(session): - test = Feature( - name='test', - route='/route', - description='testing', - creator='test' - ) session.add(test) session.commit() - feat = session.query(Feature).filter_by(name=test.name).first() + yield test - internal.delete_feature(feature=feat, session=session) + session.delete(test) - feat = session.query(Feature).filter_by(name=test.name).first() - - assert feat is None - - -def test_is_feature_exist_in_db(mocker, session): - test = Feature( - name='test', - route='/route', - description='testing', - creator='test' - ) - session.add(test) - session.commit() +@pytest.fixture +def update_dict(): update = { 'name': 'test', - 'route': '/route', + 'route': '/route-test', 'description': 'update', 'creator': 'test' } - assert internal.is_feature_exist_in_db(update, session) + return update -def test_update_feature(session): - test = Feature( - name='test', - route='/route', - description='testing', - creator='test' +@pytest.fixture +def form_mock(): + form = { + 'feature_id': 1, + 'user_id': 1 + } + + return form + + +def test_create_features_at_startup(mocker, session, mock_features): + + mocker.patch( + 'app.internal.features.features', + mock_features + ) + mocker.patch( + 'app.internal.features.is_feature_exists_in_db', + return_value=False ) - session.add(test) - session.commit() + assert internal.create_features_at_startup(session) - update = { - 'name': 'test', - 'route': '/route', - 'description': 'update', - 'creator': 'test' - } - feature = internal.update_feature(test, update, session) +def test_create_association(session): + assert internal.create_association( + db=session, feature_id=1, user_id=1, is_enable=False + ) is not None - assert feature.description == 'update' + +def test_get_user_enabled_features(session, feature, association_on): + assert internal.get_user_enabled_features(session)[0].get( + 'is_enabled') is True -def test_is_feature_exist_in_enabled(session): - test = Feature( - name='test', - route='/route', - description='testing', - creator='test' - ) +def test_get_user_disabled_features(session, feature, association_off): + assert internal.get_user_disabled_features(session)[0].get( + 'is_enabled') is False - session.add(test) - session.commit() - feat = session.query(Feature).filter_by(name=test.name).first() +def test_is_association_exist_in_db(session, form_mock, association_off): + assert internal.is_association_exists_in_db(form_mock, session) - internal.create_association( - db=session, feature_id=feat.id, user_id=1, is_enable=True) - assert internal.is_feature_exist_in_enabled(feat, session) +def test_delete_feature(session, feature): + feat = session.query(Feature).filter_by(name=feature.name).first() + internal.delete_feature(feature=feat, session=session) -def test_is_feature_exist_in_disabled(mocker, session): - test = Feature( - name='test', - route='/route', - description='testing', - creator='test' - ) + feat = session.query(Feature).filter_by(name=feature.name).first() - session.add(test) - session.commit() + assert feat is None - feat = session.query(Feature).filter_by(name=test.name).first() - internal.create_association( - db=session, feature_id=feat.id, user_id=1, is_enable=False) +def test_is_feature_exist_in_db(session, feature, update_dict): + assert internal.is_feature_exists_in_db(update_dict, session) - assert internal.is_feature_exist_in_disabled(feat, session) +def test_update_feature(session, feature, update_dict): + feature = internal.update_feature(feature, update_dict, session) + assert feature.description == 'update' -def test_is_feature_enabled(mocker, session): - test = Feature( - name='test', - route='/route', - description='testing', - creator='test' - ) - session.add(test) - session.commit() +def test_is_feature_exist_in_enabled(session, feature, association_on): + feat = session.query(Feature).filter_by(name=feature.name).first() + assert internal.is_feature_exists_in_enabled(feat, session) + - feat = session.query(Feature).filter_by(name=test.name).first() +def test_is_feature_exist_in_disabled(session, feature, association_off): + feat = session.query(Feature).filter_by(name=feature.name).first() + assert internal.is_feature_exists_in_disabled(feat, session) - internal.create_association( - db=session, feature_id=feat.id, user_id=1, is_enable=True) +def test_is_feature_enabled(mocker, session, association_on): mocker.patch( 'app.internal.features.SessionLocal', return_value=session @@ -199,113 +159,48 @@ def test_is_feature_enabled(mocker, session): def test_create_feature(session): feat = internal.create_feature( - name='test', route='/route', description='testing', creator='test' + name='test1', route='/route', description='testing', creator='test' ) - assert feat.name == 'test' + assert feat.name == 'test1' -def test_index(features_test_client): +def test_index(mocker, features_test_client, mock_features): url = route.router.url_path_for('index') resp = features_test_client.get(url) assert resp.ok -def test_add_feature_to_user(features_test_client, session): - test = Feature( - name='test', - route='/route', - description='testing', - creator='test' - ) - - session.add(test) - session.commit() - +def test_add_feature_to_user(features_test_client, feature, form_mock): url = route.router.url_path_for('add_feature_to_user') - resp = features_test_client.post(url, data={ - 'feature_id': 1, - 'user_id': 1 - }) + resp = features_test_client.post(url, data=form_mock) assert resp.ok -def test_delete_user_feature_association(features_test_client, session): - test = Feature( - name='test', - route='/route', - description='testing', - creator='test' - ) - - session.add(test) - session.commit() - - feat = session.query(Feature).filter_by(name=test.name).first() - - internal.create_association( - db=session, feature_id=feat.id, user_id=1, is_enable=True) - +def test_delete_user_feature_association( + features_test_client, form_mock, association_on +): url = route.router.url_path_for('delete_user_feature_association') - resp = features_test_client.post(url, data={ - 'feature_id': 1, - 'user_id': 1 - }) + resp = features_test_client.post(url, data=form_mock) assert resp.ok assert resp.content == b'true' -def test_enable_feature(features_test_client, session): - test = Feature( - name='test', - route='/route', - description='testing', - creator='test' - ) - - session.add(test) - session.commit() - - feat = session.query(Feature).filter_by(name=test.name).first() - - internal.create_association( - db=session, feature_id=feat.id, user_id=1, is_enable=False) - +def test_enable_feature(features_test_client, form_mock, association_off): url = route.router.url_path_for('enable_feature') - resp = features_test_client.post(url, data={ - 'feature_id': 1, - 'user_id': 1 - }) + resp = features_test_client.post(url, data=form_mock) assert resp.ok assert resp.content == b'true' -def test_disable_feature(features_test_client, session): - test = Feature( - name='test', - route='/route', - description='testing', - creator='test' - ) - - session.add(test) - session.commit() - - feat = session.query(Feature).filter_by(name=test.name).first() - - internal.create_association( - db=session, feature_id=feat.id, user_id=1, is_enable=False) - +def test_disable_feature(features_test_client, form_mock, association_off): url = route.router.url_path_for('disable_feature') - resp = features_test_client.post(url, data={ - 'feature_id': 1, - 'user_id': 1 - }) + resp = features_test_client.post(url, data=form_mock) assert resp.ok assert resp.content == b'true' @@ -338,19 +233,7 @@ def test_show_user_disabled_features(mocker, features_test_client): assert resp.content == b'true' -def test_get_user_unlinked_features( - mocker, features_test_client, session): - - test = Feature( - name='test', - route='/route', - description='testing', - creator='test' - ) - - session.add(test) - session.commit() - +def test_get_user_unlinked_features(mocker, features_test_client, feature): mocker.patch( 'app.routers.features.get_user_disabled_features', return_value=True diff --git a/tests/test_whatsapp.py b/tests/test_whatsapp.py index ab73e25a..adff367f 100644 --- a/tests/test_whatsapp.py +++ b/tests/test_whatsapp.py @@ -38,7 +38,6 @@ def test_no_number(): def test_end_to_end_testing(client): - resp = client.get( - '/whatsapp?phone_number=972536106106&message=testing') + resp = client.get('/whatsapp?phone_number=972536106106&message=testing') assert resp.ok assert resp.json From 7f0ab2778b06ea05950e34fe48f8eac275ddf300 Mon Sep 17 00:00:00 2001 From: Liran C Date: Thu, 18 Feb 2021 14:52:55 +0200 Subject: [PATCH 079/115] .example --- app/config.py.example | 113 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 app/config.py.example diff --git a/app/config.py.example b/app/config.py.example new file mode 100644 index 00000000..6a07d951 --- /dev/null +++ b/app/config.py.example @@ -0,0 +1,113 @@ +import os +import pathlib + +from fastapi_mail import ConnectionConfig +from pydantic import BaseSettings +from starlette.templating import Jinja2Templates + + +class Settings(BaseSettings): + app_name: str = "PyLander" + bot_api: str = "BOT_API" + webhook_url: str = "WEBHOOK_URL" + + class Config: + env_file = ".env" + + +# GENERAL +DOMAIN = 'Our-Domain' + +# DATABASE +DEVELOPMENT_DATABASE_STRING = "sqlite:///./dev.db" +# Set the following True if working on PSQL environment or set False otherwise +PSQL_ENVIRONMENT = False + +# MEDIA +MEDIA_DIRECTORY = 'media' +PICTURE_EXTENSION = '.png' +AVATAR_SIZE = (120, 120) + + +# DEFAULT WEBSITE LANGUAGE +WEBSITE_LANGUAGE = "en" + +# API-KEYS +# Get a free API KEY for Astronomy feature @ www.weatherapi.com/signup.aspx +ASTRONOMY_API_KEY = os.getenv('ASTRONOMY_API_KEY') +WEATHER_API_KEY = os.getenv('WEATHER_API_KEY') + +# https://developers.google.com/calendar/quickstart/python - +# follow instracions and make an env variable with the path to the file. +CLIENT_SECRET_FILE = os.environ.get('CLIENT_SECRET') + + +# EXPORT +ICAL_VERSION = '2.0' +PRODUCT_ID = '-//Our product id//' + +# EMAIL +email_conf = ConnectionConfig( + MAIL_USERNAME=os.getenv("MAIL_USERNAME") or "user", + MAIL_PASSWORD=os.getenv("MAIL_PASSWORD") or "password", + MAIL_FROM=os.getenv("MAIL_FROM") or "a@a.com", + MAIL_PORT=587, + MAIL_SERVER="smtp.gmail.com", + MAIL_TLS=True, + MAIL_SSL=False, + USE_CREDENTIALS=True, +) + + +# security +JWT_KEY = "JWT_KEY_PLACEHOLDER" +JWT_ALGORITHM = "HS256" +JWT_MIN_EXP = 60 * 24 * 7 +templates = Jinja2Templates(directory=os.path.join("app", "templates")) + +# application name +CALENDAR_SITE_NAME = "Calendar" +# link to the home page of the application +CALENDAR_HOME_PAGE = "calendar.pythonic.guru" +# link to the application registration page +CALENDAR_REGISTRATION_PAGE = r"calendar.pythonic.guru/registration" + +# import +MAX_FILE_SIZE_MB = 5 # 5MB +VALID_FILE_EXTENSION = (".txt", ".csv", ".ics") # Can import only these files. +# Events must be within 20 years range from the current year. +EVENT_VALID_YEARS = 20 +EVENT_HEADER_NOT_EMPTY = 1 # 1- for not empty, 0- for empty. +EVENT_HEADER_LIMIT = 50 # Max characters for event header. +EVENT_CONTENT_LIMIT = 500 # Max characters for event content. +MAX_EVENTS_START_DATE = 10 # Max Events with the same start date. +LOCATION_LIMIT = 50 # Max characters for Location. +EVENT_DURATION_LIMIT = 2 # the max duration in days for an event. + +# EMOTION +""" +Emotion will appear if the level of significance is +equal to or above this constraint +""" +LEVEL_OF_SIGNIFICANCE = 0.45 +# The weight of emotion based on the event title +TITLE_WEIGHTS = 0.6 +# The weight of emotion based on the event content +CONTENT_WEIGHTS = 1 - TITLE_WEIGHTS + +# PATHS +STATIC_ABS_PATH = os.path.abspath("static") + +# LOGGER +LOG_PATH = "./var/log" +LOG_FILENAME = "calendar.log" +LOG_LEVEL = "error" +LOG_ROTATION_INTERVAL = "20 days" +LOG_RETENTION_INTERVAL = "1 month" +LOG_FORMAT = ("{level: <8}" + " {time:YYYY-MM-DD HH:mm:ss.SSS}" + " - {name}:{function}" + " - {message}") + +# RESOURCES +RESOURCES_DIR = pathlib.Path(__file__).parent / 'resources' From 62a029534318f821dae5d4058412b775df515949 Mon Sep 17 00:00:00 2001 From: Aviad Amar Date: Thu, 18 Feb 2021 20:15:53 +0100 Subject: [PATCH 080/115] before pull 18.02.21 --- app/routers/calendar_grid.py | 4 ++-- app/static/global.css | 14 +++++++------- app/static/js/features.js | 19 +++++++++++++++++-- app/templates/features.html | 4 ++-- .../partials/calendar/navigation.html | 10 +++++++--- 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/app/routers/calendar_grid.py b/app/routers/calendar_grid.py index 42ba4efb..eb128eec 100644 --- a/app/routers/calendar_grid.py +++ b/app/routers/calendar_grid.py @@ -29,8 +29,8 @@ class Day: def __init__(self, date: datetime): self.date: datetime = date self.sday: str = self.date.strftime("%A") - self.dailyevents: List[Tuple] = [] - self.events: List[Tuple] = [] + self.dailyevents: List[Tuple] = [("Yam's Birthday", "Buy a present")] + self.events: List[Tuple] = [("09AP", "Meeting with yam")] self.css: Dict[str, str] = { 'day_container': 'day', 'date': 'day-number', diff --git a/app/static/global.css b/app/static/global.css index aac071fb..e4138e59 100644 --- a/app/static/global.css +++ b/app/static/global.css @@ -2,17 +2,17 @@ /* Texts */ --text_xs: 0.75rem; /* 12px */ --text_s: 1rem; /* 16px */ - --text_m: 1.2rem; /* 19px */ - --text_l: 1.5rem; /* 24px */ + --text_m: 1.2rem; /* 19px */ + --text_l: 1.5rem; /* 24px */ --text_xl: 1.75rem; /* 28px */ --text_xxl: 2rem; /* 32px */ /* Spaces */ - --space_xs: 0.5rem; /* 6px */ - --space_s: 0.75rem; /* 12px */ - --space_m: 1.25rem; /* 20px */ - --space_l: 2rem; /* 32px */ - --space_xl: 4rem; /* 64px */ + --space_xs: 0.5rem; /* 6px */ + --space_s: 0.75rem; /* 12px */ + --space_m: 1.25rem; /* 20px */ + --space_l: 2rem; /* 32px */ + --space_xl: 4rem; /* 64px */ /* colors */ --primary: #F7F7F7; diff --git a/app/static/js/features.js b/app/static/js/features.js index 7afe35b7..b86c1972 100644 --- a/app/static/js/features.js +++ b/app/static/js/features.js @@ -19,12 +19,14 @@ function display(elements, targetID) { } function searchFeature(event, rows, targetID) { - const searchValue = event.target.value.trim(); + const searchValue = event.target.value.trim().toLowerCase(); if (!searchValue) { appendElement(rows, targetID); } else { const result = rows.filter( - row => { return row.getElementsByClassName("row-feature-name")[0].innerHTML.includes(searchValue) } + row => { + return row.getElementsByClassName("row-feature-name")[0].innerHTML.toLowerCase().includes(searchValue) + } ); return result; } @@ -58,9 +60,22 @@ function setSearchBox(targetID) { }); } + +function setAdd(className) { + const allRows = document.getElementsByClassName(className); + for (let i = 0; i < allRows.length; ++i) { + if (allRows[i].dataset.state === "available") { + setAdd(allRows[i]); + } else { + setRemove(allRows[i]); + } + } +} + document.addEventListener( 'DOMContentLoaded', function () { setSearchBox("available-features"); + setAdd(className); } ) diff --git a/app/templates/features.html b/app/templates/features.html index 1d8e4bcf..9c4e9b12 100644 --- a/app/templates/features.html +++ b/app/templates/features.html @@ -40,7 +40,7 @@
Available Features
Your Features
- {% for i in range(5) %} + {% for i in range(3) %}
@@ -61,7 +61,7 @@ {% endfor %}
- {% for i in range(5) %} + {% for i in range(3) %}
diff --git a/app/templates/partials/calendar/navigation.html b/app/templates/partials/calendar/navigation.html index 5823e65b..591d7e22 100644 --- a/app/templates/partials/calendar/navigation.html +++ b/app/templates/partials/calendar/navigation.html @@ -10,14 +10,18 @@
- + + +
- + + +
From 1c1bbe70eb63bb42c7a3ffc310524a8d7524086f Mon Sep 17 00:00:00 2001 From: Liran C Date: Fri, 19 Feb 2021 11:27:38 +0200 Subject: [PATCH 081/115] requested changes --- app/database/models.py | 2 +- app/internal/features.py | 22 ++++++------------- app/routers/features.py | 10 ++++----- tests/test_feature_panel.py | 43 ++++++++++++++++++++++++++----------- 4 files changed, 44 insertions(+), 33 deletions(-) diff --git a/app/database/models.py b/app/database/models.py index fbacd2cc..227724f8 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -26,7 +26,7 @@ class UserFeature(Base): feature_id = Column('feature_id', Integer, ForeignKey('features.id')) user_id = Column('user_id', Integer, ForeignKey('users.id')) - is_enable = Column(Boolean) + is_enable = Column(Boolean, default=False) class User(Base): diff --git a/app/internal/features.py b/app/internal/features.py index 6704cd1c..7c18b8cb 100644 --- a/app/internal/features.py +++ b/app/internal/features.py @@ -1,4 +1,5 @@ from fastapi import Depends +from typing import List from app.database.models import UserFeature, Feature from app.dependencies import get_db, SessionLocal @@ -72,12 +73,9 @@ def is_feature_exists_in_disabled( def is_feature_enabled(route: str) -> bool: session = SessionLocal() - user = get_current_user(session=session) - feature = session.query(Feature).filter_by(route=route).first() - # *This condition must be before line 168 to avoid AttributeError!* if feature is None: # in case there is no feature exists in the database that match the # route that gived by to the request. @@ -97,38 +95,32 @@ def create_feature(name: str, route: str, """Creates a feature.""" db = SessionLocal() - - feature = create_model( + return create_model( db, Feature, name=name, route=route, creator=creator, description=description ) - return feature def create_association( db: SessionLocal, feature_id: int, user_id: int, is_enable: bool ) -> UserFeature: """Creates an association.""" - - association = create_model( + return create_model( db, UserFeature, user_id=user_id, feature_id=feature_id, is_enable=is_enable ) - return association - -def get_user_enabled_features(session: SessionLocal = Depends(get_db)) -> list: +def get_user_enabled_features(session: SessionLocal = Depends(get_db)) -> List: user = get_current_user(session=session) - data = [] - user_prefs = session.query(UserFeature).filter_by(user_id=user.id).all() + for pref in user_prefs: if pref.is_enable: feature = session.query(Feature).filter_by( @@ -140,11 +132,11 @@ def get_user_enabled_features(session: SessionLocal = Depends(get_db)) -> list: def get_user_disabled_features( session: SessionLocal = Depends(get_db) -) -> list: +) -> List: user = get_current_user(session=session) - data = [] user_prefs = session.query(UserFeature).filter_by(user_id=user.id).all() + for pref in user_prefs: if not pref.is_enable: feature = session.query(Feature).filter_by( diff --git a/app/routers/features.py b/app/routers/features.py index cccd05eb..daeb71b5 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -1,4 +1,5 @@ from fastapi import APIRouter, Request, Depends +from typing import List from app.dependencies import get_db, SessionLocal from app.database.models import UserFeature, Feature @@ -21,7 +22,7 @@ @router.get('/') async def index( request: Request, session: SessionLocal = Depends(get_db) -) -> list: +) -> List: features = session.query(Feature).all() return features @@ -99,7 +100,6 @@ async def enable_feature(request: Request, async def disable_feature(request: Request, session: SessionLocal = Depends(get_db)) -> bool: form = await request.form() - print(dict(form)) is_exist = is_association_exists_in_db(form=form, session=session) if not is_exist: @@ -119,21 +119,21 @@ async def disable_feature(request: Request, @router.get('/active') def show_user_enabled_features( session: SessionLocal = Depends(get_db) -) -> list: +) -> List: return get_user_enabled_features(session=session) @router.get('/deactive') def show_user_disabled_features( session: SessionLocal = Depends(get_db) -) -> list: +) -> List: return get_user_disabled_features(session=session) @router.get('/unlinked') def get_user_unlinked_features( session: SessionLocal = Depends(get_db) -) -> list: +) -> List: data = [] all_features = session.query(Feature).all() diff --git a/tests/test_feature_panel.py b/tests/test_feature_panel.py index 5cc8a96e..a2baf648 100644 --- a/tests/test_feature_panel.py +++ b/tests/test_feature_panel.py @@ -32,14 +32,14 @@ def feature(session): yield test session.query(Feature).delete() + session.commit() @pytest.fixture @pytest.mark.usefixtures('session') -def association_off(session): - print(session) +def association_off(session, user): test = UserFeature( - feature_id=1, user_id=1, is_enable=False) + feature_id=1, user_id=user.id, is_enable=False) session.add(test) session.commit() @@ -47,20 +47,22 @@ def association_off(session): yield test session.query(UserFeature).delete() + session.commit() @pytest.fixture @pytest.mark.usefixtures('session') -def association_on(session): +def association_on(session, user): test = UserFeature( - feature_id=1, user_id=1, is_enable=True) + feature_id=1, user_id=user.id, is_enable=True) session.add(test) session.commit() yield test - session.delete(test) + session.query(UserFeature).delete() + session.commit() @pytest.fixture @@ -99,9 +101,9 @@ def test_create_features_at_startup(mocker, session, mock_features): assert internal.create_features_at_startup(session) -def test_create_association(session): +def test_create_association(session, user): assert internal.create_association( - db=session, feature_id=1, user_id=1, is_enable=False + db=session, feature_id=1, user_id=user.id, is_enable=False ) is not None @@ -233,10 +235,24 @@ def test_show_user_disabled_features(mocker, features_test_client): assert resp.content == b'true' -def test_get_user_unlinked_features(mocker, features_test_client, feature): +def test_get_user_unlinked_features(mocker, features_test_client, session): + unlinked = Feature( + name='unlinked', + route='/unlinked', + description='unlinked', + creator='unlinked' + ) + + session.add(unlinked) + session.commit() + mocker.patch( - 'app.routers.features.get_user_disabled_features', - return_value=True + 'app.routers.features.is_feature_exists_in_disabled', + return_value=False + ) + mocker.patch( + 'app.routers.features.is_feature_exists_in_enabled', + return_value=False ) url = route.router.url_path_for('get_user_unlinked_features') @@ -244,4 +260,7 @@ def test_get_user_unlinked_features(mocker, features_test_client, feature): resp = features_test_client.get(url) assert resp.ok json_resp = resp.json() - assert type(json_resp) is list + print(json_resp) + session.query(Feature).delete() + session.commit() + assert len(json_resp) == 1 From 4eeaeeac457a830eb1b1bbb8a993ab43f906844d Mon Sep 17 00:00:00 2001 From: Aviad Amar Date: Sat, 20 Feb 2021 11:18:09 +0100 Subject: [PATCH 082/115] Before changit to fetch --- app/main.py | 2 +- app/static/global.css | 5 +- app/static/grid_style.css | 4 +- app/static/js/features.js | 99 ++++++++++--------- app/templates/features.html | 12 +-- .../partials/calendar/calendar_base.html | 28 +----- .../calendar/monthly_view/monthly_grid.html | 23 +---- .../partials/calendar/navigation.html | 14 +-- 8 files changed, 78 insertions(+), 109 deletions(-) diff --git a/app/main.py b/app/main.py index 1244e789..4340299f 100644 --- a/app/main.py +++ b/app/main.py @@ -43,7 +43,7 @@ def create_tables(engine, psql_environment): about_us, agenda, calendar, categories, celebrity, credits, currency, dayview, email, event, export, four_o_four, friendview, google_connect, invitation, login, logout, profile, - register, search, telegram, user, weekview, whatsapp, + register, search, telegram, user, weekview, whatsapp, features ) json_data_loader.load_to_database(next(get_db())) diff --git a/app/static/global.css b/app/static/global.css index bb4a014a..1d54173b 100644 --- a/app/static/global.css +++ b/app/static/global.css @@ -65,7 +65,7 @@ a { } .search-box { - width: 95%; + width: 98%; display: flex; flex-direction: row; align-items: center; @@ -115,13 +115,12 @@ a { } .feature-row:hover { - border: 2px solid var(--secondary); + border: 2px solid var(--primary); } .feature-row div { justify-self: start; width: 100%; - height: 100%; } .feature-button { diff --git a/app/static/grid_style.css b/app/static/grid_style.css index 718d66b9..339c7125 100644 --- a/app/static/grid_style.css +++ b/app/static/grid_style.css @@ -177,7 +177,7 @@ main { font-size: var(--text_xs); padding: 0 var(--space_s); - border: 0.095rem solid var(--surface); + border: 0.075rem solid var(--surface); overflow-y: auto; -ms-overflow-style: none; /* IE and Edge */ @@ -359,6 +359,8 @@ main { .text-darkblue {color: var(--primary);} +.text-red {color: var(--negative)} + /* Borders */ .border-dash-darkblue {border: 0.125rem dashed var(--primary);} diff --git a/app/static/js/features.js b/app/static/js/features.js index b86c1972..38b1c42e 100644 --- a/app/static/js/features.js +++ b/app/static/js/features.js @@ -13,69 +13,80 @@ function deleteElements(targetID) { } } -function display(elements, targetID) { - deleteElements(targetID); +function display(elements, targetID, empty) { + if (!empty) { + deleteElements(targetID); + } appendElement(elements, targetID); } -function searchFeature(event, rows, targetID) { - const searchValue = event.target.value.trim().toLowerCase(); - if (!searchValue) { - appendElement(rows, targetID); - } else { - const result = rows.filter( - row => { - return row.getElementsByClassName("row-feature-name")[0].innerHTML.toLowerCase().includes(searchValue) - } - ); - return result; - } +function searchFeature(searchValue, elements) { + const result = elements.filter( + element => { + return element.getElementsByClassName("row-feature-name")[0].innerHTML.toLowerCase().includes(searchValue) + } + ); + return result; } -function getAvailable() { - let allAva = [] - const allAvailable = document.getElementsByClassName("feature-row"); - for (let i = 0; i < allAvailable.length; ++i) { - if (allAvailable[i].dataset.state === "available") { - allAva.push(allAvailable[i]); +function getAvailable(elements) { + let availables = []; + for (let i = 0; i < elements.length; ++i) { + const element = elements[i]; + if (element.dataset.state === "available") { + availables.push(element); } } - return allAva; + return availables; } function setSearchBox(targetID) { - searchBox = document.getElementsByClassName("search-input"); - if (!searchBox) { - return false; - } - const rows = getAvailable(); - searchBox[0].addEventListener('input', function (evt) { - if (evt.which === null) { - appendElement(rows, targetID) - } - deleteElements(targetID); - const result = searchFeature(evt, rows, targetID); - display(result, targetID); - - }); + const rows = document.getElementsByClassName("feature-row"); + const searchBox = document.getElementsByClassName("search-input"); + searchBox[0].addEventListener( + 'input', function (evt) { + console.log(rows); + const value = evt.target.value.trim().toLowerCase(); + if (!value) { + display(rows, targetID, true); + } else { + const result = searchFeature(value, rows); + display(result, targetID, false); + } + }); } +function moveColumn(rows) { + const row = this.parentElement; + const category = row.parentElement; + if (category.id === "available-features") { + this.innerHTML = "REMOVE"; + this.classList.remove("background-green"); + this.classList.add("background-red"); + row.dataset.state = "installed"; + document.getElementById("installed-features").appendChild(row); + } else { + this.innerHTML = "ADD"; + this.classList.remove("background-red"); + this.classList.add("background-green"); + row.dataset.state = "available"; + document.getElementById("available-features").appendChild(row); + } +} -function setAdd(className) { - const allRows = document.getElementsByClassName(className); - for (let i = 0; i < allRows.length; ++i) { - if (allRows[i].dataset.state === "available") { - setAdd(allRows[i]); - } else { - setRemove(allRows[i]); - } +function setRemoveAdd(className) { + const rows = document.getElementsByClassName(className); + for (let i = 0; i < rows.length; ++i) { + const row = rows[i]; + const button = row.getElementsByClassName("feature-button")[0]; + button.addEventListener('click', moveColumn); } } document.addEventListener( 'DOMContentLoaded', function () { setSearchBox("available-features"); - setAdd(className); + setRemoveAdd("feature-row"); } ) diff --git a/app/templates/features.html b/app/templates/features.html index 9c4e9b12..27587e79 100644 --- a/app/templates/features.html +++ b/app/templates/features.html @@ -41,12 +41,12 @@
Your Features
{% for i in range(3) %} -
+
- Name + Name{{i}}
Creator: Username
@@ -54,20 +54,20 @@ Followers: 0K
-
+
ADD
{% endfor %}
-
+
{% for i in range(3) %} -
+
- Name + Name{{i * 4}}
Creator: Username
diff --git a/app/templates/partials/calendar/calendar_base.html b/app/templates/partials/calendar/calendar_base.html index c4a979c2..722ea474 100644 --- a/app/templates/partials/calendar/calendar_base.html +++ b/app/templates/partials/calendar/calendar_base.html @@ -1,21 +1,13 @@ {% extends "./partials/base.html" %} {% block head %} -<<<<<<< HEAD -{{ super() }} +{{super()}} - -======= - {{super()}} - - - ->>>>>>> 10489565b3891ff7ce2f711df7ef2897545d934f + {% endblock head %} {% block page_name %}Month View{% endblock page_name %} {% block body %} -<<<<<<< HEAD
{% include 'partials/calendar/navigation.html' %}
@@ -31,20 +23,4 @@ -======= -
- {% include 'partials/calendar/navigation.html' %} -
- {% include 'partials/calendar/feature_settings/example.html' %} -
-
- {% block content %} - {% endblock content %} -
-
- - - - ->>>>>>> 10489565b3891ff7ce2f711df7ef2897545d934f {% endblock body %} \ No newline at end of file diff --git a/app/templates/partials/calendar/monthly_view/monthly_grid.html b/app/templates/partials/calendar/monthly_view/monthly_grid.html index 8da1d6d1..45572532 100644 --- a/app/templates/partials/calendar/monthly_view/monthly_grid.html +++ b/app/templates/partials/calendar/monthly_view/monthly_grid.html @@ -13,7 +13,6 @@
-
-======= -
-
- - TODAY - - - - - - - - - - -
-
-
-
->>>>>>> 10489565b3891ff7ce2f711df7ef2897545d934f +
\ No newline at end of file diff --git a/app/templates/partials/calendar/navigation.html b/app/templates/partials/calendar/navigation.html index 9292de50..5b364b6b 100644 --- a/app/templates/partials/calendar/navigation.html +++ b/app/templates/partials/calendar/navigation.html @@ -20,7 +20,14 @@
- + + + +
+
@@ -31,10 +38,5 @@
-
- - - -
\ No newline at end of file From 107d246cc0c8c751c9501dd6f1dedc2da6a55aa7 Mon Sep 17 00:00:00 2001 From: Liran C Date: Sat, 20 Feb 2021 20:53:39 +0200 Subject: [PATCH 083/115] changes --- app/features/__init__.py | 0 app/features/utils.py | 34 ----------- app/internal/features.py | 59 ++++++++++++++----- .../index.py => internal/features_index.py} | 0 app/routers/features.py | 21 +++---- app/routers/google_connect.py | 2 +- tests/test_feature_panel.py | 16 ++--- 7 files changed, 65 insertions(+), 67 deletions(-) delete mode 100644 app/features/__init__.py delete mode 100644 app/features/utils.py rename app/{features/index.py => internal/features_index.py} (100%) diff --git a/app/features/__init__.py b/app/features/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/app/features/utils.py b/app/features/utils.py deleted file mode 100644 index 791b5828..00000000 --- a/app/features/utils.py +++ /dev/null @@ -1,34 +0,0 @@ -from functools import wraps -from starlette.responses import RedirectResponse - -from app.internal.features import is_feature_enabled - - -def feature_access_filter(call_next): - - @wraps(call_next) - async def wrapper(*args, **kwargs): - request = kwargs['request'] - - if request.headers['user-agent'] == 'testclient': - # in case it's a unit test. - return await call_next(*args, **kwargs) - - # getting the url route path for matching with the database. - route = '/' + str(request.url).replace(str(request.base_url), '') - - # getting access status. - is_enabled = is_feature_enabled(route=route) - - if is_enabled: - # in case the feature is enabled or access is allowed. - return await call_next(*args, **kwargs) - - elif 'referer' not in request.headers: - # in case request come straight from address bar in browser. - return RedirectResponse(url='/') - - # in case the feature is disabled or access isn't allowed. - return RedirectResponse(url=request.headers['referer']) - - return wrapper diff --git a/app/internal/features.py b/app/internal/features.py index 7c18b8cb..61b5d567 100644 --- a/app/internal/features.py +++ b/app/internal/features.py @@ -1,15 +1,47 @@ from fastapi import Depends +from functools import wraps +from starlette.responses import RedirectResponse from typing import List from app.database.models import UserFeature, Feature from app.dependencies import get_db, SessionLocal -from app.features.index import features +from app.internal.features_index import features from app.internal.utils import create_model, get_current_user +def feature_access_filter(call_next): + + @wraps(call_next) + async def wrapper(*args, **kwargs): + request = kwargs['request'] + + if request.headers['user-agent'] == 'testclient': + # in case it's a unit test. + return await call_next(*args, **kwargs) + + # getting the url route path for matching with the database. + route = '/' + str(request.url).replace(str(request.base_url), '') + + # getting access status. + is_enabled = is_access_allowd(route=route) + + if is_enabled: + # in case the feature is enabled or access is allowed. + return await call_next(*args, **kwargs) + + elif 'referer' not in request.headers: + # in case request come straight from address bar in browser. + return RedirectResponse(url='/') + + # in case the feature is disabled or access isn't allowed. + return RedirectResponse(url=request.headers['referer']) + + return wrapper + + def create_features_at_startup(session: SessionLocal) -> bool: for feat in features: - if not is_feature_exists_in_db(feature=feat, session=session): + if not is_feature_exists(feature=feat, session=session): create_feature(**feat, db=session) return True @@ -32,7 +64,7 @@ def delete_feature( session.commit() -def is_feature_exists_in_db(feature: dict, session: SessionLocal) -> bool: +def is_feature_exists(feature: dict, session: SessionLocal) -> bool: db_feature = session.query(Feature).filter( (Feature.name == feature['name']) | (Feature.route == feature['route'])).first() @@ -57,21 +89,21 @@ def update_feature(feature: Feature, new_feature_obj: dict, return feature -def is_feature_exists_in_enabled( +def is_feature_enabled( feature: Feature, session: SessionLocal = Depends(get_db) ) -> bool: enabled_features = get_user_enabled_features(session=session) return any(ef['feature'].id == feature.id for ef in enabled_features) -def is_feature_exists_in_disabled( +def is_feature_disabled( feature: Feature, session: SessionLocal = Depends(get_db) ) -> bool: disable_features = get_user_disabled_features(session=session) return any(ef['feature'].id == feature.id for ef in disable_features) -def is_feature_enabled(route: str) -> bool: +def is_access_allowd(route: str) -> bool: session = SessionLocal() user = get_current_user(session=session) feature = session.query(Feature).filter_by(route=route).first() @@ -93,7 +125,6 @@ def create_feature(name: str, route: str, description: str, creator: str = None, db: SessionLocal = Depends()) -> Feature: """Creates a feature.""" - db = SessionLocal() return create_model( db, Feature, @@ -104,7 +135,7 @@ def create_feature(name: str, route: str, ) -def create_association( +def create_user_feature_association( db: SessionLocal, feature_id: int, user_id: int, is_enable: bool ) -> UserFeature: """Creates an association.""" @@ -118,29 +149,29 @@ def create_association( def get_user_enabled_features(session: SessionLocal = Depends(get_db)) -> List: user = get_current_user(session=session) - data = [] + enabled = [] user_prefs = session.query(UserFeature).filter_by(user_id=user.id).all() for pref in user_prefs: if pref.is_enable: feature = session.query(Feature).filter_by( id=pref.feature_id).first() - data.append({'feature': feature, 'is_enabled': pref.is_enable}) + enabled.append({'feature': feature, 'is_enabled': pref.is_enable}) - return data + return enabled def get_user_disabled_features( session: SessionLocal = Depends(get_db) ) -> List: user = get_current_user(session=session) - data = [] + disabled = [] user_prefs = session.query(UserFeature).filter_by(user_id=user.id).all() for pref in user_prefs: if not pref.is_enable: feature = session.query(Feature).filter_by( id=pref.feature_id).first() - data.append({'feature': feature, 'is_enabled': pref.is_enable}) + disabled.append({'feature': feature, 'is_enabled': pref.is_enable}) - return data + return disabled diff --git a/app/features/index.py b/app/internal/features_index.py similarity index 100% rename from app/features/index.py rename to app/internal/features_index.py diff --git a/app/routers/features.py b/app/routers/features.py index daeb71b5..3299d763 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -1,13 +1,14 @@ +from app.internal.utils import get_current_user from fastapi import APIRouter, Request, Depends from typing import List from app.dependencies import get_db, SessionLocal from app.database.models import UserFeature, Feature from app.internal.features import ( - create_association, + create_user_feature_association, is_association_exists_in_db, - is_feature_exists_in_disabled, - is_feature_exists_in_enabled, + is_feature_disabled, + is_feature_enabled, get_user_disabled_features, get_user_enabled_features ) @@ -33,7 +34,7 @@ async def add_feature_to_user( ) -> UserFeature: form = await request.form() - user_id = form['user_id'] # OPTION - get active user id instead. + user = get_current_user(session=session) feat = session.query(Feature).filter_by(id=form['feature_id']).first() is_exist = is_association_exists_in_db(form=form, session=session) @@ -43,10 +44,10 @@ async def add_feature_to_user( # and or the association is exist return False - association = create_association( + association = create_user_feature_association( db=session, feature_id=feat.id, - user_id=user_id, + user_id=user.id, is_enable=True ) @@ -60,7 +61,7 @@ async def delete_user_feature_association( ) -> bool: form = await request.form() - user_id = form['user_id'] # OPTION - get active user id instead. + user = get_current_user(session=session) feature_id = form['feature_id'] is_exist = is_association_exists_in_db(form=form, session=session) @@ -70,7 +71,7 @@ async def delete_user_feature_association( session.query(UserFeature).filter_by( feature_id=feature_id, - user_id=user_id + user_id=user.id ).delete() session.commit() @@ -138,11 +139,11 @@ def get_user_unlinked_features( all_features = session.query(Feature).all() for feat in all_features: - in_disabled = is_feature_exists_in_disabled( + in_disabled = is_feature_disabled( feature=feat, session=session ) - in_enabled = is_feature_exists_in_enabled( + in_enabled = is_feature_enabled( feature=feat, session=session ) diff --git a/app/routers/google_connect.py b/app/routers/google_connect.py index bbc3329d..110f8d91 100644 --- a/app/routers/google_connect.py +++ b/app/routers/google_connect.py @@ -6,7 +6,7 @@ from app.dependencies import get_db from app.internal.google_connect import get_credentials, fetch_save_events from app.routers.profile import router as profile -from app.features.utils import feature_access_filter +from app.internal.features import feature_access_filter router = APIRouter( prefix="/google", diff --git a/tests/test_feature_panel.py b/tests/test_feature_panel.py index a2baf648..49279704 100644 --- a/tests/test_feature_panel.py +++ b/tests/test_feature_panel.py @@ -94,7 +94,7 @@ def test_create_features_at_startup(mocker, session, mock_features): mock_features ) mocker.patch( - 'app.internal.features.is_feature_exists_in_db', + 'app.internal.features.is_feature_exists', return_value=False ) @@ -102,7 +102,7 @@ def test_create_features_at_startup(mocker, session, mock_features): def test_create_association(session, user): - assert internal.create_association( + assert internal.create_user_feature_association( db=session, feature_id=1, user_id=user.id, is_enable=False ) is not None @@ -132,7 +132,7 @@ def test_delete_feature(session, feature): def test_is_feature_exist_in_db(session, feature, update_dict): - assert internal.is_feature_exists_in_db(update_dict, session) + assert internal.is_feature_exists(update_dict, session) def test_update_feature(session, feature, update_dict): @@ -142,12 +142,12 @@ def test_update_feature(session, feature, update_dict): def test_is_feature_exist_in_enabled(session, feature, association_on): feat = session.query(Feature).filter_by(name=feature.name).first() - assert internal.is_feature_exists_in_enabled(feat, session) + assert internal.is_feature_enabled(feat, session) def test_is_feature_exist_in_disabled(session, feature, association_off): feat = session.query(Feature).filter_by(name=feature.name).first() - assert internal.is_feature_exists_in_disabled(feat, session) + assert internal.is_feature_disabled(feat, session) def test_is_feature_enabled(mocker, session, association_on): @@ -155,7 +155,7 @@ def test_is_feature_enabled(mocker, session, association_on): 'app.internal.features.SessionLocal', return_value=session ) - assert internal.is_feature_enabled(route='/route') is True + assert internal.is_access_allowd(route='/route') is True def test_create_feature(session): @@ -247,11 +247,11 @@ def test_get_user_unlinked_features(mocker, features_test_client, session): session.commit() mocker.patch( - 'app.routers.features.is_feature_exists_in_disabled', + 'app.routers.features.is_feature_disabled', return_value=False ) mocker.patch( - 'app.routers.features.is_feature_exists_in_enabled', + 'app.routers.features.is_feature_enabled', return_value=False ) From ea84de58f1292d1ff781aa9b90d2170b200faad1 Mon Sep 17 00:00:00 2001 From: Aviad Amar Date: Sun, 21 Feb 2021 10:23:22 +0100 Subject: [PATCH 084/115] Before Passing to Liran --- app/templates/features.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/templates/features.html b/app/templates/features.html index 745aca1a..8cd63a02 100644 --- a/app/templates/features.html +++ b/app/templates/features.html @@ -49,7 +49,7 @@
{{feature.name.upper()}} - {{feature.information}} +

{{feature.information}}

Creator: {{feature.creator.title()}}
@@ -72,6 +72,7 @@
{{feature.name.upper()}} +

{{feature.information}}

Creator: {{feature.creator.title()}}
From bf7fae9f62b32cea0e85a948549c2a2ab1eed20b Mon Sep 17 00:00:00 2001 From: Aviad Amar Date: Sun, 21 Feb 2021 10:26:29 +0100 Subject: [PATCH 085/115] Before passing to Liran --- app/routers/calendar_grid.py | 4 +- app/templates/calendar/add_week.html | 21 ------- app/templates/calendar/calendar.html | 19 ------- app/templates/calendar/layout.html | 82 ---------------------------- 4 files changed, 2 insertions(+), 124 deletions(-) delete mode 100644 app/templates/calendar/add_week.html delete mode 100644 app/templates/calendar/calendar.html delete mode 100644 app/templates/calendar/layout.html diff --git a/app/routers/calendar_grid.py b/app/routers/calendar_grid.py index 2797a3b9..b8b0878f 100644 --- a/app/routers/calendar_grid.py +++ b/app/routers/calendar_grid.py @@ -29,8 +29,8 @@ class Day: def __init__(self, date: datetime): self.date: datetime = date self.sday: str = self.date.strftime("%A") - self.dailyevents: List[Tuple] = [("Yam's Birthday", "Buy a present")] - self.events: List[Tuple] = [("09AP", "Meeting with yam")] + self.dailyevents: List[Tuple] = [] + self.events: List[Tuple] = [] self.css: Dict[str, str] = { 'day_container': 'day', 'date': 'day-number', diff --git a/app/templates/calendar/add_week.html b/app/templates/calendar/add_week.html deleted file mode 100644 index 702ee572..00000000 --- a/app/templates/calendar/add_week.html +++ /dev/null @@ -1,21 +0,0 @@ -{% for week in weeks_block %} -
- {% for day in week.days %} -
-
-
{{day}}
- -
- {% for devent in day.dailyevents %} -
-
{{devent[0]}}
-
{{devent[1]}}
-
- {% endfor %} - {% for event in day.events %} -
{{event[0]}} {{event[1]}}
- {% endfor %} -
- {% endfor %} -
-{% endfor %} \ No newline at end of file diff --git a/app/templates/calendar/calendar.html b/app/templates/calendar/calendar.html deleted file mode 100644 index d5d95c15..00000000 --- a/app/templates/calendar/calendar.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends 'calendar/layout.html' %} - -{% block main %} -
-
- {% for d in week_days %} - {% if d == day.sday %} -
{{ d.upper() }}
- {% else %} -
{{ d.upper() }}
- {% endif %} - {% endfor %} -
-
- {% include 'calendar/add_week.html' %} -
-
-
-{% endblock %} \ No newline at end of file diff --git a/app/templates/calendar/layout.html b/app/templates/calendar/layout.html deleted file mode 100644 index 068fee42..00000000 --- a/app/templates/calendar/layout.html +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - - - - - - - - Calendar - - - -
- -
-
FEATURE NAME
-
-
-
-
-
{{day.display()}}
-
Location 0oc 00:00
-
- -
-
- {% block main %} {% endblock %} -
-
-
- - - \ No newline at end of file From 182b6bee7515761bbc26ebbeea69c58d95d4e417 Mon Sep 17 00:00:00 2001 From: Liran C Date: Sun, 21 Feb 2021 11:31:25 +0200 Subject: [PATCH 086/115] before cache --- app/internal/features.py | 18 ++++++++++++++++-- app/routers/features.py | 6 ++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/internal/features.py b/app/internal/features.py index 61b5d567..558798ae 100644 --- a/app/internal/features.py +++ b/app/internal/features.py @@ -1,7 +1,8 @@ from fastapi import Depends from functools import wraps +from sqlalchemy.sql.functions import session_user from starlette.responses import RedirectResponse -from typing import List +from typing import List, Dict from app.database.models import UserFeature, Feature from app.dependencies import get_db, SessionLocal @@ -39,6 +40,19 @@ async def wrapper(*args, **kwargs): return wrapper +async def create_dict_for_users_features_token( + user_id: int, session: SessionLocal = Depends(get_db) +) -> Dict: + features_dict = {} + all_features = session.query(UserFeature).filter_by(user_id=user_id).all() + + for feat in all_features: + features_dict.update( + {f'{feat.user_id}{feat.feature_id}': feat.__dict__}) + + return features_dict + + def create_features_at_startup(session: SessionLocal) -> bool: for feat in features: if not is_feature_exists(feature=feat, session=session): @@ -50,7 +64,7 @@ def create_features_at_startup(session: SessionLocal) -> bool: def is_association_exists_in_db(form: dict, session: SessionLocal) -> bool: db_association = session.query(UserFeature).filter_by( feature_id=form['feature_id'], - user_id=form['user_id'] + user_id=get_current_user(session=session).id ).first() return db_association is not None diff --git a/app/routers/features.py b/app/routers/features.py index 3299d763..ad650e3c 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -10,7 +10,8 @@ is_feature_disabled, is_feature_enabled, get_user_disabled_features, - get_user_enabled_features + get_user_enabled_features, + create_dict_for_users_features_token ) router = APIRouter( @@ -24,7 +25,8 @@ async def index( request: Request, session: SessionLocal = Depends(get_db) ) -> List: - features = session.query(Feature).all() + # features = session.query(Feature).all() + features = await create_dict_for_users_features_token(user_id=1, session=session) return features From f9145c8483edcfb915bc327effa497125d0de177 Mon Sep 17 00:00:00 2001 From: Liran C Date: Sun, 21 Feb 2021 12:46:27 +0200 Subject: [PATCH 087/115] adjusted to front --- app/internal/features.py | 42 +++-------------- app/routers/features.py | 69 ++-------------------------- app/templates/features.html | 2 +- tests/test_feature_panel.py | 89 +------------------------------------ 4 files changed, 11 insertions(+), 191 deletions(-) diff --git a/app/internal/features.py b/app/internal/features.py index 6098e008..a286f669 100644 --- a/app/internal/features.py +++ b/app/internal/features.py @@ -23,9 +23,9 @@ async def wrapper(*args, **kwargs): route = '/' + str(request.url).replace(str(request.base_url), '') # getting access status. - is_enabled = is_access_allowd(route=route) + access = is_access_allowd(route=route) - if is_enabled: + if access: # in case the feature is enabled or access is allowed. return await call_next(*args, **kwargs) @@ -100,14 +100,7 @@ def is_feature_enabled( feature: Feature, session: SessionLocal = Depends(get_db) ) -> bool: enabled_features = get_user_enabled_features(session=session) - return any(ef['feature'].id == feature.id for ef in enabled_features) - - -def is_feature_disabled( - feature: Feature, session: SessionLocal = Depends(get_db) -) -> bool: - disable_features = get_user_disabled_features(session=session) - return any(ef['feature'].id == feature.id for ef in disable_features) + return any(ef.id == feature.id for ef in enabled_features) def is_access_allowd(route: str) -> bool: @@ -169,41 +162,21 @@ def get_user_enabled_features(session: SessionLocal = Depends(get_db)) -> List: if pref.is_enable: feature = session.query(Feature).filter_by( id=pref.feature_id).first() - enabled.append({'feature': feature, 'is_enabled': pref.is_enable}) + enabled.append(feature) return enabled -def get_user_disabled_features( - session: SessionLocal = Depends(get_db) -) -> List: - user = get_current_user(session=session) - disabled = [] - user_prefs = session.query(UserFeature).filter_by(user_id=user.id).all() - - for pref in user_prefs: - if not pref.is_enable: - feature = session.query(Feature).filter_by( - id=pref.feature_id).first() - disabled.append({'feature': feature, 'is_enabled': pref.is_enable}) - - return disabled - - def get_user_uninstalled_features(session: SessionLocal): uninstalled = [] all_features = session.query(Feature).all() for feat in all_features: - in_disabled = is_feature_disabled( - feature=feat, session=session - ) - in_enabled = is_feature_enabled( feature=feat, session=session ) - if not in_enabled and not in_disabled: + if not in_enabled: uninstalled.append(feat) return uninstalled @@ -214,15 +187,12 @@ def get_user_installed_features(session: SessionLocal): all_features = session.query(Feature).all() for feat in all_features: - in_disabled = is_feature_disabled( - feature=feat, session=session - ) in_enabled = is_feature_enabled( feature=feat, session=session ) - if in_enabled or in_disabled: + if in_enabled: installed.append(feat) return installed diff --git a/app/routers/features.py b/app/routers/features.py index 5d3dca21..a4786207 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -1,5 +1,4 @@ from fastapi import APIRouter, Request, Depends -from typing import List from app.dependencies import get_db, SessionLocal, templates from app.database.models import UserFeature, Feature @@ -7,10 +6,8 @@ from app.internal.features import ( create_user_feature_association, is_association_exists_in_db, - get_user_disabled_features, - get_user_enabled_features, get_user_uninstalled_features, - get_user_installed_features + get_user_enabled_features ) router = APIRouter( @@ -23,9 +20,9 @@ @router.get('/') async def index( request: Request, session: SessionLocal = Depends(get_db) -) -> List: +) -> templates: features = { - "installed": get_user_installed_features(session=session), + "installed": get_user_enabled_features(session=session), "uninstalled": get_user_uninstalled_features(session=session) } print(features) @@ -86,63 +83,3 @@ async def delete_user_feature_association( session.commit() return True - - -@router.post('/on') -async def enable_feature(request: Request, - session: SessionLocal = Depends(get_db)) -> bool: - form = await request.form() - - is_exists = is_association_exists_in_db(form=form, session=session) - - if not is_exists: - return False - - db_association = session.query(UserFeature).filter_by( - feature_id=form['feature_id'], - user_id=form['user_id'] - ).first() - db_association.is_enable = True - session.commit() - return True - - -@router.post('/off') -async def disable_feature(request: Request, - session: SessionLocal = Depends(get_db)) -> bool: - form = await request.form() - is_exist = is_association_exists_in_db(form=form, session=session) - - if not is_exist: - return False - - db_association = session.query(UserFeature).filter_by( - feature_id=form['feature_id'], - user_id=form['user_id'] - ).first() - - db_association.is_enable = False - session.commit() - - return True - - -@router.get('/active') -def show_user_enabled_features( - session: SessionLocal = Depends(get_db) -) -> List: - return get_user_enabled_features(session=session) - - -@router.get('/deactive') -def show_user_disabled_features( - session: SessionLocal = Depends(get_db) -) -> List: - return get_user_disabled_features(session=session) - - -@router.get('/unlinked') -def get_user_unlinked_features( - session: SessionLocal = Depends(get_db) -) -> List: - return get_user_uninstalled_features(session=session) diff --git a/app/templates/features.html b/app/templates/features.html index 8cd63a02..d14fedcc 100644 --- a/app/templates/features.html +++ b/app/templates/features.html @@ -49,7 +49,7 @@
{{feature.name.upper()}} -

{{feature.information}}

+

{{feature.description}}

Creator: {{feature.creator.title()}}
diff --git a/tests/test_feature_panel.py b/tests/test_feature_panel.py index 80461eb6..d1028b9c 100644 --- a/tests/test_feature_panel.py +++ b/tests/test_feature_panel.py @@ -109,13 +109,7 @@ def test_create_association(session, user): def test_get_user_enabled_features(session, feature, association_on): - assert internal.get_user_enabled_features(session)[0].get( - 'is_enabled') is True - - -def test_get_user_disabled_features(session, feature, association_off): - assert internal.get_user_disabled_features(session)[0].get( - 'is_enabled') is False + assert internal.get_user_enabled_features(session)[0] is not None def test_is_association_exist_in_db(session, form_mock, association_off): @@ -146,11 +140,6 @@ def test_is_feature_exist_in_enabled(session, feature, association_on): assert internal.is_feature_enabled(feat, session) -def test_is_feature_exist_in_disabled(session, feature, association_off): - feat = session.query(Feature).filter_by(name=feature.name).first() - assert internal.is_feature_disabled(feat, session) - - def test_is_feature_enabled(mocker, session, association_on): mocker.patch( 'app.internal.features.SessionLocal', @@ -190,79 +179,3 @@ def test_delete_user_feature_association( resp = features_test_client.post(url, data=form_mock) assert resp.ok assert resp.content == b'true' - - -def test_enable_feature(features_test_client, form_mock, association_off): - url = route.router.url_path_for('enable_feature') - - resp = features_test_client.post(url, data=form_mock) - assert resp.ok - assert resp.content == b'true' - - -def test_disable_feature(features_test_client, form_mock, association_off): - url = route.router.url_path_for('disable_feature') - - resp = features_test_client.post(url, data=form_mock) - assert resp.ok - assert resp.content == b'true' - - -def test_show_user_enabled_features(mocker, features_test_client): - - mocker.patch( - 'app.routers.features.get_user_enabled_features', - return_value=True - ) - - url = route.router.url_path_for('show_user_enabled_features') - - resp = features_test_client.get(url) - assert resp.ok - assert resp.content == b'true' - - -def test_show_user_disabled_features(mocker, features_test_client): - - mocker.patch( - 'app.routers.features.get_user_disabled_features', - return_value=True - ) - - url = route.router.url_path_for('show_user_disabled_features') - - resp = features_test_client.get(url) - assert resp.ok - assert resp.content == b'true' - - -def test_get_user_unlinked_features(mocker, features_test_client, session): - unlinked = Feature( - name='unlinked', - route='/unlinked', - description='unlinked', - creator='unlinked', - icon='unlinked' - ) - - session.add(unlinked) - session.commit() - - mocker.patch( - 'app.routers.features.is_feature_disabled', - return_value=False - ) - mocker.patch( - 'app.routers.features.is_feature_enabled', - return_value=False - ) - - url = route.router.url_path_for('get_user_unlinked_features') - - resp = features_test_client.get(url) - assert resp.ok - json_resp = resp.json() - print(json_resp) - session.query(Feature).delete() - session.commit() - assert len(json_resp) == 1 From 21bbe871ffd45b3c1597b582494195df5d51cd02 Mon Sep 17 00:00:00 2001 From: Liran C Date: Sun, 21 Feb 2021 13:23:20 +0200 Subject: [PATCH 088/115] add followers, add tests, add front --- app/config.py.example | 113 ++++++++++++++++++++++++++++++++++++ app/database/models.py | 1 + app/internal/features.py | 25 ++++---- app/routers/features.py | 6 +- tests/test_feature_panel.py | 23 ++++++-- 5 files changed, 148 insertions(+), 20 deletions(-) create mode 100644 app/config.py.example diff --git a/app/config.py.example b/app/config.py.example new file mode 100644 index 00000000..4556dcca --- /dev/null +++ b/app/config.py.example @@ -0,0 +1,113 @@ +import os +import pathlib + +from fastapi_mail import ConnectionConfig +from pydantic import BaseSettings +from starlette.templating import Jinja2Templates + + +class Settings(BaseSettings): + app_name: str = "PyLander" + bot_api: str = "BOT_API" + webhook_url: str = "WEBHOOK_URL" + + class Config: + env_file = ".env" + + +# GENERAL +DOMAIN = 'Our-Domain' + +# DATABASE +DEVELOPMENT_DATABASE_STRING = "sqlite:///./dev.db" +# Set the following True if working on PSQL environment or set False otherwise +PSQL_ENVIRONMENT = False + +# MEDIA +MEDIA_DIRECTORY = 'media' +PICTURE_EXTENSION = '.png' +AVATAR_SIZE = (120, 120) + + +# DEFAULT WEBSITE LANGUAGE +WEBSITE_LANGUAGE = "en" + +# API-KEYS +# Get a free API KEY for Astronomy feature @ www.weatherapi.com/signup.aspx +ASTRONOMY_API_KEY = os.getenv('ASTRONOMY_API_KEY') +WEATHER_API_KEY = os.getenv('WEATHER_API_KEY') + +# https://developers.google.com/calendar/quickstart/python - +# follow instracions and make an env variable with the path to the file. +CLIENT_SECRET_FILE = os.environ.get('CLIENT_SECRET') + + +# EXPORT +ICAL_VERSION = '2.0' +PRODUCT_ID = '-//Our product id//' + +# EMAIL +email_conf = ConnectionConfig( + MAIL_USERNAME=os.getenv("MAIL_USERNAME") or "user", + MAIL_PASSWORD=os.getenv("MAIL_PASSWORD") or "password", + MAIL_FROM=os.getenv("MAIL_FROM") or "a@a.com", + MAIL_PORT=587, + MAIL_SERVER="smtp.gmail.com", + MAIL_TLS=True, + MAIL_SSL=False, + USE_CREDENTIALS=True, +) + + +# security +JWT_KEY = "JWT_KEY_PLACEHOLDER" +JWT_ALGORITHM = "HS256" +JWT_MIN_EXP = 60 * 24 * 7 +templates = Jinja2Templates(directory=os.path.join("app", "templates")) + +# application name +CALENDAR_SITE_NAME = "Calendar" +# link to the home page of the application +CALENDAR_HOME_PAGE = "calendar.pythonic.guru" +# link to the application registration page +CALENDAR_REGISTRATION_PAGE = r"calendar.pythonic.guru/registration" + +# IMPORT +MAX_FILE_SIZE_MB = 5 # 5MB +VALID_FILE_EXTENSION = (".txt", ".csv", ".ics") # Can import only these files. +# Events must be within 20 years range from the current year. +EVENT_VALID_YEARS = 20 +EVENT_HEADER_NOT_EMPTY = True +EVENT_HEADER_LIMIT = 50 # Max characters for event header. +EVENT_CONTENT_LIMIT = 500 # Max characters for event content. +MAX_EVENTS_START_DATE = 10 # Max Events with the same start date. +LOCATION_LIMIT = 50 # Max characters for Location. +EVENT_DURATION_LIMIT = 2 # the max duration in days for an event. + +# EMOTION +""" +Emotion will appear if the level of significance is +equal to or above this constraint +""" +LEVEL_OF_SIGNIFICANCE = 0.45 +# The weight of emotion based on the event title +TITLE_WEIGHTS = 0.6 +# The weight of emotion based on the event content +CONTENT_WEIGHTS = 1 - TITLE_WEIGHTS + +# PATHS +STATIC_ABS_PATH = os.path.abspath("static") + +# LOGGER +LOG_PATH = "./var/log" +LOG_FILENAME = "calendar.log" +LOG_LEVEL = "error" +LOG_ROTATION_INTERVAL = "20 days" +LOG_RETENTION_INTERVAL = "1 month" +LOG_FORMAT = ("{level: <8}" + " {time:YYYY-MM-DD HH:mm:ss.SSS}" + " - {name}:{function}" + " - {message}") + +# RESOURCES +RESOURCES_DIR = pathlib.Path(__file__).parent / 'resources' diff --git a/app/database/models.py b/app/database/models.py index 1be32aa1..1d6020d9 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -103,6 +103,7 @@ class Feature(Base): creator = Column(String, nullable=True) description = Column(String, nullable=False) icon = Column(String, nullable=False) + followers = Column(Integer, default=0) users = relationship("User", secondary=UserFeature.__tablename__) diff --git a/app/internal/features.py b/app/internal/features.py index a286f669..1962a6a7 100644 --- a/app/internal/features.py +++ b/app/internal/features.py @@ -145,6 +145,7 @@ def create_user_feature_association( db: SessionLocal, feature_id: int, user_id: int, is_enable: bool ) -> UserFeature: """Creates an association.""" + add_follower(feature_id=feature_id, session=db) return create_model( db, UserFeature, user_id=user_id, @@ -167,7 +168,7 @@ def get_user_enabled_features(session: SessionLocal = Depends(get_db)) -> List: return enabled -def get_user_uninstalled_features(session: SessionLocal): +def get_user_uninstalled_features(session: SessionLocal) -> List: uninstalled = [] all_features = session.query(Feature).all() @@ -182,17 +183,15 @@ def get_user_uninstalled_features(session: SessionLocal): return uninstalled -def get_user_installed_features(session: SessionLocal): - installed = [] - all_features = session.query(Feature).all() - - for feat in all_features: - - in_enabled = is_feature_enabled( - feature=feat, session=session - ) +def remove_follower(feature_id: int, session: SessionLocal) -> None: + feat = session.query(Feature).filter_by(id=feature_id).first() + feat.followers -= 1 + if feat.followers < 0: + feat.followers = 0 + session.commit() - if in_enabled: - installed.append(feat) - return installed +def add_follower(feature_id: int, session: SessionLocal) -> None: + feat = session.query(Feature).filter_by(id=feature_id).first() + feat.followers += 1 + session.commit() diff --git a/app/routers/features.py b/app/routers/features.py index a4786207..34acfb53 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -1,3 +1,4 @@ +from os import remove from fastapi import APIRouter, Request, Depends from app.dependencies import get_db, SessionLocal, templates @@ -7,7 +8,8 @@ create_user_feature_association, is_association_exists_in_db, get_user_uninstalled_features, - get_user_enabled_features + get_user_enabled_features, + remove_follower ) router = APIRouter( @@ -76,6 +78,8 @@ async def delete_user_feature_association( if not is_exist: return False + remove_follower(feature_id=feature_id, session=session) + session.query(UserFeature).filter_by( feature_id=feature_id, user_id=user.id diff --git a/tests/test_feature_panel.py b/tests/test_feature_panel.py index d1028b9c..cc535983 100644 --- a/tests/test_feature_panel.py +++ b/tests/test_feature_panel.py @@ -24,7 +24,8 @@ def feature(session): route='/test', description='testing', creator='test', - icon='test' + icon='test', + followers=0 ) session.add(test) @@ -102,7 +103,7 @@ def test_create_features_at_startup(mocker, session, mock_features): assert internal.create_features_at_startup(session) -def test_create_association(session, user): +def test_create_association(mocker, session, user, feature): assert internal.create_user_feature_association( db=session, feature_id=1, user_id=user.id, is_enable=False ) is not None @@ -118,11 +119,8 @@ def test_is_association_exist_in_db(session, form_mock, association_off): def test_delete_feature(session, feature): feat = session.query(Feature).filter_by(name=feature.name).first() - internal.delete_feature(feature=feat, session=session) - feat = session.query(Feature).filter_by(name=feature.name).first() - assert feat is None @@ -157,6 +155,19 @@ def test_create_feature(session): assert feat.name == 'test1' +def test_remove_follower(session, feature): + feature.followers = 1 + session.commit() + + internal.remove_follower(feature_id=feature.id, session=session) + assert feature.followers == 0 + + +def test_add_follower(session, feature): + internal.add_follower(feature_id=feature.id, session=session) + assert feature.followers == 1 + + def test_index(mocker, features_test_client, mock_features): url = route.router.url_path_for('index') @@ -172,7 +183,7 @@ def test_add_feature_to_user(features_test_client, feature, form_mock): def test_delete_user_feature_association( - features_test_client, form_mock, association_on + features_test_client, form_mock, association_on, feature ): url = route.router.url_path_for('delete_user_feature_association') From f74690a71a07334333ef1ad0e95a68db8fd652e2 Mon Sep 17 00:00:00 2001 From: Liran C Date: Sun, 21 Feb 2021 13:26:30 +0200 Subject: [PATCH 089/115] fix flake8 issue --- app/routers/features.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/routers/features.py b/app/routers/features.py index 34acfb53..3c6dcaa7 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -1,4 +1,3 @@ -from os import remove from fastapi import APIRouter, Request, Depends from app.dependencies import get_db, SessionLocal, templates From d6b26a881a44ee3ed0c715f434328f9ebc43978f Mon Sep 17 00:00:00 2001 From: Liran C Date: Sun, 21 Feb 2021 14:00:57 +0200 Subject: [PATCH 090/115] remove redundant things --- app/internal/features.py | 41 ++++++---------- app/routers/features.py | 85 +------------------------------- tests/test_feature_panel.py | 97 ++----------------------------------- 3 files changed, 22 insertions(+), 201 deletions(-) diff --git a/app/internal/features.py b/app/internal/features.py index 558798ae..d0328d44 100644 --- a/app/internal/features.py +++ b/app/internal/features.py @@ -1,6 +1,5 @@ from fastapi import Depends from functools import wraps -from sqlalchemy.sql.functions import session_user from starlette.responses import RedirectResponse from typing import List, Dict @@ -99,7 +98,6 @@ def update_feature(feature: Feature, new_feature_obj: dict, feature.description = new_feature_obj['description'] feature.creator = new_feature_obj['creator'] session.commit() - return feature @@ -107,14 +105,7 @@ def is_feature_enabled( feature: Feature, session: SessionLocal = Depends(get_db) ) -> bool: enabled_features = get_user_enabled_features(session=session) - return any(ef['feature'].id == feature.id for ef in enabled_features) - - -def is_feature_disabled( - feature: Feature, session: SessionLocal = Depends(get_db) -) -> bool: - disable_features = get_user_disabled_features(session=session) - return any(ef['feature'].id == feature.id for ef in disable_features) + return any(ef.id == feature.id for ef in enabled_features) def is_access_allowd(route: str) -> bool: @@ -136,7 +127,8 @@ def is_access_allowd(route: str) -> bool: def create_feature(name: str, route: str, - description: str, creator: str = None, + description: str, + creator: str = None, db: SessionLocal = Depends()) -> Feature: """Creates a feature.""" db = SessionLocal() @@ -145,7 +137,7 @@ def create_feature(name: str, route: str, name=name, route=route, creator=creator, - description=description + description=description, ) @@ -170,22 +162,21 @@ def get_user_enabled_features(session: SessionLocal = Depends(get_db)) -> List: if pref.is_enable: feature = session.query(Feature).filter_by( id=pref.feature_id).first() - enabled.append({'feature': feature, 'is_enabled': pref.is_enable}) + enabled.append(feature) return enabled -def get_user_disabled_features( - session: SessionLocal = Depends(get_db) -) -> List: - user = get_current_user(session=session) - disabled = [] - user_prefs = session.query(UserFeature).filter_by(user_id=user.id).all() +def get_user_uninstalled_features(session: SessionLocal) -> List: + uninstalled = [] + all_features = session.query(Feature).all() - for pref in user_prefs: - if not pref.is_enable: - feature = session.query(Feature).filter_by( - id=pref.feature_id).first() - disabled.append({'feature': feature, 'is_enabled': pref.is_enable}) + for feat in all_features: + in_enabled = is_feature_enabled( + feature=feat, session=session + ) + + if not in_enabled: + uninstalled.append(feat) - return disabled + return uninstalled diff --git a/app/routers/features.py b/app/routers/features.py index ad650e3c..23d89069 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -7,16 +7,11 @@ from app.internal.features import ( create_user_feature_association, is_association_exists_in_db, - is_feature_disabled, - is_feature_enabled, - get_user_disabled_features, - get_user_enabled_features, - create_dict_for_users_features_token ) router = APIRouter( prefix="/features", - tags=["event"], + tags=["features"], responses={404: {"description": "Not found"}}, ) @@ -25,8 +20,7 @@ async def index( request: Request, session: SessionLocal = Depends(get_db) ) -> List: - # features = session.query(Feature).all() - features = await create_dict_for_users_features_token(user_id=1, session=session) + features = session.query(Feature).all() return features @@ -78,78 +72,3 @@ async def delete_user_feature_association( session.commit() return True - - -@router.post('/on') -async def enable_feature(request: Request, - session: SessionLocal = Depends(get_db)) -> bool: - form = await request.form() - - is_exists = is_association_exists_in_db(form=form, session=session) - - if not is_exists: - return False - - db_association = session.query(UserFeature).filter_by( - feature_id=form['feature_id'], - user_id=form['user_id'] - ).first() - db_association.is_enable = True - session.commit() - return True - - -@router.post('/off') -async def disable_feature(request: Request, - session: SessionLocal = Depends(get_db)) -> bool: - form = await request.form() - is_exist = is_association_exists_in_db(form=form, session=session) - - if not is_exist: - return False - - db_association = session.query(UserFeature).filter_by( - feature_id=form['feature_id'], - user_id=form['user_id'] - ).first() - - db_association.is_enable = False - session.commit() - - return True - - -@router.get('/active') -def show_user_enabled_features( - session: SessionLocal = Depends(get_db) -) -> List: - return get_user_enabled_features(session=session) - - -@router.get('/deactive') -def show_user_disabled_features( - session: SessionLocal = Depends(get_db) -) -> List: - return get_user_disabled_features(session=session) - - -@router.get('/unlinked') -def get_user_unlinked_features( - session: SessionLocal = Depends(get_db) -) -> List: - data = [] - all_features = session.query(Feature).all() - - for feat in all_features: - in_disabled = is_feature_disabled( - feature=feat, session=session - ) - - in_enabled = is_feature_enabled( - feature=feat, session=session - ) - - if not in_enabled and not in_disabled: - data.append(feat) - - return data diff --git a/tests/test_feature_panel.py b/tests/test_feature_panel.py index 49279704..57f47216 100644 --- a/tests/test_feature_panel.py +++ b/tests/test_feature_panel.py @@ -23,7 +23,7 @@ def feature(session): name='test', route='/test', description='testing', - creator='test' + creator='test', ) session.add(test) @@ -101,20 +101,14 @@ def test_create_features_at_startup(mocker, session, mock_features): assert internal.create_features_at_startup(session) -def test_create_association(session, user): +def test_create_association(mocker, session, user, feature): assert internal.create_user_feature_association( db=session, feature_id=1, user_id=user.id, is_enable=False ) is not None def test_get_user_enabled_features(session, feature, association_on): - assert internal.get_user_enabled_features(session)[0].get( - 'is_enabled') is True - - -def test_get_user_disabled_features(session, feature, association_off): - assert internal.get_user_disabled_features(session)[0].get( - 'is_enabled') is False + assert internal.get_user_enabled_features(session)[0] is not None def test_is_association_exist_in_db(session, form_mock, association_off): @@ -123,11 +117,8 @@ def test_is_association_exist_in_db(session, form_mock, association_off): def test_delete_feature(session, feature): feat = session.query(Feature).filter_by(name=feature.name).first() - internal.delete_feature(feature=feat, session=session) - feat = session.query(Feature).filter_by(name=feature.name).first() - assert feat is None @@ -145,11 +136,6 @@ def test_is_feature_exist_in_enabled(session, feature, association_on): assert internal.is_feature_enabled(feat, session) -def test_is_feature_exist_in_disabled(session, feature, association_off): - feat = session.query(Feature).filter_by(name=feature.name).first() - assert internal.is_feature_disabled(feat, session) - - def test_is_feature_enabled(mocker, session, association_on): mocker.patch( 'app.internal.features.SessionLocal', @@ -182,85 +168,10 @@ def test_add_feature_to_user(features_test_client, feature, form_mock): def test_delete_user_feature_association( - features_test_client, form_mock, association_on + features_test_client, form_mock, association_on, feature ): url = route.router.url_path_for('delete_user_feature_association') resp = features_test_client.post(url, data=form_mock) assert resp.ok assert resp.content == b'true' - - -def test_enable_feature(features_test_client, form_mock, association_off): - url = route.router.url_path_for('enable_feature') - - resp = features_test_client.post(url, data=form_mock) - assert resp.ok - assert resp.content == b'true' - - -def test_disable_feature(features_test_client, form_mock, association_off): - url = route.router.url_path_for('disable_feature') - - resp = features_test_client.post(url, data=form_mock) - assert resp.ok - assert resp.content == b'true' - - -def test_show_user_enabled_features(mocker, features_test_client): - - mocker.patch( - 'app.routers.features.get_user_enabled_features', - return_value=True - ) - - url = route.router.url_path_for('show_user_enabled_features') - - resp = features_test_client.get(url) - assert resp.ok - assert resp.content == b'true' - - -def test_show_user_disabled_features(mocker, features_test_client): - - mocker.patch( - 'app.routers.features.get_user_disabled_features', - return_value=True - ) - - url = route.router.url_path_for('show_user_disabled_features') - - resp = features_test_client.get(url) - assert resp.ok - assert resp.content == b'true' - - -def test_get_user_unlinked_features(mocker, features_test_client, session): - unlinked = Feature( - name='unlinked', - route='/unlinked', - description='unlinked', - creator='unlinked' - ) - - session.add(unlinked) - session.commit() - - mocker.patch( - 'app.routers.features.is_feature_disabled', - return_value=False - ) - mocker.patch( - 'app.routers.features.is_feature_enabled', - return_value=False - ) - - url = route.router.url_path_for('get_user_unlinked_features') - - resp = features_test_client.get(url) - assert resp.ok - json_resp = resp.json() - print(json_resp) - session.query(Feature).delete() - session.commit() - assert len(json_resp) == 1 From 83668d3bacb96ab0bfc3060ef17c039a201e863b Mon Sep 17 00:00:00 2001 From: Aviad Amar Date: Sun, 21 Feb 2021 13:45:50 +0100 Subject: [PATCH 091/115] Information on click fixed, Followers managment added --- app/static/features.css | 6 ++---- app/static/js/features.js | 28 ++++++++++++++++++++++++++++ app/templates/features.html | 21 +++++++++++---------- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/app/static/features.css b/app/static/features.css index 7a6e0a62..2d7c3663 100644 --- a/app/static/features.css +++ b/app/static/features.css @@ -72,8 +72,6 @@ .information { display: none; -} - -.feature-icon:hover + .information{ - display: block; + grid-column: 2/6; + width: 30rem; } diff --git a/app/static/js/features.js b/app/static/js/features.js index 835b3c4c..180c6cda 100644 --- a/app/static/js/features.js +++ b/app/static/js/features.js @@ -33,6 +33,16 @@ function setSearchBox() { }); } +function updateFollowers(element, add = true) { + const followers = element.getElementsByClassName("followers")[0]; + const followersNum = parseInt(followers.innerText); + if (add) { + followers.innerText = followersNum + 1; + } else if (followersNum > 0) { + followers.innerText = followersNum - 1; + } +} + function move(button, element) { const category = element.parentElement; if (category.id === "available-features") { @@ -40,11 +50,13 @@ function move(button, element) { button.classList = "button remove-button"; element.dataset.state = "installed"; document.getElementById("installed-features").appendChild(element); + updateFollowers(element); } else { button.innerHTML = "ADD"; button.classList = "button add-button"; element.dataset.state = "available"; document.getElementById("available-features").appendChild(element); + updateFollowers(element, false); } } @@ -83,10 +95,26 @@ function setFeatures() { } } +function setInformation() { + let allInfo = document.getElementsByClassName("info-icon"); + Array.from(allInfo).map(element => { + const parent = element.parentElement; + const infoBox = parent.getElementsByClassName("information")[0]; + element.addEventListener("click", function () { + if (infoBox.style.display === "block") { + infoBox.style.display = "none"; + } else { + infoBox.style.display = "block"; + } + }) + }); +} + document.addEventListener( 'DOMContentLoaded', function () { setSearchBox(); setFeatures(); + setInformation(); } ) diff --git a/app/templates/features.html b/app/templates/features.html index 8cd63a02..1e20cc20 100644 --- a/app/templates/features.html +++ b/app/templates/features.html @@ -19,9 +19,9 @@
FEATURES PANEL
-

This panel allows you to view optional addons.
- Add, remove or customize features to your liking by clicking on the features icon in the navigation - bar. +

This panel allows you to view optional addons.
Add, remove or customize + features + to your liking by clicking on the features icon in the navigation bar.

@@ -69,19 +69,20 @@
-
+
{{feature.name.upper()}} -

{{feature.information}}

Creator: {{feature.creator.title()}}
- Followers: {{feature.followers}} + Followers: + {{feature.followers}}
REMOVE
+
{{feature.information}}
{% endfor %}
From 096d6fa65aee3286bf54aa95df2b6272976f6c61 Mon Sep 17 00:00:00 2001 From: Liran C Date: Mon, 22 Feb 2021 10:50:13 +0200 Subject: [PATCH 092/115] remove redundant things --- app/internal/features.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/app/internal/features.py b/app/internal/features.py index d0328d44..4951d946 100644 --- a/app/internal/features.py +++ b/app/internal/features.py @@ -1,7 +1,7 @@ from fastapi import Depends from functools import wraps from starlette.responses import RedirectResponse -from typing import List, Dict +from typing import List from app.database.models import UserFeature, Feature from app.dependencies import get_db, SessionLocal @@ -39,19 +39,6 @@ async def wrapper(*args, **kwargs): return wrapper -async def create_dict_for_users_features_token( - user_id: int, session: SessionLocal = Depends(get_db) -) -> Dict: - features_dict = {} - all_features = session.query(UserFeature).filter_by(user_id=user_id).all() - - for feat in all_features: - features_dict.update( - {f'{feat.user_id}{feat.feature_id}': feat.__dict__}) - - return features_dict - - def create_features_at_startup(session: SessionLocal) -> bool: for feat in features: if not is_feature_exists(feature=feat, session=session): From 4696df275eb3f163be9137e50027500c8a6b0fcc Mon Sep 17 00:00:00 2001 From: Liran C Date: Tue, 23 Feb 2021 20:40:11 +0200 Subject: [PATCH 093/115] pre-commit change some stuff --- app/internal/features.py | 156 ++++++++++++++++++++++-------------- app/routers/features.py | 47 ++++++----- tests/test_feature_panel.py | 136 ++++++++++++++++++------------- 3 files changed, 206 insertions(+), 133 deletions(-) diff --git a/app/internal/features.py b/app/internal/features.py index 4951d946..56bf25aa 100644 --- a/app/internal/features.py +++ b/app/internal/features.py @@ -1,40 +1,41 @@ -from fastapi import Depends +from fastapi import Depends, Request from functools import wraps from starlette.responses import RedirectResponse from typing import List -from app.database.models import UserFeature, Feature +from app.database.models import UserFeature, Feature, User from app.dependencies import get_db, SessionLocal +from app.internal.security.dependancies import current_user_from_db +from app.internal.security.ouath2 import get_authorization_cookie from app.internal.features_index import features -from app.internal.utils import create_model, get_current_user +from app.internal.utils import create_model def feature_access_filter(call_next): - @wraps(call_next) async def wrapper(*args, **kwargs): - request = kwargs['request'] + request = kwargs["request"] - if request.headers['user-agent'] == 'testclient': + if request.headers["user-agent"] == "testclient": # in case it's a unit test. return await call_next(*args, **kwargs) # getting the url route path for matching with the database. - route = '/' + str(request.url).replace(str(request.base_url), '') + route = "/" + str(request.url).replace(str(request.base_url), "") # getting access status. - is_enabled = is_access_allowd(route=route) + access = await is_access_allowd(route=route, request=request) - if is_enabled: + if access: # in case the feature is enabled or access is allowed. return await call_next(*args, **kwargs) - elif 'referer' not in request.headers: + elif "referer" not in request.headers: # in case request come straight from address bar in browser. - return RedirectResponse(url='/') + return RedirectResponse(url="/") # in case the feature is disabled or access isn't allowed. - return RedirectResponse(url=request.headers['referer']) + return RedirectResponse(url=request.headers["referer"]) return wrapper @@ -43,21 +44,26 @@ def create_features_at_startup(session: SessionLocal) -> bool: for feat in features: if not is_feature_exists(feature=feat, session=session): create_feature(**feat, db=session) - return True -def is_association_exists_in_db(form: dict, session: SessionLocal) -> bool: - db_association = session.query(UserFeature).filter_by( - feature_id=form['feature_id'], - user_id=get_current_user(session=session).id - ).first() +def is_association_exists_in_db( + form: dict, + session: SessionLocal, + user: User, +) -> bool: + db_association = ( + session.query(UserFeature) + .filter_by(feature_id=form["feature_id"], user_id=user.id) + .first() + ) return db_association is not None def delete_feature( - feature: Feature, session: SessionLocal = Depends(get_db) + feature: Feature, + session: SessionLocal = Depends(get_db), ) -> None: session.query(UserFeature).filter_by(feature_id=feature.id).delete() session.query(Feature).filter_by(id=feature.id).delete() @@ -65,39 +71,50 @@ def delete_feature( def is_feature_exists(feature: dict, session: SessionLocal) -> bool: - db_feature = session.query(Feature).filter( - (Feature.name == feature['name']) | - (Feature.route == feature['route'])).first() + db_feature = ( + session.query(Feature) + .filter( + (Feature.name == feature["name"]) + | (Feature.route == feature["route"]), + ) + .first() + ) - if db_feature is None: - return False + return db_feature is not None - # Update if found - update_feature( - feature=db_feature, new_feature_obj=feature, session=session) - return True - -def update_feature(feature: Feature, new_feature_obj: dict, - session: SessionLocal = Depends(get_db)) -> Feature: - feature.name = new_feature_obj['name'] - feature.route = new_feature_obj['route'] - feature.description = new_feature_obj['description'] - feature.creator = new_feature_obj['creator'] +def update_feature( + feature: Feature, + feature_dict: dict, + session: SessionLocal = Depends(get_db), +) -> Feature: + feature.name = feature_dict["name"] + feature.route = feature_dict["route"] + feature.description = feature_dict["description"] + feature.creator = feature_dict["creator"] session.commit() return feature def is_feature_enabled( - feature: Feature, session: SessionLocal = Depends(get_db) + user: User, + feature: Feature, + session: SessionLocal = Depends(get_db), ) -> bool: - enabled_features = get_user_enabled_features(session=session) + enabled_features = get_user_enabled_features(session=session, user=user) return any(ef.id == feature.id for ef in enabled_features) -def is_access_allowd(route: str) -> bool: +async def is_access_allowd(request: Request, route: str) -> bool: + session = SessionLocal() - user = get_current_user(session=session) + + # To get current user. + # Note: can't use dependency beacause its designed for routes only. + # Needed to it manualy. + jwt = await get_authorization_cookie(request=request) + user = await current_user_from_db(request=request, jwt=jwt, db=session) + feature = session.query(Feature).filter_by(route=route).first() if feature is None: @@ -105,22 +122,27 @@ def is_access_allowd(route: str) -> bool: # route that gived by to the request. return True - user_pref = session.query(UserFeature).filter_by( - feature_id=feature.id, - user_id=user.id - ).first() + user_pref = ( + session.query(UserFeature) + .filter_by(feature_id=feature.id, user_id=user.id) + .first() + ) return user_pref is not None and user_pref.is_enable -def create_feature(name: str, route: str, - description: str, - creator: str = None, - db: SessionLocal = Depends()) -> Feature: +def create_feature( + name: str, + route: str, + description: str, + creator: str = None, + db: SessionLocal = Depends(), +) -> Feature: """Creates a feature.""" db = SessionLocal() return create_model( - db, Feature, + db, + Feature, name=name, route=route, creator=creator, @@ -129,38 +151,56 @@ def create_feature(name: str, route: str, def create_user_feature_association( - db: SessionLocal, feature_id: int, user_id: int, is_enable: bool + db: SessionLocal, + feature_id: int, + user_id: int, + is_enable: bool, ) -> UserFeature: """Creates an association.""" return create_model( - db, UserFeature, + db, + UserFeature, user_id=user_id, feature_id=feature_id, - is_enable=is_enable + is_enable=is_enable, ) -def get_user_enabled_features(session: SessionLocal = Depends(get_db)) -> List: - user = get_current_user(session=session) +def get_user_enabled_features( + user_id: int, + session: SessionLocal = Depends(get_db), +) -> List[Feature]: enabled = [] - user_prefs = session.query(UserFeature).filter_by(user_id=user.id).all() + user_prefs = session.query(UserFeature).filter_by(user_id=user_id).all() for pref in user_prefs: if pref.is_enable: - feature = session.query(Feature).filter_by( - id=pref.feature_id).first() + feature = ( + session.query(Feature).filter_by(id=pref.feature_id).first() + ) enabled.append(feature) return enabled -def get_user_uninstalled_features(session: SessionLocal) -> List: +async def get_user_uninstalled_features( + request: Request, + session: SessionLocal = Depends(get_db), +) -> List[Feature]: uninstalled = [] all_features = session.query(Feature).all() + # To get current user. + # Note: can't use dependency beacause its designed for routes only. + # Needed to it manualy. + jwt = await get_authorization_cookie(request=request) + user = await current_user_from_db(request=request, jwt=jwt, db=session) + for feat in all_features: in_enabled = is_feature_enabled( - feature=feat, session=session + feature=feat, + session=session, + user_id=user.id, ) if not in_enabled: diff --git a/app/routers/features.py b/app/routers/features.py index 23d89069..761d8446 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -1,9 +1,9 @@ -from app.internal.utils import get_current_user from fastapi import APIRouter, Request, Depends from typing import List from app.dependencies import get_db, SessionLocal -from app.database.models import UserFeature, Feature +from app.database.models import User, UserFeature, Feature +from app.internal.security.dependancies import current_user_from_db from app.internal.features import ( create_user_feature_association, is_association_exists_in_db, @@ -16,24 +16,30 @@ ) -@router.get('/') +@router.get("/") async def index( - request: Request, session: SessionLocal = Depends(get_db) -) -> List: + request: Request, + session: SessionLocal = Depends(get_db), +) -> List[Feature]: features = session.query(Feature).all() return features -@router.post('/add') +@router.post("/add") async def add_feature_to_user( - request: Request, session: SessionLocal = Depends(get_db) -) -> UserFeature: + request: Request, + session: SessionLocal = Depends(get_db), + user: User = Depends(current_user_from_db), +) -> UserFeature or bool: form = await request.form() - user = get_current_user(session=session) - feat = session.query(Feature).filter_by(id=form['feature_id']).first() + feat = session.query(Feature).filter_by(id=form["feature_id"]).first() - is_exist = is_association_exists_in_db(form=form, session=session) + is_exist = is_association_exists_in_db( + form=form, + session=session, + user=user, + ) if feat is None or is_exist: # in case there is no feature in the database with that same id @@ -44,30 +50,33 @@ async def add_feature_to_user( db=session, feature_id=feat.id, user_id=user.id, - is_enable=True + is_enable=True, ) return session.query(UserFeature).filter_by(id=association.id).first() -@router.post('/delete') +@router.post("/delete") async def delete_user_feature_association( request: Request, - session: SessionLocal = Depends(get_db) + session: SessionLocal = Depends(get_db), + user: User = Depends(current_user_from_db), ) -> bool: form = await request.form() + feature_id = form["feature_id"] - user = get_current_user(session=session) - feature_id = form['feature_id'] - - is_exist = is_association_exists_in_db(form=form, session=session) + is_exist = is_association_exists_in_db( + form=form, + session=session, + user=user, + ) if not is_exist: return False session.query(UserFeature).filter_by( feature_id=feature_id, - user_id=user.id + user_id=user.id, ).delete() session.commit() diff --git a/tests/test_feature_panel.py b/tests/test_feature_panel.py index 57f47216..e706c0f5 100644 --- a/tests/test_feature_panel.py +++ b/tests/test_feature_panel.py @@ -1,5 +1,6 @@ -from app.database.models import Feature, UserFeature import pytest + +from app.database.models import Feature, UserFeature import app.internal.features as internal import app.routers.features as route @@ -8,22 +9,22 @@ def mock_features(): return [ { - "name": 'test', - "route": '/test', - "description": 'testing', - "creator": 'test' - } + "name": "test", + "route": "/test", + "description": "testing", + "creator": "test", + }, ] @pytest.fixture -@pytest.mark.usefixtures('session') +@pytest.mark.usefixtures("session") def feature(session): test = Feature( - name='test', - route='/test', - description='testing', - creator='test', + name="test", + route="/test", + description="testing", + creator="test", ) session.add(test) @@ -36,10 +37,9 @@ def feature(session): @pytest.fixture -@pytest.mark.usefixtures('session') +@pytest.mark.usefixtures("session") def association_off(session, user): - test = UserFeature( - feature_id=1, user_id=user.id, is_enable=False) + test = UserFeature(feature_id=1, user_id=user.id, is_enable=False) session.add(test) session.commit() @@ -51,10 +51,9 @@ def association_off(session, user): @pytest.fixture -@pytest.mark.usefixtures('session') +@pytest.mark.usefixtures("session") def association_on(session, user): - test = UserFeature( - feature_id=1, user_id=user.id, is_enable=True) + test = UserFeature(feature_id=1, user_id=user.id, is_enable=True) session.add(test) session.commit() @@ -68,10 +67,10 @@ def association_on(session, user): @pytest.fixture def update_dict(): update = { - 'name': 'test', - 'route': '/route-test', - 'description': 'update', - 'creator': 'test' + "name": "test", + "route": "/route-test", + "description": "update", + "creator": "test", } return update @@ -79,40 +78,40 @@ def update_dict(): @pytest.fixture def form_mock(): - form = { - 'feature_id': 1, - 'user_id': 1 - } + form = {"feature_id": 1, "user_id": 1} return form def test_create_features_at_startup(mocker, session, mock_features): - mocker.patch( - 'app.internal.features.features', - mock_features - ) - mocker.patch( - 'app.internal.features.is_feature_exists', - return_value=False - ) + mocker.patch("app.internal.features.features", mock_features) + mocker.patch("app.internal.features.is_feature_exists", return_value=False) assert internal.create_features_at_startup(session) def test_create_association(mocker, session, user, feature): - assert internal.create_user_feature_association( - db=session, feature_id=1, user_id=user.id, is_enable=False - ) is not None + assert ( + internal.create_user_feature_association( + db=session, + feature_id=1, + user_id=user.id, + is_enable=False, + ) + is not None + ) -def test_get_user_enabled_features(session, feature, association_on): - assert internal.get_user_enabled_features(session)[0] is not None +def test_get_user_enabled_features(session, feature, association_on, user): + assert ( + internal.get_user_enabled_features(session=session, user_id=user.id)[0] + is not None + ) -def test_is_association_exist_in_db(session, form_mock, association_off): - assert internal.is_association_exists_in_db(form_mock, session) +def test_is_association_exist_in_db(session, form_mock, association_off, user): + assert internal.is_association_exists_in_db(form_mock, session, user) def test_delete_feature(session, feature): @@ -128,50 +127,75 @@ def test_is_feature_exist_in_db(session, feature, update_dict): def test_update_feature(session, feature, update_dict): feature = internal.update_feature(feature, update_dict, session) - assert feature.description == 'update' + assert feature.description == "update" -def test_is_feature_exist_in_enabled(session, feature, association_on): +def test_is_feature_exist_in_enabled( + mocker, + session, + feature, + association_on, + user, +): feat = session.query(Feature).filter_by(name=feature.name).first() - assert internal.is_feature_enabled(feat, session) + mocker.patch( + "app.internal.features.get_user_enabled_features", + return_value=[feat], + ) + + assert internal.is_feature_enabled(user, feat, session) -def test_is_feature_enabled(mocker, session, association_on): + +@pytest.mark.asyncio +async def test_is_feature_enabled(mocker, session, association_on, user): + mocker.patch("app.internal.features.SessionLocal", return_value=session) + mocker.patch( + "app.internal.features.get_authorization_cookie", + return_value=None, + ) mocker.patch( - 'app.internal.features.SessionLocal', - return_value=session + "app.internal.features.current_user_from_db", + return_value=user, ) - assert internal.is_access_allowd(route='/route') is True + assert ( + await internal.is_access_allowd(route="/route", request=None) is True + ) -def test_create_feature(session): +def test_create_feature(session): feat = internal.create_feature( - name='test1', route='/route', description='testing', creator='test' + name="test1", + route="/route", + description="testing", + creator="test", ) - - assert feat.name == 'test1' + assert feat.name == "test1" def test_index(mocker, features_test_client, mock_features): - url = route.router.url_path_for('index') + url = route.router.url_path_for("index") resp = features_test_client.get(url) assert resp.ok def test_add_feature_to_user(features_test_client, feature, form_mock): - url = route.router.url_path_for('add_feature_to_user') + url = route.router.url_path_for("add_feature_to_user") resp = features_test_client.post(url, data=form_mock) assert resp.ok def test_delete_user_feature_association( - features_test_client, form_mock, association_on, feature + mocker, + features_test_client, + form_mock, + association_on, + feature, ): - url = route.router.url_path_for('delete_user_feature_association') + url = route.router.url_path_for("delete_user_feature_association") resp = features_test_client.post(url, data=form_mock) assert resp.ok - assert resp.content == b'true' From dc9fcce356e5180936408e68a234f3a6106ed427 Mon Sep 17 00:00:00 2001 From: Liran C Date: Tue, 23 Feb 2021 21:10:12 +0200 Subject: [PATCH 094/115] remove blank lines --- tests/test_feature_panel.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_feature_panel.py b/tests/test_feature_panel.py index e706c0f5..a43d2440 100644 --- a/tests/test_feature_panel.py +++ b/tests/test_feature_panel.py @@ -84,7 +84,6 @@ def form_mock(): def test_create_features_at_startup(mocker, session, mock_features): - mocker.patch("app.internal.features.features", mock_features) mocker.patch("app.internal.features.is_feature_exists", return_value=False) @@ -138,12 +137,10 @@ def test_is_feature_exist_in_enabled( user, ): feat = session.query(Feature).filter_by(name=feature.name).first() - mocker.patch( "app.internal.features.get_user_enabled_features", return_value=[feat], ) - assert internal.is_feature_enabled(user, feat, session) @@ -158,7 +155,6 @@ async def test_is_feature_enabled(mocker, session, association_on, user): "app.internal.features.current_user_from_db", return_value=user, ) - assert ( await internal.is_access_allowd(route="/route", request=None) is True ) From 6bd5e03c3ce3e8fcf65c5d994e52a4f591211559 Mon Sep 17 00:00:00 2001 From: Aviad Amar Date: Tue, 23 Feb 2021 20:11:45 +0100 Subject: [PATCH 095/115] First scrolling bug fixed --- app/static/js/grid_navigation.js | 2 +- app/static/js/grid_scripts.js | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/static/js/grid_navigation.js b/app/static/js/grid_navigation.js index fece8b87..a8fda0a9 100644 --- a/app/static/js/grid_navigation.js +++ b/app/static/js/grid_navigation.js @@ -79,7 +79,7 @@ function loadDaysIfNeeded(dateId, link) { } else { const lastDay = stringToDate(allId[allId.length - 1]); deltaDays = parseInt((targetDay - lastDay) / (1000 * 60 * 60 * 24), 10); - callLoadWeek(calcDaysToLoad(deltaDays)); + callLoadWeek(calcDaysToLoad(deltaDays), true); } jump(link); } diff --git a/app/static/js/grid_scripts.js b/app/static/js/grid_scripts.js index 9f3af312..8392d0a7 100644 --- a/app/static/js/grid_scripts.js +++ b/app/static/js/grid_scripts.js @@ -21,14 +21,6 @@ function isMonthLoaded(monthId) { return false; } -document.addEventListener( - 'DOMContentLoaded', function () { - const allDays = document.getElementsByClassName('day'); - setToggle("day", "day-view", "day-view-visible", 0, allDays.length); - weekScroll(); - } -) - function loadWeekAfter(baseUrl, day, index, daysToLoad) { if (day.dataset.last === "false") { return false; @@ -60,7 +52,7 @@ function loadWeekBefore(baseUrl, day, daysToLoad) { }); } -function callLoadWeek(daysToLoad = 42, end = true) { +function callLoadWeek(daysToLoad, end) { let day = null; const url = window.location.origin; const allDays = document.getElementsByClassName('day'); @@ -82,12 +74,20 @@ function weekScroll() { if (grid.scrollY + grid.innerHeight + tolerance < grid.scrollHeight) { return false; } - callLoadWeek(); + callLoadWeek(42, true); } ) - grid.addEventListener('wheel', function () { - if (this.scrollTop === 0) { - callLoadWeek(42, end = false); + grid.addEventListener('wheel', function (event) { + if (event.deltaY < 0 && this.scrollTop === 0) { + callLoadWeek(42, false); } }) -} \ No newline at end of file +} + +document.addEventListener( + 'DOMContentLoaded', function () { + const allDays = document.getElementsByClassName('day'); + setToggle("day", "day-view", "day-view-visible", 0, allDays.length); + weekScroll(); + } +) \ No newline at end of file From 1ce00993685843f17f62218c6aca8db01dadd166 Mon Sep 17 00:00:00 2001 From: Liran C Date: Wed, 24 Feb 2021 10:41:11 +0200 Subject: [PATCH 096/115] fix error --- app/internal/features.py | 10 ++++++---- app/routers/features.py | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/app/internal/features.py b/app/internal/features.py index edc7dd9f..1b0f833b 100644 --- a/app/internal/features.py +++ b/app/internal/features.py @@ -43,7 +43,7 @@ async def wrapper(*args, **kwargs): def create_features_at_startup(session: SessionLocal) -> bool: for feat in features: if not is_feature_exists(feature=feat, session=session): - icon = icons.get(feat['name']) + icon = icons.get(feat["name"]) create_feature(**feat, icon=icon, db=session) return True @@ -106,11 +106,13 @@ def update_feature( def is_feature_enabled( - user: User, + user_id: User, feature: Feature, session: SessionLocal = Depends(get_db), ) -> bool: - enabled_features = get_user_enabled_features(session=session, user=user) + enabled_features = get_user_enabled_features( + session=session, user_id=user_id, + ) return any(ef.id == feature.id for ef in enabled_features) @@ -160,7 +162,7 @@ def create_feature( route=route, creator=creator, description=description, - icon=icon + icon=icon, ) diff --git a/app/routers/features.py b/app/routers/features.py index 7047a56a..8e5dfd7d 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -8,7 +8,7 @@ is_association_exists_in_db, get_user_uninstalled_features, get_user_enabled_features, - remove_follower + remove_follower, ) router = APIRouter( @@ -20,21 +20,21 @@ @router.get("/") async def index( - request: Request, session: SessionLocal = Depends(get_db), + request: Request, + session: SessionLocal = Depends(get_db), user: User = Depends(current_user_from_db), ) -> templates: features = { "installed": get_user_enabled_features( - session=session, user_id=user.id), - "uninstalled": get_user_uninstalled_features(session=session) + session=session, user_id=user.id, + ), + "uninstalled": await get_user_uninstalled_features( + session=session, request=request, + ), } - print(features) + return templates.TemplateResponse( - "features.html", - { - "request": request, - "features": features - } + "features.html", {"request": request, "features": features}, ) From d677ccd47f14ebb9a147430f9bd00ec0dedc7a98 Mon Sep 17 00:00:00 2001 From: Liran C Date: Wed, 24 Feb 2021 15:00:39 +0200 Subject: [PATCH 097/115] requested changes --- app/internal/features.py | 95 +++++++++++++++---------------------- app/routers/features.py | 44 +++++++++-------- tests/client_fixture.py | 5 -- tests/test_feature_panel.py | 64 ++++++++++++++----------- 4 files changed, 98 insertions(+), 110 deletions(-) diff --git a/app/internal/features.py b/app/internal/features.py index 1e6fb60e..de7f2639 100644 --- a/app/internal/features.py +++ b/app/internal/features.py @@ -2,10 +2,11 @@ from functools import wraps from starlette.responses import RedirectResponse from typing import List +from sqlalchemy.sql import exists -from app.database.models import UserFeature, Feature, User +from app.database.models import UserFeature, Feature from app.dependencies import get_db, SessionLocal -from app.internal.security.dependencies import current_user_from_db +from app.internal.security.dependencies import current_user from app.internal.security.ouath2 import get_authorization_cookie from app.internal.features_index import features from app.internal.utils import create_model @@ -47,18 +48,19 @@ def create_features_at_startup(session: SessionLocal) -> bool: return True -def is_association_exists_in_db( - form: dict, +def is_user_has_feature( session: SessionLocal, - user: User, + feature_id: int, + user_id: int, ) -> bool: - db_association = ( - session.query(UserFeature) - .filter_by(feature_id=form["feature_id"], user_id=user.id) - .first() - ) + is_exists = session.query( + exists().where( + UserFeature.user_id == user_id + and (UserFeature.feature_id == feature_id), + ), + ).scalar() - return db_association is not None + return is_exists def delete_feature( @@ -71,16 +73,14 @@ def delete_feature( def is_feature_exists(feature: dict, session: SessionLocal) -> bool: - db_feature = ( - session.query(Feature) - .filter( - (Feature.name == feature["name"]) - | (Feature.route == feature["route"]), - ) - .first() - ) + is_exists = session.query( + exists().where( + Feature.name == feature["name"] + and Feature.route == feature["route"], + ), + ).scalar() - return db_feature is not None + return is_exists def update_feature( @@ -96,24 +96,14 @@ def update_feature( return feature -def is_feature_enabled( - user: User, - feature: Feature, - session: SessionLocal = Depends(get_db), -) -> bool: - enabled_features = get_user_enabled_features(session=session, user=user) - return any(ef.id == feature.id for ef in enabled_features) - - async def is_access_allowd(request: Request, route: str) -> bool: - session = SessionLocal() - # To get current user. + # Get current user. # Note: can't use dependency beacause its designed for routes only. - # Needed to it manualy. + # current_user return schema not an db model. jwt = await get_authorization_cookie(request=request) - user = await current_user_from_db(request=request, jwt=jwt, db=session) + user = await current_user(request=request, jwt=jwt, db=session) feature = session.query(Feature).filter_by(route=route).first() @@ -122,13 +112,14 @@ async def is_access_allowd(request: Request, route: str) -> bool: # route that gived by to the request. return True - user_pref = ( - session.query(UserFeature) - .filter_by(feature_id=feature.id, user_id=user.id) - .first() - ) + user_ptef = session.query( + exists().where( + UserFeature.feature_id == feature.id + and (UserFeature.user_id == user.user_id), + ), + ).scalar() - return user_pref is not None and user_pref.is_enable + return user_ptef def create_feature( @@ -166,21 +157,11 @@ def create_user_feature_association( ) -def get_user_enabled_features( +def get_user_installed_features( user_id: int, session: SessionLocal = Depends(get_db), ) -> List[Feature]: - enabled = [] - user_prefs = session.query(UserFeature).filter_by(user_id=user_id).all() - - for pref in user_prefs: - if pref.is_enable: - feature = ( - session.query(Feature).filter_by(id=pref.feature_id).first() - ) - enabled.append(feature) - - return enabled + return session.query(UserFeature).filter_by(user_id=user_id).all() async def get_user_uninstalled_features( @@ -190,17 +171,17 @@ async def get_user_uninstalled_features( uninstalled = [] all_features = session.query(Feature).all() - # To get current user. + # Get current user. # Note: can't use dependency beacause its designed for routes only. - # Needed to it manualy. + # current_user return schema not an db model. jwt = await get_authorization_cookie(request=request) - user = await current_user_from_db(request=request, jwt=jwt, db=session) + user = await current_user(request=request, jwt=jwt, db=session) for feat in all_features: - in_enabled = is_feature_enabled( - feature=feat, + in_enabled = is_user_has_feature( session=session, - user_id=user.id, + feature_id=feat.id, + user_id=user.user_id, ) if not in_enabled: diff --git a/app/routers/features.py b/app/routers/features.py index 72bc9909..8d63bf98 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -1,12 +1,13 @@ from fastapi import APIRouter, Request, Depends +from sqlalchemy.sql import exists from typing import List from app.dependencies import get_db, SessionLocal from app.database.models import User, UserFeature, Feature -from app.internal.security.dependencies import current_user_from_db +from app.internal.security.dependencies import current_user from app.internal.features import ( create_user_feature_association, - is_association_exists_in_db, + is_user_has_feature, ) router = APIRouter( @@ -29,46 +30,51 @@ async def index( async def add_feature_to_user( request: Request, session: SessionLocal = Depends(get_db), - user: User = Depends(current_user_from_db), -) -> UserFeature or bool: + user: User = Depends(current_user), +) -> bool: form = await request.form() - feat = session.query(Feature).filter_by(id=form["feature_id"]).first() + # feat = session.query(Feature).filter_by(id=form["feature_id"]).first() + feat = session.query( + exists().where(Feature.id == form["feature_id"]), + ).scalar() - is_exist = is_association_exists_in_db( - form=form, + is_exist = is_user_has_feature( session=session, - user=user, + feature_id=form["feature_id"], + user_id=user.user_id, ) - if feat is None or is_exist: + if not feat or is_exist: # in case there is no feature in the database with that same id # and or the association is exist return False - association = create_user_feature_association( + create_user_feature_association( db=session, - feature_id=feat.id, - user_id=user.id, + feature_id=form["feature_id"], + user_id=user.user_id, is_enable=True, ) - return session.query(UserFeature).filter_by(id=association.id).first() + return is_user_has_feature( + session=session, feature_id=form["feature_id"], user_id=user.user_id, + ) @router.post("/delete") async def delete_user_feature_association( request: Request, session: SessionLocal = Depends(get_db), - user: User = Depends(current_user_from_db), + user: User = Depends(current_user), ) -> bool: form = await request.form() - feature_id = form["feature_id"] + feature_id = int(form["feature_id"]) - is_exist = is_association_exists_in_db( - form=form, + is_exist = is_user_has_feature( session=session, - user=user, + feature_id=feature_id, + user_id=user.user_id, ) if not is_exist: @@ -76,7 +82,7 @@ async def delete_user_feature_association( session.query(UserFeature).filter_by( feature_id=feature_id, - user_id=user.id, + user_id=user.user_id, ).delete() session.commit() diff --git a/tests/client_fixture.py b/tests/client_fixture.py index 24a4e81d..c85ee748 100644 --- a/tests/client_fixture.py +++ b/tests/client_fixture.py @@ -118,11 +118,6 @@ def salary_test_client() -> Iterator[TestClient]: yield from create_test_client(salary.get_db) -@pytest.fixture(scope="session") -def features_test_client() -> Iterator[TestClient]: - yield from create_test_client(features.get_db) - - @pytest.fixture(scope="session") def meds_test_client() -> Iterator[TestClient]: yield from create_test_client(meds.get_db) diff --git a/tests/test_feature_panel.py b/tests/test_feature_panel.py index a43d2440..1b42a8ed 100644 --- a/tests/test_feature_panel.py +++ b/tests/test_feature_panel.py @@ -3,6 +3,7 @@ from app.database.models import Feature, UserFeature import app.internal.features as internal import app.routers.features as route +from tests.test_login import LOGIN_DATA, REGISTER_DETAIL @pytest.fixture @@ -104,13 +105,17 @@ def test_create_association(mocker, session, user, feature): def test_get_user_enabled_features(session, feature, association_on, user): assert ( - internal.get_user_enabled_features(session=session, user_id=user.id)[0] + internal.get_user_installed_features(session=session, user_id=user.id)[ + 0 + ] is not None ) -def test_is_association_exist_in_db(session, form_mock, association_off, user): - assert internal.is_association_exists_in_db(form_mock, session, user) +def test_is_user_has_feature(session, feature, association_off, user): + assert internal.is_user_has_feature( + session=session, user_id=user.id, feature_id=feature.id, + ) def test_delete_feature(session, feature): @@ -129,21 +134,6 @@ def test_update_feature(session, feature, update_dict): assert feature.description == "update" -def test_is_feature_exist_in_enabled( - mocker, - session, - feature, - association_on, - user, -): - feat = session.query(Feature).filter_by(name=feature.name).first() - mocker.patch( - "app.internal.features.get_user_enabled_features", - return_value=[feat], - ) - assert internal.is_feature_enabled(user, feat, session) - - @pytest.mark.asyncio async def test_is_feature_enabled(mocker, session, association_on, user): mocker.patch("app.internal.features.SessionLocal", return_value=session) @@ -152,9 +142,10 @@ async def test_is_feature_enabled(mocker, session, association_on, user): return_value=None, ) mocker.patch( - "app.internal.features.current_user_from_db", + "app.internal.features.current_user", return_value=user, ) + assert ( await internal.is_access_allowd(route="/route", request=None) is True ) @@ -170,28 +161,43 @@ def test_create_feature(session): assert feat.name == "test1" -def test_index(mocker, features_test_client, mock_features): +def test_index(security_test_client): url = route.router.url_path_for("index") - resp = features_test_client.get(url) + resp = security_test_client.get(url) assert resp.ok -def test_add_feature_to_user(features_test_client, feature, form_mock): +def test_add_feature_to_user(form_mock, security_test_client): url = route.router.url_path_for("add_feature_to_user") - resp = features_test_client.post(url, data=form_mock) + security_test_client.post( + security_test_client.app.url_path_for("register"), data=REGISTER_DETAIL, + ) + security_test_client.post( + security_test_client.app.url_path_for("login"), data=LOGIN_DATA, + ) + + resp = security_test_client.post(url, data=form_mock) assert resp.ok def test_delete_user_feature_association( - mocker, - features_test_client, - form_mock, - association_on, - feature, + form_mock, feature, security_test_client, ): + + security_test_client.post( + security_test_client.app.url_path_for("register"), data=REGISTER_DETAIL, + ) + security_test_client.post( + security_test_client.app.url_path_for("login"), data=LOGIN_DATA, + ) + security_test_client.post( + route.router.url_path_for("add_feature_to_user"), data=form_mock, + ) + url = route.router.url_path_for("delete_user_feature_association") - resp = features_test_client.post(url, data=form_mock) + resp = security_test_client.post(url, data=form_mock) assert resp.ok + assert resp.content == b"true" From 7712b0798ebc1f130b692923809cb5065b5cfe02 Mon Sep 17 00:00:00 2001 From: Liran C Date: Wed, 24 Feb 2021 15:03:50 +0200 Subject: [PATCH 098/115] update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b8da1186..3e3a65d5 100644 --- a/.gitignore +++ b/.gitignore @@ -147,6 +147,7 @@ dmypy.json .pyre/ # mac env +.DS_Store bin # register stuff From a9f9001807238cb3eff8367be7e33522b5055aab Mon Sep 17 00:00:00 2001 From: Liran C Date: Wed, 24 Feb 2021 15:25:54 +0200 Subject: [PATCH 099/115] fix semantics --- app/internal/features.py | 24 ++++++++++++------------ app/routers/features.py | 5 +++-- tests/client_fixture.py | 1 - tests/test_feature_panel.py | 24 +++++++++++++++++------- 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/app/internal/features.py b/app/internal/features.py index de7f2639..c4528218 100644 --- a/app/internal/features.py +++ b/app/internal/features.py @@ -1,8 +1,9 @@ from fastapi import Depends, Request from functools import wraps from starlette.responses import RedirectResponse -from typing import List +from typing import Dict, List from sqlalchemy.sql import exists +from sqlalchemy.orm import Session from app.database.models import UserFeature, Feature from app.dependencies import get_db, SessionLocal @@ -41,7 +42,7 @@ async def wrapper(*args, **kwargs): return wrapper -def create_features_at_startup(session: SessionLocal) -> bool: +def create_features_at_startup(session: Session) -> bool: for feat in features: if not is_feature_exists(feature=feat, session=session): create_feature(**feat, db=session) @@ -49,7 +50,7 @@ def create_features_at_startup(session: SessionLocal) -> bool: def is_user_has_feature( - session: SessionLocal, + session: Session, feature_id: int, user_id: int, ) -> bool: @@ -65,14 +66,14 @@ def is_user_has_feature( def delete_feature( feature: Feature, - session: SessionLocal = Depends(get_db), + session: Session = Depends(get_db), ) -> None: session.query(UserFeature).filter_by(feature_id=feature.id).delete() session.query(Feature).filter_by(id=feature.id).delete() session.commit() -def is_feature_exists(feature: dict, session: SessionLocal) -> bool: +def is_feature_exists(feature: Dict[str, str], session: Session) -> bool: is_exists = session.query( exists().where( Feature.name == feature["name"] @@ -85,8 +86,8 @@ def is_feature_exists(feature: dict, session: SessionLocal) -> bool: def update_feature( feature: Feature, - feature_dict: dict, - session: SessionLocal = Depends(get_db), + feature_dict: Dict[str, str], + session: Session = Depends(get_db), ) -> Feature: feature.name = feature_dict["name"] feature.route = feature_dict["route"] @@ -126,11 +127,10 @@ def create_feature( name: str, route: str, description: str, + db: Session, creator: str = None, - db: SessionLocal = Depends(), ) -> Feature: """Creates a feature.""" - db = SessionLocal() return create_model( db, Feature, @@ -142,7 +142,7 @@ def create_feature( def create_user_feature_association( - db: SessionLocal, + db: Session, feature_id: int, user_id: int, is_enable: bool, @@ -159,14 +159,14 @@ def create_user_feature_association( def get_user_installed_features( user_id: int, - session: SessionLocal = Depends(get_db), + session: Session = Depends(get_db), ) -> List[Feature]: return session.query(UserFeature).filter_by(user_id=user_id).all() async def get_user_uninstalled_features( request: Request, - session: SessionLocal = Depends(get_db), + session: Session = Depends(get_db), ) -> List[Feature]: uninstalled = [] all_features = session.query(Feature).all() diff --git a/app/routers/features.py b/app/routers/features.py index 8d63bf98..7994215a 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -34,7 +34,6 @@ async def add_feature_to_user( ) -> bool: form = await request.form() - # feat = session.query(Feature).filter_by(id=form["feature_id"]).first() feat = session.query( exists().where(Feature.id == form["feature_id"]), ).scalar() @@ -58,7 +57,9 @@ async def add_feature_to_user( ) return is_user_has_feature( - session=session, feature_id=form["feature_id"], user_id=user.user_id, + session=session, + feature_id=form["feature_id"], + user_id=user.user_id, ) diff --git a/tests/client_fixture.py b/tests/client_fixture.py index c85ee748..fbbd58e8 100644 --- a/tests/client_fixture.py +++ b/tests/client_fixture.py @@ -11,7 +11,6 @@ audio, categories, event, - features, friendview, google_connect, invitation, diff --git a/tests/test_feature_panel.py b/tests/test_feature_panel.py index 1b42a8ed..ac3eb894 100644 --- a/tests/test_feature_panel.py +++ b/tests/test_feature_panel.py @@ -114,7 +114,9 @@ def test_get_user_enabled_features(session, feature, association_on, user): def test_is_user_has_feature(session, feature, association_off, user): assert internal.is_user_has_feature( - session=session, user_id=user.id, feature_id=feature.id, + session=session, + user_id=user.id, + feature_id=feature.id, ) @@ -157,6 +159,7 @@ def test_create_feature(session): route="/route", description="testing", creator="test", + db=session, ) assert feat.name == "test1" @@ -172,10 +175,12 @@ def test_add_feature_to_user(form_mock, security_test_client): url = route.router.url_path_for("add_feature_to_user") security_test_client.post( - security_test_client.app.url_path_for("register"), data=REGISTER_DETAIL, + security_test_client.app.url_path_for("register"), + data=REGISTER_DETAIL, ) security_test_client.post( - security_test_client.app.url_path_for("login"), data=LOGIN_DATA, + security_test_client.app.url_path_for("login"), + data=LOGIN_DATA, ) resp = security_test_client.post(url, data=form_mock) @@ -183,17 +188,22 @@ def test_add_feature_to_user(form_mock, security_test_client): def test_delete_user_feature_association( - form_mock, feature, security_test_client, + form_mock, + feature, + security_test_client, ): security_test_client.post( - security_test_client.app.url_path_for("register"), data=REGISTER_DETAIL, + security_test_client.app.url_path_for("register"), + data=REGISTER_DETAIL, ) security_test_client.post( - security_test_client.app.url_path_for("login"), data=LOGIN_DATA, + security_test_client.app.url_path_for("login"), + data=LOGIN_DATA, ) security_test_client.post( - route.router.url_path_for("add_feature_to_user"), data=form_mock, + route.router.url_path_for("add_feature_to_user"), + data=form_mock, ) url = route.router.url_path_for("delete_user_feature_association") From 172a1da7b367f812b4ef5fbd2aa82a751f2dbe0c Mon Sep 17 00:00:00 2001 From: Liran C Date: Wed, 24 Feb 2021 17:02:48 +0200 Subject: [PATCH 100/115] fix tests --- app/database/models.py | 4 ++-- app/internal/features.py | 25 ++++++++++++------------- app/routers/features.py | 9 ++++++--- tests/test_feature_panel.py | 18 +++++++++--------- 4 files changed, 29 insertions(+), 27 deletions(-) diff --git a/app/database/models.py b/app/database/models.py index ce42b424..60fb591a 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -36,8 +36,8 @@ class UserFeature(Base): __tablename__ = "user_feature" id = Column(Integer, primary_key=True, index=True) - feature_id = Column('feature_id', Integer, ForeignKey('features.id')) - user_id = Column('user_id', Integer, ForeignKey('users.id')) + feature_id = Column("feature_id", Integer, ForeignKey("features.id")) + user_id = Column("user_id", Integer, ForeignKey("users.id")) is_enable = Column(Boolean, default=False) diff --git a/app/internal/features.py b/app/internal/features.py index 990bf28f..6e0af41c 100644 --- a/app/internal/features.py +++ b/app/internal/features.py @@ -57,11 +57,9 @@ def is_user_has_feature( user_id: int, ) -> bool: return session.query( - exists().where( - UserFeature.user_id == user_id - ).where( - UserFeature.feature_id == feature_id - ) + exists() + .where(UserFeature.user_id == user_id) + .where(UserFeature.feature_id == feature_id), ).scalar() @@ -76,11 +74,9 @@ def delete_feature( def is_feature_exists(feature: Dict[str, str], session: Session) -> bool: is_exists = session.query( - exists().where( - Feature.name == feature["name"] - ).where( - Feature.route == feature["route"] - ) + exists() + .where(Feature.name == feature["name"]) + .where(Feature.route == feature["route"]), ).scalar() return is_exists @@ -178,9 +174,12 @@ def get_user_installed_features( user_id: int, session: Session = Depends(get_db), ) -> List[Feature]: - return session.query(Feature).join(UserFeature).filter( - UserFeature.user_id == user_id - ).all() + return ( + session.query(Feature) + .join(UserFeature) + .filter(UserFeature.user_id == user_id) + .all() + ) async def get_user_uninstalled_features( diff --git a/app/routers/features.py b/app/routers/features.py index 802b99fe..8d14de2a 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -27,15 +27,18 @@ async def index( ) -> templates: features = { "installed": get_user_installed_features( - session=session, user_id=user.user_id, + session=session, + user_id=user.user_id, ), "uninstalled": await get_user_uninstalled_features( - session=session, request=request, + session=session, + request=request, ), } return templates.TemplateResponse( - "features.html", {"request": request, "features": features}, + "features.html", + {"request": request, "features": features}, ) diff --git a/tests/test_feature_panel.py b/tests/test_feature_panel.py index fee93a24..c82df9a2 100644 --- a/tests/test_feature_panel.py +++ b/tests/test_feature_panel.py @@ -71,7 +71,7 @@ def association_on(session, user): def update_dict(): update = { "name": "test", - "route": "/route-test", + "route": "/test", "description": "update", "creator": "test", } @@ -81,9 +81,7 @@ def update_dict(): @pytest.fixture def form_mock(): - form = {"feature_id": 1, "user_id": 1} - - return form + return {"feature_id": 1, "user_id": 1} def test_create_features_at_startup(mocker, session, mock_features): @@ -107,9 +105,8 @@ def test_create_association(mocker, session, user, feature): def test_get_user_enabled_features(session, feature, association_on, user): assert ( - internal.get_user_installed_features(session=session, user_id=user.id)[ - 0 - ] + internal.get_user_installed_features( + session=session, user_id=user.id)[0] is not None ) @@ -129,8 +126,11 @@ def test_delete_feature(session, feature): assert feat is None -def test_is_feature_exist_in_db(session, feature, update_dict): - assert internal.is_feature_exists(update_dict, session) +def test_is_feature_exist_in_db(session, feature): + assert internal.is_feature_exists({ + 'name': 'test', + 'route': '/test' + }, session) def test_update_feature(session, feature, update_dict): From 5b926340aa1b400bdc07746df6f8e1514a032f17 Mon Sep 17 00:00:00 2001 From: Liran C Date: Wed, 24 Feb 2021 17:22:25 +0200 Subject: [PATCH 101/115] fix logic --- app/internal/features.py | 23 ++++++++++------------- tests/test_feature_panel.py | 7 +++++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/internal/features.py b/app/internal/features.py index c4528218..4cd89386 100644 --- a/app/internal/features.py +++ b/app/internal/features.py @@ -54,15 +54,12 @@ def is_user_has_feature( feature_id: int, user_id: int, ) -> bool: - is_exists = session.query( - exists().where( - UserFeature.user_id == user_id - and (UserFeature.feature_id == feature_id), - ), + return session.query( + exists() + .where(UserFeature.user_id == user_id) + .where(UserFeature.feature_id == feature_id), ).scalar() - return is_exists - def delete_feature( feature: Feature, @@ -75,10 +72,9 @@ def delete_feature( def is_feature_exists(feature: Dict[str, str], session: Session) -> bool: is_exists = session.query( - exists().where( - Feature.name == feature["name"] - and Feature.route == feature["route"], - ), + exists() + .where(Feature.name == feature["name"]) + .where(Feature.route == feature["route"]), ).scalar() return is_exists @@ -116,8 +112,9 @@ async def is_access_allowd(request: Request, route: str) -> bool: user_ptef = session.query( exists().where( UserFeature.feature_id == feature.id - and (UserFeature.user_id == user.user_id), - ), + ).where( + UserFeature.user_id == user.user_id + ) ).scalar() return user_ptef diff --git a/tests/test_feature_panel.py b/tests/test_feature_panel.py index ac3eb894..1ee57b7c 100644 --- a/tests/test_feature_panel.py +++ b/tests/test_feature_panel.py @@ -127,8 +127,11 @@ def test_delete_feature(session, feature): assert feat is None -def test_is_feature_exist_in_db(session, feature, update_dict): - assert internal.is_feature_exists(update_dict, session) +def test_is_feature_exist_in_db(session, feature): + assert internal.is_feature_exists({ + 'name': 'test', + 'route': '/test' + }, session) def test_update_feature(session, feature, update_dict): From 36f841b0831176f617bc15e12360d5fe0b2b4b4f Mon Sep 17 00:00:00 2001 From: Liran C Date: Thu, 25 Feb 2021 16:13:05 +0200 Subject: [PATCH 102/115] requested changes --- app/internal/features.py | 62 +++++++++++++++++++--------------------- app/routers/features.py | 31 ++++++++++++++++---- 2 files changed, 55 insertions(+), 38 deletions(-) diff --git a/app/internal/features.py b/app/internal/features.py index 4cd89386..5b3c7dcb 100644 --- a/app/internal/features.py +++ b/app/internal/features.py @@ -1,15 +1,16 @@ -from fastapi import Depends, Request from functools import wraps -from starlette.responses import RedirectResponse from typing import Dict, List -from sqlalchemy.sql import exists + +from fastapi import Depends, Request from sqlalchemy.orm import Session +from sqlalchemy.sql import exists +from starlette.responses import RedirectResponse -from app.database.models import UserFeature, Feature -from app.dependencies import get_db, SessionLocal +from app.database.models import Feature, UserFeature +from app.dependencies import SessionLocal, get_db +from app.internal.features_index import features from app.internal.security.dependencies import current_user from app.internal.security.ouath2 import get_authorization_cookie -from app.internal.features_index import features from app.internal.utils import create_model @@ -111,20 +112,19 @@ async def is_access_allowd(request: Request, route: str) -> bool: user_ptef = session.query( exists().where( - UserFeature.feature_id == feature.id - ).where( - UserFeature.user_id == user.user_id - ) + (UserFeature.feature_id == feature.id) + & (UserFeature.user_id == user.user_id), + ), ).scalar() return user_ptef def create_feature( + db: Session, name: str, route: str, description: str, - db: Session, creator: str = None, ) -> Feature: """Creates a feature.""" @@ -158,30 +158,26 @@ def get_user_installed_features( user_id: int, session: Session = Depends(get_db), ) -> List[Feature]: - return session.query(UserFeature).filter_by(user_id=user_id).all() + return ( + session.query(Feature) + .join(UserFeature) + .filter(UserFeature.user_id == user_id) + .all() + ) -async def get_user_uninstalled_features( - request: Request, +def get_user_uninstalled_features( + user_id: int, session: Session = Depends(get_db), ) -> List[Feature]: - uninstalled = [] - all_features = session.query(Feature).all() - - # Get current user. - # Note: can't use dependency beacause its designed for routes only. - # current_user return schema not an db model. - jwt = await get_authorization_cookie(request=request) - user = await current_user(request=request, jwt=jwt, db=session) - - for feat in all_features: - in_enabled = is_user_has_feature( - session=session, - feature_id=feat.id, - user_id=user.user_id, + return ( + session.query(Feature) + .filter( + Feature.id.notin_( + session.query(UserFeature.feature_id).filter( + UserFeature.user_id == user_id, + ), + ), ) - - if not in_enabled: - uninstalled.append(feat) - - return uninstalled + .all() + ) diff --git a/app/routers/features.py b/app/routers/features.py index 7994215a..1694afe3 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -1,14 +1,17 @@ -from fastapi import APIRouter, Request, Depends -from sqlalchemy.sql import exists from typing import List -from app.dependencies import get_db, SessionLocal -from app.database.models import User, UserFeature, Feature -from app.internal.security.dependencies import current_user +from fastapi import APIRouter, Depends, Request +from sqlalchemy.sql import exists + +from app.database.models import Feature, User, UserFeature +from app.dependencies import SessionLocal, get_db from app.internal.features import ( create_user_feature_association, + get_user_installed_features, + get_user_uninstalled_features, is_user_has_feature, ) +from app.internal.security.dependencies import current_user router = APIRouter( prefix="/features", @@ -88,3 +91,21 @@ async def delete_user_feature_association( session.commit() return True + + +@router.get("/deactive") +def deactive( + request: Request, + session: SessionLocal = Depends(get_db), + user: User = Depends(current_user), +): + return get_user_uninstalled_features(user_id=user.user_id, session=session) + + +@router.get("/active") +def active( + request: Request, + session: SessionLocal = Depends(get_db), + user: User = Depends(current_user), +): + return get_user_installed_features(user_id=user.user_id, session=session) From 93b0b02199fb1ad83e5742af07c6093301ab29ed Mon Sep 17 00:00:00 2001 From: Liran C Date: Thu, 25 Feb 2021 16:23:00 +0200 Subject: [PATCH 103/115] use pre-commit on google-connect --- app/routers/google_connect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routers/google_connect.py b/app/routers/google_connect.py index 2e55ad79..6892cff9 100644 --- a/app/routers/google_connect.py +++ b/app/routers/google_connect.py @@ -3,10 +3,10 @@ from starlette.responses import RedirectResponse from app.dependencies import get_db +from app.internal.features import feature_access_filter from app.internal.google_connect import fetch_save_events, get_credentials from app.internal.utils import get_current_user from app.routers.profile import router as profile -from app.internal.features import feature_access_filter router = APIRouter( prefix="/google", From 144ff0ef3150ad707a1d4c61d856514084ba53aa Mon Sep 17 00:00:00 2001 From: Liran C Date: Thu, 25 Feb 2021 16:45:07 +0200 Subject: [PATCH 104/115] fix features back --- app/main.py | 4 ++-- app/routers/features.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/main.py b/app/main.py index db6b8d24..e52a57be 100644 --- a/app/main.py +++ b/app/main.py @@ -6,6 +6,7 @@ from fastapi.staticfiles import StaticFiles from sqlalchemy.orm import Session +import app.internal.features as internal_features from app import config from app.database import engine, models from app.dependencies import ( @@ -13,13 +14,12 @@ SOUNDS_PATH, STATIC_PATH, UPLOAD_PATH, + SessionLocal, get_db, logger, templates, - SessionLocal, ) from app.internal import daily_quotes, json_data_loader -import app.internal.features as internal_features from app.internal.languages import set_ui_language from app.internal.security.ouath2 import auth_exception_handler from app.routers.salary import routes as salary diff --git a/app/routers/features.py b/app/routers/features.py index 467898bc..6d7071f0 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -30,9 +30,9 @@ async def index( session=session, user_id=user.user_id, ), - "uninstalled": await get_user_uninstalled_features( + "uninstalled": get_user_uninstalled_features( session=session, - request=request, + user_id=user.user_id, ), } From 3bd43b2cf4198ce768ba1de20ee47f3764f01a77 Mon Sep 17 00:00:00 2001 From: Liran C Date: Fri, 26 Feb 2021 00:20:21 +0200 Subject: [PATCH 105/115] requested changes --- app/internal/features.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/internal/features.py b/app/internal/features.py index 5b3c7dcb..4ef8628e 100644 --- a/app/internal/features.py +++ b/app/internal/features.py @@ -110,14 +110,14 @@ async def is_access_allowd(request: Request, route: str) -> bool: # route that gived by to the request. return True - user_ptef = session.query( + user_feature = session.query( exists().where( (UserFeature.feature_id == feature.id) & (UserFeature.user_id == user.user_id), ), ).scalar() - return user_ptef + return user_feature def create_feature( From 37ad8334756f5729f07e563ce5771e83b6c5d963 Mon Sep 17 00:00:00 2001 From: Liran C Date: Fri, 26 Feb 2021 00:57:49 +0200 Subject: [PATCH 106/115] remove redundant things --- app/routers/features.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/app/routers/features.py b/app/routers/features.py index 6d7071f0..ef6eb031 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -106,21 +106,3 @@ async def delete_user_feature_association( session.commit() return True - - -@router.get("/deactive") -def deactive( - request: Request, - session: SessionLocal = Depends(get_db), - user: User = Depends(current_user), -): - return get_user_uninstalled_features(user_id=user.user_id, session=session) - - -@router.get("/active") -def active( - request: Request, - session: SessionLocal = Depends(get_db), - user: User = Depends(current_user), -): - return get_user_installed_features(user_id=user.user_id, session=session) From bb3b52fbee9bf4f1c6555aea0a301777e617830c Mon Sep 17 00:00:00 2001 From: Liran C Date: Fri, 26 Feb 2021 01:04:56 +0200 Subject: [PATCH 107/115] fix flake8 --- app/main.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/main.py b/app/main.py index 812682d3..a4affb6a 100644 --- a/app/main.py +++ b/app/main.py @@ -6,7 +6,6 @@ from fastapi.staticfiles import StaticFiles from sqlalchemy.orm import Session -import app.internal.features as internal_features from app import config from app.database import engine, models from app.dependencies import ( @@ -18,7 +17,6 @@ get_db, logger, templates, - SessionLocal, ) from app.internal import daily_quotes, json_data_loader import app.internal.features as internal_features From 7f1e1192ca5aebf44abfb6f1ccb503ed6d98a5b4 Mon Sep 17 00:00:00 2001 From: Liran C Date: Fri, 26 Feb 2021 09:46:20 +0200 Subject: [PATCH 108/115] fixes --- app/main.py | 2 +- app/static/features.css | 4 ++++ app/static/js/features.js | 7 +++---- app/static/js/grid_scripts.js | 10 +++++++--- app/templates/calendar_monthly_view.html | 4 ++-- app/templates/partials/base.html | 9 +++++---- 6 files changed, 22 insertions(+), 14 deletions(-) diff --git a/app/main.py b/app/main.py index a4affb6a..285514b3 100644 --- a/app/main.py +++ b/app/main.py @@ -6,6 +6,7 @@ from fastapi.staticfiles import StaticFiles from sqlalchemy.orm import Session +import app.internal.features as internal_features from app import config from app.database import engine, models from app.dependencies import ( @@ -19,7 +20,6 @@ templates, ) from app.internal import daily_quotes, json_data_loader -import app.internal.features as internal_features from app.internal.languages import set_ui_language from app.internal.security.ouath2 import auth_exception_handler from app.routers.salary import routes as salary diff --git a/app/static/features.css b/app/static/features.css index 2d7c3663..5ab64080 100644 --- a/app/static/features.css +++ b/app/static/features.css @@ -75,3 +75,7 @@ grid-column: 2/6; width: 30rem; } + +.info-box { + display: block !important; +} diff --git a/app/static/js/features.js b/app/static/js/features.js index 180c6cda..2b511a5e 100644 --- a/app/static/js/features.js +++ b/app/static/js/features.js @@ -101,10 +101,10 @@ function setInformation() { const parent = element.parentElement; const infoBox = parent.getElementsByClassName("information")[0]; element.addEventListener("click", function () { - if (infoBox.style.display === "block") { - infoBox.style.display = "none"; + if (!infoBox.classList.contains('info-box')) { + infoBox.classList.add('info-box'); } else { - infoBox.style.display = "block"; + infoBox.classList.remove('info-box'); } }) }); @@ -117,4 +117,3 @@ document.addEventListener( setInformation(); } ) - diff --git a/app/static/js/grid_scripts.js b/app/static/js/grid_scripts.js index 8392d0a7..312cd0ff 100644 --- a/app/static/js/grid_scripts.js +++ b/app/static/js/grid_scripts.js @@ -67,6 +67,8 @@ function callLoadWeek(daysToLoad, end) { } function weekScroll() { + const daysToLoad = 42; + grid = document.getElementById("calender-grid"); grid.addEventListener( 'scroll', function () { @@ -74,12 +76,14 @@ function weekScroll() { if (grid.scrollY + grid.innerHeight + tolerance < grid.scrollHeight) { return false; } - callLoadWeek(42, true); + const end = true; + callLoadWeek(daysToLoad, end); } ) grid.addEventListener('wheel', function (event) { if (event.deltaY < 0 && this.scrollTop === 0) { - callLoadWeek(42, false); + const end = false; + callLoadWeek(daysToLoad, end); } }) } @@ -90,4 +94,4 @@ document.addEventListener( setToggle("day", "day-view", "day-view-visible", 0, allDays.length); weekScroll(); } -) \ No newline at end of file +) diff --git a/app/templates/calendar_monthly_view.html b/app/templates/calendar_monthly_view.html index e23c69da..501b0128 100644 --- a/app/templates/calendar_monthly_view.html +++ b/app/templates/calendar_monthly_view.html @@ -6,7 +6,7 @@
Location 0oc 00:00
@@ -14,4 +14,4 @@
{% include 'partials/calendar/monthly_view/monthly_grid.html' %}
-{% endblock content %} \ No newline at end of file +{% endblock content %} diff --git a/app/templates/partials/base.html b/app/templates/partials/base.html index 717673e6..7000db74 100644 --- a/app/templates/partials/base.html +++ b/app/templates/partials/base.html @@ -31,6 +31,7 @@ integrity="sha512-d9xgZrVZpmmQlfonhQUvTR7lMPtO7NkZMkA0ABN3PHCbKA5nqylQ/yWlFAyY6hYgdF1Qh6nYiuADWwKB4C2WSw==" crossorigin="anonymous"> + @@ -43,8 +44,8 @@ -{% block body %} -{% endblock %} - + {% block body %} + {% endblock %} - \ No newline at end of file + + From f6d3ce05cdbdc1300a381c9c266d16b53615be45 Mon Sep 17 00:00:00 2001 From: Liran C Date: Fri, 26 Feb 2021 09:47:47 +0200 Subject: [PATCH 109/115] more fixes --- app/static/js/grid_scripts.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/static/js/grid_scripts.js b/app/static/js/grid_scripts.js index 312cd0ff..97bcf38d 100644 --- a/app/static/js/grid_scripts.js +++ b/app/static/js/grid_scripts.js @@ -76,13 +76,13 @@ function weekScroll() { if (grid.scrollY + grid.innerHeight + tolerance < grid.scrollHeight) { return false; } - const end = true; + const addAfter = true; callLoadWeek(daysToLoad, end); } ) grid.addEventListener('wheel', function (event) { if (event.deltaY < 0 && this.scrollTop === 0) { - const end = false; + const addAfter = false; callLoadWeek(daysToLoad, end); } }) From fd89e013cea39764edc0f9125f59c27f7accb739 Mon Sep 17 00:00:00 2001 From: Liran C Date: Fri, 26 Feb 2021 09:48:02 +0200 Subject: [PATCH 110/115] more fixes --- app/static/js/grid_scripts.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/static/js/grid_scripts.js b/app/static/js/grid_scripts.js index 97bcf38d..8f4582d3 100644 --- a/app/static/js/grid_scripts.js +++ b/app/static/js/grid_scripts.js @@ -77,13 +77,13 @@ function weekScroll() { return false; } const addAfter = true; - callLoadWeek(daysToLoad, end); + callLoadWeek(daysToLoad, addAfter); } ) grid.addEventListener('wheel', function (event) { if (event.deltaY < 0 && this.scrollTop === 0) { const addAfter = false; - callLoadWeek(daysToLoad, end); + callLoadWeek(daysToLoad, addAfter); } }) } From 4777fab101d24a078aba70e52ff80aa73ae632fc Mon Sep 17 00:00:00 2001 From: Liran C Date: Sun, 7 Mar 2021 10:30:16 +0200 Subject: [PATCH 111/115] get this --- app/internal/features.py | 3 +- app/internal/features_index.py | 29 ++-- app/routers/features.py | 27 +++ app/static/global.css | 30 ++++ app/static/grid_style.css | 78 +++++---- app/static/js/global.js | 88 ++++++++++ app/static/js/grid_scripts.js | 2 +- app/templates/calendar_monthly_view.html | 6 + app/templates/features.html | 157 ++++++++---------- .../partials/calendar/calendar_base.html | 16 +- .../calendar/feature_settings/example.html | 1 - .../feature_settings_base.html | 21 +++ .../partials/calendar/navigation.html | 8 +- .../features_panels/example_panel.html | 1 + tests/test_feature_panel.py | 35 ++-- 15 files changed, 333 insertions(+), 169 deletions(-) create mode 100644 app/static/js/global.js delete mode 100644 app/templates/partials/calendar/feature_settings/example.html create mode 100644 app/templates/partials/calendar/feature_settings/feature_settings_base.html create mode 100644 app/templates/partials/features_panels/example_panel.html diff --git a/app/internal/features.py b/app/internal/features.py index 09768e37..1eb3f329 100644 --- a/app/internal/features.py +++ b/app/internal/features.py @@ -11,6 +11,7 @@ from app.internal.features_index import features, icons from app.internal.security.dependencies import current_user from app.internal.security.ouath2 import get_authorization_cookie +from app.internal.security.schema import CurrentUser from app.internal.utils import create_model @@ -125,7 +126,7 @@ async def is_access_allowd(request: Request, route: str) -> bool: & (UserFeature.user_id == user.user_id), ), ).scalar() - + print(user_feature) return user_feature diff --git a/app/internal/features_index.py b/app/internal/features_index.py index 5630f8e2..b4d57731 100644 --- a/app/internal/features_index.py +++ b/app/internal/features_index.py @@ -1,4 +1,4 @@ -''' +""" This file purpose is for developers to add their features to the database in one convenient place, every time the system loads up it's adding and updating the features in the features table in the database. @@ -12,9 +12,9 @@ * Please see the example below. * Enjoy and good luck :) -''' +""" -''' +""" All icons come from https://ionicons.com/. If you want your feature to have an icon Go To: https://ionicons.com/ @@ -40,16 +40,13 @@ . . } -''' +""" # Add to last! -icons = { - "feature-panel": "albums-outline", - "Google Sync": "sync-outline" - } +icons = {"feature-panel": "albums-outline", "Google Sync": "sync-outline"} -''' +""" Example to feature stracture: { @@ -58,9 +55,9 @@ "description": "", "creator": "" } -''' +""" -''' +""" * IMPORTANT * Example to decorator placement: @@ -74,13 +71,19 @@ def my_cool_feature_route(): .. . -''' +""" features = [ { "name": "Google Sync", "route": "/google/sync", "description": "Sync Google Calendar events with Pylender", - "creator": "Liran Caduri" + "creator": "Liran Caduri", + }, + { + "name": "example", + "route": "/example", + "description": "example", + "creator": "example", }, ] diff --git a/app/routers/features.py b/app/routers/features.py index ef6eb031..563f7001 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -1,4 +1,7 @@ +from typing import List + from fastapi import APIRouter, Depends, Request +from fastapi.responses import HTMLResponse from sqlalchemy.sql import exists from app.database.models import Feature, User, UserFeature @@ -106,3 +109,27 @@ async def delete_user_feature_association( session.commit() return True + + +@router.get("/installed") +async def get_user_feature( + request: Request, + session: SessionLocal = Depends(get_db), + user: User = Depends(current_user), +) -> List[Feature]: + return get_user_installed_features(user_id=user.user_id, session=session) + + +@router.post("/settings/{feature_name}") +async def render_settings( + request: Request, + feature_name: str, +) -> HTMLResponse: + form = await request.form() + feature_name = "".join(feature_name.split()) + template = templates.get_template( + "partials/features_panels/" + feature_name + "_panel.html", + ) + if template is not None: + content = template.render() + return HTMLResponse(content=content) diff --git a/app/static/global.css b/app/static/global.css index 4228b413..24a11e22 100644 --- a/app/static/global.css +++ b/app/static/global.css @@ -64,3 +64,33 @@ a { color: inherit; } +/* Features Settings Panel */ +.feature-settings-closed { + display: none; + width: 0.1rem; +} + +.feature-settings-open { + display: block; + background-color: var(--surface); + width: 20rem; +} + +.feature-settings-panel { + padding: 0 var(--space_s); +} + +.flex-row { + display: flex; + flex-flow: row wrap; + margin-bottom: var(--space_s); +} + +.flex-row div { + flex: 1; +} + +#feature-toggle { + flex: 0 0 2rem; + text-align: right; +} diff --git a/app/static/grid_style.css b/app/static/grid_style.css index b1b0d65a..fb803759 100644 --- a/app/static/grid_style.css +++ b/app/static/grid_style.css @@ -1,20 +1,40 @@ -:root[data-color-mode="regular"] { - --backgroundcol: #F7F7F7; - --textcolor: #222831; - --start-of-month: #E9ECEf; - --primary-variant: #FFDE4D; - --secondary: #EF5454; - --borders: #E7E7E7; - --borders-variant: #F7F7F7; -} - -:root[data-color-mode="dark"] { - --backgroundcol: #000000; - --textcolor: #EEEEEE; - --start-of-month: #8C28BF; - --secondary: #EF5454; - --borders: #E7E7E7; - --borders-variant: #F7F7F7; +:root { + /* Texts */ + --text_xxs: 0.5rem; /* 8px */ + --text_xs: 0.75rem; /* 12px */ + --text_s: 1rem; /* 16px */ + --text_m: 1.2rem; /* 19px */ + --text_l: 1.5rem; /* 24px */ + --text_xl: 1.75rem; /* 28px */ + --text_xxl: 2rem; /* 32px */ + + /* Spaces */ + --space_xxs: 0.25rem; /* 4px */ + --space_xs: 0.5rem; /* 8px */ + --space_s: 0.75rem; /* 12px */ + --space_m: 1.25rem; /* 20px */ + --space_l: 2rem; /* 32px */ + --space_xl: 4rem; /* 64px */ + + /* colors */ + --primary: #24396a; + --primary-variant: #041E51; + --secondary: #fbc44a; + --secondary-variant: #ffa201; + --background: #F7F7F7; + --surface: #e6e6e6; /* borders */ + --surface-variant: #d2d1d1; /*up_surface, borders-variant*/ + --negative: #f24726; + --negative-variant: #e83305; + --positive: #4ca486; + --positive-variant: #008c73; + + --on-primary: #ffffff; + --on-secondary: #000000; + --on-background: #000000; + --on-surface: #000000; + --on-negative: #ffffff; + --on-positive: #ffffff; } * { @@ -92,24 +112,7 @@ nav { background-color: var(--secondary); } -#open-features { - font-size: var(--icon_l); - margin-top: var(--space_s); -} - -#feature-settings { - visibility: hidden; - width: 0.1rem; - background-color: var(--surface); -} - -.settings-open { - width: 20rem; -} - -img { - fill: var(--background); -} +img {fill: var(--background);} header { z-index: 5; @@ -462,6 +465,11 @@ main { height: 2rem; } +.remove-button { + background-color: var(--negative); +} + + .add-button { background-color: var(--positive); } diff --git a/app/static/js/global.js b/app/static/js/global.js new file mode 100644 index 00000000..76580b5f --- /dev/null +++ b/app/static/js/global.js @@ -0,0 +1,88 @@ +function openPanel(targetID, classToSet) { + const target = document.getElementById(targetID); + target.classList.toggle(classToSet); +} + +function setFeatureInformation(element) { + const panel = { + "feature-description": element.dataset.description, + "feature-name": element.dataset.name, + "feature-followers": element.dataset.followers, + "feature-creator": element.dataset.creator + }; + for (const key in panel) { + document.getElementById(key).innerHTML = panel[key]; + } + console.log('make a call') + renderTemplate(panel["feature-name"]); +} + +function setFeaturesSettings() { + const allFeatures = document.getElementsByClassName("feature"); + for (let i = 0; i < allFeatures.length; ++i) { + allFeatures[i].addEventListener('click', function () { + openPanel("feature-set-panel", "feature-settings-open"); + const target = document.getElementById("feature-set-panel"); + if (target.classList.contains("feature-settings-open")) { + setFeatureInformation(allFeatures[i]); + } + }) + } +} + +function appandFeatures(){ + const baseURL = window.location.origin; + const route = new URL('/features/installed', baseURL); + + fetch(route) + .then((response) => { + return response.json(); + }).then((data) => { + const iconStrip = document.getElementById('icon-strip'); + for (let i = 0; i < data.length; ++i) { + const fData = data[i] + console.log(fData) + let feature = document.createElement('div'); + feature.classList.add('feature'); + feature.id = 'feature-' + fData.id; + feature.dataset.name = fData.name; + feature.dataset.creator = fData.creator; + feature.dataset.description = fData.description; + feature.dataset.followers = fData.followers; + + console.log(feature) + + let icon = document.createElement('ion-icon'); + icon.setAttribute('name', fData.icon); + feature.appendChild(icon); + iconStrip.appendChild(feature); + } + }); +} + +function renderTemplate(featureName){ + const baseURL = window.location.origin; + const route = new URL('/features/settings/' + featureName, baseURL); + const formData = new FormData(); + formData.append('feature_name', featureName); + + fetch(route, + { + body: formData, + method: "post" + } + ).then(function (response) { + return response.text(); + }).then(function (html) { + const content = document.getElementById("feature-content") + content.innerHTML = '' + content.insertAdjacentHTML('afterbegin', html); + }); +} + +document.addEventListener( + 'DOMContentLoaded', function () { + appandFeatures(); + setTimeout(setFeaturesSettings, 200); + } +) diff --git a/app/static/js/grid_scripts.js b/app/static/js/grid_scripts.js index 8f4582d3..d4743b3f 100644 --- a/app/static/js/grid_scripts.js +++ b/app/static/js/grid_scripts.js @@ -1,6 +1,6 @@ function setToggle( elementClass, targetElement, classToAdd, - index, elementsToLoad + index, elementsToLoad, ) { const allElements = document.getElementsByClassName(elementClass); const target = document.getElementById(targetElement); diff --git a/app/templates/calendar_monthly_view.html b/app/templates/calendar_monthly_view.html index b97015a2..6ba99697 100644 --- a/app/templates/calendar_monthly_view.html +++ b/app/templates/calendar_monthly_view.html @@ -39,3 +39,9 @@

Time calculator:

{% include 'partials/calendar/monthly_view/monthly_grid.html' %} {% endblock content %} + +{% block scripts %} + + + +{% endblock scripts %} diff --git a/app/templates/features.html b/app/templates/features.html index 45361155..1e54fb2c 100644 --- a/app/templates/features.html +++ b/app/templates/features.html @@ -1,97 +1,84 @@ -{% extends "./partials/base.html" %} -{% block head %} -{{ super() }} - - - +{% extends "./partials/calendar/calendar_base.html" %} +{% block css %} +{% endblock css %} -{% endblock head %} {% block page_name %}Features Panel{% endblock page_name %} -{% block body %} -
- {% include 'partials/calendar/navigation.html' %} -
- {% include 'partials/calendar/feature_settings/example.html' %} + +{% block content %} +
+
+
FEATURES PANEL
+

This panel allows you to view optional addons.
Add, remove or customize + features + to your liking by clicking on the features icon in the navigation bar. +

+ +
+ -
- {% block content %} -
-
-
FEATURES PANEL
-

This panel allows you to view optional addons.
Add, remove or customize - features - to your liking by clicking on the features icon in the navigation bar. -

-
+ +
+
+
Available Features
+
Your Features
+
+ {% for feature in features['uninstalled'] %} +
+
+
+
+ {{feature.name.upper()}} + +
+
Creator: {{feature.creator.title()}}
+
+ + Followers: + {{feature.followers}} +
+
+ ADD +
+
{{feature.description}}
- -
- -
-
-
Available Features
-
Your Features
-
- {% for feature in features['uninstalled'] %} -
-
- -
-
- {{feature.name.upper()}} - -
-
Creator: {{feature.creator.title()}}
-
- - Followers: - {{feature.followers}} -
-
- ADD -
-
{{feature.description}}
-
- {% endfor %} + {% endfor %} +
+
+ {% for feature in features['installed'] %} +
+
+ +
+
+ {{feature.name.upper()}} + +
+
Creator: {{feature.creator.title()}}
+
+ + Followers: + {{feature.followers}}
-
- {% for feature in features['installed'] %} -
-
- -
-
- {{feature.name.upper()}} - -
-
Creator: {{feature.creator.title()}}
-
- - Followers: - {{feature.followers}} -
-
- REMOVE -
-
{{feature.description}}
-
- {% endfor %} +
+ REMOVE
+
{{feature.description}}
-
- {% endblock content %} + {% endfor %} +
-
+ +{% endblock content %} - +{% block scripts %} -{% endblock body %} \ No newline at end of file +{% endblock scripts %} \ No newline at end of file diff --git a/app/templates/partials/calendar/calendar_base.html b/app/templates/partials/calendar/calendar_base.html index cac79932..a6572499 100644 --- a/app/templates/partials/calendar/calendar_base.html +++ b/app/templates/partials/calendar/calendar_base.html @@ -1,26 +1,26 @@ {% extends "./partials/base.html" %} {% block head %} - {{super()}} +{{super()}} - - - + + + {% endblock head %} {% block page_name %}Month View{% endblock page_name %} {% block body %}
{% include 'partials/calendar/navigation.html' %} -
- {% include 'partials/calendar/feature_settings/example.html' %} -
+ {% include 'partials/calendar/feature_settings/feature_settings_base.html' %}
{% block content %} {% endblock content %}
+ -{% endblock body %} + +{% endblock body %} \ No newline at end of file diff --git a/app/templates/partials/calendar/feature_settings/example.html b/app/templates/partials/calendar/feature_settings/example.html deleted file mode 100644 index a9f4edd0..00000000 --- a/app/templates/partials/calendar/feature_settings/example.html +++ /dev/null @@ -1 +0,0 @@ -
FEATURE NAME
\ No newline at end of file diff --git a/app/templates/partials/calendar/feature_settings/feature_settings_base.html b/app/templates/partials/calendar/feature_settings/feature_settings_base.html new file mode 100644 index 00000000..65da8c05 --- /dev/null +++ b/app/templates/partials/calendar/feature_settings/feature_settings_base.html @@ -0,0 +1,21 @@ +
+
+
+
+
+
+
+ + Followers: +
+
+ Creator: +
+
+
+
+ +
+
+
diff --git a/app/templates/partials/calendar/navigation.html b/app/templates/partials/calendar/navigation.html index 45004757..3ee0ec73 100644 --- a/app/templates/partials/calendar/navigation.html +++ b/app/templates/partials/calendar/navigation.html @@ -38,13 +38,7 @@
-
-
- -
-
-
- +
diff --git a/app/templates/partials/features_panels/example_panel.html b/app/templates/partials/features_panels/example_panel.html new file mode 100644 index 00000000..6d5543d1 --- /dev/null +++ b/app/templates/partials/features_panels/example_panel.html @@ -0,0 +1 @@ +
Your feature settings
\ No newline at end of file diff --git a/tests/test_feature_panel.py b/tests/test_feature_panel.py index 438dc0b9..9d1bf73c 100644 --- a/tests/test_feature_panel.py +++ b/tests/test_feature_panel.py @@ -1,8 +1,9 @@ import pytest -from app.database.models import Feature, UserFeature import app.internal.features as internal import app.routers.features as route +from app.database.models import Feature, UserFeature +from app.internal.security.schema import CurrentUser from tests.test_login import LOGIN_DATA, REGISTER_DETAIL @@ -22,12 +23,12 @@ def mock_features(): @pytest.mark.usefixtures("session") def feature(session): test = Feature( - name='test', - route='/test', - description='testing', - creator='test', - icon='test', - followers=0 + name="test", + route="/test", + description="testing", + creator="test", + icon="test", + followers=0, ) session.add(test) @@ -105,8 +106,9 @@ def test_create_association(mocker, session, user, feature): def test_get_user_enabled_features(session, feature, association_on, user): assert ( - internal.get_user_installed_features( - session=session, user_id=user.id)[0] + internal.get_user_installed_features(session=session, user_id=user.id)[ + 0 + ] is not None ) @@ -127,10 +129,9 @@ def test_delete_feature(session, feature): def test_is_feature_exist_in_db(session, feature): - assert internal.is_feature_exists({ - 'name': 'test', - 'route': '/test' - }, session) + assert internal.is_feature_exists( + {"name": "test", "route": "/test"}, session, + ) def test_update_feature(session, feature, update_dict): @@ -139,7 +140,7 @@ def test_update_feature(session, feature, update_dict): @pytest.mark.asyncio -async def test_is_feature_enabled(mocker, session, association_on, user): +async def test_is_feature_enabled(mocker, session, association_on, feature): mocker.patch("app.internal.features.SessionLocal", return_value=session) mocker.patch( "app.internal.features.get_authorization_cookie", @@ -147,12 +148,10 @@ async def test_is_feature_enabled(mocker, session, association_on, user): ) mocker.patch( "app.internal.features.current_user", - return_value=user, + return_value=CurrentUser(user_id=1, username="test"), ) - assert ( - await internal.is_access_allowd(route="/route", request=None) is True - ) + assert await internal.is_access_allowd(route="/test", request=None) is True def test_create_feature(session): From fee36d2ed14fbc14333f76e85450b35f4f922f74 Mon Sep 17 00:00:00 2001 From: Liran C Date: Fri, 19 Mar 2021 16:15:08 +0200 Subject: [PATCH 112/115] fix front --- app/templates/partials/calendar/calendar_base.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/templates/partials/calendar/calendar_base.html b/app/templates/partials/calendar/calendar_base.html index a6572499..aba16eb7 100644 --- a/app/templates/partials/calendar/calendar_base.html +++ b/app/templates/partials/calendar/calendar_base.html @@ -5,6 +5,9 @@ +{% block css %} +{% endblock css %} + {% endblock head %} {% block page_name %}Month View{% endblock page_name %} {% block body %} @@ -23,4 +26,7 @@ +{% block scripts %} +{% endblock scripts %} + {% endblock body %} \ No newline at end of file From b77f5e788169a5ab8d426737342eb5c6754c96b7 Mon Sep 17 00:00:00 2001 From: Liran C Date: Fri, 19 Mar 2021 16:36:15 +0200 Subject: [PATCH 113/115] fix notifcation test error --- app/templates/partials/notification/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/partials/notification/base.html b/app/templates/partials/notification/base.html index 0df8fcde..93fb1723 100644 --- a/app/templates/partials/notification/base.html +++ b/app/templates/partials/notification/base.html @@ -11,7 +11,7 @@
{% include 'partials/calendar/navigation.html' %}
- {% include 'partials/calendar/feature_settings/example.html' %} + {% include 'partials/calendar/feature_settings/feature_settings_base.html' %}
{% block content %} From 7d9b9499da6a2b22e85a2626b5d07e64c2081c18 Mon Sep 17 00:00:00 2001 From: Liran C Date: Fri, 19 Mar 2021 17:57:58 +0200 Subject: [PATCH 114/115] template column and fixes --- app/database/models.py | 1 + app/internal/features.py | 4 +++- app/internal/features_index.py | 5 ++++- app/routers/features.py | 17 +++++++++-------- app/static/js/global.js | 21 +++++++-------------- tests/test_feature_panel.py | 1 + 6 files changed, 25 insertions(+), 24 deletions(-) diff --git a/app/database/models.py b/app/database/models.py index abfff534..16091cfa 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -109,6 +109,7 @@ class Feature(Base): description = Column(String, nullable=False) icon = Column(String, nullable=False) followers = Column(Integer, default=0) + template = Column(String, nullable=True) users = relationship("User", secondary=UserFeature.__tablename__) diff --git a/app/internal/features.py b/app/internal/features.py index 1eb3f329..7d9c0d44 100644 --- a/app/internal/features.py +++ b/app/internal/features.py @@ -11,7 +11,6 @@ from app.internal.features_index import features, icons from app.internal.security.dependencies import current_user from app.internal.security.ouath2 import get_authorization_cookie -from app.internal.security.schema import CurrentUser from app.internal.utils import create_model @@ -93,6 +92,7 @@ def update_feature( feature.route = feature_dict["route"] feature.description = feature_dict["description"] feature.creator = feature_dict["creator"] + feature.template = feature_dict["template"] icon = icons.get(feature.name) if icon is None: @@ -137,6 +137,7 @@ def create_feature( description: str, creator: str = None, icon: str = None, + template: str = None, ) -> Feature: """Creates a feature.""" db = SessionLocal() @@ -152,6 +153,7 @@ def create_feature( creator=creator, description=description, icon=icon, + template=template, ) diff --git a/app/internal/features_index.py b/app/internal/features_index.py index b4d57731..e94c9982 100644 --- a/app/internal/features_index.py +++ b/app/internal/features_index.py @@ -53,7 +53,9 @@ "name": "", "route": "/", "description": "", - "creator": "" + "creator": "", + "template": "" } """ @@ -79,6 +81,7 @@ def my_cool_feature_route(): "route": "/google/sync", "description": "Sync Google Calendar events with Pylender", "creator": "Liran Caduri", + "template": "example_panel", }, { "name": "example", diff --git a/app/routers/features.py b/app/routers/features.py index 563f7001..43f5db3d 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -3,6 +3,7 @@ from fastapi import APIRouter, Depends, Request from fastapi.responses import HTMLResponse from sqlalchemy.sql import exists +from starlette.responses import PlainTextResponse from app.database.models import Feature, User, UserFeature from app.dependencies import SessionLocal, get_db, templates @@ -120,16 +121,16 @@ async def get_user_feature( return get_user_installed_features(user_id=user.user_id, session=session) -@router.post("/settings/{feature_name}") +@router.post("/settings/{template}") async def render_settings( request: Request, - feature_name: str, + template: str, ) -> HTMLResponse: - form = await request.form() - feature_name = "".join(feature_name.split()) + if template == 'null': + return PlainTextResponse(content='No additional settings for this one :)') + template = templates.get_template( - "partials/features_panels/" + feature_name + "_panel.html", + "partials/features_panels/" + template + '.html' ) - if template is not None: - content = template.render() - return HTMLResponse(content=content) + content = template.render() + return HTMLResponse(content=content) diff --git a/app/static/js/global.js b/app/static/js/global.js index 76580b5f..a5122333 100644 --- a/app/static/js/global.js +++ b/app/static/js/global.js @@ -13,8 +13,7 @@ function setFeatureInformation(element) { for (const key in panel) { document.getElementById(key).innerHTML = panel[key]; } - console.log('make a call') - renderTemplate(panel["feature-name"]); + renderTemplate(element.dataset.template); } function setFeaturesSettings() { @@ -41,16 +40,16 @@ function appandFeatures(){ const iconStrip = document.getElementById('icon-strip'); for (let i = 0; i < data.length; ++i) { const fData = data[i] - console.log(fData) let feature = document.createElement('div'); + feature.classList.add('feature'); feature.id = 'feature-' + fData.id; + feature.dataset.name = fData.name; feature.dataset.creator = fData.creator; feature.dataset.description = fData.description; feature.dataset.followers = fData.followers; - - console.log(feature) + feature.dataset.template = fData.template; let icon = document.createElement('ion-icon'); icon.setAttribute('name', fData.icon); @@ -60,17 +59,11 @@ function appandFeatures(){ }); } -function renderTemplate(featureName){ +function renderTemplate(featureTemplate){ const baseURL = window.location.origin; - const route = new URL('/features/settings/' + featureName, baseURL); - const formData = new FormData(); - formData.append('feature_name', featureName); + const route = new URL('/features/settings/' + featureTemplate, baseURL); - fetch(route, - { - body: formData, - method: "post" - } + fetch(route, { method: "post" } ).then(function (response) { return response.text(); }).then(function (html) { diff --git a/tests/test_feature_panel.py b/tests/test_feature_panel.py index 9d1bf73c..8ba21887 100644 --- a/tests/test_feature_panel.py +++ b/tests/test_feature_panel.py @@ -75,6 +75,7 @@ def update_dict(): "route": "/test", "description": "update", "creator": "test", + "template": 'test' } return update From b9d0f44d97b585422db24d470235c79473566fd0 Mon Sep 17 00:00:00 2001 From: Liran C Date: Fri, 19 Mar 2021 18:07:52 +0200 Subject: [PATCH 115/115] fix flake8 --- app/routers/features.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/routers/features.py b/app/routers/features.py index 43f5db3d..cc18971f 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -127,7 +127,8 @@ async def render_settings( template: str, ) -> HTMLResponse: if template == 'null': - return PlainTextResponse(content='No additional settings for this one :)') + return PlainTextResponse( + content='No additional settings for this one :)') template = templates.get_template( "partials/features_panels/" + template + '.html'