mazdik / ng-mazdik

Angular UI component library
https://mazdik.github.io/ng-mazdik
MIT License
89 stars 34 forks source link

FLASK API Service not working #11

Closed bklingDt closed 5 years ago

bklingDt commented 5 years ago

I am having trouble getting the demo to connect to a FLASK API service. I replaced the data in the "players.json" file with my test data, so i know my table formatting is good because the table loads fine from the json file, it just wont connect to my api.

I created a new "RestlessService.ts" and call that from "basic-demo.components.ts", tried to follow your suggested code in the readme as best i could.

The page loads when i do "npm start" but i get a http error pop up:

`
TypeError: Cannot read property 'toLowerCase' of undefined
    at HttpXsrfInterceptor.push../node_modules/@angular/common/fesm5/http.js.HttpXsrfInterceptor.intercept (http.js:1721)
    at HttpInterceptorHandler.push../node_modules/@angular/common/fesm5/http.js.HttpInterceptorHandler.handle (http.js:1135)
    at HttpInterceptingHandler.push../node_modules/@angular/common/fesm5/http.js.HttpInterceptingHandler.handle (http.js:1772)
    at MergeMapSubscriber.project (http.js:975)
    at MergeMapSubscriber.push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapSubscriber._tryNext (mergeMap.js:61)
    at MergeMapSubscriber.push../node_modules/rxjs/_esm5/internal/operators/mergeMap.js.MergeMapSubscriber._next (mergeMap.js:51)
    at MergeMapSubscriber.push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.next (Subscriber.js:54)
    at Observable._subscribe (scalar.js:5)
    at Observable.push../node_modules/rxjs/_esm5/internal/Observable.js.Observable._trySubscribe (Observable.js:43)
    at Observable.push../node_modules/rxjs/_esm5/internal/Observable.js.Observable.subscribe (Observable.js:29)
push../src/ng-crud-table/services/restless.service.ts.RestlessService.handleError @ restless.service.ts:111
push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke @ zone.js:391
onInvoke @ core.js:17289
push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke @ zone.js:390
push../node_modules/zone.js/dist/zone.js.Zone.run @ zone.js:150
(anonymous) @ zone.js:889
push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask @ zone.js:423
onInvokeTask @ core.js:17280
push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask @ zone.js:422
push../node_modules/zone.js/dist/zone.js.Zone.runTask @ zone.js:195
drainMicroTaskQueue @ zone.js:601
Promise.then (async)
scheduleMicroTask @ zone.js:584
push../node_modules/zone.js/dist/zone.js.ZoneDelegate.scheduleTask @ zone.js:413
push../node_modules/zone.js/dist/zone.js.Zone.scheduleTask @ zone.js:238
push../node_modules/zone.js/dist/zone.js.Zone.scheduleMicroTask @ zone.js:258
scheduleResolveOrReject @ zone.js:879
ZoneAwarePromise.then @ zone.js:1012
push../node_modules/@angular/core/fesm5/core.js.PlatformRef.bootstrapModule @ core.js:17793
./src/main.ts @ main.ts:11
__webpack_require__ @ bootstrap:78
0 @ tree-table-module.ts:20
__webpack_require__ @ bootstrap:78
checkDeferredModules @ bootstrap:45
webpackJsonpCallback @ bootstrap:32
(anonymous) @ main.js:1
`

basic-demo.component.ts

`
import {Component, OnInit} from '@angular/core';
import {Column, CdtSettings, DataManager, RestlessService} from '../../ng-crud-table';
//import {DemoService} from './demo.service';
import {PlayersComponent} from './RestlessService';
import {getColumnsPlayers} from './columns';
import {SelectItem} from '../../lib/common';
import {DtMessages} from '../../lib/dt-translate';

@Component({
  selector: 'app-basic-demo',
  template: `<app-crud-table [dataManager]="dataManager"></app-crud-table>`,
  providers: [RestlessService]
})

export class BasicDemoComponent implements OnInit {

  columns: Column[];
  dataManager: DataManager;

  settings: CdtSettings = <CdtSettings>{
    crud: true,
    bodyHeight: 380,
    exportAction: true,
    globalFilter: true,
    columnToggleAction: true,
  };

  messages: DtMessages = <DtMessages>{
    titleDetailView: 'Customer details',
    titleCreate: 'Create a new Customer'
  };

  constructor(private service: RestlessService) {
    this.columns = getColumnsPlayers();
    this.columns[4].filterValues = this.filterValuesFunc;
    this.dataManager = new DataManager(this.columns, this.settings, this.service, this.messages);
    this.dataManager.pager.perPage = 20;
  }

  ngOnInit() {
  }

  filterValuesFunc(columnName: string): Promise<SelectItem[]> {
    return new Promise((resolve) => {
      setTimeout(() => resolve(
        [
          {id: 'MALE', name: 'MALE'},
          {id: 'FEMALE', name: 'FEMALE'},
        ]
      ), 1000);
    });
  }

}

`

