segunda-feira, 28 de março de 2016

Still REACT-ifying: improving the services API

Despite being small, the services API (offered by the server) of my previous message (REACT-ifying a selection from a select box) could be improved. That's what I do now. I try to follow the principles of "Web API Design - Creating Interfaces that Developers Love", by Brian Mulloy. This document has a very nice table that I reproduce here:

And so, I decided that I should have two resources: /selectionboxes that provides the entire application and /selected, which provides access to which names stay on the left or right boxes. So, I have the following operations:

1 - GET /selected

Gives a json list, as shown next (the spaces and line breaks are mine, for readability):

[
   [{"age": 21, "name": "Paulo"}, {"age": 25, "name": "Sandra"}, {"age": 16, "name": "Pinto"}, {"age": 18, "name": "Saul"}, {"age": 11, "name": "Camilo"}, {"age": 9, "name": "Luis"}, {"age": 21, "name": "Andre"}],
   [{"age": 15, "name": "Rui"}, {"age": 23, "name": "Adelle"}, {"age": 50, "name": "Bryan"}, {"age": 40, "name": "Castro"}]
]

First list has the names that are not selected (left box); the second list has the names that are selected (right box)


2 - POST /selected with parameter 'person'

This adds a person to the selected list (i.e., moves it from the left to the right side in the previous listing)

3 - DELETE /selected/Saul

This deletes the name 'Saul' from the selected list, passing it to the other, list of non-selected names (i.e, from the right to the left)

I could also have an additional service to list a particular person, which would look like:

4 - GET /selected/Saul

giving me {"age": 18, "name": "Saul"}, but I don't need it, in this exercise. So, I don't have this.


I removed some functions from the server that were related to other exercises, but the overall result is shorter, and hopefully cleaner. The parts of interest are in the 'operateSelected'. I also handle possible ValueError exceptions:


'''
Created on 28/03/2016

@author: filipius
'''

from flask import Flask, request, Response
import json

app = Flask(__name__)
listofpeople = [[], []]

@app.errorhandler(ValueError)
def handle_invalid_usage(error):
    response = str(error)
    return response, 400

def readpeople(filename):
    with open(filename, 'rt', encoding='utf-8') as f:
        result = []
        lines = f.readlines()
        for l in lines:
            array = l.strip().split()
            name, age = array[0], int(array[1])
            result.append({'name' : name, 'age': age})
    return result
   
def intersection(l1, l2):
    return [val for val in l1 if val in l2]

def subtract(l1, l2):
    return [val for val in l1 if val not in l2]

def updateListOfPeople():
    newlistofpeople = readpeople('names.txt')
    listofpeople[0] = subtract(newlistofpeople, listofpeople[1])
    listofpeople[1] = intersection(listofpeople[1], newlistofpeople)

def persontolist(name, peoplist):
    for p in peoplist:
        if (p['name'] == name):
            return p
    raise ValueError('No person called ' + name + ' exists')

#listofpeople[0] ---> non-selected
#listofpeople[1] ---> selected
@app.route('/selected', methods=['GET', 'POST'])
@app.route('/selected/<person>', methods=['DELETE'])
def operateSelected(person=None):
    print('Not thread-safe! Please correct me!')
    if person == None:
        if request.method == 'POST' and 'person' in request.form and request.form['person'] != '':  #method == 'POST' add people to selected
            theperson = persontolist(request.form['person'], listofpeople[0])
            listofpeople[0].remove(theperson)
            listofpeople[1].append(theperson)
    else:
        if request.method == 'DELETE' and person != 'null':  #sub people from selected
            theperson = persontolist(person, listofpeople[1])
            listofpeople[1].remove(theperson)
            listofpeople[0].append(theperson)
    updateListOfPeople()
    return Response(response=json.dumps(listofpeople), status=200, mimetype="application/json")
   

@app.route('/selectionboxes')
def getSelectHtml4():
    return app.send_static_file('react4/selectpeople4.html')

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Slight differences exist in the React code as well, e.g., to invoke the DELETE operation.

var Table =  React.createClass({
       getInitialState : function() {
              return {people : [[], []], toadd : null, tosub : null};
       },
       gotResponse : function(data) {
              this.setState({people : data})
       },
       loadPeopleFromServer : function() {
              console.log('loadPeopleFromServer. url = ' + this.props.baseurl);
              $.ajax({
                     url: this.props.baseurl,
                     dataType: 'json',
                     cache: false,
                     success: this.gotResponse,
                     error: function(xhr, status, err) {
                            console.error(this.props.url, status, err.toString());
                     }.bind(this)
              });
       },
       add : function() {
              if (this.state.toadd != null) {
                     var posting = $.post(this.props.baseurl, {person : this.state.toadd});
                     posting.done(this.gotResponse);
                     this.setState({toadd : null});                  
              }
       },
       sub : function() {
              if (this.state.tosub != null) {
                     $.ajax({
                         url: this.props.baseurl + '/' + this.state.tosub,
                         type: 'DELETE',
                         success: this.gotResponse,
                            error: function(xhr, status, err) {
                                   console.error(this.props.url, status, err.toString());
                            }.bind(this)
                     });
                     this.setState({tosub : null});                  
              }
       },
       changeAdd : function(person) {
              this.setState({toadd : person})
       },
       changeSub : function(person) {
              this.setState({tosub : person})
       },
       componentDidMount : function() {
              this.loadPeopleFromServer();
              this.timer = setInterval(this.loadPeopleFromServer, 10000);
       },
       componentWillUnmount : function() {
              clearInterval(this.timer);
       },
       render : function() {
              return (
                            <table>
                            <tbody>
                            <tr>
                                   <td>
                                          <h1>Add:</h1>
                                   </td>
                                   <td />
                                   <td>
                                          <h1>Remove:</h1>
                                   </td>
                            </tr>
                            <tr>
                                   <td>
                                          <PeopleList peoplelist={this.state.people[0]} change={this.changeAdd} value={this.state.toadd} name="from"/>
                                   </td>
                                   <td>
                                          <SelectButton handleclick={this.add} title="-->"/> <br/>
                                          <SelectButton handleclick={this.sub} title="<--"/>
                                   </td>
                                   <td>
                                          <PeopleList peoplelist={this.state.people[1]} change={this.changeSub} value={this.state.tosub} name="to"/>
                                   </td>
                            </tr>
                            </tbody>
                            </table>
                    
                            );
       }
});


var SelectButton = React.createClass({
       render : function() {
              return (<button onClick={this.props.handleclick} type="button">{this.props.title}</button>);
       }
});


var PeopleList = React.createClass({
       change : function(event) {
              this.props.change(event.target.value);
       },
       render : function() {
              var theoptions = [];
              for (var i = 0; i < this.props.peoplelist.length; i++) {
                     var person = this.props.peoplelist[i];
                     theoptions.push(<option value={person.name} key={person.name}>{person.name}, {person.age}</option>);
              }
              return (
                            <div>
                                   <select onChange={this.change} value={this.props.value} size="5">
                                          {theoptions}
                                   </select>
                            </div>
              );
       },
})


ReactDOM.render(<Table baseurl="/selected"/>, mountNode);

Good luck!