List and Read on frontend, fullecrud on back

This commit is contained in:
2023-01-10 18:48:47 +01:00
parent 399b52a272
commit 0b8a93b256
32 changed files with 623 additions and 51 deletions

2
.gitignore vendored
View File

@@ -1,2 +1,2 @@
.idea/ .idea/
__pycache__ */__pycache__

160
back/.gitignore vendored Normal file
View File

@@ -0,0 +1,160 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

View File

@@ -1,7 +1,11 @@
from beanie import PydanticObjectId from beanie import PydanticObjectId
from beanie.odm.enums import SortDirection
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from typing import TypeVar, List, Generic from fastapi_paginate import Page, Params, add_pagination
from fastapi_paginate.ext.motor import paginate
from typing import TypeVar, List, Generic, Any, Dict
T = TypeVar('T') T = TypeVar('T')
@@ -10,7 +14,21 @@ V = TypeVar('V')
W = TypeVar('W') W = TypeVar('W')
def parse_sort(sort_by):
fields = []
for field in sort_by.split(','):
dir, col = field.split('(')
fields.append((col[:-1], 1 if dir == 'asc' else -1))
return fields
def parse_query(query) -> Dict[Any, Any]:
return {}
def get_crud_router(model, model_create, model_read, model_update): def get_crud_router(model, model_create, model_read, model_update):
router = APIRouter() router = APIRouter()
@router.post("/", response_description="{} added to the database".format(model.__name__)) @router.post("/", response_description="{} added to the database".format(model.__name__))
@@ -22,12 +40,17 @@ def get_crud_router(model, model_create, model_read, model_update):
@router.get("/{id}", response_description="{} record retrieved".format(model.__name__)) @router.get("/{id}", response_description="{} record retrieved".format(model.__name__))
async def read_id(id: PydanticObjectId) -> model_read: async def read_id(id: PydanticObjectId) -> model_read:
item = await model.get(id) item = await model.get(id)
return item return model_read(**item.dict())
@router.get("/", response_description="{} records retrieved".format(model.__name__)) @router.get("/", response_model=Page[model_read], response_description="{} records retrieved".format(model.__name__))
async def read_list() -> List[model_read]: async def read_list(size: int = 50, page: int = 1, sort_by: str = None, query: str = None) -> Page[model_read]:
item = await model.find_all().to_list() sort = parse_sort(sort_by)
return item query = parse_query(query)
# limit=limit, skip=offset,
collection = model.get_motor_collection()
items = paginate(collection, query, Params(**{'size': size, 'page': page}), sort=sort)
return await items
@router.put("/{id}", response_description="{} record updated".format(model.__name__)) @router.put("/{id}", response_description="{} record updated".format(model.__name__))
async def update(id: PydanticObjectId, req: model_update) -> model_read: async def update(id: PydanticObjectId, req: model_update) -> model_read:
@@ -44,7 +67,7 @@ def get_crud_router(model, model_create, model_read, model_update):
) )
await item.update(update_query) await item.update(update_query)
return item return model_read(**item.dict())
@router.delete("/{id}", response_description="{} record deleted from the database".format(model.__name__)) @router.delete("/{id}", response_description="{} record deleted from the database".format(model.__name__))
async def delete(id: PydanticObjectId) -> dict: async def delete(id: PydanticObjectId) -> dict:
@@ -61,4 +84,7 @@ def get_crud_router(model, model_create, model_read, model_update):
"message": "{} deleted successfully".format(model.__name__) "message": "{} deleted successfully".format(model.__name__)
} }
add_pagination(router)
return router return router

View File

@@ -18,3 +18,6 @@ class Entity(Document):
address: str address: str
created_at: datetime = Field(default=datetime.utcnow(), nullable=False) created_at: datetime = Field(default=datetime.utcnow(), nullable=False)
updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False) updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
#
# class Settings:
# name = "entities"

View File

@@ -4,13 +4,14 @@ from datetime import datetime
from pydantic import BaseModel from pydantic import BaseModel
from .models import Entity, EntityType from .models import Entity, EntityType
from ..core.schemas import Writer
class EntityRead(Entity): class EntityRead(Entity):
pass pass
class EntityCreate(BaseModel): class EntityCreate(Writer):
type: EntityType type: EntityType
name: str name: str
address: str address: str

View File

@@ -1,5 +1,4 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi import Depends, Request
from .contract import contract_router from .contract import contract_router
from .db import init_db from .db import init_db

View File

