diff --git a/sipi-app/alembic/versions/2024_07_13_1615-f073180e963c_create_isp_connection_table.py b/sipi-app/alembic/versions/2024_07_13_1615-f073180e963c_create_isp_connection_table.py new file mode 100644 index 0000000..1b0d5cc --- /dev/null +++ b/sipi-app/alembic/versions/2024_07_13_1615-f073180e963c_create_isp_connection_table.py @@ -0,0 +1,36 @@ +"""create isp_connection table + +Revision ID: f073180e963c +Revises: 85fef76b3dcd +Create Date: 2024-07-13 16:15:44.004704 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "f073180e963c" +down_revision: Union[str, None] = "85fef76b3dcd" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("isp_connections", "isp_name") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "isp_connections", + sa.Column( + "isp_name", sa.VARCHAR(), autoincrement=False, nullable=False + ), + ) + # ### end Alembic commands ### diff --git a/sipi-app/crud/isp.py b/sipi-app/crud/isp.py index dcb58ba..97f4e26 100644 --- a/sipi-app/crud/isp.py +++ b/sipi-app/crud/isp.py @@ -3,13 +3,18 @@ from typing import Sequence from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import joinedload, selectinload + from models import Isp -from schemas.isp import IspCreate +from schemas.isp import IspCreate, IspRead -async def get_all_isp(session: AsyncSession) -> Sequence[Isp]: - stmt = select(Isp).order_by(Isp.id) +async def get_all_isp( + session: AsyncSession, +) -> Sequence[Isp]: + stmt = select(Isp).options(selectinload(Isp.isp_connections)).order_by(Isp.id) result = await session.scalars(stmt) + return result.all() diff --git a/sipi-app/schemas/isp.py b/sipi-app/schemas/isp.py index 0e9d172..76e4023 100644 --- a/sipi-app/schemas/isp.py +++ b/sipi-app/schemas/isp.py @@ -1,6 +1,11 @@ +from typing import TYPE_CHECKING + from pydantic import BaseModel +from .isp_connections import IspConnectionBase + + class IspBase(BaseModel): name: str manager_name: str @@ -9,6 +14,7 @@ class IspBase(BaseModel): tech_support_phone: str tesh_support_email: str comment: str + isp_connections: list["IspConnectionBase"] | None class IspCreate(IspBase): diff --git a/sipi-app/settings.py b/sipi-app/settings.py index df6ff33..6ecd69d 100644 --- a/sipi-app/settings.py +++ b/sipi-app/settings.py @@ -22,9 +22,10 @@ class ApiPrefix(BaseModel): class WebPrefix(BaseModel): - start: str = "/web" isp: str = "/web/isp" + settings: str = "/web/settings" isp_connections: str = "/web/isp_connections" + start: str = "/web" class DatabaseConfig(BaseModel): diff --git a/sipi-app/views/__init__.py b/sipi-app/views/__init__.py index 71896b4..f9f6b70 100644 --- a/sipi-app/views/__init__.py +++ b/sipi-app/views/__init__.py @@ -4,6 +4,7 @@ from settings import settings from .web import router as web_router from .isp import router as isp_router +from .setting import router as settings_router router = APIRouter() @@ -15,3 +16,7 @@ router.include_router( isp_router, prefix=settings.web.isp, ) +router.include_router( + settings_router, + prefix=settings.web.settings, +) diff --git a/sipi-app/views/isp.py b/sipi-app/views/isp.py index 6f7604a..0b80219 100644 --- a/sipi-app/views/isp.py +++ b/sipi-app/views/isp.py @@ -1,23 +1,39 @@ -from fastapi import APIRouter from starlette.requests import Request from starlette.responses import HTMLResponse from starlette.templating import Jinja2Templates -from settings import settings +from typing import Annotated + +from fastapi import APIRouter, Depends +from sqlalchemy.ext.asyncio import AsyncSession + +from models import db_helper + +from crud import isp as isp_crud +from fastapi.encoders import jsonable_encoder router = APIRouter( - tags=["Web_Isp"], + tags=["Web"], ) templates = Jinja2Templates(directory="views/templates") -@router.get("/", response_class=HTMLResponse) -async def read_item(request: Request): - print("sadf") +@router.get("/get_all", response_class=HTMLResponse) +async def get_all_isp( + request: Request, + session: Annotated[AsyncSession, Depends(db_helper.session_getter)], +): + isps = jsonable_encoder( + await isp_crud.get_all_isp( + session=session, + ) + ) + print(isps) return templates.TemplateResponse( - "index.html", + "body-isp.html", { "request": request, + "isps": isps, }, ) diff --git a/sipi-app/views/setting.py b/sipi-app/views/setting.py new file mode 100644 index 0000000..6c30a42 --- /dev/null +++ b/sipi-app/views/setting.py @@ -0,0 +1,25 @@ +from starlette.requests import Request +from starlette.responses import HTMLResponse +from starlette.templating import Jinja2Templates + + +from fastapi import APIRouter, Depends + +router = APIRouter( + tags=["Web"], +) + +templates = Jinja2Templates(directory="views/templates") + + +@router.get("/", response_class=HTMLResponse) +async def get_all_settings( + request: Request, +): + + return templates.TemplateResponse( + "body-settings.html", + { + "request": request, + }, + ) diff --git a/sipi-app/views/static/css/base.css b/sipi-app/views/static/css/base.css new file mode 100644 index 0000000..8c71428 --- /dev/null +++ b/sipi-app/views/static/css/base.css @@ -0,0 +1,32 @@ + +header { +background-color: green; +position: relative; +top: 0; +left: 0; +width: 100%; +margin-bottom: 10px; +} + +div.header-button{ +#background-color: red; +display: inline-block; +float: right; + +} +div.body{ +position: relative; + +} + +footer { +background-color: red; +position:fixed; +left: 0; +bottom:0; +width:100%; +text-align: right; + +} + + diff --git a/sipi-app/views/static/css/isp.css b/sipi-app/views/static/css/isp.css index f041709..e69de29 100644 --- a/sipi-app/views/static/css/isp.css +++ b/sipi-app/views/static/css/isp.css @@ -1,35 +0,0 @@ -h1 { - color: green; -} - -button { - background-color: gray; - width: 250px; - color: white; - text-align: left; -} -ping.button.button { - background-color: green; - width: 250px; - color: white; - text-align: left; -} - -td { - word-wrap:break-word; - border: 1px solid grey; -} - -table { - display: inline; - -} -.htmx-indicator{ - display:none; -} -.htmx-request .htmx-indicator{ - display:inline; -} -.htmx-request.htmx-indicator{ - display:inline; -} \ No newline at end of file diff --git a/sipi-app/views/static/js/htmx.js b/sipi-app/views/static/js/htmx.js new file mode 100644 index 0000000..834c677 --- /dev/null +++ b/sipi-app/views/static/js/htmx.js @@ -0,0 +1,5132 @@ +var htmx = (function() { + 'use strict' + + // Public API + const htmx = { + // Tsc madness here, assigning the functions directly results in an invalid TypeScript output, but reassigning is fine + /* Event processing */ + /** @type {typeof onLoadHelper} */ + onLoad: null, + /** @type {typeof processNode} */ + process: null, + /** @type {typeof addEventListenerImpl} */ + on: null, + /** @type {typeof removeEventListenerImpl} */ + off: null, + /** @type {typeof triggerEvent} */ + trigger: null, + /** @type {typeof ajaxHelper} */ + ajax: null, + /* DOM querying helpers */ + /** @type {typeof find} */ + find: null, + /** @type {typeof findAll} */ + findAll: null, + /** @type {typeof closest} */ + closest: null, + /** + * Returns the input values that would resolve for a given element via the htmx value resolution mechanism + * + * @see https://htmx.org/api/#values + * + * @param {Element} elt the element to resolve values on + * @param {HttpVerb} type the request type (e.g. **get** or **post**) non-GET's will include the enclosing form of the element. Defaults to **post** + * @returns {Object} + */ + values: function(elt, type) { + const inputValues = getInputValues(elt, type || 'post') + return inputValues.values + }, + /* DOM manipulation helpers */ + /** @type {typeof removeElement} */ + remove: null, + /** @type {typeof addClassToElement} */ + addClass: null, + /** @type {typeof removeClassFromElement} */ + removeClass: null, + /** @type {typeof toggleClassOnElement} */ + toggleClass: null, + /** @type {typeof takeClassForElement} */ + takeClass: null, + /** @type {typeof swap} */ + swap: null, + /* Extension entrypoints */ + /** @type {typeof defineExtension} */ + defineExtension: null, + /** @type {typeof removeExtension} */ + removeExtension: null, + /* Debugging */ + /** @type {typeof logAll} */ + logAll: null, + /** @type {typeof logNone} */ + logNone: null, + /* Debugging */ + /** + * The logger htmx uses to log with + * + * @see https://htmx.org/api/#logger + */ + logger: null, + /** + * A property holding the configuration htmx uses at runtime. + * + * Note that using a [meta tag](https://htmx.org/docs/#config) is the preferred mechanism for setting these properties. + * + * @see https://htmx.org/api/#config + */ + config: { + /** + * Whether to use history. + * @type boolean + * @default true + */ + historyEnabled: true, + /** + * The number of pages to keep in **localStorage** for history support. + * @type number + * @default 10 + */ + historyCacheSize: 10, + /** + * @type boolean + * @default false + */ + refreshOnHistoryMiss: false, + /** + * The default swap style to use if **[hx-swap](https://htmx.org/attributes/hx-swap)** is omitted. + * @type HtmxSwapStyle + * @default 'innerHTML' + */ + defaultSwapStyle: 'innerHTML', + /** + * The default delay between receiving a response from the server and doing the swap. + * @type number + * @default 0 + */ + defaultSwapDelay: 0, + /** + * The default delay between completing the content swap and settling attributes. + * @type number + * @default 20 + */ + defaultSettleDelay: 20, + /** + * If true, htmx will inject a small amount of CSS into the page to make indicators invisible unless the **htmx-indicator** class is present. + * @type boolean + * @default true + */ + includeIndicatorStyles: true, + /** + * The class to place on indicators when a request is in flight. + * @type string + * @default 'htmx-indicator' + */ + indicatorClass: 'htmx-indicator', + /** + * The class to place on triggering elements when a request is in flight. + * @type string + * @default 'htmx-request' + */ + requestClass: 'htmx-request', + /** + * The class to temporarily place on elements that htmx has added to the DOM. + * @type string + * @default 'htmx-added' + */ + addedClass: 'htmx-added', + /** + * The class to place on target elements when htmx is in the settling phase. + * @type string + * @default 'htmx-settling' + */ + settlingClass: 'htmx-settling', + /** + * The class to place on target elements when htmx is in the swapping phase. + * @type string + * @default 'htmx-swapping' + */ + swappingClass: 'htmx-swapping', + /** + * Allows the use of eval-like functionality in htmx, to enable **hx-vars**, trigger conditions & script tag evaluation. Can be set to **false** for CSP compatibility. + * @type boolean + * @default true + */ + allowEval: true, + /** + * If set to false, disables the interpretation of script tags. + * @type boolean + * @default true + */ + allowScriptTags: true, + /** + * If set, the nonce will be added to inline scripts. + * @type string + * @default '' + */ + inlineScriptNonce: '', + /** + * If set, the nonce will be added to inline styles. + * @type string + * @default '' + */ + inlineStyleNonce: '', + /** + * The attributes to settle during the settling phase. + * @type string[] + * @default ['class', 'style', 'width', 'height'] + */ + attributesToSettle: ['class', 'style', 'width', 'height'], + /** + * Allow cross-site Access-Control requests using credentials such as cookies, authorization headers or TLS client certificates. + * @type boolean + * @default false + */ + withCredentials: false, + /** + * @type number + * @default 0 + */ + timeout: 0, + /** + * The default implementation of **getWebSocketReconnectDelay** for reconnecting after unexpected connection loss by the event code **Abnormal Closure**, **Service Restart** or **Try Again Later**. + * @type {'full-jitter' | ((retryCount:number) => number)} + * @default "full-jitter" + */ + wsReconnectDelay: 'full-jitter', + /** + * The type of binary data being received over the WebSocket connection + * @type BinaryType + * @default 'blob' + */ + wsBinaryType: 'blob', + /** + * @type string + * @default '[hx-disable], [data-hx-disable]' + */ + disableSelector: '[hx-disable], [data-hx-disable]', + /** + * @type {'auto' | 'instant' | 'smooth'} + * @default 'smooth' + */ + scrollBehavior: 'instant', + /** + * If the focused element should be scrolled into view. + * @type boolean + * @default false + */ + defaultFocusScroll: false, + /** + * If set to true htmx will include a cache-busting parameter in GET requests to avoid caching partial responses by the browser + * @type boolean + * @default false + */ + getCacheBusterParam: false, + /** + * If set to true, htmx will use the View Transition API when swapping in new content. + * @type boolean + * @default false + */ + globalViewTransitions: false, + /** + * htmx will format requests with these methods by encoding their parameters in the URL, not the request body + * @type {(HttpVerb)[]} + * @default ['get', 'delete'] + */ + methodsThatUseUrlParams: ['get', 'delete'], + /** + * If set to true, disables htmx-based requests to non-origin hosts. + * @type boolean + * @default false + */ + selfRequestsOnly: true, + /** + * If set to true htmx will not update the title of the document when a title tag is found in new content + * @type boolean + * @default false + */ + ignoreTitle: false, + /** + * Whether the target of a boosted element is scrolled into the viewport. + * @type boolean + * @default true + */ + scrollIntoViewOnBoost: true, + /** + * The cache to store evaluated trigger specifications into. + * You may define a simple object to use a never-clearing cache, or implement your own system using a [proxy object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy) + * @type {Object|null} + * @default null + */ + triggerSpecsCache: null, + /** @type boolean */ + disableInheritance: false, + /** @type HtmxResponseHandlingConfig[] */ + responseHandling: [ + { code: '204', swap: false }, + { code: '[23]..', swap: true }, + { code: '[45]..', swap: false, error: true } + ], + /** + * Whether to process OOB swaps on elements that are nested within the main response element. + * @type boolean + * @default true + */ + allowNestedOobSwaps: true + }, + /** @type {typeof parseInterval} */ + parseInterval: null, + /** @type {typeof internalEval} */ + _: null, + version: '2.0.1' + } + // Tsc madness part 2 + htmx.onLoad = onLoadHelper + htmx.process = processNode + htmx.on = addEventListenerImpl + htmx.off = removeEventListenerImpl + htmx.trigger = triggerEvent + htmx.ajax = ajaxHelper + htmx.find = find + htmx.findAll = findAll + htmx.closest = closest + htmx.remove = removeElement + htmx.addClass = addClassToElement + htmx.removeClass = removeClassFromElement + htmx.toggleClass = toggleClassOnElement + htmx.takeClass = takeClassForElement + htmx.swap = swap + htmx.defineExtension = defineExtension + htmx.removeExtension = removeExtension + htmx.logAll = logAll + htmx.logNone = logNone + htmx.parseInterval = parseInterval + htmx._ = internalEval + + const internalAPI = { + addTriggerHandler, + bodyContains, + canAccessLocalStorage, + findThisElement, + filterValues, + swap, + hasAttribute, + getAttributeValue, + getClosestAttributeValue, + getClosestMatch, + getExpressionVars, + getHeaders, + getInputValues, + getInternalData, + getSwapSpecification, + getTriggerSpecs, + getTarget, + makeFragment, + mergeObjects, + makeSettleInfo, + oobSwap, + querySelectorExt, + settleImmediately, + shouldCancel, + triggerEvent, + triggerErrorEvent, + withExtensions + } + + const VERBS = ['get', 'post', 'put', 'delete', 'patch'] + const VERB_SELECTOR = VERBS.map(function(verb) { + return '[hx-' + verb + '], [data-hx-' + verb + ']' + }).join(', ') + + const HEAD_TAG_REGEX = makeTagRegEx('head') + + //= =================================================================== + // Utilities + //= =================================================================== + + /** + * @param {string} tag + * @param {boolean} global + * @returns {RegExp} + */ + function makeTagRegEx(tag, global = false) { + return new RegExp(`<${tag}(\\s[^>]*>|>)([\\s\\S]*?)<\\/${tag}>`, + global ? 'gim' : 'im') + } + + /** + * Parses an interval string consistent with the way htmx does. Useful for plugins that have timing-related attributes. + * + * Caution: Accepts an int followed by either **s** or **ms**. All other values use **parseFloat** + * + * @see https://htmx.org/api/#parseInterval + * + * @param {string} str timing string + * @returns {number|undefined} + */ + function parseInterval(str) { + if (str == undefined) { + return undefined + } + + let interval = NaN + if (str.slice(-2) == 'ms') { + interval = parseFloat(str.slice(0, -2)) + } else if (str.slice(-1) == 's') { + interval = parseFloat(str.slice(0, -1)) * 1000 + } else if (str.slice(-1) == 'm') { + interval = parseFloat(str.slice(0, -1)) * 1000 * 60 + } else { + interval = parseFloat(str) + } + return isNaN(interval) ? undefined : interval + } + + /** + * @param {Node} elt + * @param {string} name + * @returns {(string | null)} + */ + function getRawAttribute(elt, name) { + return elt instanceof Element && elt.getAttribute(name) + } + + /** + * @param {Element} elt + * @param {string} qualifiedName + * @returns {boolean} + */ + // resolve with both hx and data-hx prefixes + function hasAttribute(elt, qualifiedName) { + return !!elt.hasAttribute && (elt.hasAttribute(qualifiedName) || + elt.hasAttribute('data-' + qualifiedName)) + } + + /** + * + * @param {Node} elt + * @param {string} qualifiedName + * @returns {(string | null)} + */ + function getAttributeValue(elt, qualifiedName) { + return getRawAttribute(elt, qualifiedName) || getRawAttribute(elt, 'data-' + qualifiedName) + } + + /** + * @param {Node} elt + * @returns {Node | null} + */ + function parentElt(elt) { + const parent = elt.parentElement + if (!parent && elt.parentNode instanceof ShadowRoot) return elt.parentNode + return parent + } + + /** + * @returns {Document} + */ + function getDocument() { + return document + } + + /** + * @param {Node} elt + * @param {boolean} global + * @returns {Node|Document} + */ + function getRootNode(elt, global) { + return elt.getRootNode ? elt.getRootNode({ composed: global }) : getDocument() + } + + /** + * @param {Node} elt + * @param {(e:Node) => boolean} condition + * @returns {Node | null} + */ + function getClosestMatch(elt, condition) { + while (elt && !condition(elt)) { + elt = parentElt(elt) + } + + return elt || null + } + + /** + * @param {Element} initialElement + * @param {Element} ancestor + * @param {string} attributeName + * @returns {string|null} + */ + function getAttributeValueWithDisinheritance(initialElement, ancestor, attributeName) { + const attributeValue = getAttributeValue(ancestor, attributeName) + const disinherit = getAttributeValue(ancestor, 'hx-disinherit') + var inherit = getAttributeValue(ancestor, 'hx-inherit') + if (initialElement !== ancestor) { + if (htmx.config.disableInheritance) { + if (inherit && (inherit === '*' || inherit.split(' ').indexOf(attributeName) >= 0)) { + return attributeValue + } else { + return null + } + } + if (disinherit && (disinherit === '*' || disinherit.split(' ').indexOf(attributeName) >= 0)) { + return 'unset' + } + } + return attributeValue + } + + /** + * @param {Element} elt + * @param {string} attributeName + * @returns {string | null} + */ + function getClosestAttributeValue(elt, attributeName) { + let closestAttr = null + getClosestMatch(elt, function(e) { + return !!(closestAttr = getAttributeValueWithDisinheritance(elt, asElement(e), attributeName)) + }) + if (closestAttr !== 'unset') { + return closestAttr + } + } + + /** + * @param {Node} elt + * @param {string} selector + * @returns {boolean} + */ + function matches(elt, selector) { + // @ts-ignore: non-standard properties for browser compatibility + // noinspection JSUnresolvedVariable + const matchesFunction = elt instanceof Element && (elt.matches || elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector || elt.webkitMatchesSelector || elt.oMatchesSelector) + return !!matchesFunction && matchesFunction.call(elt, selector) + } + + /** + * @param {string} str + * @returns {string} + */ + function getStartTag(str) { + const tagMatcher = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i + const match = tagMatcher.exec(str) + if (match) { + return match[1].toLowerCase() + } else { + return '' + } + } + + /** + * @param {string} resp + * @returns {Document} + */ + function parseHTML(resp) { + const parser = new DOMParser() + return parser.parseFromString(resp, 'text/html') + } + + /** + * @param {DocumentFragment} fragment + * @param {Node} elt + */ + function takeChildrenFor(fragment, elt) { + while (elt.childNodes.length > 0) { + fragment.append(elt.childNodes[0]) + } + } + + /** + * @param {HTMLScriptElement} script + * @returns {HTMLScriptElement} + */ + function duplicateScript(script) { + const newScript = getDocument().createElement('script') + forEach(script.attributes, function(attr) { + newScript.setAttribute(attr.name, attr.value) + }) + newScript.textContent = script.textContent + newScript.async = false + if (htmx.config.inlineScriptNonce) { + newScript.nonce = htmx.config.inlineScriptNonce + } + return newScript + } + + /** + * @param {HTMLScriptElement} script + * @returns {boolean} + */ + function isJavaScriptScriptNode(script) { + return script.matches('script') && (script.type === 'text/javascript' || script.type === 'module' || script.type === '') + } + + /** + * we have to make new copies of script tags that we are going to insert because + * SOME browsers (not saying who, but it involves an element and an animal) don't + * execute scripts created in