Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Panel - Backend #283

Merged
merged 54 commits into from Feb 25, 2021
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
d09b5fd
add models to db
LiranCaduri Feb 1, 2021
013c6d5
figure out delete.
LiranCaduri Feb 2, 2021
894f234
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
LiranCaduri Feb 2, 2021
cb7947c
first structure of route with middlewere
LiranCaduri Feb 3, 2021
523c01e
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
LiranCaduri Feb 6, 2021
572036e
try middleware
LiranCaduri Feb 7, 2021
397cee3
add middleware filtering requests
LiranCaduri Feb 8, 2021
1da1dda
add: get_user_disabled_features and get_user_enabled_features
LiranCaduri Feb 9, 2021
d160605
modify middleware
LiranCaduri Feb 10, 2021
b51cd21
replace testclient, add on startup event, feature folder, is_feature_…
LiranCaduri Feb 10, 2021
9df777b
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
LiranCaduri Feb 11, 2021
e4a817a
improved is_feature_enabled and middleware code.
LiranCaduri Feb 11, 2021
0514121
add_feature_to_user function
LiranCaduri Feb 11, 2021
c715299
add checking for duplicates in association table
LiranCaduri Feb 13, 2021
6380a8c
renaming some files, and adding on and off routes
LiranCaduri Feb 13, 2021
7f50cc0
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
LiranCaduri Feb 13, 2021
2ea61cb
.example
LiranCaduri Feb 13, 2021
b2b839f
split to internal
LiranCaduri Feb 14, 2021
36af9b7
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
LiranCaduri Feb 14, 2021
8c4929c
add config
LiranCaduri Feb 14, 2021
b999bc6
add tests
LiranCaduri Feb 14, 2021
e97f29b
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
LiranCaduri Feb 16, 2021
6220ad0
access decorator, back to fastapi testclient, fix tests, new document…
LiranCaduri Feb 16, 2021
f6a6630
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
LiranCaduri Feb 16, 2021
0b27713
fix flake8 issue
LiranCaduri Feb 16, 2021
2572153
requested changes
LiranCaduri Feb 18, 2021
10c7d88
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
LiranCaduri Feb 18, 2021
7f0ab27
.example
LiranCaduri Feb 18, 2021
1c1bbe7
requested changes
LiranCaduri Feb 19, 2021
a8e8725
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
LiranCaduri Feb 19, 2021
dfe02c7
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
LiranCaduri Feb 20, 2021
2081329
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
LiranCaduri Feb 20, 2021
107d246
changes
LiranCaduri Feb 20, 2021
182b6be
before cache
LiranCaduri Feb 21, 2021
d6b26a8
remove redundant things
LiranCaduri Feb 21, 2021
4618b06
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
LiranCaduri Feb 21, 2021
c4f6cbe
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
LiranCaduri Feb 21, 2021
3c88d35
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
LiranCaduri Feb 22, 2021
096d6fa
remove redundant things
LiranCaduri Feb 22, 2021
4696df2
pre-commit change some stuff
LiranCaduri Feb 23, 2021
105d253
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
LiranCaduri Feb 23, 2021
dc9fcce
remove blank lines
LiranCaduri Feb 23, 2021
d677ccd
requested changes
LiranCaduri Feb 24, 2021
7712b07
update .gitignore
LiranCaduri Feb 24, 2021
ecd5cb8
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
LiranCaduri Feb 24, 2021
a9f9001
fix semantics
LiranCaduri Feb 24, 2021
5b92634
fix logic
LiranCaduri Feb 24, 2021
572e992
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
LiranCaduri Feb 24, 2021
571fe1a
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
LiranCaduri Feb 24, 2021
36f841b
requested changes
LiranCaduri Feb 25, 2021
c181946
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
LiranCaduri Feb 25, 2021
93b0b02
use pre-commit on google-connect
LiranCaduri Feb 25, 2021
3bd43b2
requested changes
LiranCaduri Feb 25, 2021
69ed31b
Merge branch 'develop' of https://github.com/PythonFreeCourse/calenda…
LiranCaduri Feb 25, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -157,6 +157,9 @@ app/.vscode/

app/routers/stam

bin
routes 1.py
LiranCaduri marked this conversation as resolved.
Show resolved Hide resolved

# PyCharm
.idea

Expand Down
23 changes: 23 additions & 0 deletions app/database/models.py
Expand Up @@ -19,6 +19,16 @@
Base = declarative_base()


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'))

is_enable = Column(Boolean)


class User(Base):
__tablename__ = "users"

Expand Down Expand Up @@ -46,6 +56,7 @@ class User(Base):
)
comments = relationship("Comment", back_populates="user")

features = relationship("Feature", secondary=UserFeature.__tablename__)
oauth_credentials = relationship(
"OAuthCredentials", cascade="all, delete", back_populates="owner",
uselist=False)
Expand All @@ -60,6 +71,18 @@ async def get_by_username(db: Session, username: str) -> User:
User.username == username).first()


class Feature(Base):
__tablename__ = "features"

id = Column(Integer, primary_key=True, index=True)
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__)


class Event(Base):
__tablename__ = "events"