RestlessService.ts

`
import {Component}  from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Column, CdtSettings, DataSource, RestlessService, DataManager} from '../../ng-crud-table/';
import {Validators} from '../../lib/validation/validators';
import { NotifyService } from 'src/lib/notify';

@Component({
  selector: 'my-app',
  template: `<app-crud-table [dataManager]="dataManager"></app-crud-table>
  <app-notify></app-notify>`,
  providers: [RestlessService]
})

export class PlayersComponent {

    dataManager: DataManager;
    service: RestlessService

    constructor(private http: HttpClient) {
      // YiiService | RestlessService | OrdsService | your custom service

      this.service = new RestlessService(this.http, new NotifyService());
      this.service.url = 'http://127.0.0.1:5000/api/customers';
      this.service.primaryKeys = ['custID'];
      this.dataManager = new DataManager(this.columns, this.settings, this.service);
    }

    columns: Column[] = [
        {
          title: 'custID',
          name: 'custID',

          sortable: true,
          filter: true,
          //frozen: true,
          //width: 100,
          //formHidden: true,
          type: 'number',
        },
        {
          title: 'custName',
          name: 'custName',
          sortable: true,
          filter: true,
          //validatorFunc: Validators.get({required: true, minLength: 2, pattern: '^[a-zA-Z ]+$'}),
          editable: true,
        },
        {
          title: 'pcc',
          name: 'pcc',
          sortable: true,
          filter: true,
          //validatorFunc: Validators.get({required: true, minLength: 2, pattern: '^[a-zA-Z ]+$'}),
          editable: true,
        },
        {
          title: 'gds',
          name: 'gds',
          sortable: true,
          filter: true,
          //validatorFunc: Validators.get({required: true, minLength: 2, pattern: '^[a-zA-Z ]+$'}),
          editable: true,
        },
        {
          title: 'businessUnit',
          name: 'businessUnit',
          sortable: true,
          filter: true,
          //validatorFunc: Validators.get({required: true, minLength: 2, pattern: '^[a-zA-Z ]+$'}),
          editable: true,
        },
        {
          title: 'subBusinessUnit',
          name: 'subBusinessUnit',
          sortable: true,
          filter: true,
          //validatorFunc: Validators.get({required: true, minLength: 2, pattern: '^[a-zA-Z ]+$'}),
          editable: true,

    ];

    settings: CdtSettings = {
        crud: true,
        tableWidth: 820,
        bodyHeight: 380,
        multipleSort: true
    };

}
mazdik commented 5 years ago

1) The RestlessService.ts file should be called players.component.ts 2) The component does not need to be imported into another component. You must import into app.module.ts (declarations and ROUTES) 3) providers: [RestlessService] - not needed, because a new instance of new RestlessService () is being created

bklingDt commented 5 years ago

Thank you, i am now able to get the data to load through FLASK API :)

Only issue now is that update/delete do not work, i get 405 METHOD NOT ALLOWED.

DELETE http://127.0.0.1:5000/api/customers/1 405 (METHOD NOT ALLOWED)
restless.service.ts:111 HttpErrorResponse {headers: HttpHeaders, status: 405, statusText: "METHOD NOT ALLOWED", url: "http://127.0.0.1:5000/api/customers/1", ok: false, …}error: "<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">↵<title>405 Method Not Allowed</title>↵<h1>Method Not Allowed</h1>↵<p>The method is not allowed for the requested URL.</p>↵"headers: HttpHeaders {normalizedNames: Map(0), lazyUpdate: null, lazyInit: ƒ}message: "Http failure response for http://127.0.0.1:5000/api/customers/1: 405 METHOD NOT ALLOWED"name: "HttpErrorResponse"ok: falsestatus: 405statusText: "METHOD NOT ALLOWED"url: "http://127.0.0.1:5000/api/customers/1"__proto__: HttpResponseBase

The DB user the api is connected with has db_datareader, db_datawriter, db_owner, public so DB user should have sufficient rights to update/add/delete records. Is this an issue with my FLASK API setting or something in ng-crud-table i need to adjust?

mazdik commented 5 years ago

Try testing the put and delete methods in restless.service.ts. Check {headers: this.headers}. Maybe you need a custom service. for example: ` this.service.put(row).then(res => {});

