Forráskód Böngészése

Basic ingredient adding working.

Tom Flucke 7 éve
szülő
commit
399b18fd3d

+ 1 - 0
.gitignore

@@ -1,3 +1,4 @@
 build/
 lib/
 interrupt
+ensime.sbt

+ 2 - 1
conf/ant/ivy.xml

@@ -15,8 +15,9 @@
     <!-- DB Resources -->
     <dependency org="org.mongodb.morphia" name="morphia" rev="1.3.2"/>
     <dependency org="org.springframework.boot" name="spring-boot-starter-data-mongodb" rev="2.0.5.RELEASE"/>
+    <!-- JSON API Serialization -->
+    <dependency org="com.fasterxml.jackson.core" name="jackson-databind" rev="2.9.6"/>
     <!-- UI Resources -->
-    <dependency org="org.webjars" name="bootstrap" rev="4.1.3" conf="war->default"/>
     <dependency org="org.webjars" name="angular-ui-bootstrap" rev="2.5.0" conf="war->default"/>
     <dependency org="org.webjars" name="webjars-locator" rev="0.34" conf="war->default"/>
     <dependency org="javax.servlet" name="jstl" rev="1.2" conf="war->default"/>

+ 9 - 2
conf/app/application.properties

@@ -1,9 +1,16 @@
-#mongodb
+# TODO: Integrate these into JSP where appropriate
+
+# ieat app
+app.url=localhost:8080/ieat-2.0.0
+
+# mongodb
 spring.data.mongodb.host=localhost
 spring.data.mongodb.port=27017
 spring.data.mongodb.database=ieat
 
-#logging
+# logging
 logging.level.*=error
 #logging.config=log4j.properties
 #log4j.appender.file.File=${log.file.path}/${project.artifactId}.log
+
+ndb.api.key=CfiHcUnSf0RX0jBuqiWjDK2d2ziOmoZG15CTdhQn

+ 8 - 0
conf/web.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_5_0.xsd"
+	     version="5.0">
+
+  <display-name>Online recipe manager.</display-name>
+</web-app>

+ 1 - 0
src/name/tflucke/ieat2/configs/WebConfig.java

