From d09b5fd9679e17f3242ba8640c0c82b83564f1e0 Mon Sep 17 00:00:00 2001 From: Liran C Date: Mon, 1 Feb 2021 10:19:40 +0200 Subject: [PATCH 01/34] add models to db --- .gitignore | 2 ++ app/database/models.py | 28 +++++++++++++++++++ app/routers/feature_panel.py | 54 ++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 app/routers/feature_panel.py diff --git a/.gitignore b/.gitignore index f34de73e..87d3b809 100644 --- a/.gitignore +++ b/.gitignore @@ -143,3 +143,5 @@ dmypy.json app/.vscode/ app/routers/stam + +bin \ No newline at end of file diff --git a/app/database/models.py b/app/database/models.py index 9e52a195..925f36eb 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -22,6 +22,19 @@ def __repr__(self): return f'' +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')) + + # features = relationship("Feature", back_populates="users") + # users = relationship("User", back_populates="features") + + is_enable = Column(Boolean) + + class User(Base): __tablename__ = "users" @@ -37,10 +50,25 @@ class User(Base): events = relationship("UserEvent", back_populates="participants") + features = relationship("Feature", secondary=UserFeature.__tablename__, + backref="users") + def __repr__(self): return f'' +class Feature(Base): + __tablename__ = "features" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, nullable=False) + route = Column(String, nullable=False) + + users = relationship("User", secondary=UserFeature.__tablename__, + backref="features") + + + class Event(Base): __tablename__ = "events" diff --git a/app/routers/feature_panel.py b/app/routers/feature_panel.py new file mode 100644 index 00000000..5387aaac --- /dev/null +++ b/app/routers/feature_panel.py @@ -0,0 +1,54 @@ +from fastapi import APIRouter, Request, Depends +from app.database.database import get_db, SessionLocal +from app.database.models import User, UserFeature, Feature +from app.database.models import Base + + +router = APIRouter( + prefix="/features", + tags=["event"], + responses={404: {"description": "Not found"}}, +) + + +@router.get('/') +def panel_index(request: Request, session: Depends(get_db)): + user = session.query(User).filter_by(id=1).first() + feature = create_feature(db=session, name='google', route='/profile') + + +def create_feature(db, name, route, user, is_enable): + """Creates an event and an association.""" + + feature = create_model( + db, Feature, + name=name, + route=route, + owner=user + ) + create_model( + db, UserFeature, + user_id=user.id, + feature_id=feature.id, + is_enable=is_enable + ) + return feature + + +def save(item, session: SessionLocal) -> bool: + """Commits an instance to the db. + source: app.database.database.Base""" + + if issubclass(item.__class__, Base): + session.add(item) + session.commit() + return True + return False + + +def create_model(session: SessionLocal, model_class, **kw): + """Creates and saves a db model.""" + + instance = model_class(**kw) + save(instance, session) + return instance From 013c6d5f0963f207ff2ff46f6e730a3c86f157ff Mon Sep 17 00:00:00 2001 From: Liran C Date: Tue, 2 Feb 2021 12:09:17 +0200 Subject: [PATCH 02/34] figure out delete. --- app/config.py.example | 60 ------------------------------------ app/database/models.py | 9 ++---- app/main.py | 3 +- app/routers/feature_panel.py | 14 +++++++-- 4 files changed, 16 insertions(+), 70 deletions(-) delete mode 100644 app/config.py.example diff --git a/app/config.py.example b/app/config.py.example deleted file mode 100644 index 50b524b0..00000000 --- a/app/config.py.example +++ /dev/null @@ -1,60 +0,0 @@ -import os - -from fastapi_mail import ConnectionConfig -from pydantic import BaseSettings - - -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) - -# API-KEYS -WEATHER_API_KEY = os.getenv('WEATHER_API_KEY') - -# 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, -) - -# 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}") diff --git a/app/database/models.py b/app/database/models.py index 925f36eb..4fd3d0dd 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -27,7 +27,7 @@ class UserFeature(Base): 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')) + user_id = Column('user_id', Integer, ForeignKey('users.id')) # features = relationship("Feature", back_populates="users") # users = relationship("User", back_populates="features") @@ -50,8 +50,7 @@ class User(Base): events = relationship("UserEvent", back_populates="participants") - features = relationship("Feature", secondary=UserFeature.__tablename__, - backref="users") + features = relationship("Feature", secondary=UserFeature.__tablename__) def __repr__(self): return f'' @@ -63,10 +62,8 @@ class Feature(Base): id = Column(Integer, primary_key=True, index=True) name = Column(String, nullable=False) route = Column(String, nullable=False) - - users = relationship("User", secondary=UserFeature.__tablename__, - backref="features") + users = relationship("User", secondary=UserFeature.__tablename__) class Event(Base): diff --git a/app/main.py b/app/main.py index 4bface4f..8e644be5 100644 --- a/app/main.py +++ b/app/main.py @@ -10,7 +10,7 @@ from app.internal.quotes import load_quotes, daily_quotes from app.routers import ( agenda, dayview, email, event, invitation, profile, search, telegram, - whatsapp + whatsapp, feature_panel ) from app.telegram.bot import telegram_bot @@ -43,6 +43,7 @@ def create_tables(engine, psql_environment): app.include_router(invitation.router) app.include_router(whatsapp.router) app.include_router(search.router) +app.include_router(feature_panel.router) telegram_bot.set_webhook() diff --git a/app/routers/feature_panel.py b/app/routers/feature_panel.py index 5387aaac..a156118c 100644 --- a/app/routers/feature_panel.py +++ b/app/routers/feature_panel.py @@ -1,3 +1,4 @@ +from os import name from fastapi import APIRouter, Request, Depends from app.database.database import get_db, SessionLocal from app.database.models import User, UserFeature, Feature @@ -12,9 +13,17 @@ @router.get('/') -def panel_index(request: Request, session: Depends(get_db)): +def panel_index(request: Request, session: SessionLocal = Depends(get_db)): user = session.query(User).filter_by(id=1).first() - feature = create_feature(db=session, name='google', route='/profile') + feature = create_feature(db=session, name='google1', route='/profile1', + user=user, is_enable=False) + session.commit() + session.query(UserFeature).filter_by(feature_id=8).delete() + session.commit() + return user.features + obj = session.query(Feature).all() + + return {"hello": "world", "feature": obj} def create_feature(db, name, route, user, is_enable): @@ -24,7 +33,6 @@ def create_feature(db, name, route, user, is_enable): db, Feature, name=name, route=route, - owner=user ) create_model( db, UserFeature, From cb7947c38f1aa51cdd334f0e3ed9d041a6af5ae1 Mon Sep 17 00:00:00 2001 From: Liran C Date: Wed, 3 Feb 2021 20:40:27 +0200 Subject: [PATCH 03/34] first structure of route with middlewere --- app/database/models.py | 3 +- app/main.py | 11 +++++++ app/routers/feature_panel.py | 59 ++++++++++++++++++------------------ 3 files changed, 41 insertions(+), 32 deletions(-) diff --git a/app/database/models.py b/app/database/models.py index 3ea28949..e558e091 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -30,8 +30,6 @@ class UserFeature(Base): feature_id = Column('feature_id', Integer, ForeignKey('features.id')) user_id = Column('user_id', Integer, ForeignKey('users.id')) - # features = relationship("Feature", back_populates="users") - # users = relationship("User", back_populates="features") is_enable = Column(Boolean) @@ -64,6 +62,7 @@ class Feature(Base): id = Column(Integer, primary_key=True, index=True) name = Column(String, nullable=False) route = Column(String, nullable=False) + creator = Column(String, nullable=True) users = relationship("User", secondary=UserFeature.__tablename__) diff --git a/app/main.py b/app/main.py index e92b1b83..8bc4b2c9 100644 --- a/app/main.py +++ b/app/main.py @@ -47,9 +47,20 @@ def create_tables(engine, psql_environment): telegram_bot.set_webhook() +import time # TODO: I add the quote day to the home page # until the relavent calendar view will be developed. +@app.middleware("http") +async def add_process_time_header(request: Request, call_next): + start_time = time.time() + feature_panel.is_feature_enabled(request.url) + response = await call_next(request) + process_time = time.time() - start_time + response.headers["X-Process-Time"] = str(process_time) + return response + + @app.get("/") @logger.catch() async def home(request: Request, db: Session = Depends(get_db)): diff --git a/app/routers/feature_panel.py b/app/routers/feature_panel.py index a156118c..3e8f9c5d 100644 --- a/app/routers/feature_panel.py +++ b/app/routers/feature_panel.py @@ -1,8 +1,7 @@ -from os import name from fastapi import APIRouter, Request, Depends from app.database.database import get_db, SessionLocal from app.database.models import User, UserFeature, Feature -from app.database.models import Base +from app.internal.utils import create_model router = APIRouter( @@ -15,19 +14,38 @@ @router.get('/') def panel_index(request: Request, session: SessionLocal = Depends(get_db)): user = session.query(User).filter_by(id=1).first() - feature = create_feature(db=session, name='google1', route='/profile1', - user=user, is_enable=False) - session.commit() - session.query(UserFeature).filter_by(feature_id=8).delete() + feature = create_feature(db=session, name='google2', route='/profile2', + user=user, is_enable=True) + + return {'all': user.features, "feature": feature} + # return {"hello": "world", "req": request} + + +def delete_feature(feature: Feature, session: SessionLocal = Depends(get_db)): + session.query(UserFeature).filter_by(feature_id=feature.id).delete() + session.query(Feature).filter_by(id=feature.id).delete() session.commit() - return user.features - obj = session.query(Feature).all() - return {"hello": "world", "feature": obj} +def cleanup_existing_features(): + pass + + +def is_feature_enabled(route: str, user: User, + session: SessionLocal = Depends(get_db)): + + feature = session.query(Feature).filter_by(route=route).first() + user_pref = session.query(UserFeature).filter_by( + feature_id=feature.id, user_id=user.id).first() + + if user_pref.is_enabled: + return True + return False -def create_feature(db, name, route, user, is_enable): - """Creates an event and an association.""" + +def create_feature(db: SessionLocal, name: str, route: str, + user: User, is_enable: bool): + """Creates a feature and an association.""" feature = create_model( db, Feature, @@ -41,22 +59,3 @@ def create_feature(db, name, route, user, is_enable): is_enable=is_enable ) return feature - - -def save(item, session: SessionLocal) -> bool: - """Commits an instance to the db. - source: app.database.database.Base""" - - if issubclass(item.__class__, Base): - session.add(item) - session.commit() - return True - return False - - -def create_model(session: SessionLocal, model_class, **kw): - """Creates and saves a db model.""" - - instance = model_class(**kw) - save(instance, session) - return instance From 572036e3888a1fec894a74184ee56cef553d54f1 Mon Sep 17 00:00:00 2001 From: Liran C Date: Sun, 7 Feb 2021 09:30:46 +0200 Subject: [PATCH 04/34] try middleware --- app/main.py | 23 ++++++++++++++-------- app/routers/feature_panel.py | 37 ++++++++++++++++++++++++++++-------- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/app/main.py b/app/main.py index b85d72b4..1247f243 100644 --- a/app/main.py +++ b/app/main.py @@ -56,15 +56,20 @@ def create_tables(engine, psql_environment): @app.middleware("http") -async def add_process_time_header(request: Request, call_next): - print(request.headers) - print(request.headers.values()) - if request.headers["user-agent"] == "testclient": - response = await call_next(request) - return response +async def add_process_time_header(request: Request, call_next, + ): - response = await call_next(request) - return response + route = str(request.url) + route = route.replace(str(request.base_url), '') + # resp = await call_next(request) + # print(resp['type']) + # resp['type'] = "http.response.start" + # user = session.query(models.User).filter_by(id=1).first() + + # if feature_panel.is_feature_enabled(route=route, user_id=1): + # response = await call_next(request) + # return response + return await call_next(request) # TODO: I add the quote day to the home page @@ -73,6 +78,8 @@ async def add_process_time_header(request: Request, call_next): @logger.catch() async def home(request: Request, db: Session = Depends(get_db)): quote = daily_quotes.quote_per_day(db) + print(app) + return templates.TemplateResponse("home.html", { "request": request, "message": "Hello, World!", diff --git a/app/routers/feature_panel.py b/app/routers/feature_panel.py index 3e8f9c5d..e9f5de8c 100644 --- a/app/routers/feature_panel.py +++ b/app/routers/feature_panel.py @@ -12,13 +12,18 @@ @router.get('/') -def panel_index(request: Request, session: SessionLocal = Depends(get_db)): +def get_feature_panel(request: Request, session: SessionLocal = Depends(get_db)): + pass + + +@router.get('/create-test') +def test_insert(request: Request, session: SessionLocal = Depends(get_db)): + delete_all_feature(session) user = session.query(User).filter_by(id=1).first() - feature = create_feature(db=session, name='google2', route='/profile2', - user=user, is_enable=True) + feature = create_feature(db=session, name='profile', route='/profile', + user=user, is_enable=False) return {'all': user.features, "feature": feature} - # return {"hello": "world", "req": request} def delete_feature(feature: Feature, session: SessionLocal = Depends(get_db)): @@ -27,16 +32,32 @@ def delete_feature(feature: Feature, session: SessionLocal = Depends(get_db)): session.commit() +def delete_all_feature(session: SessionLocal = Depends(get_db)): + session.query(UserFeature).all().delete() + session.query(Feature).all().delete() + session.commit() + + def cleanup_existing_features(): pass -def is_feature_enabled(route: str, user: User, - session: SessionLocal = Depends(get_db)): +def is_feature_enabled(route: str, user_id: int): + # session: SessionLocal = Depends(get_db) + session = SessionLocal() + + feature = session.query(Feature).filter_by(route=f'/{route}').first() + + if feature: + session.close() + return False - feature = session.query(Feature).filter_by(route=route).first() user_pref = session.query(UserFeature).filter_by( - feature_id=feature.id, user_id=user.id).first() + feature_id=feature.id, user_id=user_id).first() + + session.close() + if user_pref is None: + return False if user_pref.is_enabled: return True From 397cee3f9d670dd49459d594a336ab28e67f3d9e Mon Sep 17 00:00:00 2001 From: Liran C Date: Mon, 8 Feb 2021 09:31:45 +0200 Subject: [PATCH 05/34] add middleware filtering requests --- app/database/models.py | 1 - app/main.py | 57 ++++++++++++++++++++++++++---------- app/routers/feature_panel.py | 45 +++++++++++++++------------- 3 files changed, 65 insertions(+), 38 deletions(-) diff --git a/app/database/models.py b/app/database/models.py index 9ca772b7..82abc5d2 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -35,7 +35,6 @@ 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) diff --git a/app/main.py b/app/main.py index 1247f243..e572bb0a 100644 --- a/app/main.py +++ b/app/main.py @@ -1,10 +1,11 @@ -from fastapi import Depends, FastAPI, Request +from fastapi import Depends, FastAPI, Request, HTTPException from fastapi.staticfiles import StaticFiles from sqlalchemy.orm import Session +from starlette.responses import RedirectResponse from app.config import PSQL_ENVIRONMENT from app.database import models -from app.database.database import engine, get_db +from app.database.database import engine, get_db, SessionLocal from app.dependencies import (logger, MEDIA_PATH, STATIC_PATH, templates) from app.internal import daily_quotes, json_data_loader from app.routers import ( @@ -56,20 +57,44 @@ def create_tables(engine, psql_environment): @app.middleware("http") -async def add_process_time_header(request: Request, call_next, - ): - - route = str(request.url) - route = route.replace(str(request.base_url), '') - # resp = await call_next(request) - # print(resp['type']) - # resp['type'] = "http.response.start" - # user = session.query(models.User).filter_by(id=1).first() - - # if feature_panel.is_feature_enabled(route=route, user_id=1): - # response = await call_next(request) - # return response - return await call_next(request) +async def add_process_time_header(request: Request, call_next): + session = SessionLocal() + + # getting the url route path for matching with the database + route = str(request.url).replace(str(request.base_url), '')[:-1] + + # TODO - replace to active user when login will be added to the project + user = session.query(models.User).filter_by(id=1).first() + if user is None: + # if there is no user connected + raise HTTPException(status_code=401, detail="Not authenticated") + + try: + is_enabled = feature_panel.is_feature_enabled( + route=route, + user=user, + session=session + ) + session.close() + except AttributeError as e: + ''' + in case there is no feature exist in + the database that match the route pass the request. + ''' + logger.error(e) + logger.warning('Not a feature - Access is allowed.') + return await call_next(request) + + if is_enabled: + # in case the feature is enabled + return await call_next(request) + + if 'referer' not in request.headers: + # in case request come from straight from url line on browser + return RedirectResponse(url=app.url_path_for('home')) + + # in case the feature is disabled + return RedirectResponse(url=request.headers['referer']) # TODO: I add the quote day to the home page diff --git a/app/routers/feature_panel.py b/app/routers/feature_panel.py index e9f5de8c..26214642 100644 --- a/app/routers/feature_panel.py +++ b/app/routers/feature_panel.py @@ -12,18 +12,29 @@ @router.get('/') -def get_feature_panel(request: Request, session: SessionLocal = Depends(get_db)): - pass +def get_feature_panel(request: Request, + session: SessionLocal = Depends(get_db)): + features = session.query(Feature).all() + return features @router.get('/create-test') def test_insert(request: Request, session: SessionLocal = Depends(get_db)): + delete_all_feature(session) user = session.query(User).filter_by(id=1).first() - feature = create_feature(db=session, name='profile', route='/profile', - user=user, is_enable=False) + create_feature( + db=session, name='profile', + route='/profile', + user=user, is_enable=True, + ) + create_feature( + db=session, name='feature-panel', + route='/features', + user=user, is_enable=True + ) - return {'all': user.features, "feature": feature} + return {'all': user.features} def delete_feature(feature: Feature, session: SessionLocal = Depends(get_db)): @@ -33,8 +44,8 @@ def delete_feature(feature: Feature, session: SessionLocal = Depends(get_db)): def delete_all_feature(session: SessionLocal = Depends(get_db)): - session.query(UserFeature).all().delete() - session.query(Feature).all().delete() + session.query(UserFeature).delete() + session.query(Feature).delete() session.commit() @@ -42,24 +53,16 @@ def cleanup_existing_features(): pass -def is_feature_enabled(route: str, user_id: int): - # session: SessionLocal = Depends(get_db) - session = SessionLocal() - +def is_feature_enabled(route: str, user: int, session: SessionLocal): feature = session.query(Feature).filter_by(route=f'/{route}').first() - - if feature: - session.close() - return False - user_pref = session.query(UserFeature).filter_by( - feature_id=feature.id, user_id=user_id).first() + feature_id=feature.id, + user_id=user.id + ).first() - session.close() - if user_pref is None: + if feature is None: return False - - if user_pref.is_enabled: + elif user_pref is not None and user_pref.is_enable: return True return False From 1da1dda752c7a077fc2371974bebaf81e4554253 Mon Sep 17 00:00:00 2001 From: Liran C Date: Tue, 9 Feb 2021 14:09:30 +0200 Subject: [PATCH 06/34] add: get_user_disabled_features and get_user_enabled_features --- app/database/models.py | 1 + app/main.py | 11 +++++--- app/routers/feature_panel.py | 49 ++++++++++++++++++++++++++++++------ 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/app/database/models.py b/app/database/models.py index 82abc5d2..28936ab8 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -67,6 +67,7 @@ class Feature(Base): name = Column(String, nullable=False) route = Column(String, nullable=False) creator = Column(String, nullable=True) + description = Column(String, nullable=False) users = relationship("User", secondary=UserFeature.__tablename__) diff --git a/app/main.py b/app/main.py index e572bb0a..b1fc518b 100644 --- a/app/main.py +++ b/app/main.py @@ -57,15 +57,20 @@ def create_tables(engine, psql_environment): @app.middleware("http") -async def add_process_time_header(request: Request, call_next): +async def filter_access_to_features(request: Request, call_next): session = SessionLocal() + allawed_routes = { + 'home': '', + 'profile': 'profile' # just for now + } + # getting the url route path for matching with the database route = str(request.url).replace(str(request.base_url), '')[:-1] # TODO - replace to active user when login will be added to the project user = session.query(models.User).filter_by(id=1).first() - if user is None: + if user is None and route not in allawed_routes.values(): # if there is no user connected raise HTTPException(status_code=401, detail="Not authenticated") @@ -79,7 +84,7 @@ async def add_process_time_header(request: Request, call_next): except AttributeError as e: ''' in case there is no feature exist in - the database that match the route pass the request. + the database that match the route that passed to the request. ''' logger.error(e) logger.warning('Not a feature - Access is allowed.') diff --git a/app/routers/feature_panel.py b/app/routers/feature_panel.py index 26214642..08c5151c 100644 --- a/app/routers/feature_panel.py +++ b/app/routers/feature_panel.py @@ -1,4 +1,5 @@ from fastapi import APIRouter, Request, Depends + from app.database.database import get_db, SessionLocal from app.database.models import User, UserFeature, Feature from app.internal.utils import create_model @@ -21,17 +22,20 @@ def get_feature_panel(request: Request, @router.get('/create-test') def test_insert(request: Request, session: SessionLocal = Depends(get_db)): - delete_all_feature(session) user = session.query(User).filter_by(id=1).first() create_feature( db=session, name='profile', route='/profile', user=user, is_enable=True, + description='description', + creator='creator' ) create_feature( db=session, name='feature-panel', route='/features', - user=user, is_enable=True + user=user, is_enable=False, + description='description', + creator='liran caduri' ) return {'all': user.features} @@ -43,16 +47,42 @@ def delete_feature(feature: Feature, session: SessionLocal = Depends(get_db)): session.commit() -def delete_all_feature(session: SessionLocal = Depends(get_db)): - session.query(UserFeature).delete() - session.query(Feature).delete() - session.commit() +def is_feature_exist_in_db(): + pass -def cleanup_existing_features(): +def update_feature(feature, new_feature_obj): pass +@router.get('/active') +def get_user_enabled_features(user_id: int = 1, + session: SessionLocal = Depends(get_db)): + 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( + id=pref.feature_id).first() + data.append({'feature': feature, 'is_enabled': pref.is_enable}) + + return data + + +@router.get('/deactive') +def get_user_disabled_features(user_id: int = 1, + session: SessionLocal = Depends(get_db)): + 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( + id=pref.feature_id).first() + data.append({'feature': feature, 'is_enabled': pref.is_enable}) + + return data + + def is_feature_enabled(route: str, user: int, session: SessionLocal): feature = session.query(Feature).filter_by(route=f'/{route}').first() user_pref = session.query(UserFeature).filter_by( @@ -68,13 +98,16 @@ def is_feature_enabled(route: str, user: int, session: SessionLocal): def create_feature(db: SessionLocal, name: str, route: str, - user: User, is_enable: bool): + user: User, is_enable: bool, + description: str, creator: str = None): """Creates a feature and an association.""" feature = create_model( db, Feature, name=name, route=route, + creator=creator, + description=description ) create_model( db, UserFeature, From d1606051d5f15447dd60dd94a89315aca0ac749b Mon Sep 17 00:00:00 2001 From: Liran C Date: Wed, 10 Feb 2021 09:40:43 +0200 Subject: [PATCH 07/34] modify middleware --- app/main.py | 24 ++--------- app/routers/feature_panel.py | 84 +++++++++++++++++++++++++----------- 2 files changed, 62 insertions(+), 46 deletions(-) diff --git a/app/main.py b/app/main.py index b1fc518b..cd6a55ee 100644 --- a/app/main.py +++ b/app/main.py @@ -1,11 +1,11 @@ -from fastapi import Depends, FastAPI, Request, HTTPException +from fastapi import Depends, FastAPI, Request from fastapi.staticfiles import StaticFiles from sqlalchemy.orm import Session from starlette.responses import RedirectResponse from app.config import PSQL_ENVIRONMENT from app.database import models -from app.database.database import engine, get_db, SessionLocal +from app.database.database import engine, get_db from app.dependencies import (logger, MEDIA_PATH, STATIC_PATH, templates) from app.internal import daily_quotes, json_data_loader from app.routers import ( @@ -58,29 +58,13 @@ def create_tables(engine, psql_environment): @app.middleware("http") async def filter_access_to_features(request: Request, call_next): - session = SessionLocal() - - allawed_routes = { - 'home': '', - 'profile': 'profile' # just for now - } # getting the url route path for matching with the database route = str(request.url).replace(str(request.base_url), '')[:-1] - # TODO - replace to active user when login will be added to the project - user = session.query(models.User).filter_by(id=1).first() - if user is None and route not in allawed_routes.values(): - # if there is no user connected - raise HTTPException(status_code=401, detail="Not authenticated") - try: - is_enabled = feature_panel.is_feature_enabled( - route=route, - user=user, - session=session - ) - session.close() + is_enabled = feature_panel.is_feature_enabled(route=route) + # session.close() except AttributeError as e: ''' in case there is no feature exist in diff --git a/app/routers/feature_panel.py b/app/routers/feature_panel.py index 08c5151c..c0a1c1e6 100644 --- a/app/routers/feature_panel.py +++ b/app/routers/feature_panel.py @@ -13,32 +13,45 @@ @router.get('/') -def get_feature_panel(request: Request, - session: SessionLocal = Depends(get_db)): +def get_feature_panel( + request: Request, session: SessionLocal = Depends(get_db)): + features = session.query(Feature).all() return features -@router.get('/create-test') +@router.get('/create-test-features') def test_insert(request: Request, session: SessionLocal = Depends(get_db)): - user = session.query(User).filter_by(id=1).first() create_feature( - db=session, name='profile', - route='/profile', - user=user, is_enable=True, - description='description', - creator='creator' - ) + db=session, name='profile', + route='/profile', description='description', + creator='creator' + ) create_feature( - db=session, name='feature-panel', - route='/features', - user=user, is_enable=False, - description='description', - creator='liran caduri' - ) + db=session, name='feature-panel', + route='/features', description='description', + creator='liran caduri' + ) + create_feature( + db=session, name='invitations', + route='/invitations', description='description', + creator='creator2' + ) + + features = session.query(Feature).all() + + return {'all': features} + + +@router.get('/test-association') +def testAssociation(request: Request, session: SessionLocal = Depends(get_db)): + create_association(db=session, feature_id=1, user_id=1, is_enable=True) + create_association(db=session, feature_id=3, user_id=1, is_enable=False) - return {'all': user.features} + associations = session.query(UserFeature).all() + + return {'all': associations} def delete_feature(feature: Feature, session: SessionLocal = Depends(get_db)): @@ -83,24 +96,35 @@ def get_user_disabled_features(user_id: int = 1, return data -def is_feature_enabled(route: str, user: int, session: SessionLocal): +@router.get('/deactive') +def get_user_unlinked_features(user_id: int = 1, + session: SessionLocal = Depends(get_db)): + data = [] + return data + + +def is_feature_enabled(route: str): + session = SessionLocal() + # TODO - get active user id + user = session.query(User).filter_by(id=1).first() + feature = session.query(Feature).filter_by(route=f'/{route}').first() + print(route) user_pref = session.query(UserFeature).filter_by( feature_id=feature.id, user_id=user.id ).first() - + print(user_pref) if feature is None: return False - elif user_pref is not None and user_pref.is_enable: + elif user_pref is not None and user_pref.is_enable or user_pref is None: return True return False def create_feature(db: SessionLocal, name: str, route: str, - user: User, is_enable: bool, description: str, creator: str = None): - """Creates a feature and an association.""" + """Creates a feature.""" feature = create_model( db, Feature, @@ -109,10 +133,18 @@ def create_feature(db: SessionLocal, name: str, route: str, creator=creator, description=description ) - create_model( + return feature + + +def create_association( + db: SessionLocal, feature_id: int, user_id: int, is_enable: bool): + """Creates an association.""" + + association = create_model( db, UserFeature, - user_id=user.id, - feature_id=feature.id, + user_id=user_id, + feature_id=feature_id, is_enable=is_enable ) - return feature + + return association From b51cd21f9912d8375a6864c8066b150d6a3a916a Mon Sep 17 00:00:00 2001 From: Liran C Date: Wed, 10 Feb 2021 19:54:11 +0200 Subject: [PATCH 08/34] replace testclient, add on startup event, feature folder, is_feature_exist_in_db function done, and more .. --- app/features/__init__.py | 0 app/features/features.py | 20 +++++++++++ app/main.py | 13 ++++--- app/routers/feature_panel.py | 68 ++++++++++++++++++++---------------- tests/client_fixture.py | 7 +++- tests/test_whatsapp.py | 7 ++-- 6 files changed, 76 insertions(+), 39 deletions(-) create mode 100644 app/features/__init__.py create mode 100644 app/features/features.py diff --git a/app/features/__init__.py b/app/features/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/features/features.py b/app/features/features.py new file mode 100644 index 00000000..f7050399 --- /dev/null +++ b/app/features/features.py @@ -0,0 +1,20 @@ +features = [ + { + "name": 'profile', + "route": '/profile', + "description": 'description', + "creator": 'creator' + }, + { + "name": 'feature-panel', + "route": '/features', + "description": 'description', + "creator": 'liran caduri' + }, + { + "name": 'invitations', + "route": '/invitations', + "description": 'description', + "creator": 'creator2' + } +] diff --git a/app/main.py b/app/main.py index cd6a55ee..72adb581 100644 --- a/app/main.py +++ b/app/main.py @@ -5,7 +5,7 @@ from app.config import PSQL_ENVIRONMENT from app.database import models -from app.database.database import engine, get_db +from app.database.database import engine, get_db, SessionLocal from app.dependencies import (logger, MEDIA_PATH, STATIC_PATH, templates) from app.internal import daily_quotes, json_data_loader from app.routers import ( @@ -70,8 +70,7 @@ async def filter_access_to_features(request: Request, call_next): in case there is no feature exist in the database that match the route that passed to the request. ''' - logger.error(e) - logger.warning('Not a feature - Access is allowed.') + logger.error('Not a feature - Access is allowed. Error: ' + str(e)) return await call_next(request) if is_enabled: @@ -86,13 +85,19 @@ async def filter_access_to_features(request: Request, call_next): return RedirectResponse(url=request.headers['referer']) +@app.on_event("startup") +async def startup_event(): + session = SessionLocal() + feature_panel.create_features_at_startup(session=session) + session.close() + + # TODO: I add the quote day to the home page # until the relavent calendar view will be developed. @app.get("/") @logger.catch() async def home(request: Request, db: Session = Depends(get_db)): quote = daily_quotes.quote_per_day(db) - print(app) return templates.TemplateResponse("home.html", { "request": request, diff --git a/app/routers/feature_panel.py b/app/routers/feature_panel.py index c0a1c1e6..710b5752 100644 --- a/app/routers/feature_panel.py +++ b/app/routers/feature_panel.py @@ -1,8 +1,10 @@ from fastapi import APIRouter, Request, Depends +from sqlalchemy import or_ from app.database.database import get_db, SessionLocal from app.database.models import User, UserFeature, Feature from app.internal.utils import create_model +from app.features.features import features router = APIRouter( @@ -13,35 +15,20 @@ @router.get('/') -def get_feature_panel( - request: Request, session: SessionLocal = Depends(get_db)): - +def index(request: Request, session: SessionLocal = Depends(get_db)): features = session.query(Feature).all() return features -@router.get('/create-test-features') -def test_insert(request: Request, session: SessionLocal = Depends(get_db)): +def create_features_at_startup(session: SessionLocal): - create_feature( - db=session, name='profile', - route='/profile', description='description', - creator='creator' - ) - create_feature( - db=session, name='feature-panel', - route='/features', description='description', - creator='liran caduri' - ) - create_feature( - db=session, name='invitations', - route='/invitations', description='description', - creator='creator2' - ) + for feat in features: + if not is_feature_exist_in_db(feature=feat, session=session): + create_feature(**feat, db=session) - features = session.query(Feature).all() + fs = session.query(Feature).all() - return {'all': features} + return {'all': fs} @router.get('/test-association') @@ -60,12 +47,30 @@ def delete_feature(feature: Feature, session: SessionLocal = Depends(get_db)): session.commit() -def is_feature_exist_in_db(): - pass +def is_feature_exist_in_db(feature: dict, session: SessionLocal): + db_feature = session.query(Feature).filter( + or_(Feature.name == feature['name'], + Feature.route == feature['route'])).first() + + if db_feature is not None: + # Update if found + db_feature.name = feature['name'] + db_feature.route = feature['route'] + db_feature.description = feature['description'] + db_feature.creator = feature['creator'] + session.commit() + return True + return False -def update_feature(feature, new_feature_obj): - pass +def update_feature(feature: Feature, new_feature_obj: dict, + session: SessionLocal = Depends(get_db)): + # need a run + feature.name = new_feature_obj['name'] + feature.route = new_feature_obj['route'] + feature.description = new_feature_obj['description'] + feature.creator = new_feature_obj['creator'] + session.commit() @router.get('/active') @@ -96,7 +101,7 @@ def get_user_disabled_features(user_id: int = 1, return data -@router.get('/deactive') +@router.get('/unlinked') def get_user_unlinked_features(user_id: int = 1, session: SessionLocal = Depends(get_db)): data = [] @@ -109,12 +114,10 @@ def is_feature_enabled(route: str): user = session.query(User).filter_by(id=1).first() feature = session.query(Feature).filter_by(route=f'/{route}').first() - print(route) user_pref = session.query(UserFeature).filter_by( feature_id=feature.id, user_id=user.id ).first() - print(user_pref) if feature is None: return False elif user_pref is not None and user_pref.is_enable or user_pref is None: @@ -122,10 +125,13 @@ def is_feature_enabled(route: str): return False -def create_feature(db: SessionLocal, name: str, route: str, - description: str, creator: str = None): +def create_feature(name: str, route: str, + description: str, creator: str = None, + db: SessionLocal = Depends()): """Creates a feature.""" + db = SessionLocal() + feature = create_model( db, Feature, name=name, diff --git a/tests/client_fixture.py b/tests/client_fixture.py index c4890b05..7adda2d0 100644 --- a/tests/client_fixture.py +++ b/tests/client_fixture.py @@ -1,4 +1,4 @@ -from fastapi.testclient import TestClient +from async_asgi_testclient.testing import TestClient import pytest from app import main @@ -9,6 +9,11 @@ from tests.conftest import get_test_db, test_engine +# class TestClientMod(TestClient): +# def ok(self): +# return 200 >= self.status_code < 300 + + @pytest.fixture(scope="session") def client(): return TestClient(app) diff --git a/tests/test_whatsapp.py b/tests/test_whatsapp.py index a49c53e3..469a0552 100644 --- a/tests/test_whatsapp.py +++ b/tests/test_whatsapp.py @@ -1,5 +1,5 @@ from app.routers import whatsapp - +import pytest def test_whatsapp_send(): # Redirects you directly to the specified contact and the message will @@ -37,7 +37,8 @@ def test_no_number(): "number%3F"} -def test_end_to_end_testing(client): - resp = client.get('/whatsapp?phone_number=972536106106&message=testing') +@pytest.mark.asyncio +async def test_end_to_end_testing(client): + resp = await client.get('/whatsapp?phone_number=972536106106&message=testing') assert resp.ok assert resp.json From e4a817a473d1c00daf5c7f382b8d1ed2325f6646 Mon Sep 17 00:00:00 2001 From: Liran C Date: Thu, 11 Feb 2021 16:27:29 +0200 Subject: [PATCH 09/34] improved is_feature_enabled and middleware code. --- app/main.py | 16 ++----- app/routers/feature_panel.py | 87 +++++++++++++++++++++++++++++------- 2 files changed, 75 insertions(+), 28 deletions(-) diff --git a/app/main.py b/app/main.py index 53c61d71..2720447a 100644 --- a/app/main.py +++ b/app/main.py @@ -69,23 +69,15 @@ 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), '')[:-1] - try: - is_enabled = feature_panel.is_feature_enabled(route=route) - # session.close() - except AttributeError as e: - ''' - in case there is no feature exist in - the database that match the route that passed to the request. - ''' - logger.error('Not a feature - Access is allowed. Error: ' + str(e)) - return await call_next(request) + # get if feature is enabled + is_enabled = feature_panel.is_feature_enabled(route=route) if is_enabled: # in case the feature is enabled return await call_next(request) - if 'referer' not in request.headers: - # in case request come from straight from url line on browser + elif 'referer' not in request.headers: + # in case request come straight from url line in browser return RedirectResponse(url=app.url_path_for('home')) # in case the feature is disabled diff --git a/app/routers/feature_panel.py b/app/routers/feature_panel.py index 93abe28d..6f2fa0b0 100644 --- a/app/routers/feature_panel.py +++ b/app/routers/feature_panel.py @@ -1,5 +1,6 @@ from fastapi import APIRouter, Request, Depends from sqlalchemy import or_ +from sqlalchemy.sql.expression import false from app.dependencies import get_db, SessionLocal from app.database.models import User, UserFeature, Feature @@ -54,28 +55,31 @@ def is_feature_exist_in_db(feature: dict, session: SessionLocal): if db_feature is not None: # Update if found - db_feature.name = feature['name'] - db_feature.route = feature['route'] - db_feature.description = feature['description'] - db_feature.creator = feature['creator'] - session.commit() + update_feature(feature=db_feature, + new_feature_obj=feature, + session=session) return True return False def update_feature(feature: Feature, new_feature_obj: dict, session: SessionLocal = Depends(get_db)): - # need a run + feature.name = new_feature_obj['name'] feature.route = new_feature_obj['route'] feature.description = new_feature_obj['description'] feature.creator = new_feature_obj['creator'] session.commit() + return feature + @router.get('/active') -def get_user_enabled_features(user_id: int = 1, - session: SessionLocal = Depends(get_db)): +def get_user_enabled_features(session: SessionLocal = Depends(get_db)): + + # TODO - get active user id + user_id = 1 + data = [] user_prefs = session.query(UserFeature).filter_by(user_id=user_id).all() for pref in user_prefs: @@ -88,8 +92,11 @@ def get_user_enabled_features(user_id: int = 1, @router.get('/deactive') -def get_user_disabled_features(user_id: int = 1, - session: SessionLocal = Depends(get_db)): +def get_user_disabled_features(session: SessionLocal = Depends(get_db)): + + # TODO - get active user id + user_id = 1 + data = [] user_prefs = session.query(UserFeature).filter_by(user_id=user_id).all() for pref in user_prefs: @@ -105,23 +112,71 @@ def get_user_disabled_features(user_id: int = 1, def get_user_unlinked_features(user_id: int = 1, session: SessionLocal = Depends(get_db)): data = [] + all_features = session.query(Feature).all() + + for feat in all_features: + in_disabled = is_feature_exist_in_disabled( + user_id=user_id, feature=feat, session=session + ) + + in_enabled = is_feature_exist_in_enabled( + user_id=user_id, feature=feat, session=session + ) + + if not in_enabled and not in_disabled: + data.append(feat) + return data +def is_feature_exist_in_enabled(user_id: int, 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_exist_in_disabled(user_id: int, feature: Feature, + session: SessionLocal = Depends(get_db)): + disable_features = get_user_disabled_features(session=session) + + for df in disable_features: + if df['feature'].id == feature.id: + return True + + return False + + def is_feature_enabled(route: str): session = SessionLocal() + # TODO - get active user id - user = session.query(User).filter_by(id=1).first() + user_id = 1 feature = session.query(Feature).filter_by(route=f'/{route}').first() - user_pref = session.query(UserFeature).filter_by( - feature_id=feature.id, - user_id=user.id - ).first() + + # *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 route that passed to the request. + return True + + user_pref = session.query(UserFeature).filter_by( + feature_id=feature.id, + user_id=user_id + ).first() + + if user_pref is None: + # in case the feature is unlinked to user return False - elif user_pref is not None and user_pref.is_enable or user_pref is None: + elif user_pref.is_enable: + # in case the feature is enabled return True + # in case the feature is disabled return False From 0514121a02aac6477119e4719da907fa322de59b Mon Sep 17 00:00:00 2001 From: Liran C Date: Thu, 11 Feb 2021 20:07:26 +0200 Subject: [PATCH 10/34] add_feature_to_user function --- app/main.py | 10 ++++----- app/routers/feature_panel.py | 39 +++++++++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/app/main.py b/app/main.py index 2720447a..ea1fd1a2 100644 --- a/app/main.py +++ b/app/main.py @@ -66,21 +66,21 @@ def create_tables(engine, psql_environment): @app.middleware("http") async def filter_access_to_features(request: Request, call_next): - # getting the url route path for matching with the database + # getting the url route path for matching with the database. route = str(request.url).replace(str(request.base_url), '')[:-1] - # get if feature is enabled + # getting access status. is_enabled = feature_panel.is_feature_enabled(route=route) if is_enabled: - # in case the feature 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 url line in browser + # in case request come straight from url line in browser. return RedirectResponse(url=app.url_path_for('home')) - # in case the feature is disabled + # in case the feature is disabled or access isn't allowed. return RedirectResponse(url=request.headers['referer']) diff --git a/app/routers/feature_panel.py b/app/routers/feature_panel.py index 6f2fa0b0..d265889f 100644 --- a/app/routers/feature_panel.py +++ b/app/routers/feature_panel.py @@ -1,6 +1,4 @@ from fastapi import APIRouter, Request, Depends -from sqlalchemy import or_ -from sqlalchemy.sql.expression import false from app.dependencies import get_db, SessionLocal from app.database.models import User, UserFeature, Feature @@ -32,6 +30,29 @@ def create_features_at_startup(session: SessionLocal): return {'all': fs} +@router.post('/add-feature') +async def add_feature_to_user(request: Request, + session: SessionLocal = Depends(get_db)): + + form = await request.form() + + user_id = 1 # TODO - get active user id + feat = session.query(Feature).filter_by(id=form['feature_id']).first() + + if feat is not None: + # 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 + ) + + return session.query(UserFeature).filter_by(id=association.id).first() + + return 'Done - nothing made.' + + @router.get('/test-association') def testAssociation(request: Request, session: SessionLocal = Depends(get_db)): create_association(db=session, feature_id=1, user_id=1, is_enable=True) @@ -50,8 +71,8 @@ def delete_feature(feature: Feature, session: SessionLocal = Depends(get_db)): def is_feature_exist_in_db(feature: dict, session: SessionLocal): db_feature = session.query(Feature).filter( - or_(Feature.name == feature['name'], - Feature.route == feature['route'])).first() + (Feature.name == feature['name']) | + (Feature.route == feature['route'])).first() if db_feature is not None: # Update if found @@ -154,7 +175,7 @@ def is_feature_exist_in_disabled(user_id: int, feature: Feature, def is_feature_enabled(route: str): session = SessionLocal() - # TODO - get active user id + # TODO - get active user id. user_id = 1 feature = session.query(Feature).filter_by(route=f'/{route}').first() @@ -162,7 +183,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 route that passed to the request. + # the database that match the route that gived by to the request. return True user_pref = session.query(UserFeature).filter_by( @@ -171,12 +192,12 @@ def is_feature_enabled(route: str): ).first() if user_pref is None: - # in case the feature is unlinked to user + # in case the feature is unlinked to user. return False elif user_pref.is_enable: - # in case the feature is enabled + # in case the feature is enabled. return True - # in case the feature is disabled + # in case the feature is disabled. return False From c7152998417a0109653593eecbcc15cdfe29ddc4 Mon Sep 17 00:00:00 2001 From: Liran C Date: Sat, 13 Feb 2021 15:04:26 +0200 Subject: [PATCH 11/34] add checking for duplicates in association table --- app/features/features.py | 14 ++++++++++---- app/main.py | 4 ++-- app/routers/feature_panel.py | 26 +++++++++++++++++++------- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/app/features/features.py b/app/features/features.py index f7050399..f9d0df89 100644 --- a/app/features/features.py +++ b/app/features/features.py @@ -1,19 +1,25 @@ features = [ { - "name": 'profile', - "route": '/profile', + "name": 'agenda', + "route": '/agenda', "description": 'description', "creator": 'creator' }, { "name": 'feature-panel', - "route": '/features', + "route": '/features/', "description": 'description', "creator": 'liran caduri' }, { "name": 'invitations', - "route": '/invitations', + "route": '/invitations/', + "description": 'description', + "creator": 'creator2' + }, + { + "name": 'association', + "route": '/features/test-association', "description": 'description', "creator": 'creator2' } diff --git a/app/main.py b/app/main.py index ea1fd1a2..c7c68219 100644 --- a/app/main.py +++ b/app/main.py @@ -67,7 +67,7 @@ def create_tables(engine, psql_environment): 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), '')[:-1] + route = '/' + str(request.url).replace(str(request.base_url), '') # getting access status. is_enabled = feature_panel.is_feature_enabled(route=route) @@ -77,7 +77,7 @@ async def filter_access_to_features(request: Request, call_next): return await call_next(request) elif 'referer' not in request.headers: - # in case request come straight from url line in browser. + # 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. diff --git a/app/routers/feature_panel.py b/app/routers/feature_panel.py index d265889f..43ca1ba5 100644 --- a/app/routers/feature_panel.py +++ b/app/routers/feature_panel.py @@ -36,10 +36,12 @@ async def add_feature_to_user(request: Request, form = await request.form() - user_id = 1 # TODO - get active user id + user_id = form['user_id'] # TODO - get active user id feat = session.query(Feature).filter_by(id=form['feature_id']).first() - if feat is not None: + is_exist = is_association_exist_in_db(form=form, session=session) + + if feat is not None and not is_exist: # in case there is no feature in the database with that same id association = create_association( db=session, @@ -53,8 +55,18 @@ async def add_feature_to_user(request: Request, return 'Done - nothing made.' -@router.get('/test-association') -def testAssociation(request: Request, session: SessionLocal = Depends(get_db)): +def is_association_exist_in_db(form: dict, session: SessionLocal): + db_association = session.query(UserFeature).filter_by( + feature_id=int(form['feature_id']), + user_id=int(form['user_id']) + ).first() + + if db_association is not None: + return True + return False + + +def testAssociation(session: SessionLocal = Depends()): create_association(db=session, feature_id=1, user_id=1, is_enable=True) create_association(db=session, feature_id=3, user_id=1, is_enable=False) @@ -178,12 +190,12 @@ def is_feature_enabled(route: str): # TODO - get active user id. user_id = 1 - feature = session.query(Feature).filter_by(route=f'/{route}').first() + 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 exist in - # the database that match the route that gived by to the request. + # in case there is no feature exist in the database that match the + # route that gived by to the request. return True user_pref = session.query(UserFeature).filter_by( From 6380a8c5bd06e33dd1272b31dac636c87b2dfc47 Mon Sep 17 00:00:00 2001 From: Liran C Date: Sat, 13 Feb 2021 21:33:08 +0200 Subject: [PATCH 12/34] renaming some files, and adding on and off routes --- app/features/{features.py => index.py} | 12 +++ app/main.py | 8 +- app/routers/{feature_panel.py => features.py} | 90 +++++++++++++++++-- 3 files changed, 97 insertions(+), 13 deletions(-) rename app/features/{features.py => index.py} (58%) rename app/routers/{feature_panel.py => features.py} (76%) diff --git a/app/features/features.py b/app/features/index.py similarity index 58% rename from app/features/features.py rename to app/features/index.py index f9d0df89..ac788eab 100644 --- a/app/features/features.py +++ b/app/features/index.py @@ -1,3 +1,15 @@ +''' + 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 table database. + + To update the developer needs to change the name or the route and let + the system load, but not change both at the same time otherwise it will + create junk and unnecessary duplicates. + + Enjoy and good luck :) +''' + features = [ { "name": 'agenda', diff --git a/app/main.py b/app/main.py index c7c68219..44a384e2 100644 --- a/app/main.py +++ b/app/main.py @@ -36,7 +36,7 @@ def create_tables(engine, psql_environment): from app.routers import ( # noqa: E402 agenda, calendar, categories, celebrity, currency, dayview, email, event, invitation, profile, search, telegram, whatsapp, - feature_panel + features ) json_data_loader.load_to_db(next(get_db())) @@ -56,7 +56,7 @@ def create_tables(engine, psql_environment): search.router, telegram.router, whatsapp.router, - feature_panel.router, + features.router, ] for router in routers_to_include: @@ -70,7 +70,7 @@ async def filter_access_to_features(request: Request, call_next): route = '/' + str(request.url).replace(str(request.base_url), '') # getting access status. - is_enabled = feature_panel.is_feature_enabled(route=route) + is_enabled = features.is_feature_enabled(route=route) if is_enabled: # in case the feature is enabled or access is allowed. @@ -87,7 +87,7 @@ async def filter_access_to_features(request: Request, call_next): @app.on_event("startup") async def startup_event(): session = SessionLocal() - feature_panel.create_features_at_startup(session=session) + features.create_features_at_startup(session=session) session.close() diff --git a/app/routers/feature_panel.py b/app/routers/features.py similarity index 76% rename from app/routers/feature_panel.py rename to app/routers/features.py index 43ca1ba5..68fda148 100644 --- a/app/routers/feature_panel.py +++ b/app/routers/features.py @@ -1,9 +1,9 @@ from fastapi import APIRouter, Request, Depends from app.dependencies import get_db, SessionLocal -from app.database.models import User, UserFeature, Feature +from app.database.models import UserFeature, Feature from app.internal.utils import create_model -from app.features.features import features +from app.features.index import features router = APIRouter( @@ -52,13 +52,81 @@ async def add_feature_to_user(request: Request, return session.query(UserFeature).filter_by(id=association.id).first() - return 'Done - nothing made.' + return False + + +@router.post('/remove-feature') +async def delete_user_feature_association( + request: Request, + session: SessionLocal = Depends(get_db) +): + + form = await request.form() + + user_id = form['user_id'] # TODO - get active user id + feature_id = form['feature_id'] + + is_exist = is_association_exist_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() + + return True + + return False + + +@router.post('/on') +async def enable_feature(request: Request, + session: SessionLocal = Depends(get_db)): + + 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() + + db_association.is_enable = True + session.commit() + + return True + return False + + +@router.post('/off') +async def disable_feature(request: Request, + session: SessionLocal = Depends(get_db)): + + form = await request.form() + print(form['user_id'], form['feature_id']) + is_exist = is_association_exist_in_db(form=form, session=session) + print(is_exist) + if is_exist: + db_association = session.query(UserFeature).filter_by( + feature_id=form['feature_id'], + user_id=form['user_id'] + ).first() + print(db_association) + + db_association.is_enable = False + session.commit() + + return True + return False def is_association_exist_in_db(form: dict, session: SessionLocal): db_association = session.query(UserFeature).filter_by( - feature_id=int(form['feature_id']), - user_id=int(form['user_id']) + feature_id=form['feature_id'], + user_id=form['user_id'] ).first() if db_association is not None: @@ -66,12 +134,13 @@ def is_association_exist_in_db(form: dict, session: SessionLocal): return False -def testAssociation(session: SessionLocal = Depends()): +def testAssociation(): + session = SessionLocal() create_association(db=session, feature_id=1, user_id=1, is_enable=True) create_association(db=session, feature_id=3, user_id=1, is_enable=False) associations = session.query(UserFeature).all() - + session.close() return {'all': associations} @@ -142,8 +211,11 @@ def get_user_disabled_features(session: SessionLocal = Depends(get_db)): @router.get('/unlinked') -def get_user_unlinked_features(user_id: int = 1, - session: SessionLocal = Depends(get_db)): +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() From 2ea61cbccd7e2e9e5396b4ba0ebb0b52f592e96e Mon Sep 17 00:00:00 2001 From: Liran C Date: Sat, 13 Feb 2021 21:38:23 +0200 Subject: [PATCH 13/34] .example --- app/config.py.example | 99 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 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..6f64a673 --- /dev/null +++ b/app/config.py.example @@ -0,0 +1,99 @@ +import os + +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') + +# 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, +) + +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}") From b2b839fcf4a84e6f8965f1da862232b407515a5d Mon Sep 17 00:00:00 2001 From: Liran C Date: Sun, 14 Feb 2021 15:37:30 +0200 Subject: [PATCH 14/34] split to internal --- app/config.py.example | 99 -------------------- app/features/index.py | 19 +++- app/internal/features.py | 184 +++++++++++++++++++++++++++++++++++++ app/main.py | 13 ++- app/routers/features.py | 189 +++------------------------------------ 5 files changed, 220 insertions(+), 284 deletions(-) delete mode 100644 app/config.py.example create mode 100644 app/internal/features.py diff --git a/app/config.py.example b/app/config.py.example deleted file mode 100644 index 6f64a673..00000000 --- a/app/config.py.example +++ /dev/null @@ -1,99 +0,0 @@ -import os - -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') - -# 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, -) - -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}") diff --git a/app/features/index.py b/app/features/index.py index eba06c46..f03a73c7 100644 --- a/app/features/index.py +++ b/app/features/index.py @@ -1,15 +1,26 @@ ''' 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 table in the database. + updating the features in the features table in the database. - To update the developer needs to change the name or the route and let - the system load, but not change both at the same time otherwise it will - create junk and unnecessary duplicates. + To update a feature, The developer needs to change the name or the route + and let the system load, but not change both at the same time otherwise + it will create junk and unnecessary duplicates. Enjoy and good luck :) ''' +''' +Example to feature stracture: + +{ + "name": '', + "route": '/', + "description": '', + "creator": '' +} +''' + features = [ { "name": 'agenda', diff --git a/app/internal/features.py b/app/internal/features.py new file mode 100644 index 00000000..c3b28600 --- /dev/null +++ b/app/internal/features.py @@ -0,0 +1,184 @@ +from fastapi import Depends + +from app.features.index import features +from app.database.models import UserFeature, Feature +from app.internal.utils import create_model +from app.dependencies import get_db, SessionLocal + + +def create_features_at_startup(session: SessionLocal): + + for feat in features: + if not is_feature_exist_in_db(feature=feat, session=session): + create_feature(**feat, db=session) + + fs = session.query(Feature).all() + + return {'all': fs} + + +def is_association_exist_in_db(form: dict, session: SessionLocal): + 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 + + +def testAssociation(): + session = SessionLocal() + create_association(db=session, feature_id=1, user_id=1, is_enable=True) + create_association(db=session, feature_id=3, user_id=1, is_enable=False) + + associations = session.query(UserFeature).all() + session.close() + return {'all': associations} + + +def delete_feature(feature: Feature, session: SessionLocal = Depends(get_db)): + 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): + 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 + + +def update_feature(feature: Feature, new_feature_obj: dict, + session: SessionLocal = Depends(get_db)): + + feature.name = new_feature_obj['name'] + feature.route = new_feature_obj['route'] + feature.description = new_feature_obj['description'] + feature.creator = new_feature_obj['creator'] + session.commit() + + return feature + + +def is_feature_exist_in_enabled(user_id: int, 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_exist_in_disabled(user_id: int, feature: Feature, + session: SessionLocal = Depends(get_db)): + disable_features = get_user_disabled_features(session=session) + + for df in disable_features: + if df['feature'].id == feature.id: + return True + + return False + + +def is_feature_enabled(route: str): + session = SessionLocal() + + # TODO - get active user id. + user_id = 1 + + 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 exist in the database that match the + # 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() + + 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 + + +def create_feature(name: str, route: str, + description: str, creator: str = None, + db: SessionLocal = Depends()): + """Creates a feature.""" + + db = SessionLocal() + + feature = 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): + """Creates an association.""" + + association = 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)): + + # TODO - get active user id + user_id = 1 + + 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( + id=pref.feature_id).first() + data.append({'feature': feature, 'is_enabled': pref.is_enable}) + + return data + + +def get_user_disabled_features(session: SessionLocal = Depends(get_db)): + + # TODO - get active user id + user_id = 1 + + 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( + id=pref.feature_id).first() + data.append({'feature': feature, 'is_enabled': pref.is_enable}) + + return data diff --git a/app/main.py b/app/main.py index 8f76bc3c..7e663786 100644 --- a/app/main.py +++ b/app/main.py @@ -7,7 +7,8 @@ from app.database import engine, models from app.dependencies import (get_db, logger, MEDIA_PATH, STATIC_PATH, templates, SessionLocal) -from app.internal import daily_quotes, json_data_loader +from app.internal import ( + daily_quotes, json_data_loader, features as internal_features) from app.internal.languages import set_ui_language from app.routers.salary import routes as salary @@ -75,16 +76,19 @@ async def filter_access_to_features(request: Request, call_next): route = '/' + str(request.url).replace(str(request.base_url), '') # getting access status. - is_enabled = features.is_feature_enabled(route=route) - + is_enabled = internal_features.is_feature_enabled(route=route) + print(is_enabled) if is_enabled: # in case the feature is enabled or access is allowed. + print('in') return await call_next(request) elif 'referer' not in request.headers: + print('elif') # in case request come straight from address bar in browser. return RedirectResponse(url=app.url_path_for('home')) + print('ddd') # in case the feature is disabled or access isn't allowed. return RedirectResponse(url=request.headers['referer']) @@ -92,7 +96,8 @@ async def filter_access_to_features(request: Request, call_next): @app.on_event("startup") async def startup_event(): session = SessionLocal() - features.create_features_at_startup(session=session) + internal_features.create_features_at_startup(session=session) + print('done') session.close() diff --git a/app/routers/features.py b/app/routers/features.py index 68fda148..e4c04fc9 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -2,9 +2,14 @@ from app.dependencies import get_db, SessionLocal from app.database.models import UserFeature, Feature -from app.internal.utils import create_model -from app.features.index import features - +from app.internal.features import ( + create_association, + is_association_exist_in_db, + is_feature_exist_in_disabled, + is_feature_exist_in_enabled, + get_user_disabled_features, + get_user_enabled_features +) router = APIRouter( prefix="/features", @@ -19,17 +24,6 @@ def index(request: Request, session: SessionLocal = Depends(get_db)): return features -def create_features_at_startup(session: SessionLocal): - - for feat in features: - if not is_feature_exist_in_db(feature=feat, session=session): - create_feature(**feat, db=session) - - fs = session.query(Feature).all() - - return {'all': fs} - - @router.post('/add-feature') async def add_feature_to_user(request: Request, session: SessionLocal = Depends(get_db)): @@ -123,91 +117,14 @@ async def disable_feature(request: Request, return False -def is_association_exist_in_db(form: dict, session: SessionLocal): - 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 - - -def testAssociation(): - session = SessionLocal() - create_association(db=session, feature_id=1, user_id=1, is_enable=True) - create_association(db=session, feature_id=3, user_id=1, is_enable=False) - - associations = session.query(UserFeature).all() - session.close() - return {'all': associations} - - -def delete_feature(feature: Feature, session: SessionLocal = Depends(get_db)): - 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): - 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 - - -def update_feature(feature: Feature, new_feature_obj: dict, - session: SessionLocal = Depends(get_db)): - - feature.name = new_feature_obj['name'] - feature.route = new_feature_obj['route'] - feature.description = new_feature_obj['description'] - feature.creator = new_feature_obj['creator'] - session.commit() - - return feature - - @router.get('/active') -def get_user_enabled_features(session: SessionLocal = Depends(get_db)): - - # TODO - get active user id - user_id = 1 - - 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( - id=pref.feature_id).first() - data.append({'feature': feature, 'is_enabled': pref.is_enable}) - - return data +def show_user_enabled_features(session: SessionLocal = Depends(get_db)): + return get_user_enabled_features(session=session) @router.get('/deactive') -def get_user_disabled_features(session: SessionLocal = Depends(get_db)): - - # TODO - get active user id - user_id = 1 - - 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( - id=pref.feature_id).first() - data.append({'feature': feature, 'is_enabled': pref.is_enable}) - - return data +def show_user_disabled_features(session: SessionLocal = Depends(get_db)): + return get_user_disabled_features(session=session) @router.get('/unlinked') @@ -232,85 +149,3 @@ def get_user_unlinked_features(session: SessionLocal = Depends(get_db)): data.append(feat) return data - - -def is_feature_exist_in_enabled(user_id: int, 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_exist_in_disabled(user_id: int, feature: Feature, - session: SessionLocal = Depends(get_db)): - disable_features = get_user_disabled_features(session=session) - - for df in disable_features: - if df['feature'].id == feature.id: - return True - - return False - - -def is_feature_enabled(route: str): - session = SessionLocal() - - # TODO - get active user id. - user_id = 1 - - 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 exist in the database that match the - # 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() - - 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 - - -def create_feature(name: str, route: str, - description: str, creator: str = None, - db: SessionLocal = Depends()): - """Creates a feature.""" - - db = SessionLocal() - - feature = 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): - """Creates an association.""" - - association = create_model( - db, UserFeature, - user_id=user_id, - feature_id=feature_id, - is_enable=is_enable - ) - - return association From 8c4929cb74b91f648a2baef77daa1e10129aca9c Mon Sep 17 00:00:00 2001 From: Liran C Date: Sun, 14 Feb 2021 15:44:14 +0200 Subject: [PATCH 15/34] add config --- app/config.py.example | 99 ++++++++++++++++++++++++++++++++++++++++++ tests/test_whatsapp.py | 4 +- 2 files changed, 102 insertions(+), 1 deletion(-) 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..6f64a673 --- /dev/null +++ b/app/config.py.example @@ -0,0 +1,99 @@ +import os + +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') + +# 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, +) + +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}") diff --git a/tests/test_whatsapp.py b/tests/test_whatsapp.py index 6b26b03b..7a3c0073 100644 --- a/tests/test_whatsapp.py +++ b/tests/test_whatsapp.py @@ -1,6 +1,7 @@ from app.routers import whatsapp import pytest + def test_whatsapp_send(): # Redirects you directly to the specified contact and the message will # already be there (or to whatsapp web if the call is from the web) @@ -39,6 +40,7 @@ def test_no_number(): @pytest.mark.asyncio async def test_end_to_end_testing(client): - resp = await client.get('/whatsapp?phone_number=972536106106&message=testing') + resp = await client.get( + '/whatsapp?phone_number=972536106106&message=testing') assert resp.ok assert resp.json From b999bc66dc70209a161abfa095ae91d0ca8cb757 Mon Sep 17 00:00:00 2001 From: Liran C Date: Sun, 14 Feb 2021 22:09:31 +0200 Subject: [PATCH 16/34] add tests --- app/internal/features.py | 18 +- app/routers/features.py | 8 +- tests/client_fixture.py | 13 +- tests/test_feature_panel.py | 372 ++++++++++++++++++++++++++++++++++++ 4 files changed, 388 insertions(+), 23 deletions(-) create mode 100644 tests/test_feature_panel.py diff --git a/app/internal/features.py b/app/internal/features.py index c3b28600..7a2fc94c 100644 --- a/app/internal/features.py +++ b/app/internal/features.py @@ -12,9 +12,7 @@ def create_features_at_startup(session: SessionLocal): if not is_feature_exist_in_db(feature=feat, session=session): create_feature(**feat, db=session) - fs = session.query(Feature).all() - - return {'all': fs} + return True def is_association_exist_in_db(form: dict, session: SessionLocal): @@ -28,16 +26,6 @@ def is_association_exist_in_db(form: dict, session: SessionLocal): return False -def testAssociation(): - session = SessionLocal() - create_association(db=session, feature_id=1, user_id=1, is_enable=True) - create_association(db=session, feature_id=3, user_id=1, is_enable=False) - - associations = session.query(UserFeature).all() - session.close() - return {'all': associations} - - def delete_feature(feature: Feature, session: SessionLocal = Depends(get_db)): session.query(UserFeature).filter_by(feature_id=feature.id).delete() session.query(Feature).filter_by(id=feature.id).delete() @@ -70,7 +58,7 @@ def update_feature(feature: Feature, new_feature_obj: dict, return feature -def is_feature_exist_in_enabled(user_id: int, feature: Feature, +def is_feature_exist_in_enabled(feature: Feature, session: SessionLocal = Depends(get_db)): enable_features = get_user_enabled_features(session=session) @@ -81,7 +69,7 @@ def is_feature_exist_in_enabled(user_id: int, feature: Feature, return False -def is_feature_exist_in_disabled(user_id: int, feature: Feature, +def is_feature_exist_in_disabled(feature: Feature, session: SessionLocal = Depends(get_db)): disable_features = get_user_disabled_features(session=session) diff --git a/app/routers/features.py b/app/routers/features.py index 6978a771..63eabecf 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -19,7 +19,7 @@ @router.get('/') -def index(request: Request, session: SessionLocal = Depends(get_db)): +async def index(request: Request, session: SessionLocal = Depends(get_db)): features = session.query(Feature).all() return features @@ -129,18 +129,18 @@ def show_user_disabled_features(session: SessionLocal = Depends(get_db)): def get_user_unlinked_features(session: SessionLocal = Depends(get_db)): # TODO - get active user id - user_id = 1 + # user_id = 1 data = [] all_features = session.query(Feature).all() for feat in all_features: in_disabled = is_feature_exist_in_disabled( - user_id=user_id, feature=feat, session=session + feature=feat, session=session ) in_enabled = is_feature_exist_in_enabled( - user_id=user_id, feature=feat, session=session + feature=feat, session=session ) if not in_enabled and not in_disabled: diff --git a/tests/client_fixture.py b/tests/client_fixture.py index ff15b12e..18a6ecf1 100644 --- a/tests/client_fixture.py +++ b/tests/client_fixture.py @@ -1,10 +1,10 @@ -from async_asgi_testclient.testing import TestClient +from async_asgi_testclient import TestClient 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 +from app.routers import agenda, event, invitation, profile, features from app.routers.salary import routes as salary from tests.conftest import get_test_db, test_engine @@ -29,8 +29,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 - with TestClient(main.app) as client: - yield client + client = TestClient(main.app) + yield client main.app.dependency_overrides = {} Base.metadata.drop_all(bind=test_engine) @@ -73,3 +73,8 @@ def profile_test_client() -> Iterator[TestClient]: @pytest.fixture(scope="session") 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) diff --git a/tests/test_feature_panel.py b/tests/test_feature_panel.py new file mode 100644 index 00000000..2f9fc6e8 --- /dev/null +++ b/tests/test_feature_panel.py @@ -0,0 +1,372 @@ +from app.database.models import Feature +import pytest +import app.internal.features as internal +import app.routers.features as route + + +@pytest.fixture +def mock_features(): + return [ + { + "name": 'test', + "route": '/', + "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 + ) + + 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 + + +def test_get_user_disabled_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=False) + + assert internal.get_user_disabled_features(session)[0].get( + 'is_enabled') is False + + +def test_is_association_exist_in_db(session): + internal.create_association( + db=session, feature_id=1, user_id=1, is_enable=False) + + form_mock = { + 'feature_id': 1, + 'user_id': 1 + } + assert internal.is_association_exist_in_db(form_mock, session) + + +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() + + internal.delete_feature(feature=feat, session=session) + + 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() + + update = { + 'name': 'test', + 'route': '/route', + 'description': 'update', + 'creator': 'test' + } + + assert internal.is_feature_exist_in_db(update, session) + + +def test_update_feature(session): + test = Feature( + name='test', + route='/route', + description='testing', + creator='test' + ) + + session.add(test) + session.commit() + + update = { + 'name': 'test', + 'route': '/route', + 'description': 'update', + 'creator': 'test' + } + + feature = internal.update_feature(test, update, session) + + assert feature.description == 'update' + + +def test_is_feature_exist_in_enabled(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) + + assert internal.is_feature_exist_in_enabled(feat, session) + + +def test_is_feature_exist_in_disabled(mocker, 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) + + assert internal.is_feature_exist_in_disabled(feat, session) + + +def test_is_feature_enabled(mocker, 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) + + mocker.patch( + 'app.internal.features.SessionLocal', + return_value=session + ) + assert internal.is_feature_enabled(route='/route') is True + + +def test_create_feature(session): + + feat = internal.create_feature( + name='test', route='/route', description='testing', creator='test' + ) + + assert feat.name == 'test' + + +@pytest.mark.asyncio +async def test_index(features_test_client): + url = route.router.url_path_for('index') + + resp = await features_test_client.get(url) + assert resp.ok + + +@pytest.mark.asyncio +async 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() + + url = route.router.url_path_for('add_feature_to_user') + + resp = await features_test_client.post(url, form={ + 'feature_id': 1, + 'user_id': 1 + }) + assert resp.ok + + +@pytest.mark.asyncio +async 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) + + url = route.router.url_path_for('delete_user_feature_association') + + resp = await features_test_client.post(url, form={ + 'feature_id': 1, + 'user_id': 1 + }) + assert resp.ok + assert resp.content == b'true' + + +@pytest.mark.asyncio +async 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) + + url = route.router.url_path_for('enable_feature') + + resp = await features_test_client.post(url, form={ + 'feature_id': 1, + 'user_id': 1 + }) + assert resp.ok + assert resp.content == b'true' + + +@pytest.mark.asyncio +async 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) + + url = route.router.url_path_for('disable_feature') + + resp = await features_test_client.post(url, form={ + 'feature_id': 1, + 'user_id': 1 + }) + assert resp.ok + assert resp.content == b'true' + + +@pytest.mark.asyncio +async 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 = await 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): + + mocker.patch( + 'app.routers.features.get_user_disabled_features', + return_value=True + ) + + url = route.router.url_path_for('show_user_disabled_features') + + resp = await features_test_client.get(url) + assert resp.ok + assert resp.content == b'true' + + +@pytest.mark.asyncio +async 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() + + mocker.patch( + 'app.routers.features.get_user_disabled_features', + return_value=True + ) + + url = route.router.url_path_for('get_user_unlinked_features') + + resp = await features_test_client.get(url) + assert resp.ok + json_resp = resp.json()[0] + assert json_resp['id'] == 1 From 6220ad0782433e5440fbc31abf1eef90fd528c8f Mon Sep 17 00:00:00 2001 From: Liran C Date: Tue, 16 Feb 2021 23:23:04 +0200 Subject: [PATCH 17/34] 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 18/34] 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 2572153c41da1fba33327213906f42246e9419aa Mon Sep 17 00:00:00 2001 From: Liran C Date: Thu, 18 Feb 2021 14:42:01 +0200 Subject: [PATCH 19/34] 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 20/34] .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 1c1bbe70eb63bb42c7a3ffc310524a8d7524086f Mon Sep 17 00:00:00 2001 From: Liran C Date: Fri, 19 Feb 2021 11:27:38 +0200 Subject: [PATCH 21/34] 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 107d246cc0c8c751c9501dd6f1dedc2da6a55aa7 Mon Sep 17 00:00:00 2001 From: Liran C Date: Sat, 20 Feb 2021 20:53:39 +0200 Subject: [PATCH 22/34] 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 182b6bee7515761bbc26ebbeea69c58d95d4e417 Mon Sep 17 00:00:00 2001 From: Liran C Date: Sun, 21 Feb 2021 11:31:25 +0200 Subject: [PATCH 23/34] 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 d6b26a881a44ee3ed0c715f434328f9ebc43978f Mon Sep 17 00:00:00 2001 From: Liran C Date: Sun, 21 Feb 2021 14:00:57 +0200 Subject: [PATCH 24/34] 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 096d6fa65aee3286bf54aa95df2b6272976f6c61 Mon Sep 17 00:00:00 2001 From: Liran C Date: Mon, 22 Feb 2021 10:50:13 +0200 Subject: [PATCH 25/34] 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 26/34] 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 27/34] 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 d677ccd47f14ebb9a147430f9bd00ec0dedc7a98 Mon Sep 17 00:00:00 2001 From: Liran C Date: Wed, 24 Feb 2021 15:00:39 +0200 Subject: [PATCH 28/34] 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 29/34] 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 30/34] 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 5b926340aa1b400bdc07746df6f8e1514a032f17 Mon Sep 17 00:00:00 2001 From: Liran C Date: Wed, 24 Feb 2021 17:22:25 +0200 Subject: [PATCH 31/34] 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 32/34] 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 33/34] 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 3bd43b2cf4198ce768ba1de20ee47f3764f01a77 Mon Sep 17 00:00:00 2001 From: Liran C Date: Fri, 26 Feb 2021 00:20:21 +0200 Subject: [PATCH 34/34] 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(