this.service.delete(row).then(res => {}); `

bklingDt commented 5 years ago

I have all of the operations working now except update (PUT).

This is the error i am getting when i try to make an update.

restless.service.ts:115 
HttpErrorResponse {headers: HttpHeaders, status: 400, statusText: "BAD REQUEST", url: "http://127.0.0.1:5000/api/customers/1", ok: false, …}
error:
message: "Model does not have field '$$uid'"
__proto__: Object
headers: HttpHeaders
lazyInit: ƒ ()
lazyUpdate: null
normalizedNames: Map(0) {}
__proto__: Object
message: "Http failure response for http://127.0.0.1:5000/api/customers/1: 400 BAD REQUEST"
name: "HttpErrorResponse"
ok: false
status: 400
statusText: "BAD REQUEST"
url: "http://127.0.0.1:5000/api/customers/1"
__proto__: HttpResponseBase
push../src/ng-crud-table/services/restless.service.ts.RestlessService.handleError   @   restless.service.ts:115
push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke    @   zone.js:391
onInvoke    @   core.js:17289
mazdik commented 5 years ago

"Model does not have field '$$uid'" Need to remove extra fields

def remove_extra_keys(subject, **kwargs):
    mapper = inspect(subject)
    columns = [column.key for column in mapper.attrs]
    for key, value in kwargs.copy().items():
        if key not in columns:
            kwargs.pop(key, None)
        kwargs[key] = value
    return kwargs

subject - is db.Model

mazdik commented 5 years ago

instead

    constructor(private http: HttpClient) {
      this.service = new RestlessService(this.http, new NotifyService());
    constructor(private http: HttpClient, private notifyService: NotifyService) {
      this.service = new RestlessService(this.http, this.notifyService);
bklingDt commented 5 years ago

Your code to remove the extra fields, that is python and needs to be on my python code? I tried adding but does not fix problem.

#Restless.py File

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_restless import APIManager

app = Flask(__name__)
app.config.from_pyfile('config1.cfg')
db = SQLAlchemy(app)

#Define API for customers table
class customers(db.Model):
    __table_args__ = {"schema":"cust"}
    custID = db.Column(db.Integer, primary_key=True)
    custName = db.Column(db.String(50))
    businessUnit = db.Column(db.String(50))
    subBusinessUnit = db.Column(db.String(50))

subject = db.Model
def remove_extra_keys(subject, **kwargs):
    mapper = inspect(subject)
    columns = [column.key for column in mapper.attrs]
    for key, value in kwargs.copy().items():
        if key not in columns:
            kwargs.pop(key, None)
        kwargs[key] = value
    return kwargs

manager = APIManager(app, flask_sqlalchemy_db=db)

#Create the API Connections
manager.create_api(customers,methods=['GET','PUSH','POST','DELETE','PATCH', 'PUT'])

#Run App
if __name__ == "__main__":
    app.run(debug=True)

When i view the request payload in console of chrome i see this weird $$UID part of the body which i did not see on POST/DELETE requests, those were just the data fields. So why would i need to add code to the python API if it seems ng-crud-table is building a bad request body when using PUT command only?

Sorry if this is such a basic question this is my first exposure to angular/JS/python.

Greatly appreciate your help!!!

bklingDt commented 5 years ago

Payload for a POST that was successful:

{dk: "77777TEST", custName: "TEST", pcc: "TST", gds: "SABRE", businessUnit: "NONE",…}
businessUnit: "NONE"
custName: "TEST"
dk: "77777TEST"
gds: "SABRE"
inCompleat: "0"
pcc: "TST"
subBusinessUnit: "TEST"

Payload for update (PUT) failure:```