Expand Down
Empty file added app/features/__init__.py
Empty file.
51 changes: 51 additions & 0 deletions app/features/index.py
@@ -0,0 +1,51 @@
'''
LiranCaduri marked this conversation as resolved.
Show resolved Hide resolved
This file purpose is for developers to add their features to the database
in one convenient place, every time the system loads up it's adding and
updating the features in the features table in the database.

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.

* 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 :)
'''

'''
Example to feature stracture:

{
"name": '<feature name>',
"route": '/<the route like: /features >',
"description": '<description>',
"creator": '<creator name or nickname>'
LiranCaduri marked this conversation as resolved.
Show resolved Hide resolved
}
'''

'''
* IMPORTANT *

Example to decorator placement:

@router.get("/<my-route>")
@feature_access_filter <---- just above def keyword!
def my_cool_feature_route():
....
...
some code.
..
.

'''

features = [
{
"name": 'Google Sync',
LiranCaduri marked this conversation as resolved.
Show resolved Hide resolved
"route": '/google/sync',
"description": 'Sync Google Calendar events with Pylender',
"creator": 'Liran Caduri'
},
]
30 changes: 30 additions & 0 deletions 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)
LiranCaduri marked this conversation as resolved.
Show resolved Hide resolved
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
169 changes: 169 additions & 0 deletions app/internal/features.py
@@ -0,0 +1,169 @@
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
LiranCaduri marked this conversation as resolved.
Show resolved Hide resolved


def create_features_at_startup(session: SessionLocal):
LiranCaduri marked this conversation as resolved.
Show resolved Hide resolved

LiranCaduri marked this conversation as resolved.
Show resolved Hide resolved
for feat in features:
if not is_feature_exist_in_db(feature=feat, session=session):
create_feature(**feat, db=session)

return True


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
LiranCaduri marked this conversation as resolved.
Show resolved Hide resolved


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)):

LiranCaduri marked this conversation as resolved.
Show resolved Hide resolved
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(feature: Feature,
session: SessionLocal = Depends(get_db)):
enable_features = get_user_enabled_features(session=session)

for ef in enable_features:
LiranCaduri marked this conversation as resolved.
Show resolved Hide resolved
if ef['feature'].id == feature.id:
return True

return False


def is_feature_exist_in_disabled(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

LiranCaduri marked this conversation as resolved.
Show resolved Hide resolved
return False


def is_feature_enabled(route: str):
session = SessionLocal()

user = get_current_user(session=session)

feature = session.query(Feature).filter_by(route=route).first()
LiranCaduri marked this conversation as resolved.
Show resolved Hide resolved

# *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
LiranCaduri marked this conversation as resolved.
Show resolved Hide resolved


def create_feature(name: str, route: str,
description: str, creator: str = None,
db: SessionLocal = Depends()):
"""Creates a feature."""

LiranCaduri marked this conversation as resolved.
Show resolved Hide resolved
db = SessionLocal()

feature = create_model(
LiranCaduri marked this conversation as resolved.
Show resolved Hide resolved
db, Feature,
name=name,
route=route,
creator=creator,
description=description
)
return feature


def create_association(
LiranCaduri marked this conversation as resolved.
Show resolved Hide resolved
db: SessionLocal, feature_id: int, user_id: int, is_enable: bool):
"""Creates an association."""

LiranCaduri marked this conversation as resolved.
Show resolved Hide resolved
association = create_model(
LiranCaduri marked this conversation as resolved.
Show resolved Hide resolved
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)):

user = get_current_user(session=session)

LiranCaduri marked this conversation as resolved.
Show resolved Hide resolved
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)):

user = get_current_user(session=session)

data = []
LiranCaduri marked this conversation as resolved.
Show resolved Hide resolved
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
24 changes: 17 additions & 7 deletions app/main.py
@@ -1,20 +1,22 @@
from fastapi import Depends, FastAPI, Request
from fastapi.staticfiles import StaticFiles
from sqlalchemy.orm import Session

from app import config
from app.database import engine, models
from app.dependencies import get_db, logger, MEDIA_PATH, STATIC_PATH, templates
from app.internal import daily_quotes, json_data_loader

from app.dependencies import (
get_db, logger, MEDIA_PATH, STATIC_PATH, templates, SessionLocal)
from app.internal import (
daily_quotes, json_data_loader, features as internal_features)
LiranCaduri marked this conversation as resolved.
Show resolved Hide resolved
from app.internal.languages import set_ui_language
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):
Expand Down Expand Up @@ -43,7 +45,7 @@ def create_tables(engine, psql_environment):

from app.routers import ( # noqa: E402
about_us, agenda, calendar, categories, celebrity, currency, dayview,
email, event, export, four_o_four, google_connect,
email, event, export, features, four_o_four, google_connect,
invitation, login, logout, profile,
register, search, telegram, user, weekview, whatsapp,
)
Expand Down Expand Up @@ -79,6 +81,7 @@ async def swagger_ui_redirect():
email.router,
event.router,
export.router,
features.router,
four_o_four.router,
google_connect.router,
invitation.router,
Expand All @@ -97,6 +100,13 @@ async def swagger_ui_redirect():
app.include_router(router)


@app.on_event("startup")
async def startup_event():
session = SessionLocal()
internal_features.create_features_at_startup(session=session)
session.close()


# TODO: I add the quote day to the home page
# until the relevant calendar view will be developed.
@app.get("/", include_in_schema=False)
Expand Down