Compare commits

...

7 Commits

Author SHA1 Message Date
Bernhard Radermacher
85f8e47bfb wip 2025-10-14 06:38:20 +00:00
Bernhard Radermacher
8a7bb1528c wip 2025-10-10 14:28:22 +00:00
Bernhard Radermacher
904e3d2adf config for Docker 2025-09-01 12:42:10 +00:00
Bernhard Radermacher
292e296f01 wip 2025-09-01 11:36:22 +00:00
Bernhard Radermacher (hakisto)
eb1d8d793c wip 2025-08-31 18:42:46 +02:00
Bernhard Radermacher (hakisto)
bd510016de wip 2025-08-31 07:53:17 +02:00
Bernhard Radermacher (hakisto)
5614ecbba6 contact working 2025-08-30 10:06:47 +02:00
33 changed files with 1063 additions and 102 deletions

12
.idea/runConfigurations/latest.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="latest" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
<deployment type="dockerfile">
<settings>
<option name="imageTag" value="docker.ctmapp.kiongroup.net/oms-db:latest" />
<option name="buildOnly" value="true" />
<option name="sourceFilePath" value="Dockerfile" />
</settings>
</deployment>
<method v="2" />
</configuration>
</component>

View File

@@ -2,12 +2,19 @@
<configuration default="false" name="sandboxapi" type="Python.FastAPI"> <configuration default="false" name="sandboxapi" type="Python.FastAPI">
<option name="additionalOptions" value="--reload --host 0.0.0.0 --port 1234" /> <option name="additionalOptions" value="--reload --host 0.0.0.0 --port 1234" />
<option name="file" value="$PROJECT_DIR$/app/main.py" /> <option name="file" value="$PROJECT_DIR$/app/main.py" />
<module name="sandboxapi" /> <module name="vpsx-fast" />
<option name="ENV_FILES" value="" /> <option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" /> <option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" /> <option name="PARENT_ENVS" value="true" />
<envs>
<env name="DB_HOST" value="mariadb.ctmapp.kiongroup.net" />
<env name="DB_PASSWORD" value="fast" />
<env name="DB_PORT" value="3306" />
<env name="DB_USER" value="fast" />
<env name="DB_DATABASE" value="fast_vpsx" />
</envs>
<option name="SDK_HOME" value="$PROJECT_DIR$/.venv/bin/python" /> <option name="SDK_HOME" value="$PROJECT_DIR$/.venv/bin/python" />
<option name="SDK_NAME" value="uv (sandboxapi)" /> <option name="SDK_NAME" value="uv (vpsx-fast)" />
<option name="WORKING_DIRECTORY" value="" /> <option name="WORKING_DIRECTORY" value="" />
<option name="IS_MODULE_SDK" value="false" /> <option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" /> <option name="ADD_CONTENT_ROOTS" value="true" />

23
Dockerfile Normal file
View File

@@ -0,0 +1,23 @@
FROM python:3
LABEL authors="Bernhard Radermacher"
RUN pip install fastapi mariadb pymysql passlib[bcrypt] pyjwt python-multipart sqlmodel uvicorn ldap3
ARG DB_USER="must be set"
ARG DB_PASSWORD="must be set"
ENV DB_USER=$DB_USER \
DB_PASSWORD=$DB_PASSWORD \
DB_HOST="mariadb.ctmapp.kiongroup.net" \
DB_PORT=3306 \
DB_DATABASE="fast_vpsx" \
LOG_LEVEL="INFO"
COPY app /
#ENTRYPOINT ["python", "main.py"]
#ENTRYPOINT ["/bin/bash"]
ENTRYPOINT ["/usr/local/bin/uvicorn", "main:app", "--host", "0.0.0.0"]

24
app/alchemy/base.plantuml Normal file
View File

@@ -0,0 +1,24 @@
@startuml
top to bottom direction
skinparam linetype ortho
package base {
abstract Base {
+ id\t\t\t\tInteger\t\t<<PK>> {field}
# __tablename__\tstr {field}
}
object to_snake_case {
name\tstr
}
object bidirectional_relationship {
cls\t\t\t\tclass
foreign_table_cls\tclass
}
}
@enduml

View File

@@ -18,8 +18,7 @@ def bidirectional_relationship(cls, foreign_table_cls):
column_name, column_name,
relationship(cls, relationship(cls,
back_populates=foreign_table_cls.__tablename__, back_populates=foreign_table_cls.__tablename__,
cascade="all, delete-orphan", cascade="all, delete-orphan"
collection_class=set,
) )
) )
@@ -38,7 +37,3 @@ class Base(DeclarativeBase):
return to_snake_case(cls.__name__) return to_snake_case(cls.__name__)
# noinspection PyPep8Naming

View File