@@ -28,6 +28,7 @@ public class WebConfig implements WebMvcConfigurer {
     public void addResourceHandlers(ResourceHandlerRegistry registry) {
         registry.addResourceHandler("/static/**")
             .addResourceLocations("classpath:/META-INF/resources/webjars/")
+            .addResourceLocations("/js/")
             //.setCacheControl(CacheControl.maxAge(0L, TimeUnit.DAYS).cachePublic())
             .resourceChain(true)
             .addResolver(new WebJarsResourceResolver());

+ 93 - 0
src/name/tflucke/ieat2/controllers/AbstractController.java

@@ -0,0 +1,93 @@
+package name.tflucke.ieat2.controllers;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+import org.mongodb.morphia.Datastore;
+import org.mongodb.morphia.Key;
+import org.mongodb.morphia.query.Query;
+import org.mongodb.morphia.query.FindOptions;
+import org.mongodb.morphia.query.UpdateOperations;
+
+import org.bson.types.ObjectId;
+
+import name.tflucke.ieat2.models.DBObject;
+import name.tflucke.ieat2.errors.ResourceNotFoundException;
+
+public abstract class AbstractController<T extends DBObject> {
+    @Autowired
+    protected Datastore db;
+    private final Class<T> clazz;
+    private final Set<Field> fields;
+
+    protected AbstractController(Class<T> clazz)
+    {
+        this.clazz = clazz;
+        fields = Arrays.stream(clazz.getDeclaredFields())
+            .filter((Field field) -> Modifier.isPublic(field.getModifiers()))
+            .collect(Collectors.toSet());
+    }
+
+    protected Key<T> toKey(String id) {
+        return new Key(clazz, clazz.getSimpleName(), new ObjectId(id));
+    }
+
+    protected Query<T> toQuery(String id) {
+        return db.createQuery(clazz).field("id").equal(new ObjectId(id));
+    }
+    
+    protected T get(String id) {
+        T result = db.getByKey(clazz, toKey(id));
+        if (result == null)
+            {
+                throw new ResourceNotFoundException();
+            }
+        else
+            {
+                return result;
+            }
+    }
+    
+    protected T insert(T newElement) {
+        db.save(newElement);
+        return newElement;
+    }
+    
+    protected T update(final String id, final T element) {
+        final UpdateOperations<T> updateOps = db.createUpdateOperations(clazz);
+        updateOps.inc("version");
+        fields.forEach((Field field) -> {
+                try {
+                    Object value = field.get(element);
+                    if (value != null)
+                        {
+                            updateOps.set(field.getName(), value);
+                        }
+                }
+                catch (IllegalAccessException iae) {
+                    // Filter at beginning should leave only public field.
+                    // Should never get here.
+                    throw new RuntimeException("Tried to store non-public field", iae);
+                }
+            });
+        db.update(toKey(id), updateOps);
+        return get(id);
+    }
+    
+    protected T delete(String id) {
+        T result = db.findAndDelete(toQuery(id));
+        if (result == null)
+            {
+                throw new ResourceNotFoundException();
+            }
+        else
+            {
+                return result;
+            }
+    }
+}

+ 49 - 0
src/name/tflucke/ieat2/controllers/BasicFoodController.java

@@ -0,0 +1,49 @@
+package name.tflucke.ieat2.controllers;
+
+import java.util.List;
+
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+
+import name.tflucke.ieat2.models.BasicFood;
+
+@RestController("/food/")
+public class BasicFoodController extends AbstractController<BasicFood> {
+
+    public BasicFoodController()
+    {
+        super(BasicFood.class);
+    }
+    
+    
+    @GetMapping("/food/{id}")
+    public BasicFood get(@PathVariable("id") String id) {
+        return super.get(id);
+    }
+    
+    @GetMapping("/food/{query}/query")
+    public List<BasicFood> query(@PathVariable("query") String query) {
+        return db.find(BasicFood.class).field("name").containsIgnoreCase(query).asList();
+    }
+    
+    @PutMapping("/food")
+    public BasicFood insert(@RequestBody BasicFood newElement) {
+        return super.insert(newElement);
+    }
+    
+    @PostMapping("/food/{id}")
+    public BasicFood update(@PathVariable("id") final String id,
+                            @RequestBody final BasicFood element) {
+        return super.update(id, element);
+    }
+    
+    @DeleteMapping("/food/{id}")
+    public BasicFood delete(@PathVariable("id") String id) {
+        return super.delete(id);
+    }
+}

+ 8 - 0
src/name/tflucke/ieat2/errors/ResourceNotFoundException.java

@@ -0,0 +1,8 @@
+package name.tflucke.ieat2.errors;
+
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.http.HttpStatus;
+
+@ResponseStatus(value = HttpStatus.NOT_FOUND)
+public class ResourceNotFoundException extends RuntimeException {
+}

+ 7 - 0
src/name/tflucke/ieat2/models/BasicFood.java

@@ -1,8 +1,15 @@
 package name.tflucke.ieat2.models;
 
 import org.mongodb.morphia.annotations.Entity;
+import com.fasterxml.jackson.annotation.JsonProperty;
 
 @Entity
 public class BasicFood extends DBObject {
     public String name;
+    public Long ndbno;
+    @JsonProperty("default_unit")
+    public String defaultUnit;
+    public Long calories_p_100;
+    @JsonProperty("food_group")
+    public String foodGroup;
 }

+ 4 - 0
src/name/tflucke/ieat2/models/DBObject.java

@@ -5,9 +5,13 @@ import org.mongodb.morphia.annotations.Id;
 import org.mongodb.morphia.annotations.Property;
 import org.mongodb.morphia.annotations.Version;
 
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+
 public abstract class DBObject {
     @Id
     @Property("id")
+    @JsonSerialize(using=ToStringSerializer.class)
     public ObjectId id;
  
     @Version

+ 2 - 14
web/WEB-INF/tags/navItem.tag

@@ -1,21 +1,9 @@
 <%@tag description="Standard navigation bar" pageEncoding="UTF-8"%>
 
 <%@attribute name="title" required="true" type="java.lang.String"%>
+<%@attribute name="href" required="true" type="java.lang.String"%>
 <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
       
 <!-- Navigation Item -->
 <c:set var="body"><jsp:doBody/></c:set>
-<c:choose>
-  <c:when test="${title == body}">
-    <li class="nav-item active">
-      <a class="nav-link" href="#">${body}
-        <span class="sr-only">(current)</span>
-      </a>
-    </li>
-  </c:when>
-  <c:otherwise>
-    <li class="nav-item">
-      <a class="nav-link" href="#">${body}</a>
-    </li>
-  </c:otherwise>
-</c:choose>
+<li class="${title == body? 'active':''}"><a href="${href}">${body}</a></li>

+ 10 - 18
web/WEB-INF/tags/navigation.tag

@@ -3,24 +3,16 @@
 <%@attribute name="title" required="true" type="java.lang.String"%>
 
 <!-- Navigation -->
-<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
-  <div class="container">
-    <a class="navbar-brand" href="#">Start Bootstrap</a>
-    <button class="navbar-toggler" type="button"
-            data-toggle="collapse"
-            data-target="#navbarResponsive"
-            data-aria-controls="navbarResponsive"
-            data-aria-expanded="false"
-            data-aria-label="Toggle navigation">
-      <span class="navbar-toggler-icon"></span>
-    </button>
-    <div class="collapse navbar-collapse" id="navbarResponsive">
-      <ul class="navbar-nav ml-auto">
-        <t:navItem title="${title}">Home</t:navItem>
-        <t:navItem title="${title}">Add Recipie</t:navItem>
-        <t:navItem title="${title}">Add Food</t:navItem>
-        <t:navItem title="${title}">Settings</t:navItem>
-      </ul>
+<nav class="navbar navbar-inverse">
+  <div class="container-fluid">
+    <div class="navbar-header">
+      <a class="navbar-brand" href="#">iEat 2.0</a>
     </div>
+    <ul class="nav navbar-nav">
+      <t:navItem title="${title}" href="/ieat-2.0.0">Home</t:navItem>
+      <t:navItem title="${title}" href="/ieat-2.0.0/addFood">Add Food</t:navItem>
+      <t:navItem title="${title}" href="/ieat-2.0.0">Add Recipe</t:navItem>
+      <t:navItem title="${title}" href="/ieat-2.0.0">Settings</t:navItem>
+    </ul>
   </div>
 </nav>

+ 8 - 1
web/WEB-INF/tags/template.tag

@@ -2,13 +2,20 @@
 <%@taglib prefix="t" tagdir="/WEB-INF/tags" %>
       
 <%@attribute name="title" required="true" %>
+<%@attribute name="head" fragment="true" %>
 
 <!DOCTYPE html>
 <html>
   <head>
     <title>iEat - ${title}</title>
-    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
     <link type="text/css" href="static/bootstrap/css/bootstrap.min.css" type="text/css" rel="stylesheet"/>
+    <script type="text/javascript" src="static/angularjs/angular.min.js"></script>
+    <script type="text/javascript" src="static/angularjs/angular-resource.min.js"></script>
+    <script type="text/javascript" src="static/angular-ui-bootstrap/ui-bootstrap.min.js"></script>
+    <script type="text/javascript" src="static/angular-ui-bootstrap/ui-bootstrap-tpls.min.js"></script>
+    <jsp:invoke fragment="head"/>
   </head>
   <body>
     <div class="container">

+ 35 - 0
web/js/basicFoodEditor.js

@@ -0,0 +1,35 @@
+angular.module('basicFoodEditor', ['ngResource'])
+    .factory('BasicFood', ['$resource', '$q', function($resource, $q) {
+        return $resource('food/:id', {}, {
+            'get':    {method: 'GET'},
+            'query':  {method: 'GET', isArray: true},
+            'save':   {method: 'PUT'},
+            'update': {method: 'POST'},
+            'delete': {method: 'DELETE'}
+        });
+    }])
+    // TODO: Only ever used with editBasicFood template.  See if can auto link.
+    .controller('BasicFoodEditorController',
+                ['$scope', '$uibModalInstance', 'BasicFood', 'foodData',
+                 function($scope, $uibModalInstance, BasicFood, foodData) {
+                     if (foodData == null)
+                     {
+                         console.error("No food data to edit!");
+                         return;
+                     }
+                     
+                     $scope.food = new BasicFood(foodData);
+
+                     $scope.submit = function(food) {
+                         food.$save($scope.close, function (err) {
+                             // TODO: Proper error handling
+                             console.error(err);
+                         });
+                     };
+
+                     // TODO:
+                     // Will this controller ever be used non-modally?
+                     // Will it even work as a non-modal?
+                     $scope.close = $uibModalInstance != null?
+                         $uibModalInstance.close : null;
+                 }]);

+ 70 - 0
web/js/ndbDatabase.js

@@ -0,0 +1,70 @@
+angular.module('ndbDatabase', ['ngResource'])
+    .factory('NDBSearch', ['$resource', '$q', function($resource, $q) {
+        return $resource('https://api.nal.usda.gov/ndb/search?'+
+                         'q=:query&ds=Standard+Reference&api_key=:key&fg=:groups&max=:max&offset=:offset', {}, {
+                             get: {method: "GET",
+                                   isArray: true,
+                                   transformRequest: function(data, headers) {
+                                       return angular.extend({}, headers, {'Content-Type': 'application/json'});
+                                   },
+                                   headers: {
+                                       'Content-Type': 'application/json'
+                                   },
+                                   transformResponse: function(data, headersGetter, status) {
+                                       var json = angular.fromJson(data);
+                                       return status < 400 && json.list? json.list.item : json.errors.error;
+                                   }
+                                  }
+                         });
+    }]).factory('NDBList', ['$resource', '$q', function($resource, $q) {
+        return $resource('https://api.nal.usda.gov/ndb/list?' +
+                         'api_key=:key&lt=:type&nutrients=:nutrients&fg=:groups&max=:max&offset=:offset', {}, {
+                             get: {method: "GET",
+                                   isArray: true,
+                                   transformRequest: function(data, headers) {
+                                       return angular.extend({}, headers, {'Content-Type': 'application/json'});
+                                   },
+                                   headers: {
+                                       'Content-Type': 'application/json'
+                                   },
+                                   transformResponse: function(data, headersGetter, status) {
+                                       var json = angular.fromJson(data);
+                                       return status < 400 && json.list? json.list.item : json.errors.error;
+                                   }
+                                  }
+                         });
+    }]).factory('NDBReport', ['$resource', '$q', function($resource, $q) {
+        return $resource('https://api.nal.usda.gov/ndb/reports/?api_key=:key&ndbno=:ndbno', {}, {
+            get: {method: "GET",
+                  transformRequest: function(data, headers) {
+                      return angular.extend({}, headers, {
+                          'Content-Type': 'application/json'
+                      });
+                  },
+                  headers: {
+                      'Content-Type': 'application/json'
+                  },
+                  transformResponse: function(data, headersGetter, status) {
+                      var json = angular.fromJson(data);
+                      return status < 400 && json.report.food? json.report.food : json.errors.error;
+                  }
+                 }
+        });
+    }]).factory('NDBNutrients', ['$resource', '$q', function($resource, $q) {
+        return $resource('https://api.nal.usda.gov/ndb/nutrients?'+
+                         'api_key=:key&nbno=:nums&nutrients=:nutrients&fg=:groups&max=:max&offset=:offset', {}, {
+                             get: {method: "GET",
+                                   isArray: true,
+                                   transformRequest: function(data, headers) {
+                                       return angular.extend({}, headers, {'Content-Type': 'application/json'});
+                                   },
+                                   headers: {
+                                       'Content-Type': 'application/json'
+                                   },
+                                   transformResponse: function(data, headersGetter, status) {
+                                       var json = angular.fromJson(data);
+                                       return status < 400 && json.list? json.list.item : json.errors.error;
+                                   }
+                                  }
+                         });
+    }]);

+ 44 - 0
web/js/templates/editBasicFood.html

@@ -0,0 +1,44 @@
+<div>
+  <div class="form-group">
+    <table class="table">
+      <tr>
+        <td colspan="2">
+          <input type="text" style="text-align: center;" class="form-control" data-ng-model="food.name">
+        </td>
+      </tr>
+      <tr>
+        <td>
+          <label for="ndbno">ndbno:</label>
+          <input id="ndbno" class="form-control" type="number" data-ng-model="food.ndbno" readonly="readonly" />
+        </td>
+        <td>
+          <label for="calories">Calories/100:</label>
+          <input id="calories" class="form-control" type="number" data-ng-model="food.calories_p_hundred" />
+        </td>
+      </tr>
+      <tr>
+        <td>
+          <label for="unit">Unit:</label>
+          <select id="unit" class="form-control" data-ng-model="food.default_unit">
+            <!-- TODO: Load these from server -->
+            <option value="g">g (Grams)</option>
+            <option value="ml">ml (Milliliters)</option>
+          </select>
+        </td>
+        <td>
+          <label for="group">Food Group:</label>
+          <select id="group" class="form-control" data-ng-model="food.food_group">
+            <!-- TODO: Load these from server -->
+            <option>Dairy and Egg Products</option>
+          </select>
+        </td>
+      </tr>
+      <tr>
+        <td colspan="2" style="text-align: right;">
+          <button type="button" class="btn btn-danger" data-ng-click="close();">Cancel</button>
+          <button type="button" class="btn btn-success" data-ng-click="submit(food);">Submit</button>
+        </td>
+      </tr>
+    </table>
+  </div>
+</div>

+ 127 - 0
web/views/addFood.jsp

@@ -0,0 +1,127 @@
+<%@page contentType="text/html" pageEncoding="UTF-8"%>
+<%@taglib prefix="t" tagdir="/WEB-INF/tags" %>
+<t:template>
+  <jsp:attribute name="title">Add Food</jsp:attribute>
+  <jsp:attribute name="head">
+    <script type="text/javascript" src="static/ndbDatabase.js"></script>
+    <script type="text/javascript" src="static/basicFoodEditor.js"></script>
+    <script type="text/javascript">
+      var app = angular.module('ingredients', ['ndbDatabase', 'basicFoodEditor', 'ui.bootstrap']);
+      app.controller('SearchController', ['$scope', '$timeout', '$uibModal', 'NDBSearch', 'NDBReport',
+        function($scope, $timeout, $uibModal, NDBSearch, NDBReport) {
+              $scope.searchTerm = "";
+              $scope.searchOffset = 1;
+              $scope.searchSize = "10";
+              $scope.searchResults = [];
+              
+              var searchTimeout = false;
+              $scope.$watchGroup(['searchTerm', 'searchOffset'], function(newValues, oldValues, scope) {
+                  if (searchTimeout)
+                  {
+                      $timeout.cancel(searchTimeout);
+                  }
+                  if ($scope.searchTerm.length >= 3)
+                  {
+                      searchTimeout = $timeout(function() {//{ndb.api.key}
+                          // TODO: Replace with properties file key.
+                          NDBSearch.get({key: "CfiHcUnSf0RX0jBuqiWjDK2d2ziOmoZG15CTdhQn",
+                                         "query": $scope.searchTerm
+                                        }, function(data) {
+                                            console.debug(data);
+                                            $scope.searchResults = data;
+                                        }, function (err) {
+                                            console.error(err);
+                                        });
+                      }, 100);
+                  }
+              });
+              
+              var ndbToIeat = function(foodData) {
+                  return {
+                      ndbno: parseInt(foodData.ndbno),
+                      name: foodData.name,
+                      food_group: foodData.fg,
+                      default_unit: foodData.ru,
+                      calories_p_hundred: foodData.nutrients.find(function (nutrient) {
+                          // TODO: Replace 208 with soft-loaded value from database
+                          // 208 is the id for kCalories
+                          return nutrient.nutrient_id == 208;
+                      }).value   
+                  }
+              };
+              
+            $scope.promptWindow = function(ndbno) {// {ndb.api.key}
+                var foodRequest = NDBReport.get(
+                    {// TODO: Replace with properties file key.
+                        "key": "CfiHcUnSf0RX0jBuqiWjDK2d2ziOmoZG15CTdhQn",
+                        "ndbno": ndbno,
+                        "type": "f"
+                    }).$promise.then(function(data) {
+                        return ndbToIeat(data)
+                    }, function (err) {
+                        // TODO: Proper error handling
+                        console.error(err);
+                    });
+                $uibModal.open({
+                    // TODO: Figure out what these are and how they work
+                    //ariaLabelledBy: 'modal-title',
+                    //ariaDescribedBy: 'modal-body',
+                    templateUrl: 'static/templates/editBasicFood.html',
+                    controller: 'BasicFoodEditorController',
+                    size: "md",
+                    resolve: {
+                        foodData: function() {return foodRequest;}
+                    }
+                });
+            };
+          }]);
+    </script>
+  </jsp:attribute>
+  <jsp:body>
+    <div class="section container" data-ng-app="ingredients" data-ng-controller="SearchController">
+      <h2>Ingredient Querier</h2>
+      <div class="form-group">
+        <label for="search">Search: </label>
+        <input type="text" class="form-control input-sm" id="search" data-ng-model="searchTerm" />
+      </div>
+      <table class="table table-striped table-hover table-responsive">
+        <tr>
+          <th class="col-md-1"> </th>
+          <th class="col-md-1">NDB #</th>
+          <th class="col-md-6">Name</th>
+          <th class="col-md-3">Group</th>
+          <th class="col-md-3">Manufacturer</th>
+        </tr>
+        <tr data-ng-repeat="item in searchResults | limitTo:searchSize:searchSize*(searchOffset-1)"
+            data-ng-click="item.checked = !item.checked">
+          <th><input type="button"
+                     value="Add"
+                     class="checkbox-inline"
+                     data-ng-click="promptWindow(item.ndbno)"></th>
+          <td>{{ ::item.ndbno }}</td>
+          <td>{{ ::item.name }}</td>
+          <td>{{ ::item.group }}</td>
+          <td>{{ ::item.manu }}</td>
+        </tr>
+      </table>
+      <div class="form-group col-xs-5">
+        <ul class="pagination-sm"
+            style="margin: 0;"
+            data-uib-pagination=""
+            data-boundary-links="true"
+            force-ellipses="true"
+            data-total-items="searchResults.length"
+            data-ng-model="searchOffset"
+            data-items-per-page="searchSize"></ul>
+      </div>
+      <div class="form-group form-group-sm col-xs-2 pull-right">
+        <select id="sizeSelector" class="form-control input-sm" data-ng-model="searchSize">
+          <option>10</option>
+          <option>20</option>
+          <option>30</option>
+          <option>40</option>
+        </select>
+      </div>
+    </div>
+  </jsp:body>
+</t:template>