{businessUnit: "NONE", custID: 788, custName: "TEST", dk: "77777TEST2", gds: "SABRE", inCompleat: 0,…}
$$data: {businessUnit: "NONE", custID: 788, custName: "TEST", dk: "77777TEST", gds: "SABRE", inCompleat: 0,…}
$$uid: 10
businessUnit: "NONE"
custID: 788
custName: "TEST"
dk: "77777TEST"
gds: "SABRE"
inCompleat: 0
pcc: "TST"
subBusinessUnit: "TEST"
$$index: 10
$$uid: 10
businessUnit: "NONE"
custID: 788
custName: "TEST"
dk: "77777TEST2"
gds: "SABRE"
inCompleat: 0
pcc: "TST"
subBusinessUnit: "TEST"

Not sure why this extra $$data, $$index and $$uid in the PUT request that seems to be causing the error.

bklingDt commented 5 years ago

There are also some other small bugs that i think are related to the API connection.

customers.component.ts file:

import {Component}  from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Column, CdtSettings, DataSource, RestlessService, DataManager} from '../ng-crud-table';
import {Validators} from '../lib/validation/validators';
import { NotifyService } from 'src/lib/notify';

@Component({
  selector: 'my-app',
  template: `
  <app-crud-table [dataManager]="dataManager"></app-crud-table>
  <app-notify></app-notify>
  `
})

export class CustomersComponent {

    dataManager: DataManager;
    service: RestlessService

    constructor(private http: HttpClient, private notifyService: NotifyService) {
      // YiiService | RestlessService | OrdsService | your custom service

      this.service = new RestlessService(this.http, this.notifyService);
      this.service.url = 'http://127.0.0.1:5000/api/customers';
      this.service.primaryKeys = ['custID'];
      this.dataManager = new DataManager(this.columns, this.settings, this.service);
    }

    columns: Column[] = [

      {
          title: 'custID',
          name: 'custID',

          sortable: true,
          filter: true,
          editable: false,
          //frozen: true,
          width: 60,
          formHidden: true,
          //type: 'number',
        },
        {
          title: 'Account Number',
          name: 'dk',
          sortable: true,
          filter: true,
          editable: true,
        },
        {
          title: 'Customer Name',
          name: 'custName',
          sortable: true,
          filter: true,
          width: 500,
          //validatorFunc: Validators.get({required: true, minLength: 2, pattern: '^[a-zA-Z ]+$'}),
          editable: true,
        },
        {
          title: 'pcc',
          name: 'pcc',
          sortable: true,
          filter: true,
          //validatorFunc: Validators.get({required: true, minLength: 2, pattern: '^[a-zA-Z ]+$'}),
          editable: true,
        },
        {
          title: 'gds',
          name: 'gds',
          sortable: true,
          filter: true,
          //validatorFunc: Validators.get({required: true, minLength: 2, pattern: '^[a-zA-Z ]+$'}),
          editable: true,
        },
        {
          title: 'businessUnit',
          name: 'businessUnit',
          sortable: true,
          filter: true,
          //validatorFunc: Validators.get({required: true, minLength: 2, pattern: '^[a-zA-Z ]+$'}),
          editable: true,
        },
        {
          title: 'subBusinessUnit',
          name: 'subBusinessUnit',
          sortable: true,
          filter: true,
          //validatorFunc: Validators.get({required: true, minLength: 2, pattern: '^[a-zA-Z ]+$'}),
          editable: true,
        },
        {
          title: 'inCompleat',
          name: 'inCompleat',
          sortable: true,
          filter: true,
          //validatorFunc: Validators.get({required: true, minLength: 2, pattern: '^[a-zA-Z ]+$'}),
          editable: true,
        }
    ];

