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/
__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.odm.enums import SortDirection
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')
@@ -10,7 +14,21 @@ V = TypeVar('V')
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):
router = APIRouter()
@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__))
async def read_id(id: PydanticObjectId) -> model_read:
item = await model.get(id)
return item
return model_read(**item.dict())
@router.get("/", response_description="{} records retrieved".format(model.__name__))
async def read_list() -> List[model_read]:
item = await model.find_all().to_list()
return item
@router.get("/", response_model=Page[model_read], response_description="{} records retrieved".format(model.__name__))
async def read_list(size: int = 50, page: int = 1, sort_by: str = None, query: str = None) -> Page[model_read]:
sort = parse_sort(sort_by)
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__))
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)
return item
return model_read(**item.dict())
@router.delete("/{id}", response_description="{} record deleted from the database".format(model.__name__))
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__)
}
add_pagination(router)
return router

View File

@@ -18,3 +18,6 @@ class Entity(Document):
address: str
created_at: datetime = Field(default=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 .models import Entity, EntityType
from ..core.schemas import Writer
class EntityRead(Entity):
pass
class EntityCreate(BaseModel):
class EntityCreate(Writer):
type: EntityType
name: str
address: str

View File

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

View File

@@ -1,10 +1,14 @@
import uuid
from typing import Any, Dict, Generic, Optional
from bson import ObjectId
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"
@@ -66,6 +70,44 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
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)):
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 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 fastapi import Depends
from beanie import PydanticObjectId
from fastapi import APIRouter, HTTPException
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 .manager import get_user_manager
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)
from .manager import get_user_manager, get_current_user, get_auth_router
router = APIRouter()
@@ -51,10 +19,16 @@ async def create(user: UserCreate, user_manager=Depends(get_user_manager)) -> di
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")
async def read_id(id: PydanticObjectId) -> UserRead:
user = await User.get(id)
return user
return UserRead(**user.dict())
@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_db_beanie==1.1.2
motor==3.1.1
fastapi-paginate==0.1.0
uvicorn

View File

@@ -23,7 +23,7 @@ export function TranslateHttpLoaderFactory(http: HttpClient) {
}
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';
@NgModule({
@@ -49,7 +49,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
],
providers: [
{ 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,
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: '404', component: Error404Component },
{ path: '500', component: Error500Component },
{ path: 'clients', loadChildren: () => import('./clients/clients.module').then(m => m.ClientsModule) },
],
},
{

View File

@@ -1,5 +1,11 @@
{
"menu": [
{
"route": "clients/list",
"name": "clients",
"type": "link",
"icon": "group"
},
{
"route": "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-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;
}
}
}