@@ -1,10 +1,14 @@
import uuid import uuid
from typing import Any, Dict, Generic, Optional from typing import Any, Dict, Generic, Optional
from bson import ObjectId
from fastapi import Depends from fastapi import Depends
from fastapi_users import BaseUserManager, UUIDIDMixin, models, exceptions, schemas from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin, models, exceptions, schemas
from fastapi_users.authentication import BearerTransport, AuthenticationBackend
from fastapi_users.authentication.strategy.db import AccessTokenDatabase, DatabaseStrategy
from .models import User, get_user_db, AccessToken, get_access_token_db
from .models import User, get_user_db
SECRET = "SECRET" SECRET = "SECRET"
@@ -66,6 +70,44 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
return created_user return created_user
def parse_id(self, value: Any) -> uuid.UUID:
if isinstance(value, ObjectId):
return value
if isinstance(value, uuid.UUID):
return value
try:
return uuid.UUID(value)
except ValueError as e:
raise exceptions.InvalidID() from e
async def get_user_manager(user_db=Depends(get_user_db)): async def get_user_manager(user_db=Depends(get_user_db)):
yield UserManager(user_db) yield UserManager(user_db)
def get_database_strategy(
access_token_db: AccessTokenDatabase[AccessToken] = Depends(get_access_token_db),
) -> DatabaseStrategy:
return DatabaseStrategy(access_token_db, lifetime_seconds=3600)
bearer_transport = BearerTransport(tokenUrl="auth/jwt/login")
auth_backend = AuthenticationBackend(
name="db",
transport=bearer_transport,
get_strategy=get_database_strategy,
)
fastapi_users = FastAPIUsers[User, uuid.UUID](
get_user_manager,
[auth_backend],
)
get_current_user = fastapi_users.current_user(active=True)
def get_auth_router():
return fastapi_users.get_auth_router(auth_backend)

View File

@@ -1,45 +1,13 @@
import uuid from fastapi import Depends
from typing import Optional
from fastapi import Depends, Request
from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin, models, exceptions
from fastapi_users.authentication import BearerTransport, AuthenticationBackend
from fastapi_users.authentication.strategy.db import AccessTokenDatabase, DatabaseStrategy
from beanie import PydanticObjectId from beanie import PydanticObjectId
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from typing import List from typing import List
from .models import User, AccessToken, get_user_db, get_access_token_db from .models import User
from .schemas import UserRead, UserUpdate, UserCreate from .schemas import UserRead, UserUpdate, UserCreate
from .manager import get_user_manager from .manager import get_user_manager, get_current_user, get_auth_router
def get_database_strategy(
access_token_db: AccessTokenDatabase[AccessToken] = Depends(get_access_token_db),
) -> DatabaseStrategy:
return DatabaseStrategy(access_token_db, lifetime_seconds=3600)
bearer_transport = BearerTransport(tokenUrl="auth/jwt/login")
auth_backend = AuthenticationBackend(
name="db",
transport=bearer_transport,
get_strategy=get_database_strategy,
)
fastapi_users = FastAPIUsers[User, uuid.UUID](
get_user_manager,
[auth_backend],
)
def get_auth_router():
return fastapi_users.get_auth_router(auth_backend)
router = APIRouter() router = APIRouter()
@@ -51,10 +19,16 @@ async def create(user: UserCreate, user_manager=Depends(get_user_manager)) -> di
return {"message": "User added successfully"} return {"message": "User added successfully"}
@router.get("/me", response_description="User record retrieved")
async def read_me(user=Depends(get_current_user)) -> UserRead:
user = await User.get(user.id)
return UserRead(**user.dict())
@router.get("/{id}", response_description="User record retrieved") @router.get("/{id}", response_description="User record retrieved")
async def read_id(id: PydanticObjectId) -> UserRead: async def read_id(id: PydanticObjectId) -> UserRead:
user = await User.get(id) user = await User.get(id)
return user return UserRead(**user.dict())
@router.get("/", response_model=List[UserRead], response_description="User records retrieved") @router.get("/", response_model=List[UserRead], response_description="User records retrieved")

View File

@@ -2,4 +2,5 @@ fastapi==0.88.0
fastapi_users==10.2.1 fastapi_users==10.2.1
fastapi_users_db_beanie==1.1.2 fastapi_users_db_beanie==1.1.2
motor==3.1.1 motor==3.1.1
fastapi-paginate==0.1.0
uvicorn uvicorn

View File

