diff --git a/app/database/models.py b/app/database/models.py index 4466dc76..16091cfa 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -107,6 +107,9 @@ class Feature(Base): route = Column(String, nullable=False) creator = Column(String, nullable=True) description = Column(String, nullable=False) + icon = Column(String, nullable=False) + followers = Column(Integer, default=0) + template = Column(String, nullable=True) users = relationship("User", secondary=UserFeature.__tablename__) diff --git a/app/internal/features.py b/app/internal/features.py index 4ef8628e..7d9c0d44 100644 --- a/app/internal/features.py +++ b/app/internal/features.py @@ -8,7 +8,7 @@ from app.database.models import Feature, UserFeature from app.dependencies import SessionLocal, get_db -from app.internal.features_index import features +from app.internal.features_index import features, icons from app.internal.security.dependencies import current_user from app.internal.security.ouath2 import get_authorization_cookie from app.internal.utils import create_model @@ -46,7 +46,9 @@ async def wrapper(*args, **kwargs): 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) + icon = icons.get(feat["name"]) + create_feature(**feat, icon=icon, db=session) + return True @@ -90,6 +92,14 @@ def update_feature( feature.route = feature_dict["route"] feature.description = feature_dict["description"] feature.creator = feature_dict["creator"] + feature.template = feature_dict["template"] + + icon = icons.get(feature.name) + if icon is None: + icon = "extension-puzzle" + + feature.icon = icon + session.commit() return feature @@ -116,7 +126,7 @@ async def is_access_allowd(request: Request, route: str) -> bool: & (UserFeature.user_id == user.user_id), ), ).scalar() - + print(user_feature) return user_feature @@ -126,8 +136,15 @@ def create_feature( route: str, description: str, creator: str = None, + icon: str = None, + template: str = None, ) -> Feature: """Creates a feature.""" + db = SessionLocal() + + if icon is None: + icon = "extension-puzzle" + return create_model( db, Feature, @@ -135,6 +152,8 @@ def create_feature( route=route, creator=creator, description=description, + icon=icon, + template=template, ) @@ -145,6 +164,7 @@ def create_user_feature_association( is_enable: bool, ) -> UserFeature: """Creates an association.""" + add_follower(feature_id=feature_id, session=db) return create_model( db, UserFeature, @@ -181,3 +201,17 @@ def get_user_uninstalled_features( ) .all() ) + + +def remove_follower(feature_id: int, session: SessionLocal) -> None: + feat = session.query(Feature).filter_by(id=feature_id).first() + feat.followers -= 1 + if feat.followers < 0: + feat.followers = 0 + session.commit() + + +def add_follower(feature_id: int, session: SessionLocal) -> None: + feat = session.query(Feature).filter_by(id=feature_id).first() + feat.followers += 1 + session.commit() diff --git a/app/internal/features_index.py b/app/internal/features_index.py index 89743a2e..e94c9982 100644 --- a/app/internal/features_index.py +++ b/app/internal/features_index.py @@ -1,4 +1,4 @@ -''' +""" This file purpose is for developers to add their features to the database in one convenient place, every time the system loads up it's adding and updating the features in the features table in the database. @@ -12,20 +12,54 @@ * Please see the example below. * Enjoy and good luck :) -''' +""" -''' +""" +All icons come from https://ionicons.com/. + +If you want your feature to have an icon Go To: https://ionicons.com/ +and follow the next steps: + +1. pick an icon and press on it, copy the name. +2. create dict in the icons list below. +3. copy and paste your feature name and your + icon name you copied earlier (follow the example). + +4. And You Are Done. the icon will show automaticly. + +If you dont pick an icon your feature will have a default icon. + +Example to feature icon stracture: + +{ + . + . + . + "": "", + . + . + . +} +""" + +# Add to last! +icons = {"feature-panel": "albums-outline", "Google Sync": "sync-outline"} + + +""" Example to feature stracture: { "name": "", "route": "/", "description": "", - "creator": "" + "creator": "", + "template": "" } -''' +""" -''' +""" * IMPORTANT * Example to decorator placement: @@ -39,13 +73,20 @@ def my_cool_feature_route(): .. . -''' +""" features = [ { "name": "Google Sync", "route": "/google/sync", "description": "Sync Google Calendar events with Pylender", - "creator": "Liran Caduri" + "creator": "Liran Caduri", + "template": "example_panel", + }, + { + "name": "example", + "route": "/example", + "description": "example", + "creator": "example", }, ] diff --git a/app/main.py b/app/main.py index ccf4c30c..401ad993 100644 --- a/app/main.py +++ b/app/main.py @@ -123,6 +123,7 @@ async def swagger_ui_redirect(): dayview.router, email.router, event.router, + features.router, export.router, features.router, four_o_four.router, diff --git a/app/routers/features.py b/app/routers/features.py index 1694afe3..cc18971f 100644 --- a/app/routers/features.py +++ b/app/routers/features.py @@ -1,15 +1,18 @@ from typing import List from fastapi import APIRouter, Depends, Request +from fastapi.responses import HTMLResponse from sqlalchemy.sql import exists +from starlette.responses import PlainTextResponse from app.database.models import Feature, User, UserFeature -from app.dependencies import SessionLocal, get_db +from app.dependencies import SessionLocal, get_db, templates from app.internal.features import ( create_user_feature_association, get_user_installed_features, get_user_uninstalled_features, is_user_has_feature, + remove_follower, ) from app.internal.security.dependencies import current_user @@ -24,9 +27,23 @@ async def index( request: Request, session: SessionLocal = Depends(get_db), -) -> List[Feature]: - features = session.query(Feature).all() - return features + user: User = Depends(current_user), +) -> templates: + features = { + "installed": get_user_installed_features( + session=session, + user_id=user.user_id, + ), + "uninstalled": get_user_uninstalled_features( + session=session, + user_id=user.user_id, + ), + } + + return templates.TemplateResponse( + "features.html", + {"request": request, "features": features}, + ) @router.post("/add") @@ -84,6 +101,8 @@ async def delete_user_feature_association( if not is_exist: return False + remove_follower(feature_id=feature_id, session=session) + session.query(UserFeature).filter_by( feature_id=feature_id, user_id=user.user_id, @@ -93,19 +112,26 @@ async def delete_user_feature_association( return True -@router.get("/deactive") -def deactive( +@router.get("/installed") +async def get_user_feature( request: Request, session: SessionLocal = Depends(get_db), user: User = Depends(current_user), -): - return get_user_uninstalled_features(user_id=user.user_id, session=session) +) -> List[Feature]: + return get_user_installed_features(user_id=user.user_id, session=session) -@router.get("/active") -def active( +@router.post("/settings/{template}") +async def render_settings( request: Request, - session: SessionLocal = Depends(get_db), - user: User = Depends(current_user), -): - return get_user_installed_features(user_id=user.user_id, session=session) + template: str, +) -> HTMLResponse: + if template == 'null': + return PlainTextResponse( + content='No additional settings for this one :)') + + template = templates.get_template( + "partials/features_panels/" + template + '.html' + ) + content = template.render() + return HTMLResponse(content=content) diff --git a/app/static/features.css b/app/static/features.css new file mode 100644 index 00000000..5ab64080 --- /dev/null +++ b/app/static/features.css @@ -0,0 +1,81 @@ +.search-box { + width: 98%; + display: flex; + flex-direction: row; + align-items: center; + font-size: var(--text_s); + border: 2px solid var(--secondary); + border-radius: 0.25rem; + padding: 0.25rem; + background-color: white; + } + +.search-box * { + margin: 0 0.25rem; +} + +.search-input[type=text], +.search-input:focus { + width: 100%; + outline: none; + border:none; +} + +/* Feature Panel */ +#features { + display: grid; + grid-template-columns: 1fr 1fr; + grid-auto-rows: 3rem; + grid-column-gap: var(--space_l); + margin: var(--space_s); + align-items: start; + width: 100%; + + overflow-y: scroll; + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +.invisible { + display: none; + position: absolute; + top:0; +} + +.feature-row { + display: grid; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + + grid-template-columns: 6% 30% auto auto 15%; + background-color: var(--surface); + margin: var(--space_s) 0; + padding: var(--space_s); + border-radius: 0.25rem; + border: 2px solid transparent; +} + +.feature-row:hover { + border: 2px solid var(--primary); +} + +.feature-row div { + justify-self: start; + width: 100%; +} + +.feature-icon { + font-size: var(--text_m); + vertical-align: middle; +} + +.information { + display: none; + grid-column: 2/6; + width: 30rem; +} + +.info-box { + display: block !important; +} diff --git a/app/static/global.css b/app/static/global.css index 96647c6e..24a11e22 100644 --- a/app/static/global.css +++ b/app/static/global.css @@ -3,18 +3,18 @@ --text_xxs: 0.5rem; /* 8px */ --text_xs: 0.75rem; /* 12px */ --text_s: 1rem; /* 16px */ - --text_m: 1.2rem; /* 19px */ - --text_l: 1.5rem; /* 24px */ + --text_m: 1.2rem; /* 19px */ + --text_l: 1.5rem; /* 24px */ --text_xl: 1.75rem; /* 28px */ --text_xxl: 2rem; /* 32px */ /* Spaces */ --space_xxs: 0.25rem; /* 4px */ - --space_xs: 0.5rem; /* 8px */ - --space_s: 0.75rem; /* 12px */ - --space_m: 1.25rem; /* 20px */ - --space_l: 2rem; /* 32px */ - --space_xl: 4rem; /* 64px */ + --space_xs: 0.5rem; /* 8px */ + --space_s: 0.75rem; /* 12px */ + --space_m: 1.25rem; /* 20px */ + --space_l: 2rem; /* 32px */ + --space_xl: 4rem; /* 64px */ /* colors */ --primary: #24396a; @@ -62,4 +62,35 @@ body { a { text-decoration: none; color: inherit; -} \ No newline at end of file +} + +/* Features Settings Panel */ +.feature-settings-closed { + display: none; + width: 0.1rem; +} + +.feature-settings-open { + display: block; + background-color: var(--surface); + width: 20rem; +} + +.feature-settings-panel { + padding: 0 var(--space_s); +} + +.flex-row { + display: flex; + flex-flow: row wrap; + margin-bottom: var(--space_s); +} + +.flex-row div { + flex: 1; +} + +#feature-toggle { + flex: 0 0 2rem; + text-align: right; +} diff --git a/app/static/grid_style.css b/app/static/grid_style.css index 026474a0..fb803759 100644 --- a/app/static/grid_style.css +++ b/app/static/grid_style.css @@ -1,20 +1,40 @@ -:root[data-color-mode="regular"] { - --backgroundcol: #F7F7F7; - --textcolor: #222831; - --start-of-month: #E9ECEf; - --primary-variant: #FFDE4D; - --secondary: #EF5454; - --borders: #E7E7E7; - --borders-variant: #F7F7F7; -} - -:root[data-color-mode="dark"] { - --backgroundcol: #000000; - --textcolor: #EEEEEE; - --start-of-month: #8C28BF; - --secondary: #EF5454; - --borders: #E7E7E7; - --borders-variant: #F7F7F7; +:root { + /* Texts */ + --text_xxs: 0.5rem; /* 8px */ + --text_xs: 0.75rem; /* 12px */ + --text_s: 1rem; /* 16px */ + --text_m: 1.2rem; /* 19px */ + --text_l: 1.5rem; /* 24px */ + --text_xl: 1.75rem; /* 28px */ + --text_xxl: 2rem; /* 32px */ + + /* Spaces */ + --space_xxs: 0.25rem; /* 4px */ + --space_xs: 0.5rem; /* 8px */ + --space_s: 0.75rem; /* 12px */ + --space_m: 1.25rem; /* 20px */ + --space_l: 2rem; /* 32px */ + --space_xl: 4rem; /* 64px */ + + /* colors */ + --primary: #24396a; + --primary-variant: #041E51; + --secondary: #fbc44a; + --secondary-variant: #ffa201; + --background: #F7F7F7; + --surface: #e6e6e6; /* borders */ + --surface-variant: #d2d1d1; /*up_surface, borders-variant*/ + --negative: #f24726; + --negative-variant: #e83305; + --positive: #4ca486; + --positive-variant: #008c73; + + --on-primary: #ffffff; + --on-secondary: #000000; + --on-background: #000000; + --on-surface: #000000; + --on-negative: #ffffff; + --on-positive: #ffffff; } * { @@ -92,24 +112,7 @@ nav { background-color: var(--secondary); } -#open-features { - font-size: var(--icon_l); - margin-top: var(--space_s); -} - -#feature-settings { - visibility: hidden; - width: 0.1rem; - background-color: var(--surface); -} - -.settings-open { - width: 20rem; -} - -img { - fill: var(--background); -} +img {fill: var(--background);} header { z-index: 5; @@ -207,7 +210,7 @@ main { font-size: var(--text_xs); padding: 0 var(--space_s); - border: 0.095rem solid var(--surface); + border: 0.075rem solid var(--surface); overflow-y: auto; -ms-overflow-style: none; /* IE and Edge */ @@ -367,6 +370,23 @@ main { font-size: var(--text_s); } +.m-title { + font-weight: 600; + font-size: var(--text_m); +} + +.s-title { + font-size: var(--text_s); +} + +/* Paragraph */ + +.s-paragraph { + font-size: var(--text_s); + line-height: 1.25rem; + margin: var(--space_s) 0; +} + /* Text Colors */ .text-yellow { color: var(--secondary); @@ -384,6 +404,8 @@ main { color: var(--primary); } +.text-red {color: var(--negative)} + /* Borders */ .border-dash-darkblue { border: 0.125rem dashed var(--primary); @@ -426,13 +448,30 @@ main { .button { border-radius: 0.15rem; + font-weight: 500; font-size: var(--text_s); - font-weight: 800; padding: 0.15rem 0.25rem; + text-align: center; + vertical-align: middle; } .button:hover { - font-weight: 700; + font-weight: 800; +} + +.add-button, +.remove-button { + color: var(--background); + height: 2rem; +} + +.remove-button { + background-color: var(--negative); +} + + +.add-button { + background-color: var(--positive); } .dates-calc { diff --git a/app/static/js/features.js b/app/static/js/features.js new file mode 100644 index 00000000..2b511a5e --- /dev/null +++ b/app/static/js/features.js @@ -0,0 +1,119 @@ +function displayResult(rows, elements) { + const rowsArray = Array.from(rows); + const elementsArray = Array.from(elements); + rowsArray.map(row => { + if (elementsArray.includes(row)) { + return row.classList.remove("invisible"); + } else { + return row.classList.add("invisible"); + } + }); +} + +function searchFeature(searchValue, elements) { + const elementsArray = Array.from(elements); + const result = elementsArray.filter(element => { + return element.getElementsByClassName("row-feature-name")[0].innerHTML.toLowerCase().includes(searchValue); + }); + return result; +} + +function setSearchBox() { + const searchBox = document.getElementsByClassName("search-input"); + searchBox[0].addEventListener( + 'input', function (evt) { + const rows = document.getElementsByClassName("feature-row"); + const value = evt.target.value.trim().toLowerCase(); + if (!value) { + Array.from(rows).map(element => element.classList.remove("invisible")); + } else { + const result = searchFeature(value, rows); + displayResult(rows, result); + } + }); +} + +function updateFollowers(element, add = true) { + const followers = element.getElementsByClassName("followers")[0]; + const followersNum = parseInt(followers.innerText); + if (add) { + followers.innerText = followersNum + 1; + } else if (followersNum > 0) { + followers.innerText = followersNum - 1; + } +} + +function move(button, element) { + const category = element.parentElement; + if (category.id === "available-features") { + button.innerHTML = "REMOVE"; + button.classList = "button remove-button"; + element.dataset.state = "installed"; + document.getElementById("installed-features").appendChild(element); + updateFollowers(element); + } else { + button.innerHTML = "ADD"; + button.classList = "button add-button"; + element.dataset.state = "available"; + document.getElementById("available-features").appendChild(element); + updateFollowers(element, false); + } +} + +function setPath(action, url) { + if (action === "ADD") { + return new URL('/features/add', url); + } + return new URL('/features/delete', url); +} + +function update() { + const action = this.innerText; + const parent = this.parentElement; + const featureId = this.parentElement.id; + const url = window.location.origin; + let path = setPath(action, url); + const formData = new FormData(); + formData.append('feature_id', featureId); + fetch(path, + { + body: formData, + method: "post" + } + ).then(response => { + response.json(); + move(this, parent); + }); +} + +function setFeatures() { + const rows = document.getElementsByClassName("feature-row"); + for (let i = 0; i < rows.length; ++i) { + const row = rows[i]; + const button = row.getElementsByClassName("button")[0]; + button.addEventListener('click', update); + } +} + +function setInformation() { + let allInfo = document.getElementsByClassName("info-icon"); + Array.from(allInfo).map(element => { + const parent = element.parentElement; + const infoBox = parent.getElementsByClassName("information")[0]; + element.addEventListener("click", function () { + if (!infoBox.classList.contains('info-box')) { + infoBox.classList.add('info-box'); + } else { + infoBox.classList.remove('info-box'); + } + }) + }); +} + +document.addEventListener( + 'DOMContentLoaded', function () { + setSearchBox(); + setFeatures(); + setInformation(); + } +) diff --git a/app/static/js/global.js b/app/static/js/global.js new file mode 100644 index 00000000..a5122333 --- /dev/null +++ b/app/static/js/global.js @@ -0,0 +1,81 @@ +function openPanel(targetID, classToSet) { + const target = document.getElementById(targetID); + target.classList.toggle(classToSet); +} + +function setFeatureInformation(element) { + const panel = { + "feature-description": element.dataset.description, + "feature-name": element.dataset.name, + "feature-followers": element.dataset.followers, + "feature-creator": element.dataset.creator + }; + for (const key in panel) { + document.getElementById(key).innerHTML = panel[key]; + } + renderTemplate(element.dataset.template); +} + +function setFeaturesSettings() { + const allFeatures = document.getElementsByClassName("feature"); + for (let i = 0; i < allFeatures.length; ++i) { + allFeatures[i].addEventListener('click', function () { + openPanel("feature-set-panel", "feature-settings-open"); + const target = document.getElementById("feature-set-panel"); + if (target.classList.contains("feature-settings-open")) { + setFeatureInformation(allFeatures[i]); + } + }) + } +} + +function appandFeatures(){ + const baseURL = window.location.origin; + const route = new URL('/features/installed', baseURL); + + fetch(route) + .then((response) => { + return response.json(); + }).then((data) => { + const iconStrip = document.getElementById('icon-strip'); + for (let i = 0; i < data.length; ++i) { + const fData = data[i] + let feature = document.createElement('div'); + + feature.classList.add('feature'); + feature.id = 'feature-' + fData.id; + + feature.dataset.name = fData.name; + feature.dataset.creator = fData.creator; + feature.dataset.description = fData.description; + feature.dataset.followers = fData.followers; + feature.dataset.template = fData.template; + + let icon = document.createElement('ion-icon'); + icon.setAttribute('name', fData.icon); + feature.appendChild(icon); + iconStrip.appendChild(feature); + } + }); +} + +function renderTemplate(featureTemplate){ + const baseURL = window.location.origin; + const route = new URL('/features/settings/' + featureTemplate, baseURL); + + fetch(route, { method: "post" } + ).then(function (response) { + return response.text(); + }).then(function (html) { + const content = document.getElementById("feature-content") + content.innerHTML = '' + content.insertAdjacentHTML('afterbegin', html); + }); +} + +document.addEventListener( + 'DOMContentLoaded', function () { + appandFeatures(); + setTimeout(setFeaturesSettings, 200); + } +) diff --git a/app/static/js/grid_navigation.js b/app/static/js/grid_navigation.js index fece8b87..a8fda0a9 100644 --- a/app/static/js/grid_navigation.js +++ b/app/static/js/grid_navigation.js @@ -79,7 +79,7 @@ function loadDaysIfNeeded(dateId, link) { } else { const lastDay = stringToDate(allId[allId.length - 1]); deltaDays = parseInt((targetDay - lastDay) / (1000 * 60 * 60 * 24), 10); - callLoadWeek(calcDaysToLoad(deltaDays)); + callLoadWeek(calcDaysToLoad(deltaDays), true); } jump(link); } diff --git a/app/static/js/grid_scripts.js b/app/static/js/grid_scripts.js index 9f3af312..d4743b3f 100644 --- a/app/static/js/grid_scripts.js +++ b/app/static/js/grid_scripts.js @@ -1,6 +1,6 @@ function setToggle( elementClass, targetElement, classToAdd, - index, elementsToLoad + index, elementsToLoad, ) { const allElements = document.getElementsByClassName(elementClass); const target = document.getElementById(targetElement); @@ -21,14 +21,6 @@ function isMonthLoaded(monthId) { return false; } -document.addEventListener( - 'DOMContentLoaded', function () { - const allDays = document.getElementsByClassName('day'); - setToggle("day", "day-view", "day-view-visible", 0, allDays.length); - weekScroll(); - } -) - function loadWeekAfter(baseUrl, day, index, daysToLoad) { if (day.dataset.last === "false") { return false; @@ -60,7 +52,7 @@ function loadWeekBefore(baseUrl, day, daysToLoad) { }); } -function callLoadWeek(daysToLoad = 42, end = true) { +function callLoadWeek(daysToLoad, end) { let day = null; const url = window.location.origin; const allDays = document.getElementsByClassName('day'); @@ -75,6 +67,8 @@ function callLoadWeek(daysToLoad = 42, end = true) { } function weekScroll() { + const daysToLoad = 42; + grid = document.getElementById("calender-grid"); grid.addEventListener( 'scroll', function () { @@ -82,12 +76,22 @@ function weekScroll() { if (grid.scrollY + grid.innerHeight + tolerance < grid.scrollHeight) { return false; } - callLoadWeek(); + const addAfter = true; + callLoadWeek(daysToLoad, addAfter); } ) - grid.addEventListener('wheel', function () { - if (this.scrollTop === 0) { - callLoadWeek(42, end = false); + grid.addEventListener('wheel', function (event) { + if (event.deltaY < 0 && this.scrollTop === 0) { + const addAfter = false; + callLoadWeek(daysToLoad, addAfter); } }) -} \ No newline at end of file +} + +document.addEventListener( + 'DOMContentLoaded', function () { + const allDays = document.getElementsByClassName('day'); + setToggle("day", "day-view", "day-view-visible", 0, allDays.length); + weekScroll(); + } +) diff --git a/app/templates/calendar_monthly_view.html b/app/templates/calendar_monthly_view.html index b97015a2..6ba99697 100644 --- a/app/templates/calendar_monthly_view.html +++ b/app/templates/calendar_monthly_view.html @@ -39,3 +39,9 @@

Time calculator:

{% include 'partials/calendar/monthly_view/monthly_grid.html' %} {% endblock content %} + +{% block scripts %} + + + +{% endblock scripts %} diff --git a/app/templates/features.html b/app/templates/features.html new file mode 100644 index 00000000..1e54fb2c --- /dev/null +++ b/app/templates/features.html @@ -0,0 +1,84 @@ +{% extends "./partials/calendar/calendar_base.html" %} +{% block css %} + +{% endblock css %} + +{% block page_name %}Features Panel{% endblock page_name %} + +{% block content %} +
+
+
FEATURES PANEL
+

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

+ +
+ +
+ +
+
+
Available Features
+
Your Features
+
+ {% for feature in features['uninstalled'] %} +
+
+ +
+
+ {{feature.name.upper()}} + +
+
Creator: {{feature.creator.title()}}
+
+ + Followers: + {{feature.followers}} +
+
+ ADD +
+
{{feature.description}}
+
+ {% endfor %} +
+
+ {% for feature in features['installed'] %} +
+
+ +
+
+ {{feature.name.upper()}} + +
+
Creator: {{feature.creator.title()}}
+
+ + Followers: + {{feature.followers}} +
+
+ REMOVE +
+
{{feature.description}}
+
+ {% endfor %} +
+
+
+{% endblock content %} + +{% block scripts %} + +{% endblock scripts %} \ No newline at end of file diff --git a/app/templates/partials/base.html b/app/templates/partials/base.html index 5ef69d90..392b52de 100644 --- a/app/templates/partials/base.html +++ b/app/templates/partials/base.html @@ -50,4 +50,4 @@ - \ No newline at end of file + diff --git a/app/templates/partials/calendar/calendar_base.html b/app/templates/partials/calendar/calendar_base.html index a0090af6..aba16eb7 100644 --- a/app/templates/partials/calendar/calendar_base.html +++ b/app/templates/partials/calendar/calendar_base.html @@ -1,27 +1,32 @@ {% extends "./partials/base.html" %} {% block head %} - {{super()}} +{{super()}} + + + + +{% block css %} +{% endblock css %} - - - {% endblock head %} {% block page_name %}Month View{% endblock page_name %} {% block body %}
{% include 'partials/calendar/navigation.html' %} -
- {% include 'partials/calendar/feature_settings/example.html' %} -
+ {% include 'partials/calendar/feature_settings/feature_settings_base.html' %}
{% block content %} {% endblock content %}
+ - -{% endblock body %} + +{% block scripts %} +{% endblock scripts %} + +{% endblock body %} \ No newline at end of file diff --git a/app/templates/partials/calendar/feature_settings/example.html b/app/templates/partials/calendar/feature_settings/example.html deleted file mode 100644 index a9f4edd0..00000000 --- a/app/templates/partials/calendar/feature_settings/example.html +++ /dev/null @@ -1 +0,0 @@ -
FEATURE NAME
\ No newline at end of file diff --git a/app/templates/partials/calendar/feature_settings/feature_settings_base.html b/app/templates/partials/calendar/feature_settings/feature_settings_base.html new file mode 100644 index 00000000..65da8c05 --- /dev/null +++ b/app/templates/partials/calendar/feature_settings/feature_settings_base.html @@ -0,0 +1,21 @@ +
+
+
+
+
+
+
+ + Followers: +
+
+ Creator: +
+
+
+
+ +
+
+
diff --git a/app/templates/partials/calendar/monthly_view/monthly_grid.html b/app/templates/partials/calendar/monthly_view/monthly_grid.html index 84669a3b..f42fea0f 100644 --- a/app/templates/partials/calendar/monthly_view/monthly_grid.html +++ b/app/templates/partials/calendar/monthly_view/monthly_grid.html @@ -2,9 +2,9 @@
{% for d in week_days %} {% if d == day.sday %} -
{{ d.upper() }}
+
{{ d.upper() }}
{% else %} -
{{ d.upper() }}
+
{{ d.upper() }}
{% endif %} {% endfor %}
@@ -30,4 +30,4 @@ -
+
\ No newline at end of file diff --git a/app/templates/partials/calendar/navigation.html b/app/templates/partials/calendar/navigation.html index 0677bc87..3ee0ec73 100644 --- a/app/templates/partials/calendar/navigation.html +++ b/app/templates/partials/calendar/navigation.html @@ -20,10 +20,14 @@
- + + +
- + + +
- -
-
- -
-
-
- +
diff --git a/app/templates/partials/features_panels/example_panel.html b/app/templates/partials/features_panels/example_panel.html new file mode 100644 index 00000000..6d5543d1 --- /dev/null +++ b/app/templates/partials/features_panels/example_panel.html @@ -0,0 +1 @@ +
Your feature settings
\ No newline at end of file diff --git a/app/templates/partials/notification/base.html b/app/templates/partials/notification/base.html index 0df8fcde..93fb1723 100644 --- a/app/templates/partials/notification/base.html +++ b/app/templates/partials/notification/base.html @@ -11,7 +11,7 @@
{% include 'partials/calendar/navigation.html' %}
- {% include 'partials/calendar/feature_settings/example.html' %} + {% include 'partials/calendar/feature_settings/feature_settings_base.html' %}
{% block content %} diff --git a/tests/test_feature_panel.py b/tests/test_feature_panel.py index 1ee57b7c..8ba21887 100644 --- a/tests/test_feature_panel.py +++ b/tests/test_feature_panel.py @@ -1,8 +1,9 @@ import pytest -from app.database.models import Feature, UserFeature import app.internal.features as internal import app.routers.features as route +from app.database.models import Feature, UserFeature +from app.internal.security.schema import CurrentUser from tests.test_login import LOGIN_DATA, REGISTER_DETAIL @@ -26,6 +27,8 @@ def feature(session): route="/test", description="testing", creator="test", + icon="test", + followers=0, ) session.add(test) @@ -69,9 +72,10 @@ def association_on(session, user): def update_dict(): update = { "name": "test", - "route": "/route-test", + "route": "/test", "description": "update", "creator": "test", + "template": 'test' } return update @@ -79,9 +83,7 @@ def update_dict(): @pytest.fixture def form_mock(): - form = {"feature_id": 1, "user_id": 1} - - return form + return {"feature_id": 1, "user_id": 1} def test_create_features_at_startup(mocker, session, mock_features): @@ -128,10 +130,9 @@ def test_delete_feature(session, feature): def test_is_feature_exist_in_db(session, feature): - assert internal.is_feature_exists({ - 'name': 'test', - 'route': '/test' - }, session) + assert internal.is_feature_exists( + {"name": "test", "route": "/test"}, session, + ) def test_update_feature(session, feature, update_dict): @@ -140,7 +141,7 @@ def test_update_feature(session, feature, update_dict): @pytest.mark.asyncio -async def test_is_feature_enabled(mocker, session, association_on, user): +async def test_is_feature_enabled(mocker, session, association_on, feature): mocker.patch("app.internal.features.SessionLocal", return_value=session) mocker.patch( "app.internal.features.get_authorization_cookie", @@ -148,12 +149,10 @@ async def test_is_feature_enabled(mocker, session, association_on, user): ) mocker.patch( "app.internal.features.current_user", - return_value=user, + return_value=CurrentUser(user_id=1, username="test"), ) - assert ( - await internal.is_access_allowd(route="/route", request=None) is True - ) + assert await internal.is_access_allowd(route="/test", request=None) is True def test_create_feature(session): @@ -167,6 +166,19 @@ def test_create_feature(session): assert feat.name == "test1" +def test_remove_follower(session, feature): + feature.followers = 1 + session.commit() + + internal.remove_follower(feature_id=feature.id, session=session) + assert feature.followers == 0 + + +def test_add_follower(session, feature): + internal.add_follower(feature_id=feature.id, session=session) + assert feature.followers == 1 + + def test_index(security_test_client): url = route.router.url_path_for("index")