@@ -0,0 +1,27 @@
@startuml
'!include base.plantuml
!include status.plantuml
!include user.plantuml
skinparam linetype polyline
class Contact {
{field} + code\tString(80)\t<<Unique>>
{field} + address\tString(255) | None
{field} + notes\tText | None
}
abstract ContactForeignKey {
contact_id\tContact.id {field}
+ contact\t\tContact {field}
}
'Base <|--Contact
StatusForeignKey <|-[#blue]- Contact
Versioned <|-[#blue]- Contact
Contact *-- ContactForeignKey
@enduml

25
app/alchemy/contact.puml Normal file
View File

@@ -0,0 +1,25 @@
@startuml
'!include base.plantuml
!include status.puml
!include user.puml
skinparam linetype polyline
entity contact {
* id\t\t\tint
--
* code\t\tvarchar(80)\t<<UK>>
address\t\tvarchar(255)
notes\t\ttext
status_id\t\tvarchar(3)\t<<FK>>
* _created__\tdatetime
_updated__\tdatetime
_user__id\t\tint\t\t\t<<FK>>
}
status --{ contact : status_id
user ..{ contact : _user__id
@enduml

View File

@@ -12,8 +12,8 @@ __all__ = ["Contact"]
class Contact(StatusForeignKey, Versioned, Base): class Contact(StatusForeignKey, Versioned, Base):
"""Contact""" """Contact"""
name: Mapped[str] = mapped_column(String(80), unique=True) code: Mapped[str] = mapped_column(String(80), unique=True)
address: Mapped[str | None] = mapped_column(String(253)) address: Mapped[str | None] = mapped_column(String(255))
notes: Mapped[str | None] = mapped_column(Text) notes: Mapped[str | None] = mapped_column(Text)

View File

@@ -0,0 +1,72 @@
@startuml
'!include base.plantuml
!include status.plantuml
!include contact.plantuml
skinparam linetype polyline
class Country {
{field} + code\t\tString(2)\t\t<<Unique>>
{field} + name\t\tString(80)
{field} + notes\t\tText | None
+ __repr__()\t\tstr
}
abstract CountryForeignKey {
country_id\tCountry.id {field}
+ country\t\tCountry {field}
}
class LocationCode {
{field} + code\t\tString(8)\t\t<<Unique>>
{field} + description\tString(80)
{field} + notes\t\tText | None
+ __repr__()\t\tstr
}
abstract LocationCodeForeignKey {
location_code_id\tLocationCode.id
+ location_code\t\tLocationCode
}
class Location {
{field} + code\t\tString(30)
{field} + description\tString(80)
{field} + notes\t\tText | None
{field} <<Unique>>\n\tlocation_code_id\n\tcode
+ __repr__()\t\tstr
}
abstract LocationForeignKey {
location_id\tLocation.id
+ location\t\tLocation
}
Versioned <|-[#blue]- Country
StatusForeignKey <|-[#blue]- Country
Country *-- CountryForeignKey
Versioned <|-[#blue]- LocationCode
StatusForeignKey <|-[#blue]- LocationCode
LocationCode *-- LocationCodeForeignKey
CountryForeignKey <|-[#blue]- LocationCode
ContactForeignKey o.. LocationCode
Versioned <|-[#blue]- Location
StatusForeignKey <|-[#blue]- Location
Location *-- LocationForeignKey
LocationCodeForeignKey <|-[#blue]- Location
ContactForeignKey o.. Location
@enduml

66
app/alchemy/location.puml Normal file
View File

@@ -0,0 +1,66 @@
@startuml
'!include base.plantuml
!include status.puml
!include contact.puml
skinparam linetype polyline
entity country {
* id\t\t\tint
--
* code\t\tvarchar(2)\t<<UK>>
name\t\tvarchar(80)
notes\t\ttext
status_id\t\tvarchar(3)\t<<FK>>
* _created__\tdatetime
_updated__\tdatetime
_user__id\t\tint\t\t\t<<FK>>
}
entity location_code {
* id\t\t\tint
--
* code\t\tvarchar(8)\t<<UK>>
description\tvarchar(80)
notes\t\ttext
* country_id\tint\t\t\t<<FK>>
contact_id\tint\t\t\t<<FK>>
status_id\t\tvarchar(3)\t<<FK>>
* _created__\tdatetime
_updated__\tdatetime
_user__id\t\tint\t\t\t<<FK>>
}
entity location {
* id\t\t\t\tint
.. <<UK>> ..
* location_code_id\tint\t\t\t<<FK>>
* code\t\t\tvarchar(30)
--
description\t\tvarchar(80)
notes\t\t\ttext
contact_id\tint\t\t\t<<FK>>
status_id\t\t\tvarchar(3)\t<<FK>>
* _created__\t\tdatetime
_updated__\t\tdatetime
_user__id\t\t\tint\t\t\t<<FK>>
}
contact ..{ location : contact_id
contact ..{ location_code : contact_id
country --{ location_code : country_id
location_code --{ location : location_code_id
status --{ country : status_id
status --{ location : status_id
status --{ location_code : status_id
user ..{ country : _user__id
user ..{ location : _user__id
user ..{ location_code : _user__id
@enduml

View File

@@ -1,3 +1,5 @@
from pydoc import describe
from sqlalchemy import String, Text, ForeignKey, event, select, UniqueConstraint from sqlalchemy import String, Text, ForeignKey, event, select, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, declared_attr, Session from sqlalchemy.orm import Mapped, mapped_column, declared_attr, Session
@@ -13,10 +15,12 @@ __all__ = ["Country", "LocationCode", "Location"]
class Country(StatusForeignKey, Versioned, Base): class Country(StatusForeignKey, Versioned, Base):
iso: Mapped[str] = mapped_column(String(2), unique=True) code: Mapped[str] = mapped_column(String(2), unique=True)
name: Mapped[str] = mapped_column(String(80)) name: Mapped[str] = mapped_column(String(80))
notes: Mapped[str | None] = mapped_column(Text) notes: Mapped[str | None] = mapped_column(Text)
def __repr__(self):
return f"Country(id={self.id!r}, code={self.code!r}, name={self.name!r}, notes={self.notes!r}, location_codes={self.location_codes!r})"
class CountryForeignKey: class CountryForeignKey:
@@ -32,14 +36,14 @@ class CountryForeignKey:
@event.listens_for(Country.__table__, "after_create") @event.listens_for(Country.__table__, "after_create")
def initialize_country(target, connection, **kwargs): def initialize_country(target, connection, **kwargs):
with Session(connection) as session: with Session(connection) as session:
qsys = session.scalar(select(User).where(User.username == "QSYS")) qsys = session.scalar(select(User).where(User.code == "QSYS"))
for kwargs in ( for kwargs in (
dict(iso="DE", name="Germany", status_id='A'), dict(code="DE", name="Germany", status_id='A'),
dict(iso="IT", name="Italy", status_id='A'), dict(code="IT", name="Italy", status_id='A'),
dict(iso="US", name="United States"), dict(code="US", name="United States"),
dict(iso="CA", name="Canada"), dict(code="CA", name="Canada"),
dict(iso="MX", name="Mexico"), dict(code="MX", name="Mexico"),
dict(iso="ES", name="Spain", status_id='A'), dict(code="ES", name="Spain", status_id='A'),
): ):
kwargs['_user__'] = qsys kwargs['_user__'] = qsys
session.add(Country(**kwargs)) session.add(Country(**kwargs))
@@ -50,9 +54,12 @@ class LocationCode(CountryForeignKey, ContactForeignKey, StatusForeignKey, Versi
"""Location Code""" """Location Code"""
code: Mapped[str] = mapped_column(String(8), unique=True) code: Mapped[str] = mapped_column(String(8), unique=True)
description: Mapped[str] = mapped_column(String(256)) description: Mapped[str | None] = mapped_column(String(80))
notes: Mapped[str | None] = mapped_column(Text) notes: Mapped[str | None] = mapped_column(Text)
def __repr__(self):
return f"LocationCode(id={self.id!r}, code={self.code!r}, description={self.description!r}, notes={self.notes!r}, locations={self.locations!r})"
class LocationCodeForeignKey: class LocationCodeForeignKey:
"""Foreign Key Mixin for :py:class:`LocationCode`""" """Foreign Key Mixin for :py:class:`LocationCode`"""
@@ -65,17 +72,35 @@ class LocationCodeForeignKey:
return bidirectional_relationship(cls, LocationCode) return bidirectional_relationship(cls, LocationCode)
# noinspection PyUnusedLocal
@event.listens_for(LocationCode.__table__, "after_create")
def initialize_location_code(target, connection, **kwargs):
with Session(connection) as session:
qsys = session.scalar(select(User).where(User.code == "QSYS"))
de = session.scalar(select(Country).where(Country.code == "DE"))
for kwargs in (
dict(country=de, code="DEHAM", description="Hamburg", status_id='A'),
dict(country=de, code="DEFRA", description="Frankfurt/Main", status_id='A'),
):
kwargs['_user__'] = qsys
session.add(LocationCode(**kwargs))
session.commit()
class Location(LocationCodeForeignKey, ContactForeignKey, StatusForeignKey, Versioned, Base): class Location(LocationCodeForeignKey, ContactForeignKey, StatusForeignKey, Versioned, Base):
"""Location""" """Location"""
location: Mapped[str] = mapped_column(String(30)) code: Mapped[str] = mapped_column(String(30))
description: Mapped[str] = mapped_column(String(256)) description: Mapped[str | None] = mapped_column(String(80))
notes: Mapped[str | None] = mapped_column(Text) notes: Mapped[str | None] = mapped_column(Text)
__table_args__ = ( __table_args__ = (
UniqueConstraint('location_code_id', location), UniqueConstraint('location_code_id', code),
) )
def __repr__(self):
return f"Location(id={self.id!r}, code={self.code!r}, description={self.description!r}, notes={self.notes!r})"
class LocationForeignKey: class LocationForeignKey:
"""Foreign Key Mixin for :py:class:`Location`""" """Foreign Key Mixin for :py:class:`Location`"""
@@ -86,3 +111,17 @@ class LocationForeignKey:
@declared_attr @declared_attr
def location(cls) -> Mapped["Location"]: def location(cls) -> Mapped["Location"]:
return bidirectional_relationship(cls, Location) return bidirectional_relationship(cls, Location)
# noinspection PyUnusedLocal
@event.listens_for(Location.__table__, "after_create")
def initialize_location(target, connection, **kwargs):
with Session(connection) as session:
qsys = session.scalar(select(User).where(User.code == "QSYS"))
deham = session.scalar(select(LocationCode).where(LocationCode.code == "DEHAM"))
for kwargs in (
dict(location_code=deham, code="Andy's Wohnung"),
):
kwargs['_user__'] = qsys
session.add(Location(**kwargs))
session.commit()

1
app/alchemy/location.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -53,7 +53,7 @@ class PrinterModelForeignKey:
class Printer(PrinterModelForeignKey, LocationForeignKey, ContactForeignKey, StatusForeignKey, Versioned, Base): class Printer(PrinterModelForeignKey, LocationForeignKey, ContactForeignKey, StatusForeignKey, Versioned, Base):
"""Printer""" """Printer"""
name: Mapped[str] = mapped_column(String(63), unique=True) code: Mapped[str] = mapped_column(String(63), unique=True)
description: Mapped[str] = mapped_column(String(256)) description: Mapped[str] = mapped_column(String(256))
notes: Mapped[str | None] = mapped_column(Text) notes: Mapped[str | None] = mapped_column(Text)
dns_name: Mapped[str | None] = mapped_column(String(253)) dns_name: Mapped[str | None] = mapped_column(String(253))

View File

@@ -0,0 +1,20 @@
@startuml
'!include base.plantuml
class Status {
id\t\tString(3)\t\t<<Primary>> {field}
+ name\tString(30)\t<<Unique>> {field}
}
'Base <|-- Status
abstract StatusForeignKey {
status_id\t\tStatus.id {field}
+ status\t\tStatus {field}
}
Status *-- StatusForeignKey
@enduml

9
app/alchemy/status.puml Normal file
View File

@@ -0,0 +1,9 @@
@startuml
entity status {
* id\t\tvarchar(3)
--
* name\tvarchar(30)\t<<UK>>
}
@enduml

31
app/alchemy/user.plantuml Normal file
View File

@@ -0,0 +1,31 @@
@startuml
'!include base.plantuml
!include status.plantuml
skinparam linetype ortho
class User {
{field} + code\t\tString(253)\t<<Unique>>
{field} + name\t\tString(253)
{field} + password\tString(255) | None
{field} + ldap_name\tString(255) | None
{field} + notes\t\tText | None
+ __repr__()\t\tstr
}
abstract Versioned {
{field} + user__\t\tUser
{field} # _created__\tDatetime
{field} # _updated__\tDatetime | None
{field} _user__id\t\tInteger | None
{field} # __versioned__\t= True
}
'Base <|-- User
StatusForeignKey <|-[#blue]- User
Versioned <|-[#blue]- User
User *-- Versioned
@enduml

25
app/alchemy/user.puml Normal file
View File

@@ -0,0 +1,25 @@
@startuml
!include status.puml
skinparam linetype polyline
entity user {
* id\t\t\tint
--
* code\t\tvarchar(255)\t<<UK>>
* name\t\tvarchar(255)
password\tvarchar(255)
ldap_name\tvarchar(255)
notes\t\ttext
status_id\t\tvarchar(3)\t<<FK>>
* _created__\tdatetime
_updated__\tdatetime
_user__id\t\tint\t\t\t<<FK>>
}
status --{ user : status_id
user ..{ user : _user__id
@enduml

View File

@@ -61,39 +61,37 @@ class Versioned:
# noinspection PyMethodParameters # noinspection PyMethodParameters
@declared_attr @declared_attr
def _user__(cls) -> Mapped["User"]: def user__(cls) -> Mapped["User"]:
return relationship() return relationship()
class User(StatusForeignKey, Versioned, Base): class User(StatusForeignKey, Versioned, Base):
"""User""" """User"""
username: Mapped[str] = mapped_column(String(253), unique=True) code: Mapped[str] = mapped_column(String(255), unique=True)
name: Mapped[str] = mapped_column(String(253)) name: Mapped[str] = mapped_column(String(255))
password: Mapped[str | None] = mapped_column(String(255)) password: Mapped[str | None] = mapped_column(String(255))
ldap_name: Mapped[str | None] = mapped_column(String(255)) ldap_name: Mapped[str | None] = mapped_column(String(255))
notes: Mapped[str | None] = mapped_column(Text) notes: Mapped[str | None] = mapped_column(Text)
def __repr__(self): def __repr__(self):
return f'User(id={self.id!r}, username={self.username!r} name={self.name!r}, notes={self.notes!r})' return f'User(id={self.id!r}, code={self.code!r} name={self.name!r}, notes={self.notes!r})'
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
@event.listens_for(User.__table__, "after_create") @event.listens_for(User.__table__, "after_create")
def initialize_user(target, connection, **kwargs): def initialize_user(target, connection, **kwargs):
from routers.user import get_password_hash from routers.user import get_password_hash
with Session(connection) as session: with Session(connection) as session:
qsys = User(username="QSYS", name="System User", notes="internal processing", status_id='X') qsys = User(code="QSYS", name="System User", notes="internal processing", status_id='X')
session.add(qsys) session.add(qsys)
session.commit() session.commit()
qsys = session.scalar(select(User).where(User.username == "QSYS")) qsys = session.scalar(select(User).where(User.code == "QSYS"))
qsys._user__id=qsys.id qsys._user__id=qsys.id
session.commit() session.commit()
for kwargs in ( for kwargs in (
dict(username="CTM", name="Control-M", password=get_password_hash("secret"), notes="user for automation"), dict(code="CTM", name="Control-M", password=get_password_hash("secret"), notes="user for automation"),
dict(username="exde37c8", name="Bernhard Radermacher", dict(code="de31c7", name="Andy Le", ldap_name="a0032514@kiongroup.com"),
password=get_password_hash("secret"), dict(code="exde37c8", name="Bernhard Radermacher", ldap_name="a0061806@kiongroup.com"),
ldap_name="a0061806@kiongroup.com",
),
): ):
kwargs.update(dict(status_id='A', _user__id=qsys.id)) kwargs.update(dict(status_id='A', _user__id=qsys.id))
session.add(User(**kwargs)) session.add(User(**kwargs))

View File

@@ -7,14 +7,10 @@ from .engine import engine
from .session import get_session from .session import get_session
from routers.user import get_current_active_user # from routers.user import get_current_active_user
ACTIVE_USER = Annotated[alchemy.User, Depends(get_current_active_user)]
__all__ = [ __all__ = [
'engine', 'engine',
'ACTIVE_USER',
'get_session', 'get_session',
] ]

View File

@@ -1,11 +1,22 @@
from sqlalchemy import create_engine import os
import sqlalchemy
# engine_url="sqlite+pysqlite:///vpsx.db" # # engine_url="sqlite+pysqlite:///vpsx.db"
engine_url="mariadb+pymysql://fast:fast@localhost/fast_vpsx?charset=utf8mb4" # engine_url="mariadb+pymysql://fast:fast@mariadb.ctmapp.kiongroup.net/fast_vpsx?charset=utf8mb4"
engine_url = sqlalchemy.URL.create(
drivername="mariadb+pymysql",
username=os.getenv("DB_USER"),
password=os.getenv("DB_PASSWORD"),
host=os.getenv("DB_HOST"),
port=int(os.getenv("DB_PORT")),
database=os.getenv("DB_DATABASE"),
query={'charset': 'utf8mb4'},
)
def get_engine(): def get_engine():
return create_engine(engine_url) return sqlalchemy.create_engine(engine_url)
engine = get_engine() engine = get_engine()

View File

@@ -1,13 +1,13 @@
import inspect
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing import Annotated
from fastapi.security import OAuth2PasswordRequestForm import fastapi
from fastapi import FastAPI
from app.alchemy import Base
from app.dependencies import engine
from fastapi import FastAPI, Depends, HTTPException
from starlette.middleware.cors import CORSMiddleware from starlette.middleware.cors import CORSMiddleware
from app import routers
from alchemy import Base
import routers
from dependencies import engine
@asynccontextmanager @asynccontextmanager
@@ -15,10 +15,10 @@ async def lifespan(app: FastAPI):
Base.metadata.create_all(engine) Base.metadata.create_all(engine)
yield yield
app = FastAPI(lifespan=lifespan) app = FastAPI(lifespan=lifespan)
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"]) app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"])
for i in inspect.getmembers(routers):
app.include_router(routers.status) if isinstance(i[1], fastapi.routing.APIRouter):
app.include_router(routers.user) app.include_router(i[1])

View File

@@ -1,2 +1,6 @@
from .contact import router as contact
# from .country import router as country
# from .location_code import router as location_code
from .status import router as status from .status import router as status
from .user import router as user from .user import router as user
from .location import router as location

102
app/routers/contact.py Normal file
View File

@@ -0,0 +1,102 @@
import sys
from typing import Annotated
from fastapi import APIRouter, Query, Depends
from sqlmodel import SQLModel, Field
from sqlmodel import select
import alchemy
import utils
from dependencies import get_session
from utils import update_item, create_item
from .user import ACTIVE_USER
from .models import Contact, ContactCreate, ContactUpdate
PRIMARY_ANNOTATION = utils.make_primary_annotation('Contact')
# ----------------------------------------------------------------
# Routes
router = APIRouter(prefix="/contact", tags=["contact"])
@router.get("/",
response_model=list[Contact])
async def get_contacts(
offset: int = 0,
limit: Annotated[int, Query] = 100,
session=Depends(get_session)):
"""Get list of all Contacts"""
return session.exec(
select(
alchemy.Contact
).offset(offset).limit(limit or sys.maxsize)).all()
# noinspection PyTypeHints
@router.get("/{contact}",
response_model=Contact,
responses={404: {"description": "Not found"}})
async def get_contact(
contact: PRIMARY_ANNOTATION,
session=Depends(get_session)):
return utils.get_single_record(session, alchemy.Contact, 'Contact', contact)
# noinspection PyTypeHints
@router.post("/",
response_model=Contact)
async def create_contact(
data: ContactCreate,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return create_item(
session=session,
cls=alchemy.Contact,
name='Contact',
current_user=current_user,
data=data)
@router.patch("/{contact}",
response_model=Contact,
responses={404: {"description": "Not found"}})
async def update_contact(
contact: PRIMARY_ANNOTATION,
data: ContactUpdate,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return update_item(
session=session,
current_user=current_user,
item=utils.get_single_record(session, alchemy.Contact, 'Contact', contact),
data=data)
@router.put("/{contact}/activate",
response_model=Contact,
responses={404: {"description": "Not found"}})
async def activate_contact(
contact: PRIMARY_ANNOTATION,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return utils.set_item_status(
session=session,
current_user=current_user,
item=utils.get_single_record(session, alchemy.Contact, 'Contact', contact),
status='A')
@router.put("/{contact}/deactivate",
response_model=Contact,
responses={404: {"description": "Not found"}})
async def deactivate_contact(
contact: PRIMARY_ANNOTATION,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return utils.set_item_status(
session=session,
current_user=current_user,
item=utils.get_single_record(session, alchemy.Contact, 'Contact', contact),
status='I')

246
app/routers/location.py Normal file
View File

@@ -0,0 +1,246 @@
import sys
from typing import Annotated
from fastapi import APIRouter, Query, Depends, HTTPException
# from sqlmodel import SQLModel, Field
from sqlmodel import select, and_
# from sqlalchemy import select
import alchemy
import utils
from dependencies import get_session
from utils import update_item
from .models.location import LocationCode, Country, LocationCreate, LocationCodeCreate, CountryCreate, CountryUpdate
from .user import ACTIVE_USER
PRIMARY_ANNOTATION = utils.make_primary_annotation('Country')
# ----------------------------------------------------------------
# Models
# class LocationBase(SQLModel):
# code: str = Field(max_length=30)
# description: str | None = Field(max_length=256)
# notes: str | None
#
#
# class LocationCodeBase(SQLModel):
# code: str = Field(max_length=8)
# description: str | None= Field(max_length=256)
# notes: str | None
#
#
# class CountryBase(SQLModel):
# code: str = Field(max_length=2)
# name: str = Field(max_length=80)
# notes: str | None
# class ILocationCreate(LocationBase):
# description: str | None = None
# notes: str | None = None
#
#
# class ILocationCodeCreate(LocationCode):
# description: str | None = None
# notes: str | None = None
# class LocationCreate(LocationBase):
# description: str | None = None
# notes: str | None
#
#
# class LocationCodeCreate(LocationCodeBase):
# description: str | None = None
# notes: str | None = None
# locations: list[LocationCreate] | None = None
#
#
# class CountryCreate(CountryBase):
# notes: str | None = None
# location_codes: list[LocationCodeCreate] | None = None
# class Country(CountryBase):
# id: int | None = Field(default=None, primary_key=True)
#
#
# class CountryCreate(CountryBase):
# notes: str | None = None
# class CountryUpdate(CountryBase):
# code: str | None = None
# name: str | None = None
# notes: str | None = None
# ----------------------------------------------------------------
# Routes
router = APIRouter(prefix="/location", tags=["location"])
@router.get("/", response_model=list[Country])
async def get_countries(
offset: int = 0,
limit: Annotated[int, Query] = 100,
session=Depends(get_session)):
"""Get list of all countries"""
if limit < 1:
limit = sys.maxsize
result = session.exec(select(alchemy.Country).offset(offset).limit(limit)).all()
return result
@router.get("/{country}",
response_model=Country,
responses={404: {"description": "Not found"}})
async def get_country(
country: PRIMARY_ANNOTATION,
session=Depends(get_session)):
return utils.get_single_record(session, alchemy.Country, country)
@router.post("/",
response_model=Country)
async def create_country(
country: CountryCreate,
current_user: ACTIVE_USER,
session=Depends(get_session)):
item = CountryCreate.model_validate(country).model_dump(exclude_unset=True)
if session.scalar(select(alchemy.Country).where(alchemy.Country.code == item['code'])):
raise HTTPException(status_code=422,
detail=[dict(msg=f"Country {item['code']!r} already exists",
type="Database Integrity Error")])
country = alchemy.Country(**{k: v for k, v in item.items() if k in ('code', 'name', 'notes')})
country.code = country.code.upper()
country._user__id = current_user.id
for i in item.get('location_codes', []):
location_code = alchemy.LocationCode(**{k: v for k, v in i.items() if k in ('code', 'name', 'notes')})
location_code.code = location_code.code.upper()
location_code._user__id = current_user.id
for j in i.get('locations', []):
location = alchemy.Location(**j)
location._user__id = current_user.id
location_code.locations.append(location)
country.location_codes.append(location_code)
session.add(country)
try:
session.commit()
except Exception as exc:
raise HTTPException(status_code=422,
detail=[dict(msg=', '.join(exc.args),
type="Database Error")])
session.refresh(country)
return country
@router.patch("/{country}",
response_model=Country,
responses={404: {"description": "Not found"}})
async def update_country(
country: PRIMARY_ANNOTATION,
data: CountryUpdate,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return update_item(
session=session,
current_user=current_user,
item=utils.get_single_record(session, alchemy.Country, 'Country', country),
data=data)
@router.put("/{country}/activate",
response_model=Country,
responses={404: {"description": "Not found"}})
async def activate_country(
country: PRIMARY_ANNOTATION,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return utils.set_item_status(
session=session,
current_user=current_user,
item=utils.get_single_record(session, alchemy.Country, 'Country', country),
status='A')
@router.put("/{country}/deactivate",
response_model=Country,
responses={404: {"description": "Not found"}})
async def deactivate_country(
country: PRIMARY_ANNOTATION,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return utils.set_item_status(
session=session,
current_user=current_user,
item=utils.get_single_record(session, alchemy.Country, 'Country', country),
status='I')
@router.post("/{country}",
response_model=LocationCode, )
async def create_location_code(
country: PRIMARY_ANNOTATION,
data: LocationCodeCreate,
current_user: ACTIVE_USER,
session=Depends(get_session)):
item = LocationCodeCreate.model_validate(data).model_dump(exclude_unset=True)
if session.scalar(select(alchemy.LocationCode).where(alchemy.LocationCode.code == data.code)):
raise HTTPException(status_code=422,
detail=[dict(msg=f"Location Code {data.code!r} already exists",
type="Database Integrity Error")])
country = utils.get_single_record(session, alchemy.Country, 'Country', country)
location_code = alchemy.LocationCode(**{k: v for k, v in item.items() if k in ('code', 'description', 'notes')}, country=country)
location_code._user__id = current_user.id
for i in item.get('locations', []):
location = alchemy.Location(**i)
location._user__id = current_user.id
location_code.locations.append(location)
session.add(location_code)
try:
session.commit()
except Exception as exc:
raise HTTPException(status_code=422,
detail=[dict(msg=', '.join(exc.args),
type="Database Error")])
session.refresh(location_code)
return location_code
@router.post("/{country}/{location_code}",
response_model=LocationCode, )
async def create_location(
country: PRIMARY_ANNOTATION,
location_code: PRIMARY_ANNOTATION,
data: LocationCode,
current_user: ACTIVE_USER,
session=Depends(get_session)):
location_code = utils.get_single_record(session, alchemy.Country, 'Country', country)
item = LocationCreate.model_validate(data).model_dump(exclude_unset=True)
if session.scalar(select(alchemy.LocationCode).where(
and_(alchemy.Location.location_code == location_code,
alchemy.Location.code == data.code))):
raise HTTPException(status_code=422,
detail=[dict(msg=f"Location '{location_code.code}/{data.code}' already exists",
type="Database Integrity Error")])
location = alchemy.Location(**{k: v for k, v in item.items() if k in ('code', 'description', 'notes')}, location_code=location_code)
location._user__id = current_user.id
session.add(location)
try:
session.commit()
except Exception as exc:
raise HTTPException(status_code=422,
detail=[dict(msg=', '.join(exc.args),
type="Database Error")])
session.refresh(location_code)
return location_code

View File

@@ -0,0 +1,5 @@
from .contact import Contact, ContactCreate, ContactUpdate
from .location import Country, CountryCreate, CountryUpdate, LocationCode, LocationCodeCreate, Location, LocationCreate
from .status import Status

View File

@@ -0,0 +1,23 @@
from typing import Annotated
from pydantic import BaseModel, Field
from .status import Status
class Contact(BaseModel):
id: int
code: str
address: str | None
notes: str | None
status: Status | None
class ContactCreate(BaseModel):
code: str = Field(max_length=80)
address: Annotated[str | None, Field(max_length=253)] = None
notes: str | None = None
class ContactUpdate(ContactCreate):
code: Annotated[str | None, Field(max_length=80)] = None

View File

@@ -0,0 +1,66 @@
from typing import Annotated
from pydantic import BaseModel, Field
from .contact import Contact
from .status import Status
class Location(BaseModel):
id: int
code: str
description: str | None
notes: str | None
status: Status
contact: Contact | None
class LocationCreate(BaseModel):
code: str = Field(max_length=30)
description: Annotated[str | None, Field(max_length=80)] = None
notes: str | None = None
class LocationUpdate(LocationCreate):
code: Annotated[str | None, Field(max_length=30)] = None
class LocationCode(BaseModel):
id: int
code: str
description: str | None
notes: str | None
status: Status
locations: list[Location]
contact: Contact | None
class LocationCodeCreate(BaseModel):
code: str = Field(max_length=8)
description: Annotated[str | None, Field(max_length=80)] = None
notes: str | None = None
locations: list[LocationCreate] | None = None
class Country(BaseModel):
id: int
code: str
name: str
notes: str | None
status: Status
location_codes: list[LocationCode] | None
class CountryCreate(BaseModel):
code: str = Field(max_length=2)
name: str = Field(max_length=80)
notes: str | None = None
location_codes: list[LocationCodeCreate] | None = None
class CountryUpdate(BaseModel):
code: str | None = Field(default=None, max_length=2)
name: str | None = Field(default=None, max_length=80)
notes: str | None = None

View File

@@ -0,0 +1,6 @@
from pydantic import BaseModel
class Status(BaseModel):
id: str
name: str

View File

@@ -1,21 +1,11 @@
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import select, Session from sqlmodel import select, Session
from dependencies import get_session, ACTIVE_USER import alchemy
from routers.status_model import Status from dependencies import get_session
from routers.models import Status
router = APIRouter( router = APIRouter(prefix="/status", tags=["status"])
prefix="/status",
tags=["status"],
)
@router.get("/", response_model=list[Status])
async def get_statuses(
current_user: ACTIVE_USER,
session=Depends(get_session)):
"""Get list of all statuses"""
return session.exec(select(Status)).all()
def _get_status(status: str, session: Session): def _get_status(status: str, session: Session):
@@ -26,9 +16,19 @@ def _get_status(status: str, session: Session):
raise HTTPException(status_code=404, detail=f"Status {status!r} not found") raise HTTPException(status_code=404, detail=f"Status {status!r} not found")
return result return result
@router.get("/{status}", responses={404: {"description": "Not found"}})
@router.get("/",
response_model=list[Status])
async def get_statuses(
session=Depends(get_session)):
"""Get list of all statuses"""
return session.exec(select(alchemy.Status)).all()
@router.get("/{status}",
response_model=Status,
responses={404: {"description": "Not found"}})
async def get_status( async def get_status(
status: str, status: str,
current_user: ACTIVE_USER,
session=Depends(get_session)): session=Depends(get_session)):
return _get_status(status, session) return _get_status(status, session)

View File

@@ -1,6 +0,0 @@
from sqlmodel import SQLModel, Field
class Status(SQLModel):
id: str | None = Field(max_length=3, primary_key=True)
name: str = Field(max_length=30, unique=True)

View File

@@ -1,5 +1,6 @@
import datetime import datetime
import logging import logging
import sys
from typing import Annotated from typing import Annotated
import jwt import jwt
@@ -10,11 +11,12 @@ from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jwt.exceptions import InvalidTokenError from jwt.exceptions import InvalidTokenError
from passlib.context import CryptContext from passlib.context import CryptContext
from pydantic import BaseModel from pydantic import BaseModel
from sqlmodel import Session, SQLModel, Field, select from sqlmodel import SQLModel, Field, Session, select
import alchemy import alchemy
import utils
from dependencies import get_session from dependencies import get_session
from .status_model import Status from routers.models import Status
logging.getLogger('passlib').setLevel(logging.ERROR) logging.getLogger('passlib').setLevel(logging.ERROR)
@@ -51,7 +53,7 @@ class TokenData(BaseModel):
class UserBase(SQLModel): class UserBase(SQLModel):
username: str = Field(max_length=253, unique=True) code: str = Field(max_length=253, unique=True)
name: str = Field(max_length=253) name: str = Field(max_length=253)
password: str | None = Field(max_length=255) password: str | None = Field(max_length=255)
ldap_name: str | None = Field(max_length=255) ldap_name: str | None = Field(max_length=255)
@@ -74,7 +76,7 @@ class UserPublic(UserBase):
class UserUpdate(UserBase): class UserUpdate(UserBase):
username: str | None = None code: str | None = None
name: str | None = None name: str | None = None
password: str | None = None password: str | None = None
ldap_name: str | None = None ldap_name: str | None = None
@@ -93,10 +95,10 @@ def _authenticate_user(
username: str, username: str,
password: str, password: str,
session: sqlalchemy.orm.Session) -> alchemy.User | None: session: sqlalchemy.orm.Session) -> alchemy.User | None:
user = session.scalar(select(alchemy.User).where(alchemy.User.username == username)) user = session.scalar(select(alchemy.User).where(alchemy.User.code == username))
if user is None: if user is None:
return None return None
if user.username == 'QSYS' and password == 'joshua5': if user.code == 'QSYS' and password == 'josua5':
return user return user
if user.password is not None: if user.password is not None:
if not _verify_password(password, user.password): if not _verify_password(password, user.password):
@@ -122,10 +124,7 @@ def _create_access_token(
def _get_user( def _get_user(
user_id: str, user_id: str,
session: Session) -> alchemy.User: session: Session) -> alchemy.User:
result = session.get(alchemy.User, user_id) if user_id.isnumeric() else session.scalar( result = utils.get_single_record(session, alchemy.User, user_id)
select(alchemy.User).where(alchemy.User.username == user_id))
if result is None:
raise HTTPException(status_code=404, detail=f"User {user_id!r} not found")
result.password = None if result.password is None else '********' result.password = None if result.password is None else '********'
return result return result
@@ -139,7 +138,7 @@ def _process_login(
raise HTTPException(status_code=400, raise HTTPException(status_code=400,
detail="Incorrect username or password", detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"}) headers={"WWW-Authenticate": "Bearer"})
access_token = _create_access_token(data={"sub": user.username}) access_token = _create_access_token(data={"sub": user.code})
return Token(access_token=access_token, token_type="bearer") return Token(access_token=access_token, token_type="bearer")
@@ -166,7 +165,7 @@ async def _get_current_user(
if username is None: if username is None:
raise CREDENTIALS_EXCEPTION raise CREDENTIALS_EXCEPTION
user = session.scalar(select(alchemy.User).where(alchemy.User.username == username)) user = utils.get_single_record(session, alchemy.User, 'User', username)
if user is None: if user is None:
raise CREDENTIALS_EXCEPTION raise CREDENTIALS_EXCEPTION
@@ -189,19 +188,20 @@ router = APIRouter(prefix="/user", tags=["user"])
@router.post("/login") @router.post("/login")
async def login_user( async def login_user(
credentials: Credentials, credentials: Credentials,
session=Depends(get_session), session=Depends(get_session)) -> Token:
) -> Token:
return _process_login(credentials.username, credentials.password, session) return _process_login(credentials.username, credentials.password, session)
@router.get("/", response_model=list[UserPublic]) @router.get("/",
response_model=list[UserPublic])
async def get_users( async def get_users(
current_user: Annotated[alchemy.User, Depends(get_current_active_user)], current_user: Annotated[alchemy.User, Depends(get_current_active_user)],
offset: int = 0, offset: int = 0,
limit: Annotated[int, Query(le=100)] = 100, limit: Annotated[int, Query] = 100,
session=Depends(get_session), session=Depends(get_session)):
):
"""Get list of users""" """Get list of users"""
if limit < 1:
limit = sys.maxsize
result = session.exec(select(alchemy.User).where(alchemy.User.status_id != 'X').offset(offset).limit(limit)).all() result = session.exec(select(alchemy.User).where(alchemy.User.status_id != 'X').offset(offset).limit(limit)).all()
for item in result: for item in result:
if item.password is not None: if item.password is not None:
@@ -209,7 +209,9 @@ async def get_users(
return result return result
@router.get("/{user_id}", response_model=UserPublic) @router.get("/{user_id}",
response_model=UserPublic,
responses={404: {"description": "Not found"}})
async def get_user( async def get_user(
user_id: Annotated[str, Path(description='User, either id (int) or name')], user_id: Annotated[str, Path(description='User, either id (int) or name')],
current_user: Annotated[alchemy.User, Depends(get_current_active_user)], current_user: Annotated[alchemy.User, Depends(get_current_active_user)],
@@ -218,12 +220,17 @@ async def get_user(
return _get_user(user_id, session) return _get_user(user_id, session)
@router.post("/", response_model=UserPublic) @router.post("/",
response_model=UserPublic)
async def create_user( async def create_user(
user: UserCreate, user: UserCreate,
current_user: Annotated[alchemy.User, Depends(get_current_active_user)], current_user: Annotated[alchemy.User, Depends(get_current_active_user)],
session=Depends(get_session)): session=Depends(get_session)):
model_user = User.model_validate(user) model_user = User.model_validate(user)
if session.scalar(select(alchemy.User).where(alchemy.User.code == model_user.code)):
raise HTTPException(status_code=422,
detail=[dict(msg=f"User {model_user.code} already exists",
type="IntegrityError")])
db_user = alchemy.User(**model_user.model_dump()) db_user = alchemy.User(**model_user.model_dump())
if db_user.password is not None: if db_user.password is not None:
db_user.password = get_password_hash(db_user.password) db_user.password = get_password_hash(db_user.password)
@@ -234,7 +241,9 @@ async def create_user(
return db_user return db_user
@router.patch("/{user_id}", response_model=UserPublic) @router.patch("/{user_id}",
response_model=UserPublic,
responses={404: {"description": "Not found"}})
async def update_user( async def update_user(
user_id: Annotated[str, Path(description='User, either id (int) or name')], user_id: Annotated[str, Path(description='User, either id (int) or name')],
user: UserUpdate, user: UserUpdate,
@@ -242,17 +251,29 @@ async def update_user(
session=Depends(get_session)): session=Depends(get_session)):
db_user = _get_user(user_id, session) db_user = _get_user(user_id, session)
user_data = user.model_dump(exclude_unset=True) user_data = user.model_dump(exclude_unset=True)
if ('code' in user_data and
session.scalar(select(alchemy.User).where(alchemy.User.code == user_data['code'])) != db_user):
raise HTTPException(status_code=422,
detail=[dict(msg=f"User {user_data['code']} already exists",
type="IntegrityError")])
if 'password' in user_data: if 'password' in user_data:
user_data['password'] = get_password_hash(user_data['password']) user_data['password'] = get_password_hash(user_data['password'])
for item in user_data: for item in user_data:
setattr(db_user, item, user_data[item]) setattr(db_user, item, user_data[item])
db_user._user__id = current_user.id db_user._user__id = current_user.id
session.commit() try:
session.commit()
except Exception as exc:
raise HTTPException(status_code=422,
detail=[dict(msg=', '.join(exc.args),
type="Database Error")])
session.refresh(db_user) session.refresh(db_user)
return db_user return db_user
@router.put("/{user_id}/activate", response_model=UserPublic) @router.put("/{user_id}/activate",
response_model=UserPublic,
responses={404: {"description": "Not found"}})
async def activate_user( async def activate_user(
user_id: Annotated[str, Path(description='User, either id (int) or name')], user_id: Annotated[str, Path(description='User, either id (int) or name')],
current_user: Annotated[alchemy.User, Depends(get_current_active_user)], current_user: Annotated[alchemy.User, Depends(get_current_active_user)],
@@ -260,12 +281,19 @@ async def activate_user(
db_user = _get_user(user_id, session) db_user = _get_user(user_id, session)
db_user.status_id = 'A' db_user.status_id = 'A'
db_user._user__id = current_user.id db_user._user__id = current_user.id
session.commit() try:
session.commit()
except Exception as exc:
raise HTTPException(status_code=422,
detail=[dict(msg=', '.join(exc.args),
type="Database Error")])
session.refresh(db_user) session.refresh(db_user)
return db_user return db_user
@router.put("/{user_id}/deactivate", response_model=UserPublic) @router.put("/{user_id}/deactivate",
response_model=UserPublic,
responses={404: {"description": "Not found"}})
async def deactivate_user( async def deactivate_user(
user_id: Annotated[str, Path(description='User, either id (int) or name')], user_id: Annotated[str, Path(description='User, either id (int) or name')],
current_user: Annotated[alchemy.User, Depends(get_current_active_user)], current_user: Annotated[alchemy.User, Depends(get_current_active_user)],
@@ -273,7 +301,12 @@ async def deactivate_user(
db_user = _get_user(user_id, session) db_user = _get_user(user_id, session)
db_user.status_id = 'I' db_user.status_id = 'I'
db_user._user__id = current_user.id db_user._user__id = current_user.id
session.commit() try:
session.commit()
except Exception as exc:
raise HTTPException(status_code=422,
detail=[dict(msg=', '.join(exc.args),
type="Database Error")])
session.refresh(db_user) session.refresh(db_user)
return db_user return db_user
@@ -284,3 +317,6 @@ async def login_for_access_token(
session=Depends(get_session), session=Depends(get_session),
) -> Token: ) -> Token:
return _process_login(form_data.username, form_data.password, session) return _process_login(form_data.username, form_data.password, session)
ACTIVE_USER = Annotated[alchemy.User, Depends(get_current_active_user)]

71
app/utils/__init__.py Normal file
View File

@@ -0,0 +1,71 @@
from typing import Annotated
from fastapi import Path, HTTPException
from sqlalchemy import select
def make_primary_annotation(name=str):
return Annotated[str, Path(description=f'{name}, either id (int) or code')]
def get_single_record(session, cls, name: str, code: str):
result = session.get(cls, code)
if result is None:
result = session.scalar(select(cls).where(cls.code == code))
if result is None:
raise HTTPException(
status_code=404,
detail=f"{name} {code!r} not found.")
return result
def set_item_status(session, current_user, item, status: str):
item.status_id = status
item._user__id = current_user.id
try:
session.commit()
except Exception as exc:
raise HTTPException(status_code=422,
detail=[dict(msg=', '.join(exc.args),
type="Database Error")])
session.refresh(item)
return item
def update_item(session, current_user, item, data):
for k, v in data.model_dump(exclude_unset=True).items():
if k == 'code':
r = session.scalar(select(item.__class__).where(item.__class__.code == v))
if r and r != item:
raise HTTPException(status_code=422,
detail=[dict(msg=f"{item.__class__.__name__} {v!r} already exists",
type="Integrity Error")])
setattr(item, k, v)
item._user__id = current_user.id
try:
session.commit()
except Exception as exc:
raise HTTPException(status_code=422,
detail=[dict(msg=', '.join(exc.args),
type="Database Error")])
session.refresh(item)
return item
def create_item(session, cls, name, current_user, data):
item = cls(**data.model_dump())
if session.scalar(select(cls).where(cls.code == item.code)):
raise HTTPException(status_code=422,
detail=[dict(msg=f"{name} {item.code!r} already exists",
type="Integrity Error")])
item._user__id = current_user.id
session.add(item)
try:
session.commit()
except Exception as exc:
raise HTTPException(status_code=422,
detail=[dict(msg=', '.join(exc.args),
type="Database Error")])
session.refresh(item)
return item

View File

@@ -0,0 +1,27 @@
= Add User
:toc:
:icons: font
== Purpose
Add user to system.
== Required Information
[%autowidth]
[cols="l,a"]
|===
|Item |Description
|code
|Identifies the user, sometimes called _user id_ or _username_. This _should_ be the same as the respective *Windows Domain User ID*
|name
|Name of user for display.
|ldap_name
|Identity of user in LDAP to allow logon using *Windows Domain Password*.
|notes
|(optional) Internal notes.
|===