@@ -23,7 +23,7 @@ export function TranslateHttpLoaderFactory(http: HttpClient) {
} }
import { LoginService } from '@core/authentication/login.service'; import { LoginService } from '@core/authentication/login.service';
import { FakeLoginService } from './fake-login.service'; import { ChtloginService } from '@core/../custom/chtlogin.service';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({ @NgModule({
@@ -49,7 +49,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
], ],
providers: [ providers: [
{ provide: BASE_URL, useValue: environment.baseUrl }, { provide: BASE_URL, useValue: environment.baseUrl },
{ provide: LoginService, useClass: FakeLoginService }, // <= Remove it in the real APP { provide: LoginService, useClass: ChtloginService }, // <= Remove it in the real APP
httpInterceptorProviders, httpInterceptorProviders,
appInitializerProviders, appInitializerProviders,
], ],

View File

@@ -0,0 +1,41 @@
import { Injectable } from '@angular/core';
import { HttpHeaders, HttpParams } from '@angular/common/http';
import { Token, User } from '../core/authentication/interface';
import { Menu } from '@core';
import { map } from 'rxjs/operators';
import { LoginService } from '../core/authentication/login.service'
@Injectable({
providedIn: 'root',
})
export class ChtloginService extends LoginService {
login(username: string, password: string, rememberMe = false) {
const body = new HttpParams()
.set('username', username)
.set('password', password);
return this.http.post<Token>('/api/v1/auth/login', body.toString(), {
headers: new HttpHeaders()
.set('Content-Type', 'application/x-www-form-urlencoded')
});
}
refresh(params: Record<string, any>) {
return this.http.post<Token>('/api/v1/auth/refresh', params);
}
logout() {
return this.http.post<any>('/api/v1/auth/logout', {});
}
me() {
return this.http.get<User>('/api/v1/users/me');
}
menu() {
return this.http
.get<{ menu: Menu[] }>('assets/data/menu.json?_t=' + Date.now())
.pipe(map(res => res.menu));
}
}

View File

@@ -0,0 +1,28 @@
<page-header></page-header>
<button routerLink="../../list" mat-fab extended>
<mat-icon>arrow_back</mat-icon>
Back
</button>
<form [formGroup]="cardForm" (ngSubmit)="onSubmit()">
<mat-form-field appearance="fill" formControlName="name">
<mat-label>Name</mat-label>
<input matInput [value]="this.item.name">
</mat-form-field>
<mat-form-field appearance="fill" formControlName="address">
<mat-label>Address</mat-label>
<input matInput [value]="this.item.address">
</mat-form-field>
<mat-form-field appearance="fill" formControlName="type">
<mat-label>Type</mat-label>
<mat-select [value]="this.item.type">
<mat-option value="individual">Particulier</mat-option>
<mat-option value="corporation">Entreprise</mat-option>
<mat-option value="institution">Institution</mat-option>
</mat-select>
</mat-form-field>
<button class="button" type="submit">Modifier</button>
</form>

View File

@@ -0,0 +1,25 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { ClientsCardComponent } from './card.component';
describe('ClientsCardComponent', () => {
let component: ClientsCardComponent;
let fixture: ComponentFixture<ClientsCardComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ClientsCardComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ClientsCardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,48 @@
import { Component, OnInit, Input } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ClientsService } from '../clients.service'
export interface Client {
_id: string;
name: string;
address: string;
type: string;
}
@Component({
selector: 'app-clients-card',
templateUrl: './card.component.html',
styleUrls: ['./card.component.css']
})
export class ClientsCardComponent implements OnInit {
@Input() id: string = "";
item: Client = {_id: '', name: '', address: '', type: ''};
cardForm = this.formBuilder.group({
name: '',
address: '',
type: ''
});
constructor(private formBuilder: FormBuilder, private clientsService: ClientsService) {
if (this.id == "") {
const url_parts = window.location.href.split('/')
this.id = url_parts[url_parts.length - 1];
}
this.item = {_id: '', name: '', address: '', type: ''}
}
ngOnInit() {
this.getData();
}
private getData() {
this.clientsService.get(this.id).subscribe((data: any) => {
this.item = data;
});
}
onSubmit(): void {
}
}

View File

@@ -0,0 +1,14 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ClientsListComponent } from './list/list.component';
import { ClientsCardComponent } from './card/card.component';
const routes: Routes = [{ path: 'list', component: ClientsListComponent },
{ path: 'card/:id', component: ClientsCardComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ClientsRoutingModule { }

View File

@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { SharedModule } from '@shared/shared.module';
import { ClientsRoutingModule } from './clients-routing.module';
import { ClientsListComponent } from './list/list.component';
import { ClientsCardComponent } from './card/card.component';
const COMPONENTS: any[] = [ClientsListComponent, ClientsCardComponent];
const COMPONENTS_DYNAMIC: any[] = [];
@NgModule({
imports: [
SharedModule,
ClientsRoutingModule
],
declarations: [
...COMPONENTS,
...COMPONENTS_DYNAMIC
]
})
export class ClientsModule { }

View File

@@ -0,0 +1,23 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import {Client} from "./list/list.component";
@Injectable({
providedIn: 'root'
})
export class ClientsService {
constructor(private http: HttpClient) { }
public getList(page: number, size: number, sortColumn: string, sortDirection: string) {
return this.http.get<{ menu: Client[] }>(
`/api/v1/entity/?size=${size}&page=${page + 1}&sort_by=${sortDirection}(${sortColumn})`
);
}
public get(id: string) {
return this.http.get<{ menu: Client[] }>(
`/api/v1/entity/${id}`
);
}
}

View File

@@ -0,0 +1,43 @@
<page-header></page-header>
<mat-form-field>
<mat-label>Filter</mat-label>
<input matInput (keyup)="applyFilter($event)" placeholder="Ex. Mia" #input>
</mat-form-field>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8 items" matSort (matSortChange)="handleSortChange($event)">
<ng-container matColumnDef="_id">
<th mat-header-cell *matHeaderCellDef mat-sort-header> ID </th>
<td mat-cell *matCellDef="let item"> {{item._id}} </td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Nom </th>
<td mat-cell *matCellDef="let item"> {{item.name}} </td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Type </th>
<td mat-cell *matCellDef="let item"> {{item.type}} </td>
</ng-container>
<ng-container matColumnDef="address">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Adresse </th>
<td mat-cell *matCellDef="let item"> {{item.address}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row
routerLink="../card/{{row._id}}"
*matRowDef="let row; columns: displayedColumns;"
></tr>
</table>
<mat-paginator (page)="handlePageEvent($event)"
[pageSizeOptions]="[5, 10, 15, 25, 50]"
[pageSize]="pageSize"
[length]="length"
showFirstLastButtons
aria-label="Select page">
</mat-paginator>

View File

@@ -0,0 +1,25 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { ClientsListComponent } from './list.component';
describe('ClientsListComponent', () => {
let component: ClientsListComponent;
let fixture: ComponentFixture<ClientsListComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ClientsListComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ClientsListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,84 @@
import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort, Sort} from '@angular/material/sort';
import { Injectable } from '@angular/core';
import { Menu } from "@core";
import { BehaviorSubject, forkJoin, fromEvent, Observable } from "rxjs";
import { map, take } from "rxjs/operators";
import { ClientsService } from '../clients.service'
import {Location} from '@angular/common';
import { Router } from '@angular/router';
export interface Client {
_id: string;
name: string;
address: string;
type: string;
}
@Component({
selector: 'app-clients-list',
templateUrl: './list.component.html',
styleUrls: ['./list.component.css']
})
export class ClientsListComponent implements OnInit {
displayedColumns: string[] = ['type', 'name', 'address', '_id',];
dataSource: Client[] = [];
length: number = 0;
pageIndex: number = 0;
pageSize: number = 15;
sortColumn: string = "name";
sortDirection: string = "asc";
//@ViewChild(MatSort) sort: MatSort;
constructor(private location: Location, private clientsService: ClientsService, private router: Router) { }
ngAfterViewInit() {
}
ngOnInit() {
this.getData();
}
handlePageEvent(e: PageEvent) {
this.length = e.length;
this.pageSize = e.pageSize;
this.pageIndex = e.pageIndex;
this.getData();
}
handleSortChange(s: Sort) {
this.sortColumn = s.active;
this.sortDirection = s.direction;
this.getData();
}
handleClickRow(row: any) {
window.location.href=window.location.href.replace('list', `card/${row._id}`)
this.router.navigateByUrl("card");
}
private getData() {
this.clientsService.getList(this.pageIndex, this.pageSize, this.sortColumn, this.sortDirection).subscribe((data: any) => {
this.dataSource = data.items;
this.length = data.total;
this.pageSize = data.size;
this.pageIndex = data.page - 1;
});
}
applyFilter(event: Event) {
// const filterValue = (event.target as HTMLInputElement).value;
// this.dataSource.filter = filterValue.trim().toLowerCase();
//
// if (this.dataSource.paginator) {
// this.dataSource.paginator.firstPage();
// }
}
}

View File

@@ -24,6 +24,7 @@ const routes: Routes = [
{ path: '403', component: Error403Component }, { path: '403', component: Error403Component },
{ path: '404', component: Error404Component }, { path: '404', component: Error404Component },
{ path: '500', component: Error500Component }, { path: '500', component: Error500Component },
{ path: 'clients', loadChildren: () => import('./clients/clients.module').then(m => m.ClientsModule) },
], ],
}, },
{ {

View File

@@ -1,5 +1,11 @@
{ {
"menu": [ "menu": [
{
"route": "clients/list",
"name": "clients",
"type": "link",
"icon": "group"
},
{ {
"route": "dashboard", "route": "dashboard",
"name": "dashboard", "name": "dashboard",

View File

@@ -34,5 +34,13 @@ http {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name; proxy_set_header X-Forwarded-Host $server_name;
} }
location /ng-cli-ws {
proxy_pass http://docker-front ;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
} }
} }