Procházet zdrojové kódy

Added ability to create, see, and edit recipes as foods.

Thomas Flucke před 7 roky
rodič
revize
e830b033b2

+ 1 - 1
conf/ant/tomcat.properties

@@ -8,4 +8,4 @@
 
 # Changed these!
 username = admin
-password = admin
+password = admin

+ 5 - 5
src/name/tflucke/ieat2/controllers/AbstractController.java

@@ -25,7 +25,7 @@ public abstract class AbstractController<T extends DBObject> {
     private final Class<T> clazz;
     private final Set<Field> fields;
 
-    protected AbstractController(Class<T> clazz)
+    protected AbstractController(final Class<T> clazz)
     {
         this.clazz = clazz;
         fields = Arrays.stream(clazz.getDeclaredFields())
@@ -33,15 +33,15 @@ public abstract class AbstractController<T extends DBObject> {
             .collect(Collectors.toSet());
     }
 
-    protected Key<T> toKey(String id) {
+    protected Key<T> toKey(final String id) {
         return new Key(clazz, clazz.getSimpleName(), new ObjectId(id));
     }
 
-    protected Query<T> toQuery(String id) {
+    protected Query<T> toQuery(final String id) {
         return db.createQuery(clazz).field("id").equal(new ObjectId(id));
     }
     
-    protected T get(String id) {
+    protected T get(final String id) {
         T result = db.getByKey(clazz, toKey(id));
         if (result == null)
             {
@@ -79,7 +79,7 @@ public abstract class AbstractController<T extends DBObject> {
         return get(id);
     }
     
-    protected T delete(String id) {
+    protected T delete(final String id) {
         T result = db.findAndDelete(toQuery(id));
         if (result == null)
             {

+ 53 - 15
src/name/tflucke/ieat2/controllers/BasicFoodController.java

@@ -11,44 +11,82 @@ import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestBody;
 
 import name.tflucke.ieat2.models.BasicFood;
+import name.tflucke.ieat2.models.Food;
+import name.tflucke.ieat2.models.Recipe;
 
 @RestController("/food/")
-public class BasicFoodController extends AbstractController<BasicFood> {
+public class BasicFoodController extends AbstractController<Food> {
 
     public BasicFoodController()
     {
-        super(BasicFood.class);
+        super(Food.class);
     }
     
+    /* Generic Food APIs */
     
     @GetMapping("/food/{id}")
-    public BasicFood get(@PathVariable("id") String id) {
+    public Food get(@PathVariable("id") final String id) {
         return super.get(id);
     }
     
     @GetMapping("/food/query")
-    public List<BasicFood> query() {
-        return db.find(BasicFood.class).asList();
+    public List<Food> query() {
+        return db.find(Food.class).asList();
     }
     
     @GetMapping("/food/query/{query}")
-    public List<BasicFood> query(@PathVariable("query") String query) {
+    public List<Food> query(@PathVariable("query") final String query) {
+        return db.find(Food.class).field("name").containsIgnoreCase(query).asList();
+    }
+    
+    @DeleteMapping("/food/{id}")
+    public Food delete(@PathVariable("id") final String id) {
+        return super.delete(id);
+    }
+
+    /* BasicFood APIs */
+    
+    @GetMapping("/food/basic/query")
+    public List<BasicFood> queryBasicFoods() {
+        return db.find(BasicFood.class).asList();
+    }
+    
+    @GetMapping("/food/basic/query/{query}")
+    public List<BasicFood> queryBasicFoods(@PathVariable("query") final String query) {
         return db.find(BasicFood.class).field("name").containsIgnoreCase(query).asList();
     }
     
-    @PutMapping("/food")
+    @PutMapping("/food/basic")
     public BasicFood insert(@RequestBody BasicFood newElement) {
-        return super.insert(newElement);
+        return (BasicFood) super.insert(newElement);
     }
-    
-    @PostMapping("/food/{id}")
+
+    @PostMapping("/food/basic/{id}")
     public BasicFood update(@PathVariable("id") final String id,
-                            @RequestBody final BasicFood element) {
-        return super.update(id, element);
+                       @RequestBody final BasicFood element) {
+        return (BasicFood) super.update(id, element);
     }
+
+    /* Recipe APIs */
     
-    @DeleteMapping("/food/{id}")
-    public BasicFood delete(@PathVariable("id") String id) {
-        return super.delete(id);
+    @GetMapping("/food/recipe/query")
+    public List<Recipe> queryRecipes() {
+        return db.find(Recipe.class).asList();
+    }
+    
+    @GetMapping("/food/recipe/query/{query}")
+    public List<Recipe> queryRecipes(@PathVariable("query") final String query) {
+        return db.find(Recipe.class).field("name").containsIgnoreCase(query).asList();
+    }
+    
+    @PutMapping("/food/recipe")
+    public Recipe insert(@RequestBody Recipe newElement) {
+        return (Recipe) super.insert(newElement);
+    }
+    
+    @PostMapping("/food/recipe/{id}")
+    public Recipe update(@PathVariable("id") final String id,
+                       @RequestBody final Recipe element) {
+        return (Recipe) super.update(id, element);
     }
 }

+ 1 - 8
src/name/tflucke/ieat2/models/BasicFood.java

@@ -3,13 +3,6 @@ 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 class BasicFood extends Food {
     public Long ndbno;
-    @JsonProperty("default_unit")
-    public String defaultUnit;
-    public Long calories_p_100;
-    @JsonProperty("food_group")
-    public String foodGroup;
 }

+ 21 - 0
src/name/tflucke/ieat2/models/Food.java

@@ -0,0 +1,21 @@
+package name.tflucke.ieat2.models;
+
+import org.mongodb.morphia.annotations.Entity;
+import org.mongodb.morphia.annotations.Transient;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@Entity("Food")
+public abstract class Food extends DBObject {
+    public String name;
+    //public Map<String, float> nutrients;
+    @JsonProperty("default_unit")
+    public String defaultUnit;
+    public Long calories_p_100;
+    @JsonProperty("food_group")
+    public String foodGroup;
+
+    public String getType() {
+        return getClass().getSimpleName();
+    }
+    
+}

+ 19 - 0
src/name/tflucke/ieat2/models/Recipe.java

@@ -0,0 +1,19 @@
+package name.tflucke.ieat2.models;
+
+import java.util.List;
+
+import org.mongodb.morphia.annotations.Entity;
+import org.mongodb.morphia.annotations.Embedded;
+import org.mongodb.morphia.annotations.Reference;
+
+public class Recipe extends Food {
+    public List<String> steps;
+    public List<Ingredient> Ingredients;
+
+    @Embedded
+    public static class Ingredient {
+        public float amount;
+        @Reference
+        public Food item;
+    }
+}

+ 1 - 1
web/WEB-INF/tags/navigation.tag

@@ -11,7 +11,7 @@
     <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/addRecipe">Add Recipe</t:navItem>
       <t:navItem title="${title}" href="/ieat-2.0.0">Settings</t:navItem>
     </ul>
   </div>

+ 54 - 9
web/js/basicFoodEditor.js

@@ -1,5 +1,43 @@
 angular.module('basicFoodEditor', ['ndbDatabase', 'ngResource'])
     .factory('BasicFood', ['$resource', '$q', function($resource, $q) {
+        return $resource('food/basic/:id', {id: "@id"}, {
+            'get':    {
+                url: "food/:id",
+                method: 'GET'
+            },
+            'query':  {
+                url: "food/basic/query/:query",
+                method: 'GET',
+                isArray: true
+            },
+            'save': {method: 'PUT'},
+            'update': {method: 'POST'},
+            'delete': {
+                url: "food/:id",
+                method: 'DELETE'
+            }
+        });
+    }])
+    .factory('Recipe', ['$resource', '$q', function($resource, $q) {
+        return $resource('food/recipe/:id', {id: "@id"}, {
+            'get':    {
+                url: "food/:id",
+                method: 'GET'
+            },
+            'query':  {
+                url: "food/recipe/query/:query",
+                method: 'GET',
+                isArray: true
+            },
+            'save':   {method: 'PUT'},
+            'update': {method: 'POST'},
+            'delete': {
+                url: "food/:id",
+                method: 'DELETE'
+            }
+        });
+    }])
+    .factory('Food', ['$resource', '$q', function($resource, $q) {
         return $resource('food/:id', {id: "@id"}, {
             'get':    {method: 'GET'},
             'query':  {
@@ -7,8 +45,6 @@ angular.module('basicFoodEditor', ['ndbDatabase', 'ngResource'])
                 method: 'GET',
                 isArray: true
             },
-            'save':   {method: 'PUT'},
-            'update': {method: 'POST'},
             'delete': {method: 'DELETE'}
         });
     }])
@@ -28,7 +64,7 @@ angular.module('basicFoodEditor', ['ndbDatabase', 'ngResource'])
                          type: "g"
                      });
                      
-                     $scope.food = new BasicFood(foodData);
+                     $scope.food = foodData;
 
                      $scope.submit = function(food) {
                          food.$save($scope.close, function (err) {
@@ -44,13 +80,22 @@ angular.module('basicFoodEditor', ['ndbDatabase', 'ngResource'])
                          });
                      };
 
-                     var submitFnName = foodData.id == null? "$save":"$update";
-                     $scope.submit = function(food) {
-                         food[submitFnName]($scope.close, function (err) {
-                             // TODO: Proper error handling
-                             console.error(err);
-                         });
+                     var initSubmit = function() {
+                         var submitFnName = foodData.id == null? "$save":"$update";
+                         $scope.submit = function(food) {
+                             food[submitFnName]($scope.close, function (err) {
+                                 // TODO: Proper error handling
+                                 console.error(err);
+                             });
+                         };
                      };
+                     
+                     if (foodData.$promise) {
+                         foodData.$promise.then(initSubmit);
+                     }
+                     else {
+                         initSubmit();
+                     }
 
                      // TODO:
                      // Will this controller ever be used non-modally?

+ 5 - 1
web/js/templates/editBasicFood.html

@@ -34,11 +34,15 @@
       </tr>
       <tr>
         <td colspan="2" style="text-align: right;">
-          <button type="button" class="btn btn-success" data-ng-click="submit(food);">Submit</button>
+          <button type="button"
+                  class="btn btn-success"
+                  data-ng-show="submit"
+                  data-ng-click="submit(food);">Submit</button>
           <button type="button" class="btn" data-ng-click="dismiss();">Cancel</button>
           <button type="button"
                   class="btn btn-danger"
                   data-ng-show="food.id != null"
+                  data-ng-show="delete"
                   data-ng-click="delete(food);">
             Delete
           </button>

+ 5 - 5
web/views/addFood.jsp

@@ -7,8 +7,8 @@
     <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) {
+      app.controller('SearchController', ['$scope', '$timeout', '$uibModal', 'NDBSearch', 'NDBReport', 'BasicFood',
+          function($scope, $timeout, $uibModal, NDBSearch, NDBReport, BasicFood) {
               $scope.searchTerm = "";
               $scope.searchOffset = 1;
               $scope.searchSize = "10";
@@ -37,7 +37,7 @@
               });
               
               var ndbToIeat = function(foodData) {
-                  return {
+                  return new BasicFood({
                       ndbno: parseInt(foodData.ndbno),
                       name: foodData.name,
                       food_group: foodData.fg,
@@ -47,7 +47,7 @@
                           // 208 is the id for kCalories
                           return nutrient.nutrient_id == 208;
                       }).value   
-                  }
+                  });
               };
               
             $scope.promptWindow = function(ndbno) {// {ndb.api.key}
@@ -97,7 +97,7 @@
           <th><input type="button"
                      value="Add"
                      class="checkbox-inline"
-                     data-ng-click="promptWindow(item.ndbno)"></th>
+                     data-ng-click="promptWindow(item.ndbno)" /></th>
           <td>{{ ::item.ndbno }}</td>
           <td>{{ ::item.name }}</td>
           <td>{{ ::item.group }}</td>

+ 119 - 0
web/views/addRecipe.jsp

@@ -0,0 +1,119 @@
+<%@page contentType="text/html" pageEncoding="UTF-8"%>
+<%@taglib prefix="t" tagdir="/WEB-INF/tags" %>
+<t:template>
+  <jsp:attribute name="title">Add Recipe</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('recipe', ['basicFoodEditor', 'ui.bootstrap']);
+      app.directive('myOnEnter', function () {
+          return function (scope, element, attrs) {
+              element.bind("keydown keypress", function (event) {
+                  if(event.which === 13) {
+                      scope.$apply(function (){
+                          scope.$eval(attrs.myOnEnter);
+                      });
+                      event.preventDefault();
+                  }
+              });
+          };
+      });
+      app.controller('SearchController', ['$scope', 'Food', 'Recipe',
+         function($scope, Food, Recipe) {
+            $scope.recipe = new Recipe({
+                steps: [],
+                ingredients: []
+            });
+            $scope.queryFoods = function(q) {
+                return Food.query({query: q}).$promise;
+            };
+
+            $scope.addIngredient = function($item) {
+                $scope.recipe.ingredients.push({
+                    amount: parseFloat($scope.nextAmount),
+                    item: $item
+                });
+            };
+            $scope.removeIngredient = function(index) {
+                $scope.recipe.ingredients.splice(index, 1);
+            };
+
+            $scope.addStep = function(step) {
+                $scope.recipe.steps.push(step);
+            };
+            $scope.removeStep = function(index) {
+                $scope.recipe.steps.splice(index, 1);
+            };
+            
+            $scope.saveRecipe = function(index) {
+                $scope.recipe.$save(function () {
+                    window.location = "/ieat-2.0.0";
+                }, function (err) {
+                    // TODO: Proper error handling
+                    console.error(err);
+                });
+            };
+          }]);
+    </script>
+  </jsp:attribute>
+  <jsp:body>
+    <div class="section container" data-ng-app="recipe" data-ng-controller="SearchController">
+      <h2>Add Recipe</h2>
+      <label for="recipeName">Name:</label>
+      <input id="recipeName" type="text" data-ng-model="recipe.name" />
+      <input type="button" value="Add" class="checkbox-inline" data-ng-click="saveRecipe()" />
+      <table style="width: 100%;">
+        <tr>
+          <td style="width: 50%;">
+            <ol>
+              <li data-ng-repeat="step in recipe.steps track by $index">
+                {{step}}
+                <span class="glyphicon glyphicon-remove" data-ng-click="removeStep($index);"></span>
+              </li>
+            </ol>
+          </td>
+          <td style="width: 50%;" colspan="2">
+            <ul>
+              <li data-ng-repeat="ingredient in recipe.ingredients">
+                {{ingredient.amount}}{{ingredient.item.default_unit}}
+                {{ingredient.item.name}}
+                <span class="glyphicon glyphicon-remove" data-ng-click="removeIngredient($index);"></span>
+              </li>
+            </ul>
+          </td>
+        </tr>
+        <tr>
+          <td>
+            <input type="text"
+                   style="width: 100%;"
+                   placeholder="Next step..."
+                   data-ng-model="nextStep"
+                   data-my-on-enter="addStep(nextStep); nextStep='';" />
+          </td>
+          <td>
+            <input type="text"
+                   class="form-control"
+                   size="3"
+                   data-ng-model="nextAmount" />
+          </td>
+          <td>
+            <input type="text"
+                   class="form-control"
+                   placeholder="Next ingredient..."
+                   data-ng-model="nextIngredient"
+                   data-uib-typeahead="food.name for food in queryFoods($viewValue)"
+                   data-typeahead-editable="false"
+                   data-typeahead-min-length="3"
+                   data-typeahead-on-select="addIngredient($item)"
+                   data-typeahead-loading="loadingFoods"
+                   data-typeahead-no-results="ingredientNotFound" />
+            <i ng-show="loadingFoods" class="glyphicon glyphicon-refresh"></i>
+            <div ng-show="ingredientNotFound">
+              <i class="glyphicon glyphicon-remove"></i> No Results Found
+            </div>
+          </td>
+        </tr>
+      </table>
+  </jsp:body>
+</t:template>

+ 21 - 13
web/views/browseFood.jsp

@@ -7,24 +7,24 @@
     <script type="text/javascript" src="static/basicFoodEditor.js"></script>
     <script type="text/javascript">
       var app = angular.module('ingredients', ['basicFoodEditor', 'ui.bootstrap']);
-      app.controller('SearchController', ['$scope', '$timeout', '$uibModal', 'BasicFood',
-          function($scope, $timeout, $uibModal, BasicFood) {
+      app.controller('SearchController', ['$scope', '$timeout', '$uibModal', 'Food', 'BasicFood', 'Recipe',
+          function($scope, $timeout, $uibModal, Food, BasicFood, Recipe) {
               $scope.searchTerm = "";
               $scope.searchOffset = 1;
               $scope.searchSize = "10";
-              $scope.searchResults = BasicFood.query();
               
               var searchTimeout = false;
               var searchFn = function() {
-                  BasicFood.query({
+                  Food.query({
                       "query": $scope.searchTerm
                   }, function(data) {
-                      console.debug(data);
                       $scope.searchResults = data;
                   }, function (err) {
                       console.error(err);
                   });
               };
+              $scope.searchResults = searchFn();
+              
               $scope.$watchGroup(['searchTerm', 'searchOffset'], function(newValues, oldValues, scope) {
                   if (searchTimeout)
                   {
@@ -36,10 +36,19 @@
                   }
               });
 
-              $scope.promptWindow = function(id) {
-                  var item = $scope.searchResults.find(function (canidate) {
-                      return canidate.id == id;
-                  });
+              function getResource(type) {
+                  switch (type) {
+                  case "BasicFood":
+                      return BasicFood;
+                  case "Recipe":
+                      return Recipe;
+                  default:
+                      throw "Unrecognized food type: "+type;
+                  }
+              }
+              
+              $scope.promptWindow = function(id, type) {
+                  var item = getResource(type).get({"id": id});
                   $uibModal.open({
                       // TODO: Figure out what these are and how they work
                       //ariaLabelledBy: 'modal-title',
@@ -71,10 +80,9 @@
         </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="Edit"
-                     class="checkbox-inline"
-                     data-ng-click="promptWindow(item.id)"></th>
+          <th>
+            <input type="button" value="Edit" class="checkbox-inline" data-ng-click="promptWindow(item.id, item.type)" />
+          </th>
           <td>{{ ::item.name }}</td>
           <td>{{ ::item.food_group }}</td>
           <td>{{ ::item.calories_p_100 }}</td>