    settings: CdtSettings = {
        crud: true,
        //tableWidth: 820,
       //bodyHeight: 380,
        multipleSort: true,
        exportAction: true,
       globalFilter: true,
       clearAllFiltersIcon: true,
       //filter: true,
        columnToggleAction: true,
        //paginator: false,
      clientSide: true,

    };

}
mazdik commented 5 years ago

You need to modify RestlessService for your backend. It contains pagination and global search.

delete row.$$uid;
delete row.$$index;
delete row.$$data;
bklingDt commented 5 years ago

THANK YOU!!!!! That did the trick for fixing updates. :)

This is such a great app, thanks for your help.

mazdik commented 5 years ago

clientSide - not used with the service, in the future I will delete this setting. https://github.com/mazdik/ng-crud-table#custom-service

bklingDt commented 5 years ago

even when i do not enable the clientSide flag the search box does not work.

mazdik commented 5 years ago

global-filter-demo

  <dt-toolbar [table]="table"></dt-toolbar>
  <app-data-table [table]="table"></app-data-table>

app-data-table - client side app-crud-table - server side

bklingDt commented 5 years ago

I made some progress on the pagination issue. I made the following changes to my restless.service.ts file, and now the number of rows updates when i select a new value (10, 20, 50, etc).

However, the drop down stays stuck at 10. If i change to 20, the box displays 20 rows but drop down will still say 10, like it not saving the value last chosen, and i can never select 10 from the list.

I still can not get the global search to function though.

OLD

 getItems(requestMeta: RequestMetadata): Promise<PagedResult> {
    const page = requestMeta.pageMeta.currentPage;
    let url = this.url;
    if (page > 1) {
      if (url.indexOf('?') === -1) {
        url = url + '?page=' + page;
      } else {
        url = url + '&page=' + page;
      }
    }
    if (this.filterObject(requestMeta)) {
      url += (url.indexOf('?') === -1) ? '?' : '&';
      url = url + this.filterObject(requestMeta);
    }

    console.log(url)
    console.log(page)
    console.log(requestMeta)

    return this.http.get<PagedResult>(url, {headers: this.headers})
      .toPromise()
      .then(this.extractData)
      .catch(this.handleError.bind(this));

  }

NEW

 getItems(requestMeta: RequestMetadata): Promise<PagedResult> {
    const page = requestMeta.pageMeta.currentPage;
    const perPage = requestMeta.pageMeta.perPage;
    let url = this.url;
    if (page > 1) {
      if (url.indexOf('?') === -1) {
        url = url + '?page=' + page;
      } else {
        url = url + '&page=' + page;
      }
    }
    if (url.indexOf('?') === -1) {
      url = url + '?results_per_page=' + perPage;
    } else {
      url = url + '&results_per_page=' + perPage;
    }

    if (this.filterObject(requestMeta)) {
      url += (url.indexOf('?') === -1) ? '?' : '&';
      url = url + this.filterObject(requestMeta);
    }

    console.log(url)
    console.log(page)
    console.log(requestMeta)

    return this.http.get<PagedResult>(url, {headers: this.headers})
      .toPromise()
      .then(this.extractData)
      .catch(this.handleError.bind(this));

  }

OLD

  getItem(row: any): Promise<any> {
    const filters = {};
    for (const key of this.primaryKeys) {
      filters[key] = {value: row[key]};
    }
    const requestMeta = <RequestMetadata> {
      pageMeta: {currentPage: 1},
      filters: filters,
    };
    return this.getItems(requestMeta)
      .then(data => data.items[0]);
  }

NEW

  getItem(row: any): Promise<any> {
    const filters = {};
    for (const key of this.primaryKeys) {
      filters[key] = {value: row[key]};
    }
    const requestMeta = <RequestMetadata> {
      pageMeta: {currentPage: 1, perPage: 10},
      filters: filters,
    };
    return this.getItems(requestMeta)
      .then(data => data.items[0]);
  }

When i tweak the number in this part of restless.service.ts it will change the value the dropdown defaults to, which makes me thing body.limit is not being set somehow?

 private extractData(res: any) {
    let body = res;
    const meta = {
      'totalCount': body.num_results,
      'perPage': body.limit || 10
    };
    const items = (body.objects) ? body.objects : body;
    body = {'items': items, '_meta': meta};
    return body;
  }