From ef99e5c56d1ad953ebb55dd1fbca010a5d19ad3b Mon Sep 17 00:00:00 2001 From: SkulderLock <78735770+SkulderLock@users.noreply.github.com> Date: Thu, 7 Sep 2023 15:20:06 +0200 Subject: [PATCH 01/97] =?UTF-8?q?=F0=9F=9A=A7=20Adding=20barcode=20scanner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/capacitor.build.gradle | 3 +- android/app/src/main/AndroidManifest.xml | 8 ++++ android/capacitor.settings.gradle | 3 ++ .../models/mongo/VisitedLinkModel.java | 2 + .../src/app/pages/pantry/pantry.page.html | 36 ++++++++-------- ios/App/App/Info.plist | 2 + ios/App/Podfile | 1 + package-lock.json | 43 ++++++++++++++++++- package.json | 4 +- 9 files changed, 81 insertions(+), 21 deletions(-) diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index e15a0e9c..e93da780 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -9,12 +9,13 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { + implementation project(':capacitor-mlkit-barcode-scanning') implementation project(':capacitor-app') implementation project(':capacitor-haptics') implementation project(':capacitor-http') implementation project(':capacitor-keyboard') implementation project(':capacitor-status-bar') - + implementation "androidx.webkit:webkit:1.4.0" } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4d7ca380..166ce48a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -9,6 +9,8 @@ android:supportsRtl="true" android:theme="@style/AppTheme"> + + + + + + + + diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 1be23e75..96c35706 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -2,6 +2,9 @@ include ':capacitor-android' project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') +include ':capacitor-mlkit-barcode-scanning' +project(':capacitor-mlkit-barcode-scanning').projectDir = new File('../node_modules/@capacitor-mlkit/barcode-scanning/android') + include ':capacitor-app' project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android') diff --git a/backend/src/main/java/fellowship/mealmaestro/models/mongo/VisitedLinkModel.java b/backend/src/main/java/fellowship/mealmaestro/models/mongo/VisitedLinkModel.java index 35d17eaa..b80dd542 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/mongo/VisitedLinkModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/mongo/VisitedLinkModel.java @@ -2,6 +2,7 @@ import java.time.LocalDate; +import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; import lombok.Getter; @@ -11,6 +12,7 @@ @Setter @Getter public class VisitedLinkModel { + @Id private String link; private LocalDate lastVisited; diff --git a/frontend/src/app/pages/pantry/pantry.page.html b/frontend/src/app/pages/pantry/pantry.page.html index 1d55ef00..3079b88f 100644 --- a/frontend/src/app/pages/pantry/pantry.page.html +++ b/frontend/src/app/pages/pantry/pantry.page.html @@ -95,17 +95,17 @@ horizontal="end" *ngSwitchCase="'pantry'" > - - + + - + + + + - - + + - + UIViewControllerBasedStatusBarAppearance + NSCameraUsageDescription + $(PRODUCT_NAME) uses the camera to scan barcodes. diff --git a/ios/App/Podfile b/ios/App/Podfile index 97f0aea8..eed38fa3 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -11,6 +11,7 @@ install! 'cocoapods', :disable_input_output_paths => true def capacitor_pods pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' + pod 'CapacitorMlkitBarcodeScanning', :path => '../../node_modules/@capacitor-mlkit/barcode-scanning' pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics' pod 'CapacitorHttp', :path => '../../node_modules/@capacitor/http' diff --git a/package-lock.json b/package-lock.json index eaaeadfd..b981493c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,8 @@ "@angular/platform-browser-dynamic": "^15.0.0", "@angular/router": "^15.0.0", "@awesome-cordova-plugins/http": "^6.3.0", - "@capacitor/android": "5.0.5", + "@capacitor-mlkit/barcode-scanning": "^5.1.0", + "@capacitor/android": "^5.0.5", "@capacitor/app": "^5.0.3", "@capacitor/core": "5.0.5", "@capacitor/haptics": "^5.0.4", @@ -27,6 +28,7 @@ "@types/chart.js": "^2.9.37", "chart.js": "^4.3.0", "cordova-plugin-advanced-http": "^3.3.1", + "cordova-plugin-file": "^8.0.0", "dotenv": "^16.3.1", "ionicons": "^7.0.0", "neo4j-driver": "^5.10.0", @@ -2573,6 +2575,24 @@ "node": ">=6.9.0" } }, + "node_modules/@capacitor-mlkit/barcode-scanning": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@capacitor-mlkit/barcode-scanning/-/barcode-scanning-5.1.0.tgz", + "integrity": "sha512-4ddwHQa8uUL9+px6Z/khXV/46V00l218pagsoMOGbo8dSYbpu8RCgsvnacG4igJy3DAQsPoRoLbSbgSqNif4QA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/capawesome-team/" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/capawesome" + } + ], + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } + }, "node_modules/@capacitor/android": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-5.0.5.tgz", @@ -6102,6 +6122,27 @@ } ] }, + "node_modules/cordova-plugin-file": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/cordova-plugin-file/-/cordova-plugin-file-8.0.0.tgz", + "integrity": "sha512-pgxCJtDjDKzyeqvrn0KnDubf9b1VLv+OyWTXjUR7T52o7oGDUkR3ubT89i/1ugHtRU6mY7XIGHD4drUByDQClw==", + "engines": { + "cordovaDependencies": { + "5.0.0": { + "cordova-android": ">=6.3.0" + }, + "7.0.0": { + "cordova-android": ">=10.0.0" + }, + "8.0.0": { + "cordova-android": ">=12.0.0" + }, + "9.0.0": { + "cordova": ">100" + } + } + } + }, "node_modules/core-js-compat": { "version": "3.30.2", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.30.2.tgz", diff --git a/package.json b/package.json index a36fd174..936d32f6 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "@angular/platform-browser-dynamic": "^15.0.0", "@angular/router": "^15.0.0", "@awesome-cordova-plugins/http": "^6.3.0", - "@capacitor/android": "5.0.5", + "@capacitor-mlkit/barcode-scanning": "^5.1.0", + "@capacitor/android": "^5.0.5", "@capacitor/app": "^5.0.3", "@capacitor/core": "5.0.5", "@capacitor/haptics": "^5.0.4", @@ -34,6 +35,7 @@ "@types/chart.js": "^2.9.37", "chart.js": "^4.3.0", "cordova-plugin-advanced-http": "^3.3.1", + "cordova-plugin-file": "^8.0.0", "dotenv": "^16.3.1", "ionicons": "^7.0.0", "neo4j-driver": "^5.10.0", From 6c81c19a9f33f6db3298b2a532f74567945e968b Mon Sep 17 00:00:00 2001 From: SkulderLock <78735770+SkulderLock@users.noreply.github.com> Date: Thu, 7 Sep 2023 16:55:37 +0200 Subject: [PATCH 02/97] =?UTF-8?q?=E2=9C=A8=20Frontend=20barcode=20function?= =?UTF-8?q?ality?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/app/pages/pantry/pantry.page.html | 16 ++++++- .../src/app/pages/pantry/pantry.page.scss | 8 ++++ frontend/src/app/pages/pantry/pantry.page.ts | 43 +++++++++++++++++++ .../barcode-api/barcode-api.service.spec.ts | 16 +++++++ .../barcode-api/barcode-api.service.ts | 23 ++++++++++ 5 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 frontend/src/app/services/barcode-api/barcode-api.service.spec.ts create mode 100644 frontend/src/app/services/barcode-api/barcode-api.service.ts diff --git a/frontend/src/app/pages/pantry/pantry.page.html b/frontend/src/app/pages/pantry/pantry.page.html index 3079b88f..a7d5cdde 100644 --- a/frontend/src/app/pages/pantry/pantry.page.html +++ b/frontend/src/app/pages/pantry/pantry.page.html @@ -102,7 +102,13 @@ - + + + @@ -185,7 +191,13 @@ - + + + diff --git a/frontend/src/app/pages/pantry/pantry.page.scss b/frontend/src/app/pages/pantry/pantry.page.scss index d6856927..58f0506b 100644 --- a/frontend/src/app/pages/pantry/pantry.page.scss +++ b/frontend/src/app/pages/pantry/pantry.page.scss @@ -72,3 +72,11 @@ ion-segment-button.md { --color-checked: var(--ion-color-primary); --indicator-height: 4px; } + +ion-fab-button.fab-button-disabled { + --background: #7fb688 !important; +} + +ion-fab-button.fab-button-in-list { + --background: #acf7b7; +} diff --git a/frontend/src/app/pages/pantry/pantry.page.ts b/frontend/src/app/pages/pantry/pantry.page.ts index a921fe2b..663b4bd2 100644 --- a/frontend/src/app/pages/pantry/pantry.page.ts +++ b/frontend/src/app/pages/pantry/pantry.page.ts @@ -12,6 +12,11 @@ import { FormsModule } from '@angular/forms'; import { FoodListItemComponent } from '../../components/food-list-item/food-list-item.component'; import { FoodItemI } from '../../models/interfaces'; import { OverlayEventDetail } from '@ionic/core/components'; +import { + BarcodeScanner, + Barcode, + ScanResult, +} from '@capacitor-mlkit/barcode-scanning'; import { AuthenticationService, ErrorHandlerService, @@ -32,6 +37,7 @@ export class PantryPage implements OnInit, ViewWillEnter { foodListItem!: QueryList; @ViewChild(IonModal) modal!: IonModal; + isBarcodeSupported: boolean = false; segment: 'pantry' | 'shopping' | null = 'pantry'; isLoading: boolean = false; pantryItems: FoodItemI[] = []; @@ -54,6 +60,10 @@ export class PantryPage implements OnInit, ViewWillEnter { ) {} async ngOnInit() { + BarcodeScanner.isSupported().then((result) => { + this.isBarcodeSupported = result.supported; + }); + this.fetchItems(); } @@ -441,4 +451,37 @@ export class PantryPage implements OnInit, ViewWillEnter { this.shoppingItems.sort(sortFunction); } } + + async scan(): Promise { + const granted = await this.requestPermissions(); + if (!granted) { + this.errorHandlerService.presentErrorToast( + 'Please grant camera permissions to use this feature', + 'Camera permissions not granted' + ); + return; + } + + const result = await BarcodeScanner.scan(); + + if ( + result.barcodes.length === 0 || + result.barcodes[0].displayValue === '' || + result.barcodes[0].displayValue === null || + result.barcodes[0].displayValue === undefined + ) { + return; + } + + this.sendBarcode(result); + } + + async requestPermissions(): Promise { + const { camera } = await BarcodeScanner.requestPermissions(); + return camera === 'granted' || camera === 'limited'; + } + + async sendBarcode(result: ScanResult): Promise { + let code = result.barcodes[0].displayValue; + } } diff --git a/frontend/src/app/services/barcode-api/barcode-api.service.spec.ts b/frontend/src/app/services/barcode-api/barcode-api.service.spec.ts new file mode 100644 index 00000000..e07f0099 --- /dev/null +++ b/frontend/src/app/services/barcode-api/barcode-api.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { BarcodeApiService } from './barcode-api.service'; + +describe('BarcodeApiService', () => { + let service: BarcodeApiService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(BarcodeApiService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/barcode-api/barcode-api.service.ts b/frontend/src/app/services/barcode-api/barcode-api.service.ts new file mode 100644 index 00000000..db9685d9 --- /dev/null +++ b/frontend/src/app/services/barcode-api/barcode-api.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { FoodItemI } from '../../models/interfaces'; + +@Injectable({ + providedIn: 'root', +}) +export class BarcodeApiService { + url: String = 'http://localhost:8080'; + + constructor(private http: HttpClient) {} + + findProduct(barcode: String): Observable> { + return this.http.post( + this.url + '/findProduct', + { + barcode: barcode, + }, + { observe: 'response' } + ); + } +} From a409bcc8d7146cdbc8f29fb3999c58ab5a38072d Mon Sep 17 00:00:00 2001 From: SkulderLock <78735770+SkulderLock@users.noreply.github.com> Date: Thu, 7 Sep 2023 20:29:55 +0200 Subject: [PATCH 03/97] =?UTF-8?q?=F0=9F=90=9B=20Bug=20fix=20and=20Barcode?= =?UTF-8?q?=20frontend=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/ProductController.java | 37 ++++++++ .../mealmaestro/models/mongo/Barcode.java | 17 ++++ .../mealmaestro/models/mongo/FoodModelM.java | 6 +- .../models/mongo/VisitedLinkModel.java | 3 +- .../repositories/mongo/FoodMRepository.java | 2 +- .../mongo/VisitedLinkRepository.java | 3 +- .../mealmaestro/services/BarcodeService.java | 34 +++++++ .../services/MealDatabaseService.java | 2 + .../services/MealManagementService.java | 1 + .../app/pages/acc-profile/acc-profile.page.ts | 4 +- frontend/src/app/pages/home/home.page.ts | 2 +- frontend/src/app/pages/pantry/pantry.page.ts | 92 +++++++++++++------ .../src/app/pages/profile/profile.page.ts | 33 +------ .../app/pages/recipe-book/recipe-book.page.ts | 8 +- .../src/app/services/login/login.service.ts | 13 +++ frontend/src/app/services/services.ts | 1 + 16 files changed, 185 insertions(+), 73 deletions(-) create mode 100644 backend/src/main/java/fellowship/mealmaestro/controllers/ProductController.java create mode 100644 backend/src/main/java/fellowship/mealmaestro/models/mongo/Barcode.java create mode 100644 backend/src/main/java/fellowship/mealmaestro/services/BarcodeService.java diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/ProductController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/ProductController.java new file mode 100644 index 00000000..92a0e478 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/ProductController.java @@ -0,0 +1,37 @@ +package fellowship.mealmaestro.controllers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +import fellowship.mealmaestro.models.mongo.Barcode; +import fellowship.mealmaestro.models.mongo.FoodModelM; +import fellowship.mealmaestro.services.BarcodeService; + +@RestController +public class ProductController { + + @Autowired + private BarcodeService barcodeService; + + @PostMapping("/findProduct") + public ResponseEntity findProduct(@RequestBody Barcode request, + @RequestHeader("Authorization") String token) { + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + return ResponseEntity.ok(barcodeService.findProduct(request.getBarcode())); + } + + @PostMapping("/addProduct") + public ResponseEntity addProduct(@RequestBody FoodModelM product, + @RequestHeader("Authorization") String token) { + if (token == null || token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + return ResponseEntity.ok(barcodeService.addProduct(product)); + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/mongo/Barcode.java b/backend/src/main/java/fellowship/mealmaestro/models/mongo/Barcode.java new file mode 100644 index 00000000..1bce9832 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/mongo/Barcode.java @@ -0,0 +1,17 @@ +package fellowship.mealmaestro.models.mongo; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class Barcode { + String barcode; + + public Barcode() { + } + + public Barcode(String barcode) { + this.barcode = barcode; + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java b/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java index fcf141f3..8dc7e542 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java @@ -21,7 +21,8 @@ public class FoodModelM { private double price; - public FoodModelM(String barcode, String name, double quantity, String unit, double price) { + public FoodModelM(String barcode, String name, double quantity, String unit, + double price) { this.barcode = barcode; this.name = name; this.quantity = quantity; @@ -34,7 +35,8 @@ public FoodModelM() { @Override public String toString() { - return "FoodModelM [barcode=" + barcode + ", name=" + name + ", price=" + price + ", quantity=" + quantity + return "FoodModelM [barcode=" + barcode + ", name=" + name + ", price=" + + price + ", quantity=" + quantity + ", unit=" + unit + "]"; } } diff --git a/backend/src/main/java/fellowship/mealmaestro/models/mongo/VisitedLinkModel.java b/backend/src/main/java/fellowship/mealmaestro/models/mongo/VisitedLinkModel.java index b80dd542..9a2ea0be 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/mongo/VisitedLinkModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/mongo/VisitedLinkModel.java @@ -27,6 +27,7 @@ public VisitedLinkModel() { @Override public String toString() { - return "VisitedLinkModel [lastVisited=" + lastVisited + ", link=" + link + "]"; + return "VisitedLinkModel [lastVisited=" + lastVisited + ", link=" + link + + "]"; } } diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/FoodMRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/FoodMRepository.java index 8105354d..71ebbd97 100644 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/FoodMRepository.java +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/FoodMRepository.java @@ -5,5 +5,5 @@ import fellowship.mealmaestro.models.mongo.FoodModelM; public interface FoodMRepository extends MongoRepository { - + FoodModelM findByBarcode(String barcode); } diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/VisitedLinkRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/VisitedLinkRepository.java index bb7aeb9f..3a3226a9 100644 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/VisitedLinkRepository.java +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/VisitedLinkRepository.java @@ -4,6 +4,7 @@ import fellowship.mealmaestro.models.mongo.VisitedLinkModel; -public interface VisitedLinkRepository extends MongoRepository { +public interface VisitedLinkRepository extends + MongoRepository { } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/BarcodeService.java b/backend/src/main/java/fellowship/mealmaestro/services/BarcodeService.java new file mode 100644 index 00000000..727ed20b --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/services/BarcodeService.java @@ -0,0 +1,34 @@ +package fellowship.mealmaestro.services; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import fellowship.mealmaestro.models.mongo.FoodModelM; +import fellowship.mealmaestro.repositories.mongo.FoodMRepository; + +@Service +public class BarcodeService { + + @Autowired + private FoodMRepository foodMRepository; + + public FoodModelM findProduct(String code) { + System.out.println(code); + Optional product = foodMRepository.findById(code); + if (product.isEmpty()) { + FoodModelM nullProduct = new FoodModelM(); + nullProduct.setName(""); + nullProduct.setQuantity(0); + nullProduct.setUnit("pcs"); + return nullProduct; + } else { + return product.get(); + } + } + + public FoodModelM addProduct(FoodModelM product) { + return foodMRepository.save(product); + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/services/MealDatabaseService.java b/backend/src/main/java/fellowship/mealmaestro/services/MealDatabaseService.java index 515f6d0f..524784f7 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/MealDatabaseService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/MealDatabaseService.java @@ -72,6 +72,7 @@ public List saveMeals(List mealsToSave, LocalDate date, St return meals; } + @Transactional public List findUsersMealPlanForDate(LocalDate date, String token) { removeOldMeals(token); @@ -117,6 +118,7 @@ public void removeOldMeals(String token) { userRepository.save(user); } + @Transactional public Optional findMealTypeForUser(String type, String token) { String email = jwtService.extractUserEmail(token); diff --git a/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java b/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java index b240700b..4c51817e 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java @@ -32,6 +32,7 @@ public MealModel generateMeal(String mealType, String token) { MealModel defaultMeal = new MealModel("Bread", "1. Toast the bread", "Delicious Bread", "https://images.unsplash.com/photo-1598373182133-52452f7691ef?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80", "Bread", "5 minutes"); + defaultMeal.setType("breakfast"); try { JsonNode mealJson = objectMapper.readTree(openaiApiService.fetchMealResponse(mealType, token)); int i = 0; diff --git a/frontend/src/app/pages/acc-profile/acc-profile.page.ts b/frontend/src/app/pages/acc-profile/acc-profile.page.ts index 964389eb..6deea09a 100644 --- a/frontend/src/app/pages/acc-profile/acc-profile.page.ts +++ b/frontend/src/app/pages/acc-profile/acc-profile.page.ts @@ -52,9 +52,7 @@ export class AccProfilePage implements OnInit, ViewWillEnter { this.getUserInfo(); } - ngOnInit() { - this.getUserInfo(); - } + ngOnInit() {} async getUserInfo() { this.auth.getUser().subscribe({ diff --git a/frontend/src/app/pages/home/home.page.ts b/frontend/src/app/pages/home/home.page.ts index 66dc6a10..6fe1a632 100644 --- a/frontend/src/app/pages/home/home.page.ts +++ b/frontend/src/app/pages/home/home.page.ts @@ -64,7 +64,7 @@ export class HomePage implements OnInit, ViewWillEnter { ); }); - await this.getMeals(); + // await this.getMeals(); } async ionViewWillEnter() { diff --git a/frontend/src/app/pages/pantry/pantry.page.ts b/frontend/src/app/pages/pantry/pantry.page.ts index 663b4bd2..d19f73f2 100644 --- a/frontend/src/app/pages/pantry/pantry.page.ts +++ b/frontend/src/app/pages/pantry/pantry.page.ts @@ -23,6 +23,7 @@ import { LoginService, PantryApiService, ShoppingListApiService, + BarcodeApiService, } from '../../services/services'; @Component({ @@ -37,7 +38,7 @@ export class PantryPage implements OnInit, ViewWillEnter { foodListItem!: QueryList; @ViewChild(IonModal) modal!: IonModal; - isBarcodeSupported: boolean = false; + isBarcodeSupported: boolean = true; segment: 'pantry' | 'shopping' | null = 'pantry'; isLoading: boolean = false; pantryItems: FoodItemI[] = []; @@ -56,15 +57,14 @@ export class PantryPage implements OnInit, ViewWillEnter { private shoppingListService: ShoppingListApiService, private errorHandlerService: ErrorHandlerService, private auth: AuthenticationService, - private loginService: LoginService + private loginService: LoginService, + private barcodeApiService: BarcodeApiService ) {} async ngOnInit() { - BarcodeScanner.isSupported().then((result) => { - this.isBarcodeSupported = result.supported; - }); - - this.fetchItems(); + // BarcodeScanner.isSupported().then((result) => { + // this.isBarcodeSupported = result.supported; + // }); } async ionViewWillEnter() { @@ -453,25 +453,32 @@ export class PantryPage implements OnInit, ViewWillEnter { } async scan(): Promise { - const granted = await this.requestPermissions(); - if (!granted) { - this.errorHandlerService.presentErrorToast( - 'Please grant camera permissions to use this feature', - 'Camera permissions not granted' - ); - return; - } - - const result = await BarcodeScanner.scan(); - - if ( - result.barcodes.length === 0 || - result.barcodes[0].displayValue === '' || - result.barcodes[0].displayValue === null || - result.barcodes[0].displayValue === undefined - ) { - return; - } + // const granted = await this.requestPermissions(); + // if (!granted) { + // this.errorHandlerService.presentErrorToast( + // 'Please grant camera permissions to use this feature', + // 'Camera permissions not granted' + // ); + // return; + // } + + // const result = await BarcodeScanner.scan(); + + // if ( + // result.barcodes.length === 0 || + // result.barcodes[0].displayValue === '' || + // result.barcodes[0].displayValue === null || + // result.barcodes[0].displayValue === undefined + // ) { + // return; + // } + let result = { + barcodes: [ + { + displayValue: '13761238123', // for testing + }, + ], + }; this.sendBarcode(result); } @@ -481,7 +488,38 @@ export class PantryPage implements OnInit, ViewWillEnter { return camera === 'granted' || camera === 'limited'; } - async sendBarcode(result: ScanResult): Promise { + async sendBarcode(result: any): Promise { + // replace any with ScanResult let code = result.barcodes[0].displayValue; + + this.barcodeApiService.findProduct(code).subscribe({ + next: (response) => { + if (response.status === 200) { + if (response.body) { + this.newItem = { + name: response.body.name, + quantity: response.body.quantity, + unit: response.body.unit, + price: response.body.price, + }; + this.modal.present(); + } + } + }, + error: (err) => { + if (err.status === 403) { + this.errorHandlerService.presentErrorToast( + 'Unauthorized access. Please login again.', + err + ); + this.auth.logout(); + } else { + this.errorHandlerService.presentErrorToast( + 'Error finding product', + err + ); + } + }, + }); } } diff --git a/frontend/src/app/pages/profile/profile.page.ts b/frontend/src/app/pages/profile/profile.page.ts index 69a28181..7097eea9 100644 --- a/frontend/src/app/pages/profile/profile.page.ts +++ b/frontend/src/app/pages/profile/profile.page.ts @@ -109,37 +109,10 @@ export class ProfilePage implements OnInit, ViewWillEnter { cookingToggle: boolean = false; BMIToggle: boolean = false; - ngOnInit() { - this.loadUserSettings(); - this.auth.getUser().subscribe({ - next: (response) => { - if (response.status == 200) { - if (response.body && response.body.name) { - this.user.username = response.body.name; - this.user.email = response.body.email; - this.user.password = response.body.password; - } - } - }, - error: (error) => { - if (error.status === 403) { - this.errorHandlerService.presentErrorToast( - 'Unauthorized access. Please login again.', - error - ); - this.auth.logout(); - } else { - this.errorHandlerService.presentErrorToast( - 'Unexpected error while loading user data', - error - ); - } - }, - }); - } + ngOnInit() {} ionViewWillEnter(): void { - if (this.loginService.isSettingsRefreshed()) { + if (!this.loginService.isSettingsRefreshed()) { this.loadUserSettings(); this.auth.getUser().subscribe({ next: (response) => { @@ -166,7 +139,7 @@ export class ProfilePage implements OnInit, ViewWillEnter { } }, }); - this.loginService.setSettingsRefreshed(false); + this.loginService.setSettingsRefreshed(true); } } diff --git a/frontend/src/app/pages/recipe-book/recipe-book.page.ts b/frontend/src/app/pages/recipe-book/recipe-book.page.ts index 2d52679f..596a0d8a 100644 --- a/frontend/src/app/pages/recipe-book/recipe-book.page.ts +++ b/frontend/src/app/pages/recipe-book/recipe-book.page.ts @@ -32,13 +32,7 @@ export class RecipeBookPage implements OnInit { private loginService: LoginService ) {} - ngOnInit() { - this.addService.recipeItem$.subscribe((recipeItem) => { - if (recipeItem) { - this.addRecipe(recipeItem); - } - }); - } + ngOnInit() {} async ionViewWillEnter() { if (!this.loginService.isRecipeBookRefreshed()) { diff --git a/frontend/src/app/services/login/login.service.ts b/frontend/src/app/services/login/login.service.ts index d50e48c6..5c9ed999 100644 --- a/frontend/src/app/services/login/login.service.ts +++ b/frontend/src/app/services/login/login.service.ts @@ -49,4 +49,17 @@ export class LoginService { this.recipeBookRefreshed = false; this.settingsRefreshed = false; } + + toString(): string { + return ( + 'homeRefreshed: ' + + this.homeRefreshed + + ', pantryRefreshed: ' + + this.pantryRefreshed + + ', recipeBookRefreshed: ' + + this.recipeBookRefreshed + + ', settingsRefreshed: ' + + this.settingsRefreshed + ); + } } diff --git a/frontend/src/app/services/services.ts b/frontend/src/app/services/services.ts index 284eb170..cc9cf886 100644 --- a/frontend/src/app/services/services.ts +++ b/frontend/src/app/services/services.ts @@ -6,3 +6,4 @@ export { RecipeBookApiService } from './recipe-book/recipe-book-api.service'; export { SettingsApiService } from './settings-api/settings-api.service'; export { MealGenerationService } from './meal-generation/meal-generation.service'; export { LoginService } from './login/login.service'; +export { BarcodeApiService } from './barcode-api/barcode-api.service'; From 3a880272d877f49437aa22b853c9e85853570b29 Mon Sep 17 00:00:00 2001 From: SkulderLock <78735770+SkulderLock@users.noreply.github.com> Date: Fri, 8 Sep 2023 15:21:30 +0200 Subject: [PATCH 04/97] =?UTF-8?q?=E2=9C=A8=20Added=20Dynamic=20collections?= =?UTF-8?q?=20for=20products?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/ProductController.java | 6 ++-- .../mealmaestro/models/mongo/Barcode.java | 17 ----------- .../mealmaestro/models/mongo/FoodModelM.java | 2 ++ .../models/mongo/findBarcodeRequest.java | 19 +++++++++++++ .../mongo/DynamicFoodMRepository.java | 12 ++++++++ .../mongo/DynamicFoodMRepositoryImpl.java | 28 +++++++++++++++++++ .../repositories/mongo/FoodMRepository.java | 2 +- .../mealmaestro/services/BarcodeService.java | 10 ++++--- frontend/src/app/pages/pantry/pantry.page.ts | 22 ++++++++++----- 9 files changed, 86 insertions(+), 32 deletions(-) delete mode 100644 backend/src/main/java/fellowship/mealmaestro/models/mongo/Barcode.java create mode 100644 backend/src/main/java/fellowship/mealmaestro/models/mongo/findBarcodeRequest.java create mode 100644 backend/src/main/java/fellowship/mealmaestro/repositories/mongo/DynamicFoodMRepository.java create mode 100644 backend/src/main/java/fellowship/mealmaestro/repositories/mongo/DynamicFoodMRepositoryImpl.java diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/ProductController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/ProductController.java index 92a0e478..e590aadf 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/ProductController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/ProductController.java @@ -7,7 +7,7 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; -import fellowship.mealmaestro.models.mongo.Barcode; +import fellowship.mealmaestro.models.mongo.findBarcodeRequest; import fellowship.mealmaestro.models.mongo.FoodModelM; import fellowship.mealmaestro.services.BarcodeService; @@ -18,12 +18,12 @@ public class ProductController { private BarcodeService barcodeService; @PostMapping("/findProduct") - public ResponseEntity findProduct(@RequestBody Barcode request, + public ResponseEntity findProduct(@RequestBody findBarcodeRequest request, @RequestHeader("Authorization") String token) { if (token == null || token.isEmpty()) { return ResponseEntity.badRequest().build(); } - return ResponseEntity.ok(barcodeService.findProduct(request.getBarcode())); + return ResponseEntity.ok(barcodeService.findProduct(request)); } @PostMapping("/addProduct") diff --git a/backend/src/main/java/fellowship/mealmaestro/models/mongo/Barcode.java b/backend/src/main/java/fellowship/mealmaestro/models/mongo/Barcode.java deleted file mode 100644 index 1bce9832..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/models/mongo/Barcode.java +++ /dev/null @@ -1,17 +0,0 @@ -package fellowship.mealmaestro.models.mongo; - -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class Barcode { - String barcode; - - public Barcode() { - } - - public Barcode(String barcode) { - this.barcode = barcode; - } -} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java b/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java index 8dc7e542..ca0401d8 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java @@ -13,6 +13,8 @@ public class FoodModelM { @Id private String barcode; + private String store; + private String name; private double quantity; diff --git a/backend/src/main/java/fellowship/mealmaestro/models/mongo/findBarcodeRequest.java b/backend/src/main/java/fellowship/mealmaestro/models/mongo/findBarcodeRequest.java new file mode 100644 index 00000000..a8bd7ffb --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/mongo/findBarcodeRequest.java @@ -0,0 +1,19 @@ +package fellowship.mealmaestro.models.mongo; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class findBarcodeRequest { + String barcode; + String store; + + public findBarcodeRequest() { + } + + public findBarcodeRequest(String barcode, String store) { + this.barcode = barcode; + this.store = store; + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/DynamicFoodMRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/DynamicFoodMRepository.java new file mode 100644 index 00000000..3c5b8ba8 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/DynamicFoodMRepository.java @@ -0,0 +1,12 @@ +package fellowship.mealmaestro.repositories.mongo; + +import java.util.Optional; + +import fellowship.mealmaestro.models.mongo.FoodModelM; + +public interface DynamicFoodMRepository { + FoodModelM saveInDynamicCollection(FoodModelM food); + + // find + Optional findInDynamicCollection(String barcode, String store); +} \ No newline at end of file diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/DynamicFoodMRepositoryImpl.java b/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/DynamicFoodMRepositoryImpl.java new file mode 100644 index 00000000..720216a3 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/DynamicFoodMRepositoryImpl.java @@ -0,0 +1,28 @@ +package fellowship.mealmaestro.repositories.mongo; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mongodb.core.MongoTemplate; + +import fellowship.mealmaestro.models.mongo.FoodModelM; + +public class DynamicFoodMRepositoryImpl implements DynamicFoodMRepository { + private final MongoTemplate mongoTemplate; + + @Autowired + public DynamicFoodMRepositoryImpl(MongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + } + + @Override + public FoodModelM saveInDynamicCollection(FoodModelM food) { + return mongoTemplate.save(food, food.getStore()); + } + + @Override + public Optional findInDynamicCollection(String barcode, String store) { + FoodModelM m = mongoTemplate.findById(barcode, FoodModelM.class, store); + return Optional.ofNullable(m); + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/FoodMRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/FoodMRepository.java index 71ebbd97..b35f876b 100644 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/FoodMRepository.java +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/FoodMRepository.java @@ -4,6 +4,6 @@ import fellowship.mealmaestro.models.mongo.FoodModelM; -public interface FoodMRepository extends MongoRepository { +public interface FoodMRepository extends MongoRepository, DynamicFoodMRepository { FoodModelM findByBarcode(String barcode); } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/BarcodeService.java b/backend/src/main/java/fellowship/mealmaestro/services/BarcodeService.java index 727ed20b..e36c10c3 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/BarcodeService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/BarcodeService.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Service; import fellowship.mealmaestro.models.mongo.FoodModelM; +import fellowship.mealmaestro.models.mongo.findBarcodeRequest; import fellowship.mealmaestro.repositories.mongo.FoodMRepository; @Service @@ -14,9 +15,10 @@ public class BarcodeService { @Autowired private FoodMRepository foodMRepository; - public FoodModelM findProduct(String code) { - System.out.println(code); - Optional product = foodMRepository.findById(code); + public FoodModelM findProduct(findBarcodeRequest request) { + System.out.println(request.getStore()); + Optional product = foodMRepository.findInDynamicCollection(request.getBarcode(), + request.getStore()); if (product.isEmpty()) { FoodModelM nullProduct = new FoodModelM(); nullProduct.setName(""); @@ -29,6 +31,6 @@ public FoodModelM findProduct(String code) { } public FoodModelM addProduct(FoodModelM product) { - return foodMRepository.save(product); + return foodMRepository.saveInDynamicCollection(product); } } diff --git a/frontend/src/app/pages/pantry/pantry.page.ts b/frontend/src/app/pages/pantry/pantry.page.ts index d19f73f2..5e37201f 100644 --- a/frontend/src/app/pages/pantry/pantry.page.ts +++ b/frontend/src/app/pages/pantry/pantry.page.ts @@ -496,13 +496,17 @@ export class PantryPage implements OnInit, ViewWillEnter { next: (response) => { if (response.status === 200) { if (response.body) { - this.newItem = { - name: response.body.name, - quantity: response.body.quantity, - unit: response.body.unit, - price: response.body.price, - }; - this.modal.present(); + if (response.body.name === '') { + this.barcodeNotFound(code); + } else { + this.newItem = { + name: response.body.name, + quantity: response.body.quantity, + unit: response.body.unit, + price: response.body.price, + }; + this.modal.present(); + } } } }, @@ -522,4 +526,8 @@ export class PantryPage implements OnInit, ViewWillEnter { }, }); } + + async barcodeNotFound(code: string) { + //IMPLEMENT + } } From ecbea6089a370210608bee57ce030b95f490e73c Mon Sep 17 00:00:00 2001 From: SkulderLock <78735770+SkulderLock@users.noreply.github.com> Date: Fri, 8 Sep 2023 19:22:31 +0200 Subject: [PATCH 05/97] =?UTF-8?q?=F0=9F=9A=A7=20Added=20methods=20to=20cho?= =?UTF-8?q?ose=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/app/pages/pantry/pantry.page.ts | 165 ++++++++++++++---- .../authentication/authentication.service.ts | 2 +- .../barcode-api/barcode-api.service.ts | 14 +- .../src/app/services/login/login.service.ts | 20 +++ .../meal-generation.service.ts | 2 +- .../services/pantry-api/pantry-api.service.ts | 2 +- .../recipe-book/recipe-book-api.service.ts | 2 +- .../shopping-list-api.service.ts | 2 +- .../app/services/user-api/user-api.service.ts | 2 +- 9 files changed, 169 insertions(+), 42 deletions(-) diff --git a/frontend/src/app/pages/pantry/pantry.page.ts b/frontend/src/app/pages/pantry/pantry.page.ts index 5e37201f..4ed6c6de 100644 --- a/frontend/src/app/pages/pantry/pantry.page.ts +++ b/frontend/src/app/pages/pantry/pantry.page.ts @@ -5,7 +5,13 @@ import { ViewChildren, ViewChild, } from '@angular/core'; -import { IonModal, IonicModule, ViewWillEnter } from '@ionic/angular'; +import { + AlertController, + AlertInput, + IonModal, + IonicModule, + ViewWillEnter, +} from '@ionic/angular'; import { Router } from '@angular/router'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; @@ -45,6 +51,7 @@ export class PantryPage implements OnInit, ViewWillEnter { shoppingItems: FoodItemI[] = []; searchTerm: string = ''; currentSort: string = 'name-down'; + stores: string[] = []; newItem: FoodItemI = { name: '', quantity: null, @@ -58,7 +65,8 @@ export class PantryPage implements OnInit, ViewWillEnter { private errorHandlerService: ErrorHandlerService, private auth: AuthenticationService, private loginService: LoginService, - private barcodeApiService: BarcodeApiService + private barcodeApiService: BarcodeApiService, + private alertController: AlertController ) {} async ngOnInit() { @@ -72,6 +80,18 @@ export class PantryPage implements OnInit, ViewWillEnter { this.fetchItems(); this.loginService.setPantryRefreshed(true); } + if (!this.loginService.isStoresRefreshed()) { + this.barcodeApiService.fetchStores().subscribe({ + next: (response) => { + if (response.status === 200) { + if (response.body) { + this.stores = response.body; + } + } + }, + }); + this.loginService.setStoresRefreshed(true); + } } async fetchItems() { @@ -480,7 +500,11 @@ export class PantryPage implements OnInit, ViewWillEnter { ], }; - this.sendBarcode(result); + if (this.loginService.isShoppingAt() === '') { + this.askShoppingLocation(result); + } else { + this.sendBarcode(result); + } } async requestPermissions(): Promise { @@ -490,44 +514,117 @@ export class PantryPage implements OnInit, ViewWillEnter { async sendBarcode(result: any): Promise { // replace any with ScanResult + console.log(result.barcodes[0].displayValue); let code = result.barcodes[0].displayValue; - this.barcodeApiService.findProduct(code).subscribe({ - next: (response) => { - if (response.status === 200) { - if (response.body) { - if (response.body.name === '') { - this.barcodeNotFound(code); - } else { - this.newItem = { - name: response.body.name, - quantity: response.body.quantity, - unit: response.body.unit, - price: response.body.price, - }; - this.modal.present(); + this.barcodeApiService + .findProduct(code, this.loginService.isShoppingAt()) + .subscribe({ + next: (response) => { + if (response.status === 200) { + if (response.body) { + if (response.body.name === '') { + this.barcodeNotFound(code); + } else { + this.newItem = { + name: response.body.name, + quantity: response.body.quantity, + unit: response.body.unit, + price: response.body.price, + }; + this.modal.present(); + } } } - } - }, - error: (err) => { - if (err.status === 403) { - this.errorHandlerService.presentErrorToast( - 'Unauthorized access. Please login again.', - err - ); - this.auth.logout(); - } else { - this.errorHandlerService.presentErrorToast( - 'Error finding product', - err - ); - } - }, - }); + }, + error: (err) => { + if (err.status === 403) { + this.errorHandlerService.presentErrorToast( + 'Unauthorized access. Please login again.', + err + ); + this.auth.logout(); + } else { + this.errorHandlerService.presentErrorToast( + 'Error finding product', + err + ); + } + }, + }); } async barcodeNotFound(code: string) { //IMPLEMENT } + + async askShoppingLocation(code: any) { + const alert = await this.alertController.create({ + header: 'Shopping Location', + message: 'Where are you shopping?', + inputs: [ + ...this.stores.map((store) => ({ + label: store, + type: 'radio' as const, + value: store, + })), + { + label: 'Other', + type: 'radio', + value: 'Other', + }, + ], + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Confirm', + handler: (data) => { + if (data === 'Other') { + this.askNewShoppingLocation(code); + return; + } + this.loginService.setShoppingAt(data); + this.sendBarcode(code); + }, + }, + ], + }); + + await alert.present(); + } + + async askNewShoppingLocation(code: any) { + const alert = await this.alertController.create({ + header: 'Shopping Location', + message: 'Where are you shopping?', + inputs: [ + { + name: 'store', + type: 'text', + placeholder: 'Store', + }, + ], + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Confirm', + handler: (data) => { + //Need to do some error handling here + + this.loginService.setShoppingAt(data.store); + this.loginService.setStoresRefreshed(false); + this.sendBarcode(code); + }, + }, + ], + }); + + await alert.present(); + } } diff --git a/frontend/src/app/services/authentication/authentication.service.ts b/frontend/src/app/services/authentication/authentication.service.ts index cef83066..f141a48a 100644 --- a/frontend/src/app/services/authentication/authentication.service.ts +++ b/frontend/src/app/services/authentication/authentication.service.ts @@ -10,7 +10,7 @@ import { LoginService } from '../login/login.service'; providedIn: 'root', }) export class AuthenticationService { - url: String = 'http://localhost:8080'; + url: string = 'http://localhost:8080'; constructor( private http: HttpClient, diff --git a/frontend/src/app/services/barcode-api/barcode-api.service.ts b/frontend/src/app/services/barcode-api/barcode-api.service.ts index db9685d9..9112d9a2 100644 --- a/frontend/src/app/services/barcode-api/barcode-api.service.ts +++ b/frontend/src/app/services/barcode-api/barcode-api.service.ts @@ -7,17 +7,27 @@ import { FoodItemI } from '../../models/interfaces'; providedIn: 'root', }) export class BarcodeApiService { - url: String = 'http://localhost:8080'; + url: string = 'http://localhost:8080'; constructor(private http: HttpClient) {} - findProduct(barcode: String): Observable> { + findProduct( + barcode: string, + store: string + ): Observable> { return this.http.post( this.url + '/findProduct', { barcode: barcode, + store: store, }, { observe: 'response' } ); } + + fetchStores(): Observable> { + return this.http.get(this.url + '/fetchStores', { + observe: 'response', + }); + } } diff --git a/frontend/src/app/services/login/login.service.ts b/frontend/src/app/services/login/login.service.ts index 5c9ed999..70f47bb3 100644 --- a/frontend/src/app/services/login/login.service.ts +++ b/frontend/src/app/services/login/login.service.ts @@ -8,6 +8,8 @@ export class LoginService { private pantryRefreshed: boolean = false; private recipeBookRefreshed: boolean = false; private settingsRefreshed: boolean = false; + private storesRefreshed: boolean = false; + private shoppingLocation: string | '' = ''; constructor() {} @@ -43,11 +45,29 @@ export class LoginService { this.settingsRefreshed = refreshed; } + isStoresRefreshed(): boolean { + return this.storesRefreshed; + } + + setStoresRefreshed(refreshed: boolean): void { + this.storesRefreshed = refreshed; + } + + isShoppingAt(): string { + return this.shoppingLocation; + } + + setShoppingAt(location: string): void { + this.shoppingLocation = location; + } + resetRefreshed(): void { this.homeRefreshed = false; this.pantryRefreshed = false; this.recipeBookRefreshed = false; this.settingsRefreshed = false; + this.storesRefreshed = false; + this.shoppingLocation = ''; } toString(): string { diff --git a/frontend/src/app/services/meal-generation/meal-generation.service.ts b/frontend/src/app/services/meal-generation/meal-generation.service.ts index 1c3b1462..14ad361f 100644 --- a/frontend/src/app/services/meal-generation/meal-generation.service.ts +++ b/frontend/src/app/services/meal-generation/meal-generation.service.ts @@ -7,7 +7,7 @@ import { MealI, RegenerateMealRequestI } from '../../models/interfaces'; providedIn: 'root', }) export class MealGenerationService { - url: String = 'http://localhost:8080'; + url: string = 'http://localhost:8080'; constructor(private http: HttpClient) {} diff --git a/frontend/src/app/services/pantry-api/pantry-api.service.ts b/frontend/src/app/services/pantry-api/pantry-api.service.ts index 761dd275..1a17228c 100644 --- a/frontend/src/app/services/pantry-api/pantry-api.service.ts +++ b/frontend/src/app/services/pantry-api/pantry-api.service.ts @@ -7,7 +7,7 @@ import { FoodItemI } from '../../models/interfaces'; providedIn: 'root', }) export class PantryApiService { - url: String = 'http://localhost:8080'; + url: string = 'http://localhost:8080'; constructor(private http: HttpClient) {} diff --git a/frontend/src/app/services/recipe-book/recipe-book-api.service.ts b/frontend/src/app/services/recipe-book/recipe-book-api.service.ts index 2ed0a874..2f44dd01 100644 --- a/frontend/src/app/services/recipe-book/recipe-book-api.service.ts +++ b/frontend/src/app/services/recipe-book/recipe-book-api.service.ts @@ -13,7 +13,7 @@ export class RecipeBookApiService { password: '', }; - url: String = 'http://localhost:8080'; + url: string = 'http://localhost:8080'; constructor(private http: HttpClient) {} diff --git a/frontend/src/app/services/shopping-list-api/shopping-list-api.service.ts b/frontend/src/app/services/shopping-list-api/shopping-list-api.service.ts index 112b9452..8fe447fe 100644 --- a/frontend/src/app/services/shopping-list-api/shopping-list-api.service.ts +++ b/frontend/src/app/services/shopping-list-api/shopping-list-api.service.ts @@ -7,7 +7,7 @@ import { FoodItemI } from '../../models/interfaces'; providedIn: 'root', }) export class ShoppingListApiService { - url: String = 'http://localhost:8080'; + url: string = 'http://localhost:8080'; constructor(private http: HttpClient) {} diff --git a/frontend/src/app/services/user-api/user-api.service.ts b/frontend/src/app/services/user-api/user-api.service.ts index 11207342..acbcbc8c 100644 --- a/frontend/src/app/services/user-api/user-api.service.ts +++ b/frontend/src/app/services/user-api/user-api.service.ts @@ -7,7 +7,7 @@ import { UserI } from '../../models/user.model'; providedIn: 'root', }) export class UserApiService { - url: String = 'http://localhost:8080'; + url: string = 'http://localhost:8080'; constructor(private http: HttpClient) {} From 5379238f90180eb1c70cb8750ddc55b4acf15e6a Mon Sep 17 00:00:00 2001 From: SkulderLock <78735770+SkulderLock@users.noreply.github.com> Date: Sat, 9 Sep 2023 13:29:17 +0200 Subject: [PATCH 06/97] =?UTF-8?q?=F0=9F=9A=A7=20Webscraper=20code=20added?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/build.gradle | 2 + .../mealmaestro/MealmaestroApplication.java | 4 +- .../mealmaestro/models/mongo/FoodModelM.java | 21 +- .../services/webscraping/CheckersScraper.java | 250 ++++++++++++++++++ .../webscraping/WebscrapeService.java | 20 ++ frontend/src/app/pages/pantry/pantry.page.ts | 65 ++--- .../barcode-api/barcode-api.service.ts | 6 - .../src/app/services/login/login.service.ts | 10 - 8 files changed, 317 insertions(+), 61 deletions(-) create mode 100644 backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java create mode 100644 backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java diff --git a/backend/build.gradle b/backend/build.gradle index 85154fed..a3d59903 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -25,6 +25,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-neo4j' implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.seleniumhq.selenium:selenium-java:4.12.1' + implementation 'org.jsoup:jsoup:1.16.1' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' diff --git a/backend/src/main/java/fellowship/mealmaestro/MealmaestroApplication.java b/backend/src/main/java/fellowship/mealmaestro/MealmaestroApplication.java index 6f094f5f..bd66dedc 100644 --- a/backend/src/main/java/fellowship/mealmaestro/MealmaestroApplication.java +++ b/backend/src/main/java/fellowship/mealmaestro/MealmaestroApplication.java @@ -1,13 +1,13 @@ package fellowship.mealmaestro; - import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; import com.fasterxml.jackson.core.JsonProcessingException; - @SpringBootApplication +@EnableScheduling public class MealmaestroApplication { public static void main(String[] args) throws JsonProcessingException { diff --git a/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java b/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java index ca0401d8..0078ba7f 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java @@ -1,5 +1,8 @@ package fellowship.mealmaestro.models.mongo; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; @@ -41,4 +44,20 @@ public String toString() { price + ", quantity=" + quantity + ", unit=" + unit + "]"; } -} + + public void setPrice(String price) { + // remove R sign + price = price.substring(1); + this.price = Double.parseDouble(price); + } + + public void setAmount(String amount) { + Pattern pattern = Pattern.compile("([0-9.]+)([a-zA-Z]+)"); + Matcher matcher = pattern.matcher(amount); + + if (matcher.find()) { + this.quantity = Double.parseDouble(matcher.group(1)); + this.unit = matcher.group(2); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java new file mode 100644 index 00000000..2bf276e4 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java @@ -0,0 +1,250 @@ +package fellowship.mealmaestro.services.webscraping; + +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeOptions; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +import fellowship.mealmaestro.models.mongo.FoodModelM; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +public class CheckersScraper { + + public void scrape() { + System.setProperty("webdriver.chrome.driver", + "C:\\Users\\Ethan\\Downloads\\chromedriver-win64\\chromedriver-win64\\chromedriver.exe"); + + ChromeOptions options = new ChromeOptions(); + // options.addArguments("--headless"); + options.addArguments("--blink-settings=imagesEnabled=false"); + options.addArguments("--disable-extensions"); + options.addArguments("--user-agent=FellowshipBot-UniversityOfPretoria-FinalYearProject"); + + WebDriver driver = new ChromeDriver(options); + Duration timeout = Duration.ofSeconds(15); + WebDriverWait wait = new WebDriverWait(driver, timeout); + + // Visit categories sitemap to get all locs + // driver.get("https://www.checkers.co.za/sitemap/medias/Category-checkersZA-0.xml"); + + // String domString = driver.getPageSource(); + + // Document doc = Jsoup.parse(domString); + // Elements links = doc.select("loc"); + + // Filter out non-food links + List foodLinks = new ArrayList(); + // for (int i = 0; i < links.size(); i++) { + // String link = links.get(i).text(); + // if (link.contains("food") || link.contains("Food")) { + // foodLinks.add(link); + // } + // } + + long lastRequestTime = 0; + + foodLinks.add("file:///D:\\Code\\MessingAround\\app\\src\\main\\java\\messingaround\\applep1.html"); + foodLinks.add("file:///D:\\Code\\MessingAround\\app\\src\\main\\java\\messingaround\\milk.html"); + + // Visit each food category page and get all product links + Set visitedLinks = new HashSet(); + List productLinks = new ArrayList(); + List paginationLinks = new ArrayList(); + List foodModels = new ArrayList(); + + for (String link : foodLinks) { + // Check if link has been visited + if (visitedLinks.contains(link)) { + continue; + } + + long currentTime = System.currentTimeMillis(); + long timeSinceLastRequest = currentTime - lastRequestTime; + + // Wait 10 seconds between requests + if (timeSinceLastRequest < 10000) { + try { + System.out.println("Waiting " + (10000 - timeSinceLastRequest) + "ms"); + Thread.sleep(10000 - timeSinceLastRequest); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + visitedLinks.add(link); + driver.get(link); + System.out.println("Visiting " + link); + + // Wait for page to load + WebElement element = wait + .until(ExpectedConditions.elementToBeClickable(By.cssSelector("h3.item-product__name > a"))); + + // Full products page dom + String dom = driver.getPageSource(); + Document productPageDoc = Jsoup.connect(link).get(); + + // Get product links + Elements productPageLinks = productPageDoc.select("h3.item-product__name > a"); + + for (int i = 0; i < productPageLinks.size(); i++) { + String productPageLink = productPageLinks.get(i).attr("href"); + productLinks.add(productPageLink); + } + + // Find pagination links if they exist + Element paginationBar = productPageDoc.selectFirst("div.pagination-bar.bottom"); + Elements paginationLinksEl = paginationBar.select("a"); + + for (Element paginationLink : paginationLinksEl) { + String paginationLinkHref = paginationLink.attr("href"); + + // add to pagination links if they aren't in visitedLinks + if (!visitedLinks.contains(paginationLinkHref)) { + paginationLinks.add(paginationLinkHref); + } + } + + // Update last request time + lastRequestTime = System.currentTimeMillis(); + } + + System.out.println("############################################"); + System.out.println("Main links finished, starting pagination links"); + System.out.println("############################################"); + + // Visit each pagination link and get all product links + for (String link : paginationLinks) { + // Check if link has been visited + if (visitedLinks.contains(link)) { + continue; + } + + long currentTime = System.currentTimeMillis(); + long timeSinceLastRequest = currentTime - lastRequestTime; + + // Wait 10 seconds between requests + if (timeSinceLastRequest < 10000) { + try { + System.out.println("Waiting " + (10000 - timeSinceLastRequest) + "ms"); + Thread.sleep(10000 - timeSinceLastRequest); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + visitedLinks.add(link); + driver.get("file:///D:\\Code\\MessingAround" + link); + System.out.println("Visiting " + link); + + // Wait for page to load + WebElement element = wait + .until(ExpectedConditions.elementToBeClickable(By.cssSelector("h3.item-product__name > a"))); + + // Full products page dom + String dom = driver.getPageSource(); + Document productPageDoc = Jsoup.parse(dom); + + // Get product links + Elements productPageLinks = productPageDoc.select("h3.item-product__name > a"); + + for (int i = 0; i < productPageLinks.size(); i++) { + String productPageLink = productPageLinks.get(i).attr("href"); + productLinks.add(productPageLink); + } + + // Update last request time + lastRequestTime = System.currentTimeMillis(); + } + + System.out.println("############################################"); + System.out.println("Finished getting product links"); + System.out.println("############################################"); + + // Visit each product page and get product info + for (String link : productLinks) { + // Check if link has been visited + if (visitedLinks.contains(link)) { + continue; + } + long currentTime = System.currentTimeMillis(); + long timeSinceLastRequest = currentTime - lastRequestTime; + + // Wait 10 seconds between requests + if (timeSinceLastRequest < 10000) { + try { + System.out.println("Waiting " + (10000 - timeSinceLastRequest) + "ms"); + Thread.sleep(10000 - timeSinceLastRequest); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + visitedLinks.add(link); + driver.get("file:///D:\\Code\\MessingAround" + link); + System.out.println("Visiting " + driver.getTitle()); + + // Wait for page to load + WebElement element = wait.until( + ExpectedConditions.elementToBeClickable(By.cssSelector("#accessibletabsnavigation0-1"))); + + // Full product page dom + String dom = driver.getPageSource(); + Document productDoc = Jsoup.parse(dom); + + // Get product info + FoodModelM food = new FoodModelM(); + // product name + String productName = productDoc.selectFirst("h1.pdp__name").text(); + System.out.println("Product name: " + productName); + food.setName(productName); + + // product price + String productPrice = productDoc.selectFirst("div.special-price__price").text(); + System.out.println("Product price: " + productPrice); + food.setPrice(productPrice); + + // product details + Elements productDetails = productDoc.select("table.pdp__product-information > tbody > tr"); + + String barcode = ""; + String quantity = ""; + + for (Element productDetail : productDetails) { + if (productDetail.text().toLowerCase().contains("barcode")) { + // select second td + barcode = productDetail.selectFirst("td:nth-child(2)").text(); + System.out.println("Barcode: " + barcode); + food.setBarcode(barcode); + } + + if (productDetail.text().toLowerCase().contains("weight") + || productDetail.text().toLowerCase().contains("volume")) { + // select second td + quantity = productDetail.selectFirst("td:nth-child(2)").text(); + System.out.println("Quantity: " + quantity); + food.setAmount(quantity); + } + } + + // Add food to list + foodModels.add(food); + + lastRequestTime = System.currentTimeMillis(); + } + + driver.quit(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java new file mode 100644 index 00000000..a2abc472 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java @@ -0,0 +1,20 @@ +package fellowship.mealmaestro.services.webscraping; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +@Service +public class WebscrapeService { + + private volatile boolean isScrapingAllowed = true; + + @Scheduled(cron = "0 0 6 * * ?") + public void startScrape() { + System.out.println("Scraping started..."); + } + + @Scheduled(cron = "0 42 10 * * ?") + public void stopScraping() { + System.out.println("Scraping stopped..."); + } +} diff --git a/frontend/src/app/pages/pantry/pantry.page.ts b/frontend/src/app/pages/pantry/pantry.page.ts index 4ed6c6de..97b58055 100644 --- a/frontend/src/app/pages/pantry/pantry.page.ts +++ b/frontend/src/app/pages/pantry/pantry.page.ts @@ -51,7 +51,6 @@ export class PantryPage implements OnInit, ViewWillEnter { shoppingItems: FoodItemI[] = []; searchTerm: string = ''; currentSort: string = 'name-down'; - stores: string[] = []; newItem: FoodItemI = { name: '', quantity: null, @@ -80,18 +79,6 @@ export class PantryPage implements OnInit, ViewWillEnter { this.fetchItems(); this.loginService.setPantryRefreshed(true); } - if (!this.loginService.isStoresRefreshed()) { - this.barcodeApiService.fetchStores().subscribe({ - next: (response) => { - if (response.status === 200) { - if (response.body) { - this.stores = response.body; - } - } - }, - }); - this.loginService.setStoresRefreshed(true); - } } async fetchItems() { @@ -524,7 +511,7 @@ export class PantryPage implements OnInit, ViewWillEnter { if (response.status === 200) { if (response.body) { if (response.body.name === '') { - this.barcodeNotFound(code); + this.barcodeNotFound(); } else { this.newItem = { name: response.body.name, @@ -554,24 +541,16 @@ export class PantryPage implements OnInit, ViewWillEnter { }); } - async barcodeNotFound(code: string) { + async barcodeNotFound() { //IMPLEMENT - } - - async askShoppingLocation(code: any) { const alert = await this.alertController.create({ - header: 'Shopping Location', - message: 'Where are you shopping?', + header: 'Barcode not found', + message: 'Please enter the name of the item', inputs: [ - ...this.stores.map((store) => ({ - label: store, - type: 'radio' as const, - value: store, - })), { - label: 'Other', - type: 'radio', - value: 'Other', + name: 'name', + type: 'text', + placeholder: 'Name', }, ], buttons: [ @@ -582,12 +561,12 @@ export class PantryPage implements OnInit, ViewWillEnter { { text: 'Confirm', handler: (data) => { - if (data === 'Other') { - this.askNewShoppingLocation(code); - return; - } - this.loginService.setShoppingAt(data); - this.sendBarcode(code); + this.newItem = { + name: data.name, + quantity: null, + unit: 'pcs', + }; + this.modal.present(); }, }, ], @@ -596,15 +575,20 @@ export class PantryPage implements OnInit, ViewWillEnter { await alert.present(); } - async askNewShoppingLocation(code: any) { + async askShoppingLocation(code: any) { const alert = await this.alertController.create({ header: 'Shopping Location', message: 'Where are you shopping?', inputs: [ { - name: 'store', - type: 'text', - placeholder: 'Store', + label: 'Woolworths', + type: 'radio', + value: 'Woolworths', + }, + { + label: 'Checkers', + type: 'radio', + value: 'Checkers', }, ], buttons: [ @@ -615,10 +599,7 @@ export class PantryPage implements OnInit, ViewWillEnter { { text: 'Confirm', handler: (data) => { - //Need to do some error handling here - - this.loginService.setShoppingAt(data.store); - this.loginService.setStoresRefreshed(false); + this.loginService.setShoppingAt(data); this.sendBarcode(code); }, }, diff --git a/frontend/src/app/services/barcode-api/barcode-api.service.ts b/frontend/src/app/services/barcode-api/barcode-api.service.ts index 9112d9a2..ef8ce40d 100644 --- a/frontend/src/app/services/barcode-api/barcode-api.service.ts +++ b/frontend/src/app/services/barcode-api/barcode-api.service.ts @@ -24,10 +24,4 @@ export class BarcodeApiService { { observe: 'response' } ); } - - fetchStores(): Observable> { - return this.http.get(this.url + '/fetchStores', { - observe: 'response', - }); - } } diff --git a/frontend/src/app/services/login/login.service.ts b/frontend/src/app/services/login/login.service.ts index 70f47bb3..2c090226 100644 --- a/frontend/src/app/services/login/login.service.ts +++ b/frontend/src/app/services/login/login.service.ts @@ -8,7 +8,6 @@ export class LoginService { private pantryRefreshed: boolean = false; private recipeBookRefreshed: boolean = false; private settingsRefreshed: boolean = false; - private storesRefreshed: boolean = false; private shoppingLocation: string | '' = ''; constructor() {} @@ -45,14 +44,6 @@ export class LoginService { this.settingsRefreshed = refreshed; } - isStoresRefreshed(): boolean { - return this.storesRefreshed; - } - - setStoresRefreshed(refreshed: boolean): void { - this.storesRefreshed = refreshed; - } - isShoppingAt(): string { return this.shoppingLocation; } @@ -66,7 +57,6 @@ export class LoginService { this.pantryRefreshed = false; this.recipeBookRefreshed = false; this.settingsRefreshed = false; - this.storesRefreshed = false; this.shoppingLocation = ''; } From 292f8ddfadca550f46202957d29c84e19ea3e1a2 Mon Sep 17 00:00:00 2001 From: SkulderLock <78735770+SkulderLock@users.noreply.github.com> Date: Sat, 9 Sep 2023 14:46:52 +0200 Subject: [PATCH 07/97] =?UTF-8?q?=F0=9F=9A=A7=20Get=20sitemap=20links=20Ch?= =?UTF-8?q?eckers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../models/mongo/ToVisitLinkModel.java | 22 ++++++++ .../mongo/ToVisitLinkRepository.java | 10 ++++ .../services/webscraping/CheckersScraper.java | 54 ++++++++++++++----- 3 files changed, 74 insertions(+), 12 deletions(-) create mode 100644 backend/src/main/java/fellowship/mealmaestro/models/mongo/ToVisitLinkModel.java create mode 100644 backend/src/main/java/fellowship/mealmaestro/repositories/mongo/ToVisitLinkRepository.java diff --git a/backend/src/main/java/fellowship/mealmaestro/models/mongo/ToVisitLinkModel.java b/backend/src/main/java/fellowship/mealmaestro/models/mongo/ToVisitLinkModel.java new file mode 100644 index 00000000..9d60d49b --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/mongo/ToVisitLinkModel.java @@ -0,0 +1,22 @@ +package fellowship.mealmaestro.models.mongo; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import lombok.Getter; +import lombok.Setter; + +@Document(collection = "ToVisitLinks") +@Setter +@Getter +public class ToVisitLinkModel { + @Id + private String link; + + public ToVisitLinkModel(String link) { + this.link = link; + } + + public ToVisitLinkModel() { + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/ToVisitLinkRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/ToVisitLinkRepository.java new file mode 100644 index 00000000..d7b903ce --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/ToVisitLinkRepository.java @@ -0,0 +1,10 @@ +package fellowship.mealmaestro.repositories.mongo; + +import org.springframework.data.mongodb.repository.MongoRepository; + +import fellowship.mealmaestro.models.mongo.ToVisitLinkModel; + +public interface ToVisitLinkRepository extends + MongoRepository { + +} diff --git a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java index 2bf276e4..1523db19 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java @@ -7,13 +7,20 @@ import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; +import org.springframework.beans.factory.annotation.Autowired; import fellowship.mealmaestro.models.mongo.FoodModelM; +import fellowship.mealmaestro.models.mongo.ToVisitLinkModel; +import fellowship.mealmaestro.models.mongo.VisitedLinkModel; +import fellowship.mealmaestro.repositories.mongo.ToVisitLinkRepository; +import fellowship.mealmaestro.repositories.mongo.VisitedLinkRepository; +import fellowship.mealmaestro.services.BarcodeService; import java.time.Duration; import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import org.jsoup.Jsoup; @@ -22,20 +29,43 @@ import org.jsoup.select.Elements; public class CheckersScraper { + @Autowired + private ToVisitLinkRepository toVisitLinkRepository; + + @Autowired + private VisitedLinkRepository visitedLinkRepository; + + @Autowired + private BarcodeService barcodeService; + + public void getLocLinks() { + // Visit categories sitemap to get all locs + Optional visited = visitedLinkRepository + .findById("https://www.checkers.co.za/sitemap/medias/Category-checkersZA-0.xml"); + + if (visited.isPresent()) { + System.out.println("Skipping sitemap, already visited"); + return; + } + + try { + Document doc = Jsoup.connect("https://www.checkers.co.za/sitemap/medias/Category-checkersZA-0.xml").get(); + + Elements links = doc.select("loc"); + + // Filter out non-food links + for (int i = 0; i < links.size(); i++) { + String link = links.get(i).text(); + if (link.contains("food") || link.contains("Food")) { + toVisitLinkRepository.save(new ToVisitLinkModel(link)); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } public void scrape() { - System.setProperty("webdriver.chrome.driver", - "C:\\Users\\Ethan\\Downloads\\chromedriver-win64\\chromedriver-win64\\chromedriver.exe"); - - ChromeOptions options = new ChromeOptions(); - // options.addArguments("--headless"); - options.addArguments("--blink-settings=imagesEnabled=false"); - options.addArguments("--disable-extensions"); - options.addArguments("--user-agent=FellowshipBot-UniversityOfPretoria-FinalYearProject"); - - WebDriver driver = new ChromeDriver(options); - Duration timeout = Duration.ofSeconds(15); - WebDriverWait wait = new WebDriverWait(driver, timeout); // Visit categories sitemap to get all locs // driver.get("https://www.checkers.co.za/sitemap/medias/Category-checkersZA-0.xml"); From 69b7cfcb4c6667b412fab011177e53b60400f331 Mon Sep 17 00:00:00 2001 From: SkulderLock <78735770+SkulderLock@users.noreply.github.com> Date: Sat, 9 Sep 2023 15:14:08 +0200 Subject: [PATCH 08/97] =?UTF-8?q?=F0=9F=9A=A7=20Logic=20to=20process=20lin?= =?UTF-8?q?ks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../models/mongo/ToVisitLinkModel.java | 5 ++- .../models/mongo/VisitedLinkModel.java | 11 +++++- .../mongo/ToVisitLinkRepository.java | 6 +++- .../services/webscraping/CheckersScraper.java | 36 ++++++++----------- 4 files changed, 34 insertions(+), 24 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/models/mongo/ToVisitLinkModel.java b/backend/src/main/java/fellowship/mealmaestro/models/mongo/ToVisitLinkModel.java index 9d60d49b..eb705e0b 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/mongo/ToVisitLinkModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/mongo/ToVisitLinkModel.java @@ -13,8 +13,11 @@ public class ToVisitLinkModel { @Id private String link; - public ToVisitLinkModel(String link) { + private String type; + + public ToVisitLinkModel(String link, String type) { this.link = link; + this.type = type; } public ToVisitLinkModel() { diff --git a/backend/src/main/java/fellowship/mealmaestro/models/mongo/VisitedLinkModel.java b/backend/src/main/java/fellowship/mealmaestro/models/mongo/VisitedLinkModel.java index 9a2ea0be..9a5bc355 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/mongo/VisitedLinkModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/mongo/VisitedLinkModel.java @@ -17,9 +17,18 @@ public class VisitedLinkModel { private LocalDate lastVisited; - public VisitedLinkModel(String link, LocalDate lastVisited) { + private String type; + + public VisitedLinkModel(String link, LocalDate lastVisited, String type) { this.link = link; this.lastVisited = lastVisited; + this.type = type; + } + + public VisitedLinkModel(String link, String type) { + this.link = link; + this.type = type; + this.lastVisited = LocalDate.now(); } public VisitedLinkModel() { diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/ToVisitLinkRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/ToVisitLinkRepository.java index d7b903ce..09996678 100644 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/ToVisitLinkRepository.java +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/ToVisitLinkRepository.java @@ -1,10 +1,14 @@ package fellowship.mealmaestro.repositories.mongo; +import java.util.Optional; + import org.springframework.data.mongodb.repository.MongoRepository; import fellowship.mealmaestro.models.mongo.ToVisitLinkModel; public interface ToVisitLinkRepository extends - MongoRepository { + MongoRepository { + + Optional findNext(); // TODO: add query to find next link } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java index 1523db19..885ed817 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java @@ -38,8 +38,11 @@ public class CheckersScraper { @Autowired private BarcodeService barcodeService; + private long lastRequestTime; + public void getLocLinks() { // Visit categories sitemap to get all locs + lastRequestTime = 0; Optional visited = visitedLinkRepository .findById("https://www.checkers.co.za/sitemap/medias/Category-checkersZA-0.xml"); @@ -57,7 +60,7 @@ public void getLocLinks() { for (int i = 0; i < links.size(); i++) { String link = links.get(i).text(); if (link.contains("food") || link.contains("Food")) { - toVisitLinkRepository.save(new ToVisitLinkModel(link)); + toVisitLinkRepository.save(new ToVisitLinkModel(link, "category")); } } } catch (Exception e) { @@ -65,32 +68,23 @@ public void getLocLinks() { } } - public void scrape() { - - // Visit categories sitemap to get all locs - // driver.get("https://www.checkers.co.za/sitemap/medias/Category-checkersZA-0.xml"); - - // String domString = driver.getPageSource(); + public ToVisitLinkModel getNextLink() { + // Get next link to visit + Optional toVisitLink = toVisitLinkRepository.findNext(); - // Document doc = Jsoup.parse(domString); - // Elements links = doc.select("loc"); + if (toVisitLink.isPresent()) { + return toVisitLink.get(); + } - // Filter out non-food links - List foodLinks = new ArrayList(); - // for (int i = 0; i < links.size(); i++) { - // String link = links.get(i).text(); - // if (link.contains("food") || link.contains("Food")) { - // foodLinks.add(link); - // } - // } + return null; + } - long lastRequestTime = 0; + public void scrape() { - foodLinks.add("file:///D:\\Code\\MessingAround\\app\\src\\main\\java\\messingaround\\applep1.html"); - foodLinks.add("file:///D:\\Code\\MessingAround\\app\\src\\main\\java\\messingaround\\milk.html"); + // foodLinks.add("file:///D:\\Code\\MessingAround\\app\\src\\main\\java\\messingaround\\applep1.html"); + // foodLinks.add("file:///D:\\Code\\MessingAround\\app\\src\\main\\java\\messingaround\\milk.html"); // Visit each food category page and get all product links - Set visitedLinks = new HashSet(); List productLinks = new ArrayList(); List paginationLinks = new ArrayList(); List foodModels = new ArrayList(); From 0930af56da83c07bff8c467fde73ceb12dbdc168 Mon Sep 17 00:00:00 2001 From: SkulderLock <78735770+SkulderLock@users.noreply.github.com> Date: Sun, 10 Sep 2023 13:52:51 +0200 Subject: [PATCH 09/97] =?UTF-8?q?=F0=9F=9A=A7=20Method=20to=20handle=20cat?= =?UTF-8?q?egory=20links?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../models/mongo/ToVisitLinkModel.java | 5 +- .../models/mongo/VisitedLinkModel.java | 8 +- .../mongo/ToVisitLinkRepository.java | 4 - .../services/webscraping/CheckersScraper.java | 164 +++++++----------- .../services/webscraping/LinkService.java | 31 ++++ 5 files changed, 106 insertions(+), 106 deletions(-) create mode 100644 backend/src/main/java/fellowship/mealmaestro/services/webscraping/LinkService.java diff --git a/backend/src/main/java/fellowship/mealmaestro/models/mongo/ToVisitLinkModel.java b/backend/src/main/java/fellowship/mealmaestro/models/mongo/ToVisitLinkModel.java index eb705e0b..a731db2f 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/mongo/ToVisitLinkModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/mongo/ToVisitLinkModel.java @@ -15,9 +15,12 @@ public class ToVisitLinkModel { private String type; - public ToVisitLinkModel(String link, String type) { + private String store; + + public ToVisitLinkModel(String link, String type, String store) { this.link = link; this.type = type; + this.store = store; } public ToVisitLinkModel() { diff --git a/backend/src/main/java/fellowship/mealmaestro/models/mongo/VisitedLinkModel.java b/backend/src/main/java/fellowship/mealmaestro/models/mongo/VisitedLinkModel.java index 9a5bc355..b02d27c8 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/mongo/VisitedLinkModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/mongo/VisitedLinkModel.java @@ -19,16 +19,20 @@ public class VisitedLinkModel { private String type; - public VisitedLinkModel(String link, LocalDate lastVisited, String type) { + private String store; + + public VisitedLinkModel(String link, LocalDate lastVisited, String type, String store) { this.link = link; this.lastVisited = lastVisited; this.type = type; + this.store = store; } - public VisitedLinkModel(String link, String type) { + public VisitedLinkModel(String link, String type, String store) { this.link = link; this.type = type; this.lastVisited = LocalDate.now(); + this.store = store; } public VisitedLinkModel() { diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/ToVisitLinkRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/ToVisitLinkRepository.java index 09996678..88b4a375 100644 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/ToVisitLinkRepository.java +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/ToVisitLinkRepository.java @@ -1,7 +1,5 @@ package fellowship.mealmaestro.repositories.mongo; -import java.util.Optional; - import org.springframework.data.mongodb.repository.MongoRepository; import fellowship.mealmaestro.models.mongo.ToVisitLinkModel; @@ -9,6 +7,4 @@ public interface ToVisitLinkRepository extends MongoRepository { - Optional findNext(); // TODO: add query to find next link - } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java index 885ed817..00a51fcf 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java @@ -16,7 +16,9 @@ import fellowship.mealmaestro.repositories.mongo.VisitedLinkRepository; import fellowship.mealmaestro.services.BarcodeService; +import java.io.IOException; import java.time.Duration; +import java.time.LocalDate; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -35,6 +37,9 @@ public class CheckersScraper { @Autowired private VisitedLinkRepository visitedLinkRepository; + @Autowired + private LinkService linkService; + @Autowired private BarcodeService barcodeService; @@ -60,7 +65,7 @@ public void getLocLinks() { for (int i = 0; i < links.size(); i++) { String link = links.get(i).text(); if (link.contains("food") || link.contains("Food")) { - toVisitLinkRepository.save(new ToVisitLinkModel(link, "category")); + toVisitLinkRepository.save(new ToVisitLinkModel(link, "category", "Checkers")); } } } catch (Exception e) { @@ -70,7 +75,22 @@ public void getLocLinks() { public ToVisitLinkModel getNextLink() { // Get next link to visit - Optional toVisitLink = toVisitLinkRepository.findNext(); + + long currentTime = System.currentTimeMillis(); + long timeSinceLastRequest = currentTime - lastRequestTime; + + // Wait 10 seconds between requests + if (timeSinceLastRequest < 10000) { + try { + System.out.println("Waiting " + (10000 - timeSinceLastRequest) + "ms"); + Thread.sleep(10000 - timeSinceLastRequest); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + Optional toVisitLink = linkService.getNextLink(); + lastRequestTime = System.currentTimeMillis(); if (toVisitLink.isPresent()) { return toVisitLink.get(); @@ -79,123 +99,69 @@ public ToVisitLinkModel getNextLink() { return null; } - public void scrape() { - - // foodLinks.add("file:///D:\\Code\\MessingAround\\app\\src\\main\\java\\messingaround\\applep1.html"); - // foodLinks.add("file:///D:\\Code\\MessingAround\\app\\src\\main\\java\\messingaround\\milk.html"); - - // Visit each food category page and get all product links - List productLinks = new ArrayList(); - List paginationLinks = new ArrayList(); - List foodModels = new ArrayList(); - - for (String link : foodLinks) { - // Check if link has been visited - if (visitedLinks.contains(link)) { - continue; - } - - long currentTime = System.currentTimeMillis(); - long timeSinceLastRequest = currentTime - lastRequestTime; - - // Wait 10 seconds between requests - if (timeSinceLastRequest < 10000) { - try { - System.out.println("Waiting " + (10000 - timeSinceLastRequest) + "ms"); - Thread.sleep(10000 - timeSinceLastRequest); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } + public void handleLink(ToVisitLinkModel toVisitLink) { + // Handle link based on type + if (toVisitLink.getType().equals("category")) { + handleCategoryLink(toVisitLink); + } else if (toVisitLink.getType().equals("product")) { + handleProductLink(toVisitLink); + } + } - visitedLinks.add(link); - driver.get(link); - System.out.println("Visiting " + link); + public void handleCategoryLink(ToVisitLinkModel link) { + // Visit category page and get all product links and pagination links + Optional visited = visitedLinkRepository.findById(link.getLink()); - // Wait for page to load - WebElement element = wait - .until(ExpectedConditions.elementToBeClickable(By.cssSelector("h3.item-product__name > a"))); + // if link has been visited and it has been less than 1 month since last visit, + // skip + if (visited.isPresent() && visited.get().getLastVisited().plusMonths(1).isAfter(LocalDate.now())) { + System.out.println("Skipping " + link.getLink() + ", already visited..."); + return; + } - // Full products page dom - String dom = driver.getPageSource(); - Document productPageDoc = Jsoup.connect(link).get(); + try { + Document doc = Jsoup.connect("https://www.checkers.co.za" + link.getLink()).get(); // Get product links - Elements productPageLinks = productPageDoc.select("h3.item-product__name > a"); + Elements productPageLinks = doc.select("h3.item-product__name > a"); for (int i = 0; i < productPageLinks.size(); i++) { String productPageLink = productPageLinks.get(i).attr("href"); - productLinks.add(productPageLink); - } - - // Find pagination links if they exist - Element paginationBar = productPageDoc.selectFirst("div.pagination-bar.bottom"); - Elements paginationLinksEl = paginationBar.select("a"); - for (Element paginationLink : paginationLinksEl) { - String paginationLinkHref = paginationLink.attr("href"); - - // add to pagination links if they aren't in visitedLinks - if (!visitedLinks.contains(paginationLinkHref)) { - paginationLinks.add(paginationLinkHref); + if (productPageLink != null && !productPageLink.isEmpty()) { + toVisitLinkRepository.save(new ToVisitLinkModel(productPageLink, "product", "Checkers")); } } - // Update last request time - lastRequestTime = System.currentTimeMillis(); - } + // Get pagination links + Element paginationBar = doc.selectFirst("div.pagination-bar.bottom"); + if (paginationBar != null) { + Elements paginationLinksEl = paginationBar.select("a"); - System.out.println("############################################"); - System.out.println("Main links finished, starting pagination links"); - System.out.println("############################################"); - - // Visit each pagination link and get all product links - for (String link : paginationLinks) { - // Check if link has been visited - if (visitedLinks.contains(link)) { - continue; - } + for (Element paginationLink : paginationLinksEl) { + String paginationLinkHref = paginationLink.attr("href"); - long currentTime = System.currentTimeMillis(); - long timeSinceLastRequest = currentTime - lastRequestTime; + // add to ToVisitLinks if they aren't in VisitedLinks + if (!visitedLinkRepository.existsById(paginationLinkHref)) { + toVisitLinkRepository.save(new ToVisitLinkModel(paginationLinkHref, "category", "Checkers")); + } - // Wait 10 seconds between requests - if (timeSinceLastRequest < 10000) { - try { - System.out.println("Waiting " + (10000 - timeSinceLastRequest) + "ms"); - Thread.sleep(10000 - timeSinceLastRequest); - } catch (InterruptedException e) { - e.printStackTrace(); } } - visitedLinks.add(link); - driver.get("file:///D:\\Code\\MessingAround" + link); - System.out.println("Visiting " + link); - - // Wait for page to load - WebElement element = wait - .until(ExpectedConditions.elementToBeClickable(By.cssSelector("h3.item-product__name > a"))); - - // Full products page dom - String dom = driver.getPageSource(); - Document productPageDoc = Jsoup.parse(dom); - - // Get product links - Elements productPageLinks = productPageDoc.select("h3.item-product__name > a"); - - for (int i = 0; i < productPageLinks.size(); i++) { - String productPageLink = productPageLinks.get(i).attr("href"); - productLinks.add(productPageLink); - } - - // Update last request time - lastRequestTime = System.currentTimeMillis(); + // Add link to visited links + visitedLinkRepository.save(new VisitedLinkModel(link.getLink(), "category", "Checkers")); + System.out.println("Visited " + link.getLink()); + } catch (IOException e) { + System.out.println("Error visiting " + link.getLink() + ", skipping..."); + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); } - System.out.println("############################################"); - System.out.println("Finished getting product links"); - System.out.println("############################################"); + } + + public void scrape() { // Visit each product page and get product info for (String link : productLinks) { diff --git a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/LinkService.java b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/LinkService.java new file mode 100644 index 00000000..90d9e603 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/LinkService.java @@ -0,0 +1,31 @@ +package fellowship.mealmaestro.services.webscraping; + +import java.util.List; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.stereotype.Service; + +import fellowship.mealmaestro.models.mongo.ToVisitLinkModel; + +@Service +public class LinkService { + + @Autowired + private MongoTemplate mongoTemplate; + + public Optional getNextLink() { + Aggregation aggregation = Aggregation.newAggregation( + Aggregation.sample(1)); + + AggregationResults results = mongoTemplate.aggregate(aggregation, "ToVisitLinks", + ToVisitLinkModel.class); + + List links = results.getMappedResults(); + + return links.isEmpty() ? Optional.empty() : Optional.of(links.get(0)); + } +} From 294fd6bd597626deb078ffc05b8fae3d857ab174 Mon Sep 17 00:00:00 2001 From: SkulderLock <78735770+SkulderLock@users.noreply.github.com> Date: Sun, 10 Sep 2023 14:57:40 +0200 Subject: [PATCH 10/97] =?UTF-8?q?=F0=9F=9A=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/webscraping/CheckersScraper.java | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java index 00a51fcf..4e796640 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java @@ -68,6 +68,14 @@ public void getLocLinks() { toVisitLinkRepository.save(new ToVisitLinkModel(link, "category", "Checkers")); } } + + // Add sitemap to visited links + visitedLinkRepository.save(new VisitedLinkModel( + "https://www.checkers.co.za/sitemap/medias/Category-checkersZA-0.xml", "category", "Checkers")); + // Remove sitemap from ToVisitLinks + toVisitLinkRepository.deleteById( + "https://www.checkers.co.za/sitemap/medias/Category-checkersZA-0.xml"); + } catch (Exception e) { e.printStackTrace(); } @@ -151,6 +159,10 @@ public void handleCategoryLink(ToVisitLinkModel link) { // Add link to visited links visitedLinkRepository.save(new VisitedLinkModel(link.getLink(), "category", "Checkers")); + + // Remove link from ToVisitLinks + toVisitLinkRepository.deleteById(link.getLink()); + System.out.println("Visited " + link.getLink()); } catch (IOException e) { System.out.println("Error visiting " + link.getLink() + ", skipping..."); @@ -161,6 +173,10 @@ public void handleCategoryLink(ToVisitLinkModel link) { } + public void handleProductLink(ToVisitLinkModel link) { + + } + public void scrape() { // Visit each product page and get product info @@ -169,18 +185,6 @@ public void scrape() { if (visitedLinks.contains(link)) { continue; } - long currentTime = System.currentTimeMillis(); - long timeSinceLastRequest = currentTime - lastRequestTime; - - // Wait 10 seconds between requests - if (timeSinceLastRequest < 10000) { - try { - System.out.println("Waiting " + (10000 - timeSinceLastRequest) + "ms"); - Thread.sleep(10000 - timeSinceLastRequest); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } visitedLinks.add(link); driver.get("file:///D:\\Code\\MessingAround" + link); From b433ecc16387e3334f2bd49b71dfa015be5c2d08 Mon Sep 17 00:00:00 2001 From: SkulderLock <78735770+SkulderLock@users.noreply.github.com> Date: Sun, 10 Sep 2023 15:46:51 +0200 Subject: [PATCH 11/97] =?UTF-8?q?=F0=9F=9A=A7=20Method=20to=20handle=20pro?= =?UTF-8?q?duct=20link?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mealmaestro/models/mongo/FoodModelM.java | 4 + .../services/webscraping/CheckersScraper.java | 109 +++++++++++------- .../services/webscraping/LinkService.java | 15 ++- .../webscraping/WebscrapeService.java | 6 + 4 files changed, 89 insertions(+), 45 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java b/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java index 0078ba7f..ed08d77d 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java @@ -51,6 +51,10 @@ public void setPrice(String price) { this.price = Double.parseDouble(price); } + public void setPrice(Double price) { + this.price = price; + } + public void setAmount(String amount) { Pattern pattern = Pattern.compile("([0-9.]+)([a-zA-Z]+)"); Matcher matcher = pattern.matcher(amount); diff --git a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java index 4e796640..1720fd00 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java @@ -1,12 +1,5 @@ package fellowship.mealmaestro.services.webscraping; -import org.openqa.selenium.By; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.chrome.ChromeDriver; -import org.openqa.selenium.chrome.ChromeOptions; -import org.openqa.selenium.support.ui.ExpectedConditions; -import org.openqa.selenium.support.ui.WebDriverWait; import org.springframework.beans.factory.annotation.Autowired; import fellowship.mealmaestro.models.mongo.FoodModelM; @@ -17,13 +10,8 @@ import fellowship.mealmaestro.services.BarcodeService; import java.io.IOException; -import java.time.Duration; import java.time.LocalDate; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; import java.util.Optional; -import java.util.Set; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; @@ -97,7 +85,7 @@ public ToVisitLinkModel getNextLink() { } } - Optional toVisitLink = linkService.getNextLink(); + Optional toVisitLink = linkService.getNextCheckersLink(); lastRequestTime = System.currentTimeMillis(); if (toVisitLink.isPresent()) { @@ -174,44 +162,52 @@ public void handleCategoryLink(ToVisitLinkModel link) { } public void handleProductLink(ToVisitLinkModel link) { + // Visit product page and get product info - } - - public void scrape() { - - // Visit each product page and get product info - for (String link : productLinks) { - // Check if link has been visited - if (visitedLinks.contains(link)) { - continue; - } - - visitedLinks.add(link); - driver.get("file:///D:\\Code\\MessingAround" + link); - System.out.println("Visiting " + driver.getTitle()); + // Check if link has been visited + Optional visited = visitedLinkRepository.findById(link.getLink()); - // Wait for page to load - WebElement element = wait.until( - ExpectedConditions.elementToBeClickable(By.cssSelector("#accessibletabsnavigation0-1"))); + if (visited.isPresent() && visited.get().getLastVisited().plusMonths(1).isAfter(LocalDate.now())) { + System.out.println("Skipping " + link.getLink() + ", already visited..."); + return; + } - // Full product page dom - String dom = driver.getPageSource(); - Document productDoc = Jsoup.parse(dom); + try { + // Visit product page + Document doc = Jsoup.connect("https://www.checkers.co.za" + link.getLink()).get(); // Get product info FoodModelM food = new FoodModelM(); + // product name - String productName = productDoc.selectFirst("h1.pdp__name").text(); + Element productNameEl = doc.selectFirst("h1.pdp__name"); + if (productNameEl == null) { + System.out.println("Skipping " + link.getLink() + ", no product name..."); + return; + } + String productName = productNameEl.text(); + if (productName == null || productName.isEmpty()) { + System.out.println("Skipping " + link.getLink() + ", no product name..."); + return; + } System.out.println("Product name: " + productName); food.setName(productName); // product price - String productPrice = productDoc.selectFirst("div.special-price__price").text(); + Element productPriceEl = doc.selectFirst("div.special-price__price"); + if (productPriceEl == null) { + System.out.println("Skipping " + link.getLink() + ", no product price..."); + return; + } + String productPrice = productPriceEl.text(); + if (productPrice == null || productPrice.isEmpty()) { + food.setPrice(-1.0); + } System.out.println("Product price: " + productPrice); food.setPrice(productPrice); // product details - Elements productDetails = productDoc.select("table.pdp__product-information > tbody > tr"); + Elements productDetails = doc.select("table.pdp__product-information > tbody > tr"); String barcode = ""; String quantity = ""; @@ -219,7 +215,12 @@ public void scrape() { for (Element productDetail : productDetails) { if (productDetail.text().toLowerCase().contains("barcode")) { // select second td - barcode = productDetail.selectFirst("td:nth-child(2)").text(); + Element barcodeEl = productDetail.selectFirst("td:nth-child(2)"); + if (barcodeEl == null) { + System.out.println("Skipping " + link.getLink() + ", no barcode..."); + return; + } + barcode = barcodeEl.text(); System.out.println("Barcode: " + barcode); food.setBarcode(barcode); } @@ -227,18 +228,38 @@ public void scrape() { if (productDetail.text().toLowerCase().contains("weight") || productDetail.text().toLowerCase().contains("volume")) { // select second td - quantity = productDetail.selectFirst("td:nth-child(2)").text(); + Element quantityEl = productDetail.selectFirst("td:nth-child(2)"); + if (quantityEl == null) { + System.out.println("Skipping " + link.getLink() + ", no quantity..."); + return; + } + quantity = quantityEl.text(); System.out.println("Quantity: " + quantity); food.setAmount(quantity); } } - // Add food to list - foodModels.add(food); + if (barcode.isEmpty() || food.getBarcode().equals("")) { + System.out.println("Skipping " + link.getLink() + ", no barcode..."); + return; + } - lastRequestTime = System.currentTimeMillis(); - } + // Add food to database + barcodeService.addProduct(food); + + // Add link to visited links + visitedLinkRepository.save(new VisitedLinkModel(link.getLink(), "product", "Checkers")); + + // Remove link from ToVisitLinks + toVisitLinkRepository.deleteById(link.getLink()); - driver.quit(); + System.out.println("Visited " + link.getLink()); + } catch (IOException e) { + System.out.println("Error visiting " + link.getLink() + ", skipping..."); + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + + } } -} \ No newline at end of file +} diff --git a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/LinkService.java b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/LinkService.java index 90d9e603..cdb09b0d 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/LinkService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/LinkService.java @@ -7,6 +7,8 @@ import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.data.mongodb.core.aggregation.MatchOperation; +import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.stereotype.Service; import fellowship.mealmaestro.models.mongo.ToVisitLinkModel; @@ -17,8 +19,11 @@ public class LinkService { @Autowired private MongoTemplate mongoTemplate; - public Optional getNextLink() { + public Optional getNextLink(String store) { + MatchOperation match = Aggregation.match(Criteria.where("store").is(store)); + Aggregation aggregation = Aggregation.newAggregation( + match, Aggregation.sample(1)); AggregationResults results = mongoTemplate.aggregate(aggregation, "ToVisitLinks", @@ -28,4 +33,12 @@ public Optional getNextLink() { return links.isEmpty() ? Optional.empty() : Optional.of(links.get(0)); } + + public Optional getNextCheckersLink() { + return getNextLink("Checkers"); + } + + public Optional getNextWoolworthsLink() { + return getNextLink("Woolworths"); + } } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java index a2abc472..ed6ef9be 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java @@ -1,11 +1,17 @@ package fellowship.mealmaestro.services.webscraping; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @Service public class WebscrapeService { + @Autowired + private LinkService linkService; + + private CheckersScraper checkersScraper; + private volatile boolean isScrapingAllowed = true; @Scheduled(cron = "0 0 6 * * ?") From 3a0741fbb8447ae05da2b3c3181c6ad136b20181 Mon Sep 17 00:00:00 2001 From: SkulderLock <78735770+SkulderLock@users.noreply.github.com> Date: Sun, 10 Sep 2023 17:19:53 +0200 Subject: [PATCH 12/97] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Change=20to=20use=20?= =?UTF-8?q?constructors=20instead=20of=20autowired?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mealmaestro/config/ApplicationConfig.java | 11 ++++ .../controllers/BrowseController.java | 8 +-- .../controllers/MealManagementController.java | 13 +++-- .../controllers/PantryController.java | 8 +-- .../controllers/ProductController.java | 8 +-- .../controllers/SettingsController.java | 8 +-- .../controllers/ShoppingListController.java | 8 +-- .../controllers/UserController.java | 7 ++- .../mongo/DynamicFoodMRepositoryImpl.java | 2 - .../mealmaestro/services/BarcodeService.java | 8 +-- .../mealmaestro/services/BrowseService.java | 8 +-- .../services/MealDatabaseService.java | 16 +++--- .../services/MealManagementService.java | 16 +++--- .../services/OpenaiApiService.java | 13 ++--- .../services/OpenaiPromptBuilder.java | 12 +++-- .../mealmaestro/services/PantryService.java | 24 ++++----- .../services/RecipeBookService.java | 17 +++--- .../mealmaestro/services/SettingsService.java | 17 +++--- .../services/ShoppingListService.java | 29 +++++----- .../mealmaestro/services/UserService.java | 11 ++-- .../services/webscraping/CheckersScraper.java | 53 +++++++++---------- .../services/webscraping/LinkService.java | 8 +-- .../webscraping/WebscrapeService.java | 20 ++----- 23 files changed, 173 insertions(+), 152 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/config/ApplicationConfig.java b/backend/src/main/java/fellowship/mealmaestro/config/ApplicationConfig.java index d2abc988..78b64f6e 100644 --- a/backend/src/main/java/fellowship/mealmaestro/config/ApplicationConfig.java +++ b/backend/src/main/java/fellowship/mealmaestro/config/ApplicationConfig.java @@ -2,6 +2,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; @@ -45,4 +47,13 @@ public PasswordEncoder passwordEncoder() { public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } + + @Bean + public TaskScheduler taskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setPoolSize(4); + scheduler.initialize(); + return scheduler; + } + } diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/BrowseController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/BrowseController.java index 19861125..382e9850 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/BrowseController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/BrowseController.java @@ -2,7 +2,6 @@ import java.util.List; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestHeader; @@ -15,8 +14,11 @@ @RestController public class BrowseController { - @Autowired - private BrowseService browseService; + private final BrowseService browseService; + + public BrowseController(BrowseService browseService) { + this.browseService = browseService; + } @GetMapping("/getPopularMeals") public ResponseEntity> getPopularMeals(@RequestHeader("Authorization") String token) { diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java index bdd63bda..6451f0c5 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java @@ -3,7 +3,6 @@ import java.time.LocalDate; import java.util.List; import java.util.Optional; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -25,10 +24,14 @@ @RestController public class MealManagementController { - @Autowired - private MealManagementService mealManagementService; - @Autowired - private MealDatabaseService mealDatabaseService; + private final MealManagementService mealManagementService; + private final MealDatabaseService mealDatabaseService; + + public MealManagementController(MealManagementService mealManagementService, + MealDatabaseService mealDatabaseService) { + this.mealManagementService = mealManagementService; + this.mealDatabaseService = mealDatabaseService; + } @PostMapping("/getMealPlanForDay") public ResponseEntity> dailyMeals(@Valid @RequestBody DateModel request, diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/PantryController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/PantryController.java index 064c1fbc..5153d937 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/PantryController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/PantryController.java @@ -3,7 +3,6 @@ import java.util.List; import java.util.UUID; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -17,8 +16,11 @@ @RestController public class PantryController { - @Autowired - private PantryService pantryService; + private final PantryService pantryService; + + public PantryController(PantryService pantryService) { + this.pantryService = pantryService; + } @PostMapping("/addToPantry") public ResponseEntity addToPantry(@Valid @RequestBody FoodModel request, diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/ProductController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/ProductController.java index e590aadf..22251ea4 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/ProductController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/ProductController.java @@ -1,6 +1,5 @@ package fellowship.mealmaestro.controllers; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -14,8 +13,11 @@ @RestController public class ProductController { - @Autowired - private BarcodeService barcodeService; + private final BarcodeService barcodeService; + + public ProductController(BarcodeService barcodeService) { + this.barcodeService = barcodeService; + } @PostMapping("/findProduct") public ResponseEntity findProduct(@RequestBody findBarcodeRequest request, diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/SettingsController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/SettingsController.java index 89b7ab82..30b7f302 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/SettingsController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/SettingsController.java @@ -1,6 +1,5 @@ package fellowship.mealmaestro.controllers; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -14,8 +13,11 @@ @RestController public class SettingsController { - @Autowired - private SettingsService settingsService; + private final SettingsService settingsService; + + public SettingsController(SettingsService settingsService) { + this.settingsService = settingsService; + } @PostMapping("/getSettings") public ResponseEntity getSettings(@RequestHeader("Authorization") String token) { diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/ShoppingListController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/ShoppingListController.java index ff444866..a41cffa5 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/ShoppingListController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/ShoppingListController.java @@ -3,7 +3,6 @@ import java.util.List; import java.util.UUID; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -17,8 +16,11 @@ @RestController public class ShoppingListController { - @Autowired - private ShoppingListService shoppingListService; + private final ShoppingListService shoppingListService; + + public ShoppingListController(ShoppingListService shoppingListService) { + this.shoppingListService = shoppingListService; + } @PostMapping("/addToShoppingList") public ResponseEntity addToShoppingList(@Valid @RequestBody FoodModel request, diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/UserController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/UserController.java index d14b3b60..ed17d8a7 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/UserController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/UserController.java @@ -2,7 +2,6 @@ import java.util.Optional; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -22,13 +21,13 @@ @RestController public class UserController { - @Autowired - private UserService userService; + private final UserService userService; private final AuthenticationService authenticationService; - public UserController(AuthenticationService authenticationService) { + public UserController(AuthenticationService authenticationService, UserService userService) { this.authenticationService = authenticationService; + this.userService = userService; } @PostMapping("/findByEmail") diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/DynamicFoodMRepositoryImpl.java b/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/DynamicFoodMRepositoryImpl.java index 720216a3..67c57f8d 100644 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/DynamicFoodMRepositoryImpl.java +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/mongo/DynamicFoodMRepositoryImpl.java @@ -2,7 +2,6 @@ import java.util.Optional; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.MongoTemplate; import fellowship.mealmaestro.models.mongo.FoodModelM; @@ -10,7 +9,6 @@ public class DynamicFoodMRepositoryImpl implements DynamicFoodMRepository { private final MongoTemplate mongoTemplate; - @Autowired public DynamicFoodMRepositoryImpl(MongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/BarcodeService.java b/backend/src/main/java/fellowship/mealmaestro/services/BarcodeService.java index e36c10c3..ea2e86ad 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/BarcodeService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/BarcodeService.java @@ -2,7 +2,6 @@ import java.util.Optional; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import fellowship.mealmaestro.models.mongo.FoodModelM; @@ -12,8 +11,11 @@ @Service public class BarcodeService { - @Autowired - private FoodMRepository foodMRepository; + private final FoodMRepository foodMRepository; + + public BarcodeService(FoodMRepository foodMRepository) { + this.foodMRepository = foodMRepository; + } public FoodModelM findProduct(findBarcodeRequest request) { System.out.println(request.getStore()); diff --git a/backend/src/main/java/fellowship/mealmaestro/services/BrowseService.java b/backend/src/main/java/fellowship/mealmaestro/services/BrowseService.java index 969c187c..7b5d1c5f 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/BrowseService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/BrowseService.java @@ -2,7 +2,6 @@ import java.util.List; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import fellowship.mealmaestro.models.neo4j.MealModel; @@ -11,8 +10,11 @@ @Service public class BrowseService { - @Autowired - private MealRepository mealRepository; + private final MealRepository mealRepository; + + public BrowseService(MealRepository mealRepository) { + this.mealRepository = mealRepository; + } public List getPopularMeals() { diff --git a/backend/src/main/java/fellowship/mealmaestro/services/MealDatabaseService.java b/backend/src/main/java/fellowship/mealmaestro/services/MealDatabaseService.java index 524784f7..1e1460f9 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/MealDatabaseService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/MealDatabaseService.java @@ -7,7 +7,6 @@ import java.util.List; import java.util.Optional; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -22,14 +21,17 @@ @Service public class MealDatabaseService { - @Autowired - private JwtService jwtService; + private final JwtService jwtService; - @Autowired - private MealRepository mealRepository; + private final MealRepository mealRepository; - @Autowired - private UserRepository userRepository; + private final UserRepository userRepository; + + public MealDatabaseService(JwtService jwtService, MealRepository mealRepository, UserRepository userRepository) { + this.jwtService = jwtService; + this.mealRepository = mealRepository; + this.userRepository = userRepository; + } @Transactional public List saveMeals(List mealsToSave, LocalDate date, String token) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java b/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java index 4c51817e..2ce340f5 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java @@ -3,7 +3,6 @@ import java.io.File; import java.io.IOException; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.fasterxml.jackson.core.JsonProcessingException; @@ -20,13 +19,16 @@ @Service public class MealManagementService { - @Autowired - private OpenaiApiService openaiApiService; - @Autowired - private ObjectMapper objectMapper; + private final OpenaiApiService openaiApiService; + private final ObjectMapper objectMapper; + private final UnsplashService unsplashService; - @Autowired - private UnsplashService unsplashService; + public MealManagementService(OpenaiApiService openaiApiService, ObjectMapper objectMapper, + UnsplashService unsplashService) { + this.openaiApiService = openaiApiService; + this.objectMapper = objectMapper; + this.unsplashService = unsplashService; + } public MealModel generateMeal(String mealType, String token) { MealModel defaultMeal = new MealModel("Bread", "1. Toast the bread", "Delicious Bread", diff --git a/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java b/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java index 66a019f9..300d4662 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java @@ -3,7 +3,6 @@ import java.time.Duration; import java.util.concurrent.TimeoutException; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; @@ -21,13 +20,16 @@ @Service public class OpenaiApiService { private static final String OPENAI_URL = "https://api.openai.com/v1/chat/completions"; - private final static String API_KEY; private final WebClient webClient; + private final ObjectMapper jsonMapper; + private final OpenaiPromptBuilder pBuilder; - public OpenaiApiService(WebClient.Builder webClientBuilder) { + public OpenaiApiService(WebClient.Builder webClientBuilder, ObjectMapper jsonMapper, OpenaiPromptBuilder pBuilder) { this.webClient = webClientBuilder.build(); + this.jsonMapper = jsonMapper; + this.pBuilder = pBuilder; } static { @@ -49,11 +51,6 @@ public OpenaiApiService(WebClient.Builder webClientBuilder) { API_KEY = apiKey; } - @Autowired - private ObjectMapper jsonMapper = new ObjectMapper(); - @Autowired - private OpenaiPromptBuilder pBuilder = new OpenaiPromptBuilder(); - public String fetchMealResponse(String type, String token) throws JsonMappingException, JsonProcessingException { String jsonResponse = getJSONResponse(type, token); if (jsonResponse.equals("Timeout")) { diff --git a/backend/src/main/java/fellowship/mealmaestro/services/OpenaiPromptBuilder.java b/backend/src/main/java/fellowship/mealmaestro/services/OpenaiPromptBuilder.java index e3673f0c..f8c5f0a8 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/OpenaiPromptBuilder.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/OpenaiPromptBuilder.java @@ -3,7 +3,6 @@ import java.util.List; import java.util.Random; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.fasterxml.jackson.core.JsonProcessingException; @@ -18,14 +17,17 @@ @Service public class OpenaiPromptBuilder { - @Autowired - private JwtService jwtService; + private final JwtService jwtService; - @Autowired - private UserRepository userRepository; + private final UserRepository userRepository; private Random rand; + public OpenaiPromptBuilder(JwtService jwtService, UserRepository userRepository) { + this.jwtService = jwtService; + this.userRepository = userRepository; + } + @PostConstruct public void init() { rand = new Random(System.currentTimeMillis()); diff --git a/backend/src/main/java/fellowship/mealmaestro/services/PantryService.java b/backend/src/main/java/fellowship/mealmaestro/services/PantryService.java index 441f41e5..d5b2715b 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/PantryService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/PantryService.java @@ -3,7 +3,6 @@ import java.util.ArrayList; import java.util.List; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -18,17 +17,18 @@ @Service public class PantryService { - @Autowired - private JwtService jwtService; - - @Autowired - private PantryRepository pantryRepository; - - @Autowired - private UserRepository userRepository; - - @Autowired - private FoodRepository foodRepository; + private final JwtService jwtService; + private final PantryRepository pantryRepository; + private final UserRepository userRepository; + private final FoodRepository foodRepository; + + public PantryService(JwtService jwtService, PantryRepository pantryRepository, UserRepository userRepository, + FoodRepository foodRepository) { + this.jwtService = jwtService; + this.pantryRepository = pantryRepository; + this.userRepository = userRepository; + this.foodRepository = foodRepository; + } @Transactional public FoodModel addToPantry(FoodModel food, String token) { diff --git a/backend/src/main/java/fellowship/mealmaestro/services/RecipeBookService.java b/backend/src/main/java/fellowship/mealmaestro/services/RecipeBookService.java index beab57a5..4deaa018 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/RecipeBookService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/RecipeBookService.java @@ -1,6 +1,5 @@ package fellowship.mealmaestro.services; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @@ -15,14 +14,16 @@ @Service public class RecipeBookService { - @Autowired - private JwtService jwtService; + private final JwtService jwtService; + private final RecipeBookRepository recipeBookRepository; + private final UserRepository userRepository; - @Autowired - private RecipeBookRepository recipeBookRepository; - - @Autowired - private UserRepository userRepository; + public RecipeBookService(JwtService jwtService, RecipeBookRepository recipeBookRepository, + UserRepository userRepository) { + this.jwtService = jwtService; + this.recipeBookRepository = recipeBookRepository; + this.userRepository = userRepository; + } public MealModel addRecipe(MealModel recipe, String token) { String email = jwtService.extractUserEmail(token); diff --git a/backend/src/main/java/fellowship/mealmaestro/services/SettingsService.java b/backend/src/main/java/fellowship/mealmaestro/services/SettingsService.java index 6a8ad56a..4cbf0ae7 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/SettingsService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/SettingsService.java @@ -1,6 +1,5 @@ package fellowship.mealmaestro.services; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import fellowship.mealmaestro.models.neo4j.SettingsModel; @@ -12,14 +11,16 @@ @Service public class SettingsService { - @Autowired - private JwtService jwtService; + private final JwtService jwtService; + private final SettingsRepository SettingsRepository; + private final UserRepository userRepository; - @Autowired - private SettingsRepository SettingsRepository; - - @Autowired - private UserRepository userRepository; + public SettingsService(JwtService jwtService, SettingsRepository SettingsRepository, + UserRepository userRepository) { + this.jwtService = jwtService; + this.SettingsRepository = SettingsRepository; + this.userRepository = userRepository; + } public SettingsModel getSettings(String token) { String email = jwtService.extractUserEmail(token); diff --git a/backend/src/main/java/fellowship/mealmaestro/services/ShoppingListService.java b/backend/src/main/java/fellowship/mealmaestro/services/ShoppingListService.java index c7c727c7..bf1026a6 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/ShoppingListService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/ShoppingListService.java @@ -3,7 +3,6 @@ import java.util.ArrayList; import java.util.List; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,20 +19,20 @@ @Service public class ShoppingListService { - @Autowired - private JwtService jwtService; - - @Autowired - private ShoppingListRepository shoppingListRepository; - - @Autowired - private UserRepository userRepository; - - @Autowired - private PantryRepository pantryRepository; - - @Autowired - private FoodRepository foodRepository; + private final JwtService jwtService; + private final ShoppingListRepository shoppingListRepository; + private final UserRepository userRepository; + private final PantryRepository pantryRepository; + private final FoodRepository foodRepository; + + public ShoppingListService(JwtService jwtService, ShoppingListRepository shoppingListRepository, + UserRepository userRepository, PantryRepository pantryRepository, FoodRepository foodRepository) { + this.jwtService = jwtService; + this.shoppingListRepository = shoppingListRepository; + this.userRepository = userRepository; + this.pantryRepository = pantryRepository; + this.foodRepository = foodRepository; + } @Transactional public FoodModel addToShoppingList(FoodModel food, String token) { diff --git a/backend/src/main/java/fellowship/mealmaestro/services/UserService.java b/backend/src/main/java/fellowship/mealmaestro/services/UserService.java index b3cf3333..b2f14868 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/UserService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/UserService.java @@ -2,7 +2,6 @@ import java.util.Optional; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import fellowship.mealmaestro.models.UpdateUserRequestModel; @@ -13,11 +12,13 @@ @Service public class UserService { - @Autowired - private UserRepository userRepository; + private final UserRepository userRepository; + private final JwtService jwtService; - @Autowired - private JwtService jwtService; + public UserService(UserRepository userRepository, JwtService jwtService) { + this.userRepository = userRepository; + this.jwtService = jwtService; + } public Optional findByEmail(String email) { return userRepository.findByEmail(email); diff --git a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java index 1720fd00..11b41072 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java @@ -1,7 +1,5 @@ package fellowship.mealmaestro.services.webscraping; -import org.springframework.beans.factory.annotation.Autowired; - import fellowship.mealmaestro.models.mongo.FoodModelM; import fellowship.mealmaestro.models.mongo.ToVisitLinkModel; import fellowship.mealmaestro.models.mongo.VisitedLinkModel; @@ -19,23 +17,24 @@ import org.jsoup.select.Elements; public class CheckersScraper { - @Autowired - private ToVisitLinkRepository toVisitLinkRepository; + private final ToVisitLinkRepository toVisitLinkRepository; - @Autowired - private VisitedLinkRepository visitedLinkRepository; + private final VisitedLinkRepository visitedLinkRepository; - @Autowired - private LinkService linkService; + private final LinkService linkService; - @Autowired - private BarcodeService barcodeService; + private final BarcodeService barcodeService; - private long lastRequestTime; + public CheckersScraper(ToVisitLinkRepository toVisitLinkRepository, VisitedLinkRepository visitedLinkRepository, + LinkService linkService, BarcodeService barcodeService) { + this.toVisitLinkRepository = toVisitLinkRepository; + this.visitedLinkRepository = visitedLinkRepository; + this.linkService = linkService; + this.barcodeService = barcodeService; + } public void getLocLinks() { // Visit categories sitemap to get all locs - lastRequestTime = 0; Optional visited = visitedLinkRepository .findById("https://www.checkers.co.za/sitemap/medias/Category-checkersZA-0.xml"); @@ -71,22 +70,7 @@ public void getLocLinks() { public ToVisitLinkModel getNextLink() { // Get next link to visit - - long currentTime = System.currentTimeMillis(); - long timeSinceLastRequest = currentTime - lastRequestTime; - - // Wait 10 seconds between requests - if (timeSinceLastRequest < 10000) { - try { - System.out.println("Waiting " + (10000 - timeSinceLastRequest) + "ms"); - Thread.sleep(10000 - timeSinceLastRequest); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - Optional toVisitLink = linkService.getNextCheckersLink(); - lastRequestTime = System.currentTimeMillis(); if (toVisitLink.isPresent()) { return toVisitLink.get(); @@ -111,6 +95,7 @@ public void handleCategoryLink(ToVisitLinkModel link) { // if link has been visited and it has been less than 1 month since last visit, // skip if (visited.isPresent() && visited.get().getLastVisited().plusMonths(1).isAfter(LocalDate.now())) { + toVisitLinkRepository.deleteById(link.getLink()); System.out.println("Skipping " + link.getLink() + ", already visited..."); return; } @@ -168,6 +153,7 @@ public void handleProductLink(ToVisitLinkModel link) { Optional visited = visitedLinkRepository.findById(link.getLink()); if (visited.isPresent() && visited.get().getLastVisited().plusMonths(1).isAfter(LocalDate.now())) { + toVisitLinkRepository.deleteById(link.getLink()); System.out.println("Skipping " + link.getLink() + ", already visited..."); return; } @@ -262,4 +248,17 @@ public void handleProductLink(ToVisitLinkModel link) { } } + + public void scrape() { + // Get next link to visit + ToVisitLinkModel toVisitLink = getNextLink(); + + if (toVisitLink == null) { + System.out.println("No links to visit..."); + return; + } + + // Handle link + handleLink(toVisitLink); + } } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/LinkService.java b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/LinkService.java index cdb09b0d..8e838fa5 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/LinkService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/LinkService.java @@ -3,7 +3,6 @@ import java.util.List; import java.util.Optional; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationResults; @@ -16,8 +15,11 @@ @Service public class LinkService { - @Autowired - private MongoTemplate mongoTemplate; + private final MongoTemplate mongoTemplate; + + public LinkService(MongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + } public Optional getNextLink(String store) { MatchOperation match = Aggregation.match(Criteria.where("store").is(store)); diff --git a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java index ed6ef9be..4e35f3f7 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java @@ -1,26 +1,16 @@ package fellowship.mealmaestro.services.webscraping; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @Service public class WebscrapeService { - @Autowired - private LinkService linkService; + private final LinkService linkService; - private CheckersScraper checkersScraper; + private final CheckersScraper checkersScraper; - private volatile boolean isScrapingAllowed = true; - - @Scheduled(cron = "0 0 6 * * ?") - public void startScrape() { - System.out.println("Scraping started..."); - } - - @Scheduled(cron = "0 42 10 * * ?") - public void stopScraping() { - System.out.println("Scraping stopped..."); + public WebscrapeService(LinkService linkService, CheckersScraper checkersScraper) { + this.linkService = linkService; + this.checkersScraper = checkersScraper; } } From 9418dfd6ebf18d52e58722cd5a3ef0bc0de225d1 Mon Sep 17 00:00:00 2001 From: SkulderLock <78735770+SkulderLock@users.noreply.github.com> Date: Sun, 10 Sep 2023 18:40:56 +0200 Subject: [PATCH 13/97] =?UTF-8?q?=F0=9F=9A=A7=20Scraping=20scheduling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mealmaestro/config/ApplicationConfig.java | 2 +- .../services/webscraping/CheckersScraper.java | 1 + .../webscraping/WebscrapeService.java | 70 +++++++++++++++++-- 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/config/ApplicationConfig.java b/backend/src/main/java/fellowship/mealmaestro/config/ApplicationConfig.java index 78b64f6e..db57ef1f 100644 --- a/backend/src/main/java/fellowship/mealmaestro/config/ApplicationConfig.java +++ b/backend/src/main/java/fellowship/mealmaestro/config/ApplicationConfig.java @@ -51,7 +51,7 @@ public AuthenticationManager authenticationManager(AuthenticationConfiguration c @Bean public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - scheduler.setPoolSize(4); + scheduler.setPoolSize(6); scheduler.initialize(); return scheduler; } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java index 11b41072..99203e62 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java @@ -259,6 +259,7 @@ public void scrape() { } // Handle link + System.out.println("###" + System.currentTimeMillis() + "###"); handleLink(toVisitLink); } } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java index 4e35f3f7..9fddce11 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java @@ -1,16 +1,78 @@ package fellowship.mealmaestro.services.webscraping; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.concurrent.ScheduledFuture; + +import org.springframework.scheduling.TaskScheduler; import org.springframework.stereotype.Service; +import jakarta.annotation.PostConstruct; + @Service public class WebscrapeService { - private final LinkService linkService; - private final CheckersScraper checkersScraper; + private final TaskScheduler taskScheduler; - public WebscrapeService(LinkService linkService, CheckersScraper checkersScraper) { - this.linkService = linkService; + private ScheduledFuture checkersScrapingTask; + + public WebscrapeService(CheckersScraper checkersScraper, TaskScheduler taskScheduler) { this.checkersScraper = checkersScraper; + this.taskScheduler = taskScheduler; + } + + @PostConstruct + public void init() { + startScraping(); + } + + private LocalDateTime getStartTime() { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime next6AM = now.withHour(6).withMinute(0).withSecond(0); + + if (now.isAfter(next6AM) || now.isEqual(next6AM)) { + return next6AM.plusDays(1); + } else { + return next6AM; + } + } + + private LocalDateTime getStopTime() { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime next1040AM = now.withHour(10).withMinute(40).withSecond(0); + + if (now.isAfter(next1040AM) || now.isEqual(next1040AM)) { + return next1040AM.plusDays(1); + } else { + return next1040AM; + } + } + + private void startScraping() { + LocalDateTime startTime = getStartTime(); + + // schedule task to start at 6am and use a 10s fixedDelay + Duration tenSeconds = Duration.ofSeconds(10); + checkersScrapingTask = taskScheduler.scheduleWithFixedDelay(() -> { + checkersScraper.scrape(); + }, startTime.toInstant(ZoneOffset.ofHours(2)), tenSeconds); + + // schedule task to stop at 10:40am + LocalDateTime stopTime = getStopTime(); + taskScheduler.schedule(() -> { + stopScraping(); + }, stopTime.toInstant(ZoneOffset.ofHours(2))); } + + private void stopScraping() { + if (checkersScrapingTask != null) { + checkersScrapingTask.cancel(false); + } + + // schedule tasks for next day + startScraping(); + } + } From 74fc509b8583c97c1289331288215c0cbaedd6b4 Mon Sep 17 00:00:00 2001 From: SkulderLock <78735770+SkulderLock@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:06:39 +0200 Subject: [PATCH 14/97] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F=20Added=20index?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fellowship/mealmaestro/models/mongo/ToVisitLinkModel.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/main/java/fellowship/mealmaestro/models/mongo/ToVisitLinkModel.java b/backend/src/main/java/fellowship/mealmaestro/models/mongo/ToVisitLinkModel.java index a731db2f..e547f8a7 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/mongo/ToVisitLinkModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/mongo/ToVisitLinkModel.java @@ -1,6 +1,7 @@ package fellowship.mealmaestro.models.mongo; import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.Indexed; import org.springframework.data.mongodb.core.mapping.Document; import lombok.Getter; @@ -15,6 +16,7 @@ public class ToVisitLinkModel { private String type; + @Indexed private String store; public ToVisitLinkModel(String link, String type, String store) { From 8512585d1fdbe072467e83573e98549c7f118caf Mon Sep 17 00:00:00 2001 From: SkulderLock <78735770+SkulderLock@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:27:09 +0200 Subject: [PATCH 15/97] =?UTF-8?q?=F0=9F=90=9B=20small=20fixes=20and=20exte?= =?UTF-8?q?nded=20timeout=20duration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fellowship/mealmaestro/services/MealManagementService.java | 1 + .../java/fellowship/mealmaestro/services/OpenaiApiService.java | 2 +- frontend/src/app/pages/profile/profile.page.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java b/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java index b240700b..4c51817e 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java @@ -32,6 +32,7 @@ public MealModel generateMeal(String mealType, String token) { MealModel defaultMeal = new MealModel("Bread", "1. Toast the bread", "Delicious Bread", "https://images.unsplash.com/photo-1598373182133-52452f7691ef?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80", "Bread", "5 minutes"); + defaultMeal.setType("breakfast"); try { JsonNode mealJson = objectMapper.readTree(openaiApiService.fetchMealResponse(mealType, token)); int i = 0; diff --git a/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java b/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java index 66a019f9..c25e558b 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java @@ -107,7 +107,7 @@ public String getJSONResponse(String Type, String token) throws JsonProcessingEx .body(Mono.just(jsonRequest), String.class) .retrieve() .bodyToMono(String.class) - .timeout(Duration.ofSeconds(10)) + .timeout(Duration.ofSeconds(30)) .block(); return response; diff --git a/frontend/src/app/pages/profile/profile.page.ts b/frontend/src/app/pages/profile/profile.page.ts index e4dd6a4c..2a4a4537 100644 --- a/frontend/src/app/pages/profile/profile.page.ts +++ b/frontend/src/app/pages/profile/profile.page.ts @@ -166,7 +166,7 @@ export class ProfilePage implements OnInit, ViewWillEnter { } }, }); - this.loginService.setSettingsRefreshed(false); + this.loginService.setSettingsRefreshed(true); } } From 90d8a6be47c8da4e51614d67f6ef34fa2aedfdf6 Mon Sep 17 00:00:00 2001 From: SkulderLock <78735770+SkulderLock@users.noreply.github.com> Date: Tue, 12 Sep 2023 15:54:24 +0200 Subject: [PATCH 16/97] =?UTF-8?q?=E2=9C=A8=20Checkers=20Scraper=20+=20logs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 + backend/build.gradle | 1 + .../controllers/ProductController.java | 12 +- .../mealmaestro/models/mongo/FoodModelM.java | 8 +- .../services/webscraping/CheckersScraper.java | 106 ++++++++++++++---- .../webscraping/WebscrapeService.java | 12 +- .../src/main/resources/application.properties | 2 + 7 files changed, 120 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index c96a4da0..caec0976 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,12 @@ $RECYCLE.BIN/ *.log log.txt +mealmaestro-logs.txt + + +# ios +ios/ +android/ .env diff --git a/backend/build.gradle b/backend/build.gradle index a3d59903..2cf073f5 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -25,6 +25,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-neo4j' implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-logging' implementation 'org.seleniumhq.selenium:selenium-java:4.12.1' implementation 'org.jsoup:jsoup:1.16.1' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/ProductController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/ProductController.java index 22251ea4..3bcffa10 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/ProductController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/ProductController.java @@ -1,6 +1,7 @@ package fellowship.mealmaestro.controllers; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; @@ -9,14 +10,17 @@ import fellowship.mealmaestro.models.mongo.findBarcodeRequest; import fellowship.mealmaestro.models.mongo.FoodModelM; import fellowship.mealmaestro.services.BarcodeService; +import fellowship.mealmaestro.services.webscraping.CheckersScraper; @RestController public class ProductController { private final BarcodeService barcodeService; + private final CheckersScraper checkersScraper; - public ProductController(BarcodeService barcodeService) { + public ProductController(BarcodeService barcodeService, CheckersScraper checkersScraper) { this.barcodeService = barcodeService; + this.checkersScraper = checkersScraper; } @PostMapping("/findProduct") @@ -36,4 +40,10 @@ public ResponseEntity addProduct(@RequestBody FoodModelM product, } return ResponseEntity.ok(barcodeService.addProduct(product)); } + + @GetMapping("/loc") + public ResponseEntity loc() { + checkersScraper.getLocLinks(); + return ResponseEntity.ok("loc"); + } } diff --git a/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java b/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java index ed08d77d..a8b3d84c 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/mongo/FoodModelM.java @@ -47,8 +47,12 @@ public String toString() { public void setPrice(String price) { // remove R sign - price = price.substring(1); - this.price = Double.parseDouble(price); + Pattern pattern = Pattern.compile("(R)([0-9.]+)"); + Matcher matcher = pattern.matcher(price); + + if (matcher.find()) { + this.price = Double.parseDouble(matcher.group(2)); + } } public void setPrice(Double price) { diff --git a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java index 99203e62..3c6ab80b 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java @@ -15,7 +15,11 @@ import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +@Service public class CheckersScraper { private final ToVisitLinkRepository toVisitLinkRepository; @@ -25,6 +29,8 @@ public class CheckersScraper { private final BarcodeService barcodeService; + private static final Logger logger = LoggerFactory.getLogger(CheckersScraper.class); + public CheckersScraper(ToVisitLinkRepository toVisitLinkRepository, VisitedLinkRepository visitedLinkRepository, LinkService linkService, BarcodeService barcodeService) { this.toVisitLinkRepository = toVisitLinkRepository; @@ -90,22 +96,53 @@ public void handleLink(ToVisitLinkModel toVisitLink) { public void handleCategoryLink(ToVisitLinkModel link) { // Visit category page and get all product links and pagination links + logger.info("Handling Category Link: " + link.getLink()); Optional visited = visitedLinkRepository.findById(link.getLink()); // if link has been visited and it has been less than 1 month since last visit, // skip if (visited.isPresent() && visited.get().getLastVisited().plusMonths(1).isAfter(LocalDate.now())) { toVisitLinkRepository.deleteById(link.getLink()); - System.out.println("Skipping " + link.getLink() + ", already visited..."); + logger.info("Skipping " + link.getLink() + ", already visited..."); return; } try { - Document doc = Jsoup.connect("https://www.checkers.co.za" + link.getLink()).get(); + // if link starts with /, add domain + Document doc; + if (link.getLink().startsWith("/")) { + doc = Jsoup.connect("http://www.checkers.co.za" + link.getLink()).userAgent( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.76") + .header("Accept", + "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8") + .header("Accept-Encoding", "gzip, deflate, br") + .header("Accept-Language", "en-US,en;q=0.9") + .get(); + } else { + doc = Jsoup.connect(link.getLink()).userAgent( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.76") + .header("Accept", + "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8") + .header("Accept-Encoding", "gzip, deflate, br") + .header("Accept-Language", "en-US,en;q=0.9") + .get(); + } + logger.info("Connecting to " + doc.title() + "..."); + if (doc.title().contains("Captcha")) { + logger.info("######################################"); + logger.info("Error encountered captcha, skipping..."); + logger.info("######################################"); + return; + } // Get product links Elements productPageLinks = doc.select("h3.item-product__name > a"); + if (productPageLinks == null) { + logger.info("Skipping " + link.getLink() + ", no product links..."); + return; + } + for (int i = 0; i < productPageLinks.size(); i++) { String productPageLink = productPageLinks.get(i).attr("href"); @@ -132,13 +169,14 @@ public void handleCategoryLink(ToVisitLinkModel link) { // Add link to visited links visitedLinkRepository.save(new VisitedLinkModel(link.getLink(), "category", "Checkers")); + logger.info("Saving " + link.getLink() + " to visited links..."); // Remove link from ToVisitLinks toVisitLinkRepository.deleteById(link.getLink()); - System.out.println("Visited " + link.getLink()); + logger.info("Visited " + link.getLink()); } catch (IOException e) { - System.out.println("Error visiting " + link.getLink() + ", skipping..."); + logger.info("Error visiting " + link.getLink() + ", skipping..."); e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); @@ -150,30 +188,49 @@ public void handleProductLink(ToVisitLinkModel link) { // Visit product page and get product info // Check if link has been visited + logger.info("Handling Product Link: " + link.getLink()); Optional visited = visitedLinkRepository.findById(link.getLink()); if (visited.isPresent() && visited.get().getLastVisited().plusMonths(1).isAfter(LocalDate.now())) { toVisitLinkRepository.deleteById(link.getLink()); - System.out.println("Skipping " + link.getLink() + ", already visited..."); + logger.info("Skipping " + link.getLink() + ", already visited..."); return; } try { // Visit product page - Document doc = Jsoup.connect("https://www.checkers.co.za" + link.getLink()).get(); - + // if link starts with /, add domain + Document doc; + if (link.getLink().startsWith("/")) { + doc = Jsoup.connect("http://www.checkers.co.za" + link.getLink()).userAgent( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36") + .header("Accept", + "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8") + .header("Accept-Encoding", "gzip, deflate, br") + .header("Accept-Language", "en-US,en;q=0.9") + .get(); + } else { + doc = Jsoup.connect(link.getLink()).userAgent( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36") + .header("Accept", + "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8") + .header("Accept-Encoding", "gzip, deflate, br") + .header("Accept-Language", "en-US,en;q=0.9") + .get(); + } + logger.info("Connecting to " + doc.title() + "..."); // Get product info FoodModelM food = new FoodModelM(); // product name Element productNameEl = doc.selectFirst("h1.pdp__name"); if (productNameEl == null) { - System.out.println("Skipping " + link.getLink() + ", no product name..."); + logger.info("Skipping " + link.getLink() + ", no product name..."); return; } String productName = productNameEl.text(); if (productName == null || productName.isEmpty()) { - System.out.println("Skipping " + link.getLink() + ", no product name..."); + logger.info("Skipping " + link.getLink() + ", no product name..."); return; } System.out.println("Product name: " + productName); @@ -182,15 +239,17 @@ public void handleProductLink(ToVisitLinkModel link) { // product price Element productPriceEl = doc.selectFirst("div.special-price__price"); if (productPriceEl == null) { - System.out.println("Skipping " + link.getLink() + ", no product price..."); - return; - } - String productPrice = productPriceEl.text(); - if (productPrice == null || productPrice.isEmpty()) { + logger.info("No product price"); food.setPrice(-1.0); + } else { + + String productPrice = productPriceEl.text(); + if (productPrice == null || productPrice.isEmpty()) { + food.setPrice(-1.0); + } + System.out.println("Product price: " + productPrice); + food.setPrice(productPrice); } - System.out.println("Product price: " + productPrice); - food.setPrice(productPrice); // product details Elements productDetails = doc.select("table.pdp__product-information > tbody > tr"); @@ -203,7 +262,7 @@ public void handleProductLink(ToVisitLinkModel link) { // select second td Element barcodeEl = productDetail.selectFirst("td:nth-child(2)"); if (barcodeEl == null) { - System.out.println("Skipping " + link.getLink() + ", no barcode..."); + logger.info("Skipping " + link.getLink() + ", no barcode..."); return; } barcode = barcodeEl.text(); @@ -216,7 +275,7 @@ public void handleProductLink(ToVisitLinkModel link) { // select second td Element quantityEl = productDetail.selectFirst("td:nth-child(2)"); if (quantityEl == null) { - System.out.println("Skipping " + link.getLink() + ", no quantity..."); + logger.info("Skipping " + link.getLink() + ", no quantity..."); return; } quantity = quantityEl.text(); @@ -226,22 +285,25 @@ public void handleProductLink(ToVisitLinkModel link) { } if (barcode.isEmpty() || food.getBarcode().equals("")) { - System.out.println("Skipping " + link.getLink() + ", no barcode..."); + logger.info("Skipping " + link.getLink() + ", no barcode..."); return; } // Add food to database + food.setStore("Checkers"); barcodeService.addProduct(food); + logger.info("Saving " + food.getName() + " to database..."); // Add link to visited links visitedLinkRepository.save(new VisitedLinkModel(link.getLink(), "product", "Checkers")); + logger.info("Saving " + link.getLink() + " to visited links..."); // Remove link from ToVisitLinks toVisitLinkRepository.deleteById(link.getLink()); - System.out.println("Visited " + link.getLink()); + logger.info("Visited " + link.getLink()); } catch (IOException e) { - System.out.println("Error visiting " + link.getLink() + ", skipping..."); + logger.info("Error visiting " + link.getLink() + ", skipping..."); e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); @@ -259,7 +321,7 @@ public void scrape() { } // Handle link - System.out.println("###" + System.currentTimeMillis() + "###"); + logger.info("### " + System.currentTimeMillis() + " ###"); handleLink(toVisitLink); } } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java index 9fddce11..858f93b2 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java @@ -5,6 +5,8 @@ import java.time.ZoneOffset; import java.util.concurrent.ScheduledFuture; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.scheduling.TaskScheduler; import org.springframework.stereotype.Service; @@ -16,6 +18,8 @@ public class WebscrapeService { private final CheckersScraper checkersScraper; private final TaskScheduler taskScheduler; + private static final Logger logger = LoggerFactory.getLogger(WebscrapeService.class); + private ScheduledFuture checkersScrapingTask; public WebscrapeService(CheckersScraper checkersScraper, TaskScheduler taskScheduler) { @@ -25,12 +29,13 @@ public WebscrapeService(CheckersScraper checkersScraper, TaskScheduler taskSched @PostConstruct public void init() { + System.out.println("WebscrapeService init"); startScraping(); } private LocalDateTime getStartTime() { LocalDateTime now = LocalDateTime.now(); - LocalDateTime next6AM = now.withHour(6).withMinute(0).withSecond(0); + LocalDateTime next6AM = now.withHour(13).withMinute(55).withSecond(0); if (now.isAfter(next6AM) || now.isEqual(next6AM)) { return next6AM.plusDays(1); @@ -54,22 +59,25 @@ private void startScraping() { LocalDateTime startTime = getStartTime(); // schedule task to start at 6am and use a 10s fixedDelay - Duration tenSeconds = Duration.ofSeconds(10); + Duration tenSeconds = Duration.ofSeconds(24); checkersScrapingTask = taskScheduler.scheduleWithFixedDelay(() -> { checkersScraper.scrape(); }, startTime.toInstant(ZoneOffset.ofHours(2)), tenSeconds); + logger.info("Scheduled scraping task to start at {}", startTime); // schedule task to stop at 10:40am LocalDateTime stopTime = getStopTime(); taskScheduler.schedule(() -> { stopScraping(); }, stopTime.toInstant(ZoneOffset.ofHours(2))); + logger.info("Scheduled scraping task to stop at {}", stopTime); } private void stopScraping() { if (checkersScrapingTask != null) { checkersScrapingTask.cancel(false); } + logger.info("Stopped scraping task"); // schedule tasks for next day startScraping(); diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index e69de29b..7ccfd96f 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -0,0 +1,2 @@ +logging.level.root=INFO +logging.file.name=logs/mealmaestro-logs.txt \ No newline at end of file From 7c22dba691aede69dfeedce01669264ce6468ba3 Mon Sep 17 00:00:00 2001 From: Skulderlock <78735770+SkulderLock@users.noreply.github.com> Date: Fri, 15 Sep 2023 12:01:31 +0200 Subject: [PATCH 17/97] =?UTF-8?q?=E2=9C=A8=20Price=20shown=20in=20modal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/app/models/fooditem.model.ts | 2 +- .../src/app/pages/pantry/pantry.page.html | 7 ++ frontend/src/app/pages/pantry/pantry.page.ts | 67 ++++++++++--------- 3 files changed, 44 insertions(+), 32 deletions(-) diff --git a/frontend/src/app/models/fooditem.model.ts b/frontend/src/app/models/fooditem.model.ts index 2afc89fa..14c865a2 100644 --- a/frontend/src/app/models/fooditem.model.ts +++ b/frontend/src/app/models/fooditem.model.ts @@ -1,7 +1,7 @@ export interface FoodItemI { name: string; quantity: number | null; - unit: 'kg' | 'g' | 'l' | 'ml' | 'pcs'; + unit: 'kg' | 'g' | 'l' | 'ml' | 'pcs' | undefined; id?: number; price?: number; } diff --git a/frontend/src/app/pages/pantry/pantry.page.html b/frontend/src/app/pages/pantry/pantry.page.html index a7d5cdde..b35dbf4b 100644 --- a/frontend/src/app/pages/pantry/pantry.page.html +++ b/frontend/src/app/pages/pantry/pantry.page.html @@ -262,6 +262,13 @@ + + + + Price: R{{ newItem.price }} + + + diff --git a/frontend/src/app/pages/pantry/pantry.page.ts b/frontend/src/app/pages/pantry/pantry.page.ts index 97b58055..7cfc0e2c 100644 --- a/frontend/src/app/pages/pantry/pantry.page.ts +++ b/frontend/src/app/pages/pantry/pantry.page.ts @@ -44,7 +44,7 @@ export class PantryPage implements OnInit, ViewWillEnter { foodListItem!: QueryList; @ViewChild(IonModal) modal!: IonModal; - isBarcodeSupported: boolean = true; + isBarcodeSupported: boolean = false; segment: 'pantry' | 'shopping' | null = 'pantry'; isLoading: boolean = false; pantryItems: FoodItemI[] = []; @@ -54,7 +54,8 @@ export class PantryPage implements OnInit, ViewWillEnter { newItem: FoodItemI = { name: '', quantity: null, - unit: 'pcs', + unit: undefined, + price: undefined, }; constructor( @@ -69,9 +70,9 @@ export class PantryPage implements OnInit, ViewWillEnter { ) {} async ngOnInit() { - // BarcodeScanner.isSupported().then((result) => { - // this.isBarcodeSupported = result.supported; - // }); + BarcodeScanner.isSupported().then((result) => { + this.isBarcodeSupported = result.supported; + }); } async ionViewWillEnter() { @@ -152,6 +153,7 @@ export class PantryPage implements OnInit, ViewWillEnter { name: '', quantity: null, unit: 'pcs', + price: undefined, }; } } @@ -186,6 +188,7 @@ export class PantryPage implements OnInit, ViewWillEnter { name: '', quantity: null, unit: 'pcs', + price: undefined, }; } } @@ -313,6 +316,7 @@ export class PantryPage implements OnInit, ViewWillEnter { name: '', quantity: null, unit: 'pcs', + price: undefined, }; } @@ -460,32 +464,32 @@ export class PantryPage implements OnInit, ViewWillEnter { } async scan(): Promise { - // const granted = await this.requestPermissions(); - // if (!granted) { - // this.errorHandlerService.presentErrorToast( - // 'Please grant camera permissions to use this feature', - // 'Camera permissions not granted' - // ); - // return; - // } - - // const result = await BarcodeScanner.scan(); - - // if ( - // result.barcodes.length === 0 || - // result.barcodes[0].displayValue === '' || - // result.barcodes[0].displayValue === null || - // result.barcodes[0].displayValue === undefined - // ) { - // return; - // } - let result = { - barcodes: [ - { - displayValue: '13761238123', // for testing - }, - ], - }; + const granted = await this.requestPermissions(); + if (!granted) { + this.errorHandlerService.presentErrorToast( + 'Please grant camera permissions to use this feature', + 'Camera permissions not granted' + ); + return; + } + + const result = await BarcodeScanner.scan(); + + if ( + result.barcodes.length === 0 || + result.barcodes[0].displayValue === '' || + result.barcodes[0].displayValue === null || + result.barcodes[0].displayValue === undefined + ) { + return; + } + // let result = { + // barcodes: [ + // { + // displayValue: '13761238123', // for testing + // }, + // ], + // }; if (this.loginService.isShoppingAt() === '') { this.askShoppingLocation(result); @@ -565,6 +569,7 @@ export class PantryPage implements OnInit, ViewWillEnter { name: data.name, quantity: null, unit: 'pcs', + price: undefined, }; this.modal.present(); }, From 13177e2622fbd92627b319ec5bfa019e46073837 Mon Sep 17 00:00:00 2001 From: Skulderlock <78735770+SkulderLock@users.noreply.github.com> Date: Fri, 15 Sep 2023 12:39:14 +0200 Subject: [PATCH 18/97] =?UTF-8?q?=E2=9C=A8=20Total=20Price=20showing=20on?= =?UTF-8?q?=20shopping=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/app/pages/pantry/pantry.page.html | 3 +++ frontend/src/app/pages/pantry/pantry.page.scss | 10 ++++++++++ frontend/src/app/pages/pantry/pantry.page.ts | 14 ++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/frontend/src/app/pages/pantry/pantry.page.html b/frontend/src/app/pages/pantry/pantry.page.html index b35dbf4b..ed842486 100644 --- a/frontend/src/app/pages/pantry/pantry.page.html +++ b/frontend/src/app/pages/pantry/pantry.page.html @@ -87,6 +87,9 @@
Shopping list is empty :(
+ + Total cost: R {{totalShoppingPrice}} + i.name !== item.name ); + this.calculateTotalPrice(); } }, error: (err) => { @@ -273,6 +277,7 @@ export class PantryPage implements OnInit, ViewWillEnter { (i) => i.name !== item.name ); this.errorHandlerService.presentSuccessToast('Item Bought!'); + this.calculateTotalPrice(); } } }, @@ -295,6 +300,15 @@ export class PantryPage implements OnInit, ViewWillEnter { }); } + calculateTotalPrice() { + this.totalShoppingPrice = 0; + this.shoppingItems.forEach((item) => { + if (item.price) { + this.totalShoppingPrice += item.price; + } + }); + } + closeSlidingItems() { this.foodListItem.forEach((item) => { item.closeItem(); From 96eb0c5a0024ee189bbed4fbc8c4e5468cb8fa95 Mon Sep 17 00:00:00 2001 From: Skulderlock <78735770+SkulderLock@users.noreply.github.com> Date: Fri, 15 Sep 2023 13:28:22 +0200 Subject: [PATCH 19/97] =?UTF-8?q?=F0=9F=90=9B=20bug=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/app/pages/home/home.page.ts | 2 + frontend/src/app/pages/login/login.page.ts | 49 ++++++++++++------- .../recipe-book/add-recipe.service.ts | 7 +-- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/frontend/src/app/pages/home/home.page.ts b/frontend/src/app/pages/home/home.page.ts index 6fe1a632..4d8703f0 100644 --- a/frontend/src/app/pages/home/home.page.ts +++ b/frontend/src/app/pages/home/home.page.ts @@ -151,9 +151,11 @@ export class HomePage implements OnInit, ViewWillEnter { hideLoading() { this.showLoading = false; + this.isLoading = false; setTimeout(() => { this.showLoading = false; + this.isLoading = false; }, 200); } } diff --git a/frontend/src/app/pages/login/login.page.ts b/frontend/src/app/pages/login/login.page.ts index 0c6f9ce0..8688ddb1 100644 --- a/frontend/src/app/pages/login/login.page.ts +++ b/frontend/src/app/pages/login/login.page.ts @@ -3,7 +3,10 @@ import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { IonicModule } from '@ionic/angular'; import { Router } from '@angular/router'; -import { AuthenticationService, ErrorHandlerService } from '../../services/services'; +import { + AuthenticationService, + ErrorHandlerService, +} from '../../services/services'; import { UserI } from '../../models/user.model'; @Component({ @@ -11,30 +14,29 @@ import { UserI } from '../../models/user.model'; templateUrl: './login.page.html', styleUrls: ['./login.page.scss'], standalone: true, - imports: [IonicModule, CommonModule, FormsModule] + imports: [IonicModule, CommonModule, FormsModule], }) export class LoginPage implements OnInit { user: UserI = { username: '', email: '', password: '', - } - + }; - constructor(private router: Router, - private errorHandlerService: ErrorHandlerService, - private auth: AuthenticationService - ) { } + constructor( + private router: Router, + private errorHandlerService: ErrorHandlerService, + private auth: AuthenticationService + ) {} - ngOnInit() { - } + ngOnInit() {} async login(form: any) { const loginUser: UserI = { username: '', email: form.email, password: form.password, - } + }; this.auth.login(loginUser).subscribe({ next: (response) => { if (response.status == 200) { @@ -46,21 +48,32 @@ export class LoginPage implements OnInit { } }, error: (error) => { - if (error.status == 403){ - this.errorHandlerService.presentErrorToast('Invalid credentials', 'Invalid credentials'); + if (error.status == 403) { + this.errorHandlerService.presentErrorToast( + 'Invalid credentials', + 'Invalid credentials' + ); localStorage.removeItem('token'); - }else if(error.status == 404){ - this.errorHandlerService.presentErrorToast('Email or password incorrect', 'Email or password incorrect'); + } else if (error.status == 404) { + this.errorHandlerService.presentErrorToast( + 'Email or password incorrect', + 'Email or password incorrect' + ); localStorage.removeItem('token'); - }else{ - this.errorHandlerService.presentErrorToast('Unexpected error. Please try again', error); + } else { + this.errorHandlerService.presentErrorToast( + 'Unexpected error. Please try again', + error + ); } - } + }, }); } goToSignup() { this.router.navigate(['../signup']); + // this.router.navigate(['app/tabs/home']); + localStorage.removeItem('token'); } } diff --git a/frontend/src/app/services/recipe-book/add-recipe.service.ts b/frontend/src/app/services/recipe-book/add-recipe.service.ts index e29f5aee..901652e3 100644 --- a/frontend/src/app/services/recipe-book/add-recipe.service.ts +++ b/frontend/src/app/services/recipe-book/add-recipe.service.ts @@ -3,11 +3,12 @@ import { BehaviorSubject } from 'rxjs'; import { MealI } from '../../models/meal.model'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AddRecipeService { - private recipeSource: BehaviorSubject = new BehaviorSubject(undefined); - constructor() { } + private recipeSource: BehaviorSubject = + new BehaviorSubject(undefined); + constructor() {} recipeItem$ = this.recipeSource.asObservable(); From 7eab97ffbd36669861c8452af6da00efabeeadd9 Mon Sep 17 00:00:00 2001 From: Skulderlock <78735770+SkulderLock@users.noreply.github.com> Date: Sun, 17 Sep 2023 21:32:12 +0200 Subject: [PATCH 20/97] =?UTF-8?q?=E2=9E=96=20remove=20confilicting=20depen?= =?UTF-8?q?dencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + package-lock.json | 12 ------------ package.json | 1 - 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index caec0976..9db77445 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ $RECYCLE.BIN/ *.log log.txt mealmaestro-logs.txt +logs/ # ios diff --git a/package-lock.json b/package-lock.json index b981493c..3ce0d53c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,6 @@ "@ionic/angular": "^7.0.0", "@types/chart.js": "^2.9.37", "chart.js": "^4.3.0", - "cordova-plugin-advanced-http": "^3.3.1", "cordova-plugin-file": "^8.0.0", "dotenv": "^16.3.1", "ionicons": "^7.0.0", @@ -6111,17 +6110,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cordova-plugin-advanced-http": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/cordova-plugin-advanced-http/-/cordova-plugin-advanced-http-3.3.1.tgz", - "integrity": "sha512-hESuB3mxIHCUrzb5lm7juda6PSNcC5N8Invizj5wGV2rSldCapiNxMTEpzKR1UVPDDP2XOtBzO0SAYS+3+g/ig==", - "engines": [ - { - "name": "cordova", - "version": ">=4.0.0" - } - ] - }, "node_modules/cordova-plugin-file": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/cordova-plugin-file/-/cordova-plugin-file-8.0.0.tgz", diff --git a/package.json b/package.json index 936d32f6..3c326890 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "@ionic/angular": "^7.0.0", "@types/chart.js": "^2.9.37", "chart.js": "^4.3.0", - "cordova-plugin-advanced-http": "^3.3.1", "cordova-plugin-file": "^8.0.0", "dotenv": "^16.3.1", "ionicons": "^7.0.0", From ee491a89dafd3df346659917100bfe90355bfb0b Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Mon, 18 Sep 2023 17:22:45 +0200 Subject: [PATCH 21/97] Update home.page.ts --- frontend/src/app/pages/home/home.page.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/app/pages/home/home.page.ts b/frontend/src/app/pages/home/home.page.ts index 39a7d341..3da718ba 100644 --- a/frontend/src/app/pages/home/home.page.ts +++ b/frontend/src/app/pages/home/home.page.ts @@ -104,6 +104,7 @@ export class HomePage implements OnInit, ViewWillEnter { await new Promise((resolve, reject) => { this.mealGenerationservice.getDailyMeals(date).subscribe({ next: (data) => { + console.log('Received data:', data); if (data.body) { let mealsForDay: DaysMealsI = { breakfast: undefined, From f64233cc1c11c8c541559393385d77d8836b29f2 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Tue, 19 Sep 2023 09:33:06 +0200 Subject: [PATCH 22/97] images fixed --- .../src/app/components/daily-meals/daily-meals.component.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/app/components/daily-meals/daily-meals.component.scss b/frontend/src/app/components/daily-meals/daily-meals.component.scss index cabdf0b2..71d263d8 100644 --- a/frontend/src/app/components/daily-meals/daily-meals.component.scss +++ b/frontend/src/app/components/daily-meals/daily-meals.component.scss @@ -18,6 +18,7 @@ ion-card { } .div1 img { + min-width: 100%; width: 100%; height: 100%; object-fit: cover; From 38fec0f09291d0662bce145b4a846d11fda7c112 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Tue, 19 Sep 2023 09:40:07 +0200 Subject: [PATCH 23/97] more css fixed --- .../app/components/daily-meals/daily-meals.component.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/src/app/components/daily-meals/daily-meals.component.scss b/frontend/src/app/components/daily-meals/daily-meals.component.scss index 71d263d8..7125e3dd 100644 --- a/frontend/src/app/components/daily-meals/daily-meals.component.scss +++ b/frontend/src/app/components/daily-meals/daily-meals.component.scss @@ -11,6 +11,7 @@ ion-item { width: 100%; display: block; --ion-padding: 0px; + } ion-card { padding: 0%; @@ -29,6 +30,11 @@ ion-card { --padding-start: 0; --padding-end: 0; padding-right: 0%; + --border-style:none; +} + +.item-inner { + border-style: none !important; } .side { display: inline; From 7b0d43191d25c8c9457cc51a84571c951c8d6bfb Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Tue, 19 Sep 2023 12:28:48 +0200 Subject: [PATCH 24/97] Create HydrationService.java --- .../fellowship/mealmaestro/services/HydrationService.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java diff --git a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java new file mode 100644 index 00000000..fdd54976 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java @@ -0,0 +1,8 @@ +package fellowship.mealmaestro.services; + +import org.springframework.stereotype.Service; + +@Service +public class HydrationService { + +} From 5f156c211f2a6efcbcc89094f35a545cdafca9f9 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Tue, 19 Sep 2023 12:53:59 +0200 Subject: [PATCH 25/97] Create TriggerService.java --- .../fellowship/mealmaestro/services/TriggerService.java | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 backend/src/main/java/fellowship/mealmaestro/services/TriggerService.java diff --git a/backend/src/main/java/fellowship/mealmaestro/services/TriggerService.java b/backend/src/main/java/fellowship/mealmaestro/services/TriggerService.java new file mode 100644 index 00000000..4a2e7f2f --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/services/TriggerService.java @@ -0,0 +1,6 @@ +package fellowship.mealmaestro.services; + +public class TriggerService { + + +} From 225c5eea76ae68f3ccf649a7d3e71095c4005a5f Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Tue, 19 Sep 2023 15:21:26 +0200 Subject: [PATCH 26/97] Create RecommendationService.java --- .../fellowship/mealmaestro/services/RecommendationService.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java diff --git a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java new file mode 100644 index 00000000..e69de29b From f46224a37b5d92a29c52274de97e3914334d3e9b Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 09:32:47 +0200 Subject: [PATCH 27/97] Update daily-meals.component.scss --- .../src/app/components/daily-meals/daily-meals.component.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/app/components/daily-meals/daily-meals.component.scss b/frontend/src/app/components/daily-meals/daily-meals.component.scss index 7125e3dd..f61d9637 100644 --- a/frontend/src/app/components/daily-meals/daily-meals.component.scss +++ b/frontend/src/app/components/daily-meals/daily-meals.component.scss @@ -24,6 +24,7 @@ ion-card { height: 100%; object-fit: cover; padding-right: 0px !important; + overflow: hidden; } .no-style { From affd6372be4fa3206439d7561868430b3bc446df Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 09:41:01 +0200 Subject: [PATCH 28/97] Create LogEntryModel.java --- .../fellowship/mealmaestro/models/neo4j/LogEntryModel.java | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 backend/src/main/java/fellowship/mealmaestro/models/neo4j/LogEntryModel.java diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/LogEntryModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/LogEntryModel.java new file mode 100644 index 00000000..8934b059 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/LogEntryModel.java @@ -0,0 +1,5 @@ +package fellowship.mealmaestro.models.neo4j; + +public class LogEntryModel { + +} From b33082fe7365270d04abea151f645ce66c88f27b Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 09:43:42 +0200 Subject: [PATCH 29/97] Update RecommendationService.java --- .../mealmaestro/services/RecommendationService.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java index e69de29b..de60a33e 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java @@ -0,0 +1,5 @@ +package fellowship.mealmaestro.services; + +public class RecommendationService { + +} From df3bb9ac4e89e0526480bff5ebf70343ce383322 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 09:45:36 +0200 Subject: [PATCH 30/97] Relationship over new nodes --- .../fellowship/mealmaestro/models/neo4j/LogEntryModel.java | 5 ----- .../mealmaestro/models/neo4j/relationships/HasLogEntry.java | 0 2 files changed, 5 deletions(-) delete mode 100644 backend/src/main/java/fellowship/mealmaestro/models/neo4j/LogEntryModel.java create mode 100644 backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasLogEntry.java diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/LogEntryModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/LogEntryModel.java deleted file mode 100644 index 8934b059..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/LogEntryModel.java +++ /dev/null @@ -1,5 +0,0 @@ -package fellowship.mealmaestro.models.neo4j; - -public class LogEntryModel { - -} diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasLogEntry.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasLogEntry.java new file mode 100644 index 00000000..e69de29b From 18615f658ebd85ce5e3c58afedcd27cad4695182 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 09:47:29 +0200 Subject: [PATCH 31/97] Update HasLogEntry.java --- .../neo4j/relationships/HasLogEntry.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasLogEntry.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasLogEntry.java index e69de29b..e383362d 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasLogEntry.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasLogEntry.java @@ -0,0 +1,62 @@ +package fellowship.mealmaestro.models.neo4j.relationships; + +import java.time.LocalDate; + +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.RelationshipProperties; +import org.springframework.data.neo4j.core.schema.TargetNode; + +import fellowship.mealmaestro.models.neo4j.MealModel; + +@RelationshipProperties +public class HasLogEntry { +@Id + @GeneratedValue + private Long id; + + @TargetNode + private MealModel meal; + + private LocalDate date; + + private String entryType; + + public HasLogEntry(MealModel meal, LocalDate date, String entryType) { + this.meal = meal; + this.date = date; + this.entryType = entryType; + } + + public Long getId() { + return this.id; + } + + public void setId(Long id) { + this.id = id; + } + + public MealModel getMeal() { + return this.meal; + } + + public void setMeal(MealModel meal) { + this.meal = meal; + } + + public LocalDate getDate() { + return this.date; + } + + public void setDate(LocalDate date) { + this.date = date; + } + + public String getEntryType() { + return this.entryType; + } + + public void setEntryType(String entryType) { + this.entryType = entryType; + } +} From bd64707c22ac8c9b28876a5c9c7073ed434e8b8b Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 09:51:32 +0200 Subject: [PATCH 32/97] added relationships to user --- .../mealmaestro/models/neo4j/UserModel.java | 12 ++++++++++++ .../models/neo4j/relationships/HasLogEntry.java | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/UserModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/UserModel.java index 8da39dbb..a390a0f2 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/UserModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/UserModel.java @@ -12,6 +12,7 @@ import org.springframework.security.core.userdetails.UserDetails; import fellowship.mealmaestro.models.auth.AuthorityRoleModel; +import fellowship.mealmaestro.models.neo4j.relationships.HasLogEntry; import fellowship.mealmaestro.models.neo4j.relationships.HasMeal; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; @@ -54,6 +55,9 @@ public class UserModel implements UserDetails { @Relationship(type = "HAS_MEAL") private List meals; + @Relationship(type = "HAS_LOG_ENTRY") + private List entries; + public UserModel() { this.authorityRole = AuthorityRoleModel.USER; } @@ -167,4 +171,12 @@ public List getMeals() { public void setMeals(List meals) { this.meals = meals; } + + public List getLogEntries() { + return entries; + } + + public void setLogEntries(List entries) { + this.entries = entries; + } } diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasLogEntry.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasLogEntry.java index e383362d..e7afe458 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasLogEntry.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasLogEntry.java @@ -11,7 +11,7 @@ @RelationshipProperties public class HasLogEntry { -@Id + @Id @GeneratedValue private Long id; From 8e14a743fad9b59d0f42532c595e1dc830cdc9ab Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 09:53:35 +0200 Subject: [PATCH 33/97] removed empty constructor --- .../mealmaestro/models/neo4j/relationships/HasMeal.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasMeal.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasMeal.java index d526dcee..1aa85be8 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasMeal.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasMeal.java @@ -22,9 +22,6 @@ public class HasMeal { private String mealType; - public HasMeal() { - } - public HasMeal(MealModel meal, LocalDate date, String mealType) { this.meal = meal; this.date = date; From f5c987267bdcfd56dac399700701efd68739b44c Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 09:53:49 +0200 Subject: [PATCH 34/97] mb gang --- .../mealmaestro/models/neo4j/relationships/HasMeal.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasMeal.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasMeal.java index 1aa85be8..d526dcee 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasMeal.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasMeal.java @@ -22,6 +22,9 @@ public class HasMeal { private String mealType; + public HasMeal() { + } + public HasMeal(MealModel meal, LocalDate date, String mealType) { this.meal = meal; this.date = date; From a449e08977f207481e0128836fffcd8d30b8bbe5 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 10:31:44 +0200 Subject: [PATCH 35/97] Update MealManagementController.java --- .../controllers/MealManagementController.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java index bdd63bda..e25f6ddb 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java @@ -138,16 +138,4 @@ public ResponseEntity regenerate(@RequestBody RegenerateMealRequest r return ResponseEntity.ok(returnedMeal); } - // @GetMapping("/getPopularMeals") - // public String popularMeals() throws JsonMappingException, - // JsonProcessingException{ - // return mealManagementService.generatePopularMeals(); - // } - - // @GetMapping("/getSearchedMeals") - // public String searchedMeals(@RequestParam String query) throws - // JsonMappingException, JsonProcessingException { - // // Call the mealManagementService to search meals based on the query - // return mealManagementService.generateSearchedMeals(query); - // } } From a72c55e25b3271ff2e0d381f795fef7484f14a69 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 10:35:07 +0200 Subject: [PATCH 36/97] Create LogService.java LogService to handle creating storing and retrieving of entries --- .../java/fellowship/mealmaestro/services/LogService.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 backend/src/main/java/fellowship/mealmaestro/services/LogService.java diff --git a/backend/src/main/java/fellowship/mealmaestro/services/LogService.java b/backend/src/main/java/fellowship/mealmaestro/services/LogService.java new file mode 100644 index 00000000..aeb608d6 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/services/LogService.java @@ -0,0 +1,8 @@ +package fellowship.mealmaestro.services; + +import org.springframework.stereotype.Service; + +@Service +public class LogService { + +} From ffac33e8125015aacf2125f53b1443ff246d20d6 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 10:52:37 +0200 Subject: [PATCH 37/97] added findbymealNameFor logservice --- .../mealmaestro/services/LogService.java | 13 ++++++++++++- .../services/MealDatabaseService.java | 17 +++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/LogService.java b/backend/src/main/java/fellowship/mealmaestro/services/LogService.java index aeb608d6..5a885b19 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/LogService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/LogService.java @@ -1,8 +1,19 @@ package fellowship.mealmaestro.services; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.stereotype.Service; +import fellowship.mealmaestro.models.neo4j.MealModel; +import fellowship.mealmaestro.models.neo4j.UserModel; + @Service public class LogService { - + @Autowired + private UserService userService; + + public void logMeal(String token, MealModel meal, String entryType){ + UserModel user = userService.getUser(token); + + } } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/MealDatabaseService.java b/backend/src/main/java/fellowship/mealmaestro/services/MealDatabaseService.java index 515f6d0f..da4f2316 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/MealDatabaseService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/MealDatabaseService.java @@ -134,6 +134,23 @@ public Optional findMealTypeForUser(String type, String token) { return Optional.empty(); } + public Optional findMealForUser(String mealName, String token) { + String email = jwtService.extractUserEmail(token); + + UserModel user = userRepository.findByEmail(email).get(); + List randomMeals = mealRepository.get100RandomMeals(); + + // if meal with meal type is present in randomMeals, return it + for (MealModel meal : randomMeals) { + if (meal.getName().equals(mealName)) { + if (canMakeMeal(user.getPantry().getFoods(), meal.getIngredients())) { + return Optional.of(meal); + } + } + } + + return Optional.empty(); + } public boolean canMakeMeal(List pantryItems, String ingredients) { String[] ingredientsArray = ingredients.split(","); From be387d682fadd48b45c4e263931789409dea5fbe Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 11:06:20 +0200 Subject: [PATCH 38/97] logging of meals added --- .../mealmaestro/services/LogService.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/LogService.java b/backend/src/main/java/fellowship/mealmaestro/services/LogService.java index 5a885b19..0c6d8592 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/LogService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/LogService.java @@ -1,19 +1,36 @@ package fellowship.mealmaestro.services; +import java.io.Console; +import java.time.LocalDate; +import java.util.Optional; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.stereotype.Service; import fellowship.mealmaestro.models.neo4j.MealModel; import fellowship.mealmaestro.models.neo4j.UserModel; +import fellowship.mealmaestro.models.neo4j.relationships.HasLogEntry; +import fellowship.mealmaestro.repositories.neo4j.UserRepository; @Service public class LogService { @Autowired private UserService userService; + @Autowired + private MealDatabaseService mealDatabaseService; + @Autowired + private UserRepository userRepository; public void logMeal(String token, MealModel meal, String entryType){ UserModel user = userService.getUser(token); - + Optional dbMeal = mealDatabaseService.findMealForUser(meal.getName(), token); + if(dbMeal.isPresent()) + { + HasLogEntry entry = new HasLogEntry(dbMeal.get(),LocalDate.now(), entryType); + user.getEntries().add(entry); + userRepository.save(user); + System.out.println("LogEntry saved! ("+ meal.getName() +","+ entryType +")"); + } } } From bd5d27cd76114521efb98416d1d894bd24571059 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 11:08:12 +0200 Subject: [PATCH 39/97] regen logged --- .../mealmaestro/controllers/MealManagementController.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java index e25f6ddb..e44b72df 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java @@ -19,6 +19,7 @@ import fellowship.mealmaestro.models.RegenerateMealRequest; import fellowship.mealmaestro.models.neo4j.DateModel; import fellowship.mealmaestro.models.neo4j.MealModel; +import fellowship.mealmaestro.services.LogService; import fellowship.mealmaestro.services.MealDatabaseService; import fellowship.mealmaestro.services.MealManagementService; import jakarta.validation.Valid; @@ -29,6 +30,8 @@ public class MealManagementController { private MealManagementService mealManagementService; @Autowired private MealDatabaseService mealDatabaseService; + @Autowired + private LogService logService; @PostMapping("/getMealPlanForDay") public ResponseEntity> dailyMeals(@Valid @RequestBody DateModel request, @@ -121,7 +124,7 @@ public ResponseEntity regenerate(@RequestBody RegenerateMealRequest r throws JsonMappingException, JsonProcessingException { token = token.substring(7); - + logService.logMeal(token, request.getMeal(), "regenerate"); // Try find an appropriate meal in the database Optional replacementMeal = mealDatabaseService.findMealTypeForUser(request.getMeal().getType(), token); From 9d10ca9b2be19b0ca43918c789982b865fb089e7 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 11:46:27 +0200 Subject: [PATCH 40/97] Logging Functional --- .../controllers/MealManagementController.java | 4 ++- .../neo4j/relationships/HasLogEntry.java | 3 ++ .../mealmaestro/services/LogService.java | 28 +++++++++++++------ 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java index e44b72df..d00c4619 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java @@ -123,8 +123,10 @@ public ResponseEntity regenerate(@RequestBody RegenerateMealRequest r @RequestHeader("Authorization") String token) throws JsonMappingException, JsonProcessingException { - token = token.substring(7); logService.logMeal(token, request.getMeal(), "regenerate"); + + token = token.substring(7); + // Try find an appropriate meal in the database Optional replacementMeal = mealDatabaseService.findMealTypeForUser(request.getMeal().getType(), token); diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasLogEntry.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasLogEntry.java index e7afe458..16548761 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasLogEntry.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasLogEntry.java @@ -22,6 +22,9 @@ public class HasLogEntry { private String entryType; + public HasLogEntry() { + } + public HasLogEntry(MealModel meal, LocalDate date, String entryType) { this.meal = meal; this.date = date; diff --git a/backend/src/main/java/fellowship/mealmaestro/services/LogService.java b/backend/src/main/java/fellowship/mealmaestro/services/LogService.java index 0c6d8592..74a9b735 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/LogService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/LogService.java @@ -11,6 +11,7 @@ import fellowship.mealmaestro.models.neo4j.MealModel; import fellowship.mealmaestro.models.neo4j.UserModel; import fellowship.mealmaestro.models.neo4j.relationships.HasLogEntry; +import fellowship.mealmaestro.models.neo4j.relationships.HasMeal; import fellowship.mealmaestro.repositories.neo4j.UserRepository; @Service @@ -19,18 +20,29 @@ public class LogService { private UserService userService; @Autowired private MealDatabaseService mealDatabaseService; - @Autowired + @Autowired private UserRepository userRepository; - public void logMeal(String token, MealModel meal, String entryType){ + public void logMeal(String token, MealModel meal, String entryType) { UserModel user = userService.getUser(token); - Optional dbMeal = mealDatabaseService.findMealForUser(meal.getName(), token); - if(dbMeal.isPresent()) + MealModel dbMeal = null; + for(HasMeal m : user.getMeals()) + { + if(m.getMeal().getName().equals(meal.getName())) + { + dbMeal = m.getMeal(); + } + } + if(dbMeal == null) { - HasLogEntry entry = new HasLogEntry(dbMeal.get(),LocalDate.now(), entryType); - user.getEntries().add(entry); - userRepository.save(user); - System.out.println("LogEntry saved! ("+ meal.getName() +","+ entryType +")"); + System.out.println("logging failed"); + return; } + HasLogEntry entry = new HasLogEntry(dbMeal, LocalDate.now(), entryType); + user.getEntries().add(entry); + userRepository.save(user); + System.out.println("LogEntry saved! (" + meal.getName() + "," + entryType + ")"); + } + } From 173c8aca9995445f864fe4959716109397016cdd Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 11:48:20 +0200 Subject: [PATCH 41/97] cleanup --- .../java/fellowship/mealmaestro/services/LogService.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/LogService.java b/backend/src/main/java/fellowship/mealmaestro/services/LogService.java index 74a9b735..b7d4169b 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/LogService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/LogService.java @@ -1,11 +1,7 @@ package fellowship.mealmaestro.services; -import java.io.Console; import java.time.LocalDate; -import java.util.Optional; - import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.stereotype.Service; import fellowship.mealmaestro.models.neo4j.MealModel; @@ -19,8 +15,6 @@ public class LogService { @Autowired private UserService userService; @Autowired - private MealDatabaseService mealDatabaseService; - @Autowired private UserRepository userRepository; public void logMeal(String token, MealModel meal, String entryType) { From 9a3b7e27a85ead58868b2445ea41db9a153b5321 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 12:16:12 +0200 Subject: [PATCH 42/97] poll user list and set if processed. --- .../models/neo4j/relationships/HasLogEntry.java | 14 ++++++++++++++ .../repositories/neo4j/UserRepository.java | 4 ++++ .../mealmaestro/services/HydrationService.java | 17 ++++++++++++++++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasLogEntry.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasLogEntry.java index 16548761..c5315bad 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasLogEntry.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/relationships/HasLogEntry.java @@ -22,6 +22,20 @@ public class HasLogEntry { private String entryType; + private boolean processed; + + public boolean isProcessed() { + return this.processed; + } + + public boolean getProcessed() { + return this.processed; + } + + public void setProcessed(boolean processed) { + this.processed = processed; + } + public HasLogEntry() { } diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/UserRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/UserRepository.java index 63492a05..34f983bf 100644 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/UserRepository.java +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/UserRepository.java @@ -1,5 +1,6 @@ package fellowship.mealmaestro.repositories.neo4j; +import java.util.List; import java.util.Optional; import org.springframework.data.neo4j.repository.Neo4jRepository; @@ -13,4 +14,7 @@ public interface UserRepository extends Neo4jRepository { @Query("MATCH (n0:User {email: $email}) SET n0.name = $name RETURN n0") UserModel updateUser(String email, String username); + + @Query("MATCH (u:User)-[r:HAS_LOG_ENTRY]->(logEntry) WHERE NOT EXISTS(r.processed) OR NOT r.processed RETURN DISTINCT u") + List findUsersWithNewLogEntries(); } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java index fdd54976..15115ff6 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java @@ -1,8 +1,23 @@ package fellowship.mealmaestro.services; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import fellowship.mealmaestro.models.neo4j.UserModel; +import fellowship.mealmaestro.repositories.neo4j.UserRepository; +import java.util.List; + @Service public class HydrationService { - + @Autowired + private UserRepository userRepository; + + @Scheduled(fixedRate = 60 * 1000) + public void pollLogs() { + List userList = userRepository.findUsersWithNewLogEntries(); + + + } + } From 19771b38042db32c1f8953c0692c5c92f2a5f6f6 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 13:10:21 +0200 Subject: [PATCH 43/97] Tweaked poll rate --- .../fellowship/mealmaestro/services/HydrationService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java index 15115ff6..2a008047 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java @@ -13,10 +13,10 @@ public class HydrationService { @Autowired private UserRepository userRepository; - @Scheduled(fixedRate = 60 * 1000) + @Scheduled(fixedRate = 20 * 60 * 1000) public void pollLogs() { List userList = userRepository.findUsersWithNewLogEntries(); - + } From 8d1c23e01acea3496d955cf1dd4fb46f07c27f70 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 13:10:26 +0200 Subject: [PATCH 44/97] Create ViewModel.java --- .../mealmaestro/models/ViewModel.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 backend/src/main/java/fellowship/mealmaestro/models/ViewModel.java diff --git a/backend/src/main/java/fellowship/mealmaestro/models/ViewModel.java b/backend/src/main/java/fellowship/mealmaestro/models/ViewModel.java new file mode 100644 index 00000000..17484b16 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/models/ViewModel.java @@ -0,0 +1,27 @@ +package fellowship.mealmaestro.models; + +import java.util.HashMap; + +import org.springframework.data.neo4j.core.schema.Node; + +@Node("View") +public class ViewModel { + private HashMap ScoreMap; + + public ViewModel() { + } + + public ViewModel(HashMap ScoreMap) { + this.ScoreMap = ScoreMap; + } + + public HashMap getScoreMap() { + return this.ScoreMap; + } + + public void setScoreMap(HashMap ScoreMap) { + this.ScoreMap = ScoreMap; + } + + +} From 55eb4ff310687ab2bb1e48e961e91daece258a40 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 13:11:05 +0200 Subject: [PATCH 45/97] moved to correct folder --- .../fellowship/mealmaestro/models/{ => neo4j}/ViewModel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename backend/src/main/java/fellowship/mealmaestro/models/{ => neo4j}/ViewModel.java (91%) diff --git a/backend/src/main/java/fellowship/mealmaestro/models/ViewModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java similarity index 91% rename from backend/src/main/java/fellowship/mealmaestro/models/ViewModel.java rename to backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java index 17484b16..0c1e90f7 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/ViewModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java @@ -1,4 +1,4 @@ -package fellowship.mealmaestro.models; +package fellowship.mealmaestro.models.neo4j; import java.util.HashMap; From 7b4b5d94f405e1440183640b5782d084efb28e3c Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 13:18:41 +0200 Subject: [PATCH 46/97] updated models to support view --- .../mealmaestro/models/neo4j/UserModel.java | 27 +++++++++++++++++++ .../mealmaestro/models/neo4j/ViewModel.java | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/UserModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/UserModel.java index a390a0f2..119e3527 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/UserModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/UserModel.java @@ -58,6 +58,33 @@ public class UserModel implements UserDetails { @Relationship(type = "HAS_LOG_ENTRY") private List entries; + @Relationship(type = "HAS_VIEW", direction = Relationship.Direction.OUTGOING) + private ViewModel view; + + public Long getVersion() { + return this.version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public List getEntries() { + return this.entries; + } + + public void setEntries(List entries) { + this.entries = entries; + } + + public ViewModel getView() { + return this.view; + } + + public void setView(ViewModel view) { + this.view = view; + } + public UserModel() { this.authorityRole = AuthorityRoleModel.USER; } diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java index 0c1e90f7..cde88f1e 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java @@ -1,11 +1,11 @@ package fellowship.mealmaestro.models.neo4j; import java.util.HashMap; - import org.springframework.data.neo4j.core.schema.Node; @Node("View") public class ViewModel { + private HashMap ScoreMap; public ViewModel() { From 84eb71cd71fe2e557d15ac8f5c4d957beb0adeae Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 13:19:47 +0200 Subject: [PATCH 47/97] ensure hashmap exists --- .../java/fellowship/mealmaestro/models/neo4j/ViewModel.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java index cde88f1e..dbf505f6 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java @@ -5,8 +5,8 @@ @Node("View") public class ViewModel { - - private HashMap ScoreMap; + + private HashMap ScoreMap = new HashMap<>(); public ViewModel() { } From 7f688fea6ebd2585abc8be8379a1a8b9ad099ad3 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 13:31:29 +0200 Subject: [PATCH 48/97] added ability to find a users unprocessed log entries --- .../repositories/neo4j/UserRepository.java | 6 +++++- .../mealmaestro/services/LogService.java | 20 ++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/UserRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/UserRepository.java index 34f983bf..f7dfa887 100644 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/UserRepository.java +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/UserRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.neo4j.repository.query.Query; import fellowship.mealmaestro.models.neo4j.UserModel; +import fellowship.mealmaestro.models.neo4j.relationships.HasLogEntry; public interface UserRepository extends Neo4jRepository { @@ -14,7 +15,10 @@ public interface UserRepository extends Neo4jRepository { @Query("MATCH (n0:User {email: $email}) SET n0.name = $name RETURN n0") UserModel updateUser(String email, String username); - + @Query("MATCH (u:User)-[r:HAS_LOG_ENTRY]->(logEntry) WHERE NOT EXISTS(r.processed) OR NOT r.processed RETURN DISTINCT u") List findUsersWithNewLogEntries(); + + @Query("MATCH (user:User {id: $user.id})-[:HAS_LOG_ENTRY]->(logEntry:HasLogEntry) WHERE NOT logEntry.processed RETURN logEntry") + List findUnprocessedLogEntriesForUser(UserModel user); } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/LogService.java b/backend/src/main/java/fellowship/mealmaestro/services/LogService.java index b7d4169b..30d5a565 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/LogService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/LogService.java @@ -1,6 +1,8 @@ package fellowship.mealmaestro.services; import java.time.LocalDate; +import java.util.List; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -20,18 +22,15 @@ public class LogService { public void logMeal(String token, MealModel meal, String entryType) { UserModel user = userService.getUser(token); MealModel dbMeal = null; - for(HasMeal m : user.getMeals()) - { - if(m.getMeal().getName().equals(meal.getName())) - { + for (HasMeal m : user.getMeals()) { + if (m.getMeal().getName().equals(meal.getName())) { dbMeal = m.getMeal(); } } - if(dbMeal == null) - { - System.out.println("logging failed"); - return; - } + if (dbMeal == null) { + System.out.println("logging failed"); + return; + } HasLogEntry entry = new HasLogEntry(dbMeal, LocalDate.now(), entryType); user.getEntries().add(entry); userRepository.save(user); @@ -39,4 +38,7 @@ public void logMeal(String token, MealModel meal, String entryType) { } + public List findUnprocessedLogEntriesForUser(UserModel user) { + return userRepository.findUnprocessedLogEntriesForUser(user); + } } From fa0bedf7cf45cc535b1cec82da50c67bfdd2d75e Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 13:46:26 +0200 Subject: [PATCH 49/97] update view model and efficient processing --- .../mealmaestro/models/neo4j/ViewModel.java | 21 ++++++++++++++----- .../services/HydrationService.java | 10 ++++++++- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java index dbf505f6..a5989e0f 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java @@ -3,25 +3,36 @@ import java.util.HashMap; import org.springframework.data.neo4j.core.schema.Node; +import lombok.Data; + @Node("View") public class ViewModel { + @Data + private class StatValues { + public Double Score; + public Double minScore; + public Double maxScore; + } - private HashMap ScoreMap = new HashMap<>(); + private HashMap ScoreMap = new HashMap<>(); public ViewModel() { } - public ViewModel(HashMap ScoreMap) { + public ViewModel(HashMap ScoreMap) { this.ScoreMap = ScoreMap; } - public HashMap getScoreMap() { + public HashMap getScoreMap() { return this.ScoreMap; } - public void setScoreMap(HashMap ScoreMap) { + public void setScoreMap(HashMap ScoreMap) { this.ScoreMap = ScoreMap; } + + public void updateScore(String ingredient, Double Score){ + + } - } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java index 2a008047..0543113a 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java @@ -5,6 +5,7 @@ import org.springframework.stereotype.Service; import fellowship.mealmaestro.models.neo4j.UserModel; +import fellowship.mealmaestro.models.neo4j.relationships.HasLogEntry; import fellowship.mealmaestro.repositories.neo4j.UserRepository; import java.util.List; @@ -16,7 +17,14 @@ public class HydrationService { @Scheduled(fixedRate = 20 * 60 * 1000) public void pollLogs() { List userList = userRepository.findUsersWithNewLogEntries(); - + //per user + for(UserModel user : userList){ + List logEntries = userRepository.findUnprocessedLogEntriesForUser(user); + for(HasLogEntry entry : logEntries) + { + + } + } } From 79c8ba95ca0d1ce5b87fe22dee56bd308aad9e0a Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 15:27:22 +0200 Subject: [PATCH 50/97] data procesing and normalisation for hashmap --- .../mealmaestro/models/neo4j/ViewModel.java | 59 +++++++++++++++---- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java index a5989e0f..eb500dc6 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java @@ -1,6 +1,9 @@ package fellowship.mealmaestro.models.neo4j; import java.util.HashMap; + +import javax.script.ScriptEngine; + import org.springframework.data.neo4j.core.schema.Node; import lombok.Data; @@ -8,31 +11,65 @@ @Node("View") public class ViewModel { @Data - private class StatValues { - public Double Score; - public Double minScore; - public Double maxScore; + private class Scores { + public Double score; + public Double nScore; } - - private HashMap ScoreMap = new HashMap<>(); + private HashMap ScoreMap = new HashMap<>(); + private Double max; + private Double min; public ViewModel() { } - public ViewModel(HashMap ScoreMap) { + public ViewModel(HashMap ScoreMap) { this.ScoreMap = ScoreMap; } - public HashMap getScoreMap() { + public HashMap getScoreMap() { return this.ScoreMap; } - public void setScoreMap(HashMap ScoreMap) { + public void setScoreMap(HashMap ScoreMap) { this.ScoreMap = ScoreMap; } - - public void updateScore(String ingredient, Double Score){ + public void updateScore(String ingredient, Double Score) { + if (ScoreMap.containsKey(ingredient)) { + Boolean changed = false; + Scores scores = ScoreMap.get(ingredient); + scores.score += Score; + if (Score > max) { + max = Score; + changed = true; + } + + if (Score < min) { + min = Score; + changed = true; + } + scores.nScore = normalise(scores.score); + if(changed){ + normalise(); + } + + } else { + Scores scores = new Scores(); + scores.score = Score; + scores.nScore = 0.0; + ScoreMap.put(ingredient, scores); + } + } + + public Double normalise(Double Score) { + return 2 * ((Score - min) / (max - min)) - 1; + + } + public void normalise() { + // return 2 * ((Score - min) / (max - min)) - 1; + for(Scores scores : ScoreMap.values()){ + scores.nScore = 2 * ((scores.score - min) / (max - min)) - 1; + } } } From 79739c0bd64e3fe5dcac97763c2c2e595e84b4d9 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 15:27:47 +0200 Subject: [PATCH 51/97] cleanup --- .../mealmaestro/models/neo4j/ViewModel.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java index eb500dc6..def60301 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java @@ -1,11 +1,7 @@ package fellowship.mealmaestro.models.neo4j; import java.util.HashMap; - -import javax.script.ScriptEngine; - import org.springframework.data.neo4j.core.schema.Node; - import lombok.Data; @Node("View") @@ -15,6 +11,7 @@ private class Scores { public Double score; public Double nScore; } + private HashMap ScoreMap = new HashMap<>(); private Double max; private Double min; @@ -49,7 +46,7 @@ public void updateScore(String ingredient, Double Score) { changed = true; } scores.nScore = normalise(scores.score); - if(changed){ + if (changed) { normalise(); } @@ -65,11 +62,12 @@ public Double normalise(Double Score) { return 2 * ((Score - min) / (max - min)) - 1; } + public void normalise() { - // return 2 * ((Score - min) / (max - min)) - 1; - for(Scores scores : ScoreMap.values()){ + // return 2 * ((Score - min) / (max - min)) - 1; + for (Scores scores : ScoreMap.values()) { scores.nScore = 2 * ((scores.score - min) / (max - min)) - 1; - } + } } } From 4d403de06db6ded5647c34ac40053b1f19262152 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 15:31:54 +0200 Subject: [PATCH 52/97] flesh out hydration system, i take break now --- .../services/HydrationService.java | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java index 0543113a..387fa57d 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java @@ -5,6 +5,7 @@ import org.springframework.stereotype.Service; import fellowship.mealmaestro.models.neo4j.UserModel; +import fellowship.mealmaestro.models.neo4j.ViewModel; import fellowship.mealmaestro.models.neo4j.relationships.HasLogEntry; import fellowship.mealmaestro.repositories.neo4j.UserRepository; import java.util.List; @@ -20,12 +21,29 @@ public void pollLogs() { //per user for(UserModel user : userList){ List logEntries = userRepository.findUnprocessedLogEntriesForUser(user); + ViewModel viewModel = user.getView(); + for(HasLogEntry entry : logEntries) { - + String ingredientString = entry.getMeal().getIngredients(); + //trim ingredient list + + //convert to List + + //Scores ++ * multiplier + + //update view model + + //set processed + entry.setProcessed(true); } } } - + // helper functions to be done + //trim + + //convert + + //scores } From 8a7233262510fdde0b53d1f22242b4e08e90f2d0 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 16:51:26 +0200 Subject: [PATCH 53/97] View Creation --- .../services/HydrationService.java | 68 +++++++++++++++---- 1 file changed, 54 insertions(+), 14 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java index 387fa57d..aa30cc9b 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java @@ -8,6 +8,8 @@ import fellowship.mealmaestro.models.neo4j.ViewModel; import fellowship.mealmaestro.models.neo4j.relationships.HasLogEntry; import fellowship.mealmaestro.repositories.neo4j.UserRepository; + +import java.util.Arrays; import java.util.List; @Service @@ -18,32 +20,70 @@ public class HydrationService { @Scheduled(fixedRate = 20 * 60 * 1000) public void pollLogs() { List userList = userRepository.findUsersWithNewLogEntries(); - //per user - for(UserModel user : userList){ + // per user + for (UserModel user : userList) { List logEntries = userRepository.findUnprocessedLogEntriesForUser(user); ViewModel viewModel = user.getView(); - for(HasLogEntry entry : logEntries) - { - String ingredientString = entry.getMeal().getIngredients(); - //trim ingredient list - - //convert to List + for (HasLogEntry entry : logEntries) { - //Scores ++ * multiplier + String ingredientString = entry.getMeal().getIngredients(); + // trim ingredient list + ingredientString = trimCharacters(ingredientString); + // convert to List + List ingredientList = parseCommaSeparatedString(ingredientString); + // Scores ++ * multiplier + Double S_MULTIPLIER = getScoreValue(entry.getEntryType()); - //update view model + // update view model + for (String ingredient : ingredientList) { + viewModel.updateScore(ingredient, S_MULTIPLIER); + } - //set processed + // set processed entry.setProcessed(true); } + + user.setView(viewModel); + userRepository.save(user); } } + // helper functions to be done - //trim + // trim + private static String trimCharacters(String input) { + String regex = "[0-9\\s]+"; + String result = input.replaceAll(regex, ""); + return result; + } - //convert + // convert + private static List parseCommaSeparatedString(String input) { + String[] elements = input.split(","); + List result = Arrays.asList(elements); - //scores + return result; + } + + // scores + private static Double getScoreValue(String entryType) { + switch (entryType.toLowerCase()) { + case "regenerated": + return -0.9; + + case "like": + return 1.0; + + case "dislike": + return -0.5; + + case "save": + return 0.7; + + default: + return 0.5; + + } + } } From c6a3c3a7e9a3f90e03ff8244c000e8b62cfcd63a Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 16:58:32 +0200 Subject: [PATCH 54/97] Delete TriggerService.java --- .../fellowship/mealmaestro/services/TriggerService.java | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 backend/src/main/java/fellowship/mealmaestro/services/TriggerService.java diff --git a/backend/src/main/java/fellowship/mealmaestro/services/TriggerService.java b/backend/src/main/java/fellowship/mealmaestro/services/TriggerService.java deleted file mode 100644 index 4a2e7f2f..00000000 --- a/backend/src/main/java/fellowship/mealmaestro/services/TriggerService.java +++ /dev/null @@ -1,6 +0,0 @@ -package fellowship.mealmaestro.services; - -public class TriggerService { - - -} From aeaefec0dc7d931291b092f32941484246227fa2 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 16:59:37 +0200 Subject: [PATCH 55/97] done for the day i cant think --- .../mealmaestro/services/RecommendationService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java index de60a33e..e0ed7af4 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java @@ -1,5 +1,10 @@ package fellowship.mealmaestro.services; +import org.springframework.beans.factory.annotation.Autowired; + +import fellowship.mealmaestro.repositories.neo4j.UserRepository; + public class RecommendationService { - + @Autowired + private UserRepository userRepository; } From fd321446b21906d971a151d1c87170d0e6213483 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Thu, 21 Sep 2023 17:01:52 +0200 Subject: [PATCH 56/97] i lied, save request mapped, now im done --- .../mealmaestro/controllers/RecipeBookController.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/RecipeBookController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/RecipeBookController.java index 69c64210..131eb55e 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/RecipeBookController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/RecipeBookController.java @@ -1,9 +1,11 @@ package fellowship.mealmaestro.controllers; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import fellowship.mealmaestro.models.neo4j.MealModel; +import fellowship.mealmaestro.services.LogService; import fellowship.mealmaestro.services.RecipeBookService; import jakarta.validation.Valid; @@ -11,7 +13,8 @@ @RestController public class RecipeBookController { - + @Autowired + private LogService logService; private final RecipeBookService recipeBookService; public RecipeBookController(RecipeBookService recipeBookService) { @@ -25,6 +28,8 @@ public ResponseEntity addRecipe(@Valid @RequestBody MealModel request return ResponseEntity.badRequest().build(); } + logService.logMeal(token, request, "save"); + String authToken = token.substring(7); return ResponseEntity.ok(recipeBookService.addRecipe(request, authToken)); } From ddcfba98771c9ccda9351d622fd61aafe08ac332 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Fri, 22 Sep 2023 10:43:34 +0200 Subject: [PATCH 57/97] fixed build error --- .../mealmaestro/models/neo4j/ViewModel.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java index def60301..45edbd9d 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java @@ -1,11 +1,22 @@ package fellowship.mealmaestro.models.neo4j; import java.util.HashMap; + +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import lombok.Data; - +import lombok.Getter; +import lombok.Setter; +@Getter +@Setter @Node("View") public class ViewModel { + + @Id + @GeneratedValue + private Long id; + @Data private class Scores { public Double score; From 9a2f03d38101a4c004b3dbc056a1d8fdde4e09f6 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Fri, 22 Sep 2023 10:55:58 +0200 Subject: [PATCH 58/97] score tweaks --- .../fellowship/mealmaestro/services/HydrationService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java index aa30cc9b..f6e11c30 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java @@ -70,13 +70,13 @@ private static List parseCommaSeparatedString(String input) { private static Double getScoreValue(String entryType) { switch (entryType.toLowerCase()) { case "regenerated": - return -0.9; + return -0.4; case "like": return 1.0; case "dislike": - return -0.5; + return -0.7; case "save": return 0.7; From e8e27adbdb7ddb0e23b9e5af073d0dffbe4ac3c5 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Fri, 22 Sep 2023 11:07:09 +0200 Subject: [PATCH 59/97] enabled scheduliung and lowered polling rate --- .../java/fellowship/mealmaestro/MealmaestroApplication.java | 3 ++- .../java/fellowship/mealmaestro/services/HydrationService.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/MealmaestroApplication.java b/backend/src/main/java/fellowship/mealmaestro/MealmaestroApplication.java index 6f094f5f..730c79b9 100644 --- a/backend/src/main/java/fellowship/mealmaestro/MealmaestroApplication.java +++ b/backend/src/main/java/fellowship/mealmaestro/MealmaestroApplication.java @@ -3,10 +3,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; import com.fasterxml.jackson.core.JsonProcessingException; - +@EnableScheduling @SpringBootApplication public class MealmaestroApplication { diff --git a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java index f6e11c30..1062c4f6 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java @@ -17,7 +17,7 @@ public class HydrationService { @Autowired private UserRepository userRepository; - @Scheduled(fixedRate = 20 * 60 * 1000) + @Scheduled(fixedRate = 1 * 60 * 1000) public void pollLogs() { List userList = userRepository.findUsersWithNewLogEntries(); // per user From 6da0b3d4b2ba0cd390b0b40ea6d881dcd2d6fb50 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Fri, 22 Sep 2023 12:20:01 +0200 Subject: [PATCH 60/97] many fixes --- .../mealmaestro/MealmaestroApplication.java | 2 +- .../mealmaestro/models/neo4j/UserModel.java | 2 +- .../mealmaestro/models/neo4j/ViewModel.java | 42 ++++++++----------- .../repositories/neo4j/UserRepository.java | 4 +- .../services/HydrationService.java | 19 +++++---- .../mealmaestro/services/LogService.java | 3 +- 6 files changed, 35 insertions(+), 37 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/MealmaestroApplication.java b/backend/src/main/java/fellowship/mealmaestro/MealmaestroApplication.java index 730c79b9..a8e02c5d 100644 --- a/backend/src/main/java/fellowship/mealmaestro/MealmaestroApplication.java +++ b/backend/src/main/java/fellowship/mealmaestro/MealmaestroApplication.java @@ -7,8 +7,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; -@EnableScheduling @SpringBootApplication +@EnableScheduling public class MealmaestroApplication { public static void main(String[] args) throws JsonProcessingException { diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/UserModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/UserModel.java index 119e3527..f8612a33 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/UserModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/UserModel.java @@ -55,7 +55,7 @@ public class UserModel implements UserDetails { @Relationship(type = "HAS_MEAL") private List meals; - @Relationship(type = "HAS_LOG_ENTRY") + @Relationship(type = "HAS_LOG_ENTRY", direction = Relationship.Direction.OUTGOING) private List entries; @Relationship(type = "HAS_VIEW", direction = Relationship.Direction.OUTGOING) diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java index 45edbd9d..3f87720f 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java @@ -6,10 +6,7 @@ import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import lombok.Data; -import lombok.Getter; -import lombok.Setter; -@Getter -@Setter + @Node("View") public class ViewModel { @@ -17,55 +14,50 @@ public class ViewModel { @GeneratedValue private Long id; - @Data - private class Scores { - public Double score; - public Double nScore; - } - - private HashMap ScoreMap = new HashMap<>(); + private HashMap ScoreMap = new HashMap<>(); + private HashMap nScoreMap = new HashMap<>(); private Double max; private Double min; public ViewModel() { } - public ViewModel(HashMap ScoreMap) { + public ViewModel(HashMap ScoreMap) { this.ScoreMap = ScoreMap; } - public HashMap getScoreMap() { + public HashMap getScoreMap() { return this.ScoreMap; } - public void setScoreMap(HashMap ScoreMap) { + public void setScoreMap(HashMap ScoreMap) { this.ScoreMap = ScoreMap; } public void updateScore(String ingredient, Double Score) { if (ScoreMap.containsKey(ingredient)) { Boolean changed = false; - Scores scores = ScoreMap.get(ingredient); - scores.score += Score; - if (Score > max) { + Double score = ScoreMap.get(ingredient); + Double nScore = nScoreMap.get(ingredient); + score += Score; + if (Score > max || max == null) { max = Score; changed = true; } - if (Score < min) { + if (Score < min || min == null) { min = Score; changed = true; } - scores.nScore = normalise(scores.score); + nScore = normalise(Score); if (changed) { normalise(); } } else { - Scores scores = new Scores(); - scores.score = Score; - scores.nScore = 0.0; - ScoreMap.put(ingredient, scores); + + ScoreMap.put(ingredient, Score); + nScoreMap.put(ingredient,normalise(Score)); } } @@ -76,8 +68,8 @@ public Double normalise(Double Score) { public void normalise() { // return 2 * ((Score - min) / (max - min)) - 1; - for (Scores scores : ScoreMap.values()) { - scores.nScore = 2 * ((scores.score - min) / (max - min)) - 1; + for (String ingredient : ScoreMap.keySet()) { + nScoreMap.put(ingredient, 2 * ((ScoreMap.get(ingredient) - min) / (max - min)) - 1) ; } } diff --git a/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/UserRepository.java b/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/UserRepository.java index f7dfa887..67ee04d8 100644 --- a/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/UserRepository.java +++ b/backend/src/main/java/fellowship/mealmaestro/repositories/neo4j/UserRepository.java @@ -16,9 +16,9 @@ public interface UserRepository extends Neo4jRepository { @Query("MATCH (n0:User {email: $email}) SET n0.name = $name RETURN n0") UserModel updateUser(String email, String username); - @Query("MATCH (u:User)-[r:HAS_LOG_ENTRY]->(logEntry) WHERE NOT EXISTS(r.processed) OR NOT r.processed RETURN DISTINCT u") + @Query("MATCH (u:User)-[r:HAS_LOG_ENTRY]->(logEntry) WHERE r.processed IS NULL OR NOT r.processed RETURN DISTINCT u") List findUsersWithNewLogEntries(); - @Query("MATCH (user:User {id: $user.id})-[:HAS_LOG_ENTRY]->(logEntry:HasLogEntry) WHERE NOT logEntry.processed RETURN logEntry") + @Query("MATCH (user:User {id: $user.id})-[:HAS_LOG_ENTRY]->(logEntry) WHERE NOT logEntry.processed RETURN logEntry") List findUnprocessedLogEntriesForUser(UserModel user); } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java index 1062c4f6..0eb95306 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java @@ -3,6 +3,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import fellowship.mealmaestro.models.neo4j.UserModel; import fellowship.mealmaestro.models.neo4j.ViewModel; @@ -16,15 +17,19 @@ public class HydrationService { @Autowired private UserRepository userRepository; - + @Transactional @Scheduled(fixedRate = 1 * 60 * 1000) public void pollLogs() { List userList = userRepository.findUsersWithNewLogEntries(); // per user - for (UserModel user : userList) { + for (UserModel nuser : userList) { + UserModel user = userRepository.findByEmail(nuser.getEmail()).get(); List logEntries = userRepository.findUnprocessedLogEntriesForUser(user); ViewModel viewModel = user.getView(); - + if(viewModel == null) + { + viewModel = new ViewModel(); + } for (HasLogEntry entry : logEntries) { String ingredientString = entry.getMeal().getIngredients(); @@ -42,8 +47,9 @@ public void pollLogs() { // set processed entry.setProcessed(true); - } + } + user.setEntries(logEntries); user.setView(viewModel); userRepository.save(user); } @@ -61,9 +67,8 @@ private static String trimCharacters(String input) { // convert private static List parseCommaSeparatedString(String input) { String[] elements = input.split(","); - List result = Arrays.asList(elements); - - return result; + + return Arrays.asList(elements); } // scores diff --git a/backend/src/main/java/fellowship/mealmaestro/services/LogService.java b/backend/src/main/java/fellowship/mealmaestro/services/LogService.java index 30d5a565..d0bd4371 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/LogService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/LogService.java @@ -5,6 +5,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import fellowship.mealmaestro.models.neo4j.MealModel; import fellowship.mealmaestro.models.neo4j.UserModel; @@ -18,7 +19,7 @@ public class LogService { private UserService userService; @Autowired private UserRepository userRepository; - + @Transactional public void logMeal(String token, MealModel meal, String entryType) { UserModel user = userService.getUser(token); MealModel dbMeal = null; From 028608422ac7e593cf646497e4c55e9e60286bbb Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Fri, 22 Sep 2023 13:17:28 +0200 Subject: [PATCH 61/97] working now, lottta fixes --- .../mealmaestro/models/neo4j/UserModel.java | 4 +- .../mealmaestro/models/neo4j/ViewModel.java | 74 ++++++++++++++----- .../services/HydrationService.java | 7 +- 3 files changed, 60 insertions(+), 25 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/UserModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/UserModel.java index f8612a33..83a25ec5 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/UserModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/UserModel.java @@ -55,10 +55,10 @@ public class UserModel implements UserDetails { @Relationship(type = "HAS_MEAL") private List meals; - @Relationship(type = "HAS_LOG_ENTRY", direction = Relationship.Direction.OUTGOING) + @Relationship(type = "HAS_LOG_ENTRY") private List entries; - @Relationship(type = "HAS_VIEW", direction = Relationship.Direction.OUTGOING) + @Relationship(type = "HAS_VIEW") private ViewModel view; public Long getVersion() { diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java index 3f87720f..b07d1042 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java @@ -5,7 +5,6 @@ import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; -import lombok.Data; @Node("View") public class ViewModel { @@ -14,39 +13,44 @@ public class ViewModel { @GeneratedValue private Long id; - private HashMap ScoreMap = new HashMap<>(); - private HashMap nScoreMap = new HashMap<>(); - private Double max; - private Double min; + String[] Keys; + Double[] scoreValues; + Double[] nScoreValues; + + private Double max =0.0; + private Double min =0.0; public ViewModel() { } - public ViewModel(HashMap ScoreMap) { - this.ScoreMap = ScoreMap; - } public HashMap getScoreMap() { - return this.ScoreMap; + return arraysToHashMap(Keys, nScoreValues); } - public void setScoreMap(HashMap ScoreMap) { - this.ScoreMap = ScoreMap; - } public void updateScore(String ingredient, Double Score) { + HashMap ScoreMap = new HashMap<>(); + HashMap nScoreMap = new HashMap<>(); + + if(Keys != null){ + ScoreMap = arraysToHashMap(Keys, scoreValues); + nScoreMap = arraysToHashMap(Keys, nScoreValues); + } + + if (ScoreMap.containsKey(ingredient)) { Boolean changed = false; Double score = ScoreMap.get(ingredient); Double nScore = nScoreMap.get(ingredient); score += Score; - if (Score > max || max == null) { - max = Score; + if (score > max || max == null) { + max = score; changed = true; } - if (Score < min || min == null) { - min = Score; + if (score < min || min == null) { + min = score; changed = true; } nScore = normalise(Score); @@ -54,11 +58,17 @@ public void updateScore(String ingredient, Double Score) { normalise(); } + ScoreMap.put(ingredient, score); + nScoreMap.put(ingredient, nScore); + } else { - + ScoreMap.put(ingredient, Score); - nScoreMap.put(ingredient,normalise(Score)); + nScoreMap.put(ingredient, normalise(Score)); } + Keys = hashMapKeysToArray(nScoreMap); + scoreValues = hashMapValuesToArray(ScoreMap); + nScoreValues = hashMapValuesToArray(nScoreMap); } public Double normalise(Double Score) { @@ -68,9 +78,35 @@ public Double normalise(Double Score) { public void normalise() { // return 2 * ((Score - min) / (max - min)) - 1; + HashMap ScoreMap = new HashMap<>(); + HashMap nScoreMap = new HashMap<>(); for (String ingredient : ScoreMap.keySet()) { - nScoreMap.put(ingredient, 2 * ((ScoreMap.get(ingredient) - min) / (max - min)) - 1) ; + nScoreMap.put(ingredient, 2 * ((ScoreMap.get(ingredient) - min) / (max - min)) - 1); + } + Keys = hashMapKeysToArray(nScoreMap); + scoreValues = hashMapValuesToArray(ScoreMap); + nScoreValues = hashMapValuesToArray(nScoreMap); + } + + public static Double[] hashMapValuesToArray(HashMap hashMap) { + return hashMap.values().toArray(new Double[0]); + } + + public static String[] hashMapKeysToArray(HashMap hashMap) { + return hashMap.keySet().toArray(new String[0]); + } + + public static HashMap arraysToHashMap(String[] keys, Double[] values) { + if (keys.length != values.length) { + throw new IllegalArgumentException("Keys and values arrays must have the same length."); + } + + HashMap hashMap = new HashMap<>(); + for (int i = 0; i < keys.length; i++) { + hashMap.put(keys[i], values[i]); } + return hashMap; } + } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java index 0eb95306..85a0ada3 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java @@ -30,8 +30,8 @@ public void pollLogs() { { viewModel = new ViewModel(); } - for (HasLogEntry entry : logEntries) { - + for (int i = 0; i < user.getEntries().size();i++) { + HasLogEntry entry = user.getEntries().remove(i); String ingredientString = entry.getMeal().getIngredients(); // trim ingredient list ingredientString = trimCharacters(ingredientString); @@ -47,9 +47,8 @@ public void pollLogs() { // set processed entry.setProcessed(true); - + user.getEntries().add(entry); } - user.setEntries(logEntries); user.setView(viewModel); userRepository.save(user); } From f8396c5349f88ac0011958c935c252dfb147906b Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Fri, 22 Sep 2023 13:58:18 +0200 Subject: [PATCH 62/97] Update HydrationService.java --- .../java/fellowship/mealmaestro/services/HydrationService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java index 85a0ada3..cfbeb164 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java @@ -24,7 +24,6 @@ public void pollLogs() { // per user for (UserModel nuser : userList) { UserModel user = userRepository.findByEmail(nuser.getEmail()).get(); - List logEntries = userRepository.findUnprocessedLogEntriesForUser(user); ViewModel viewModel = user.getView(); if(viewModel == null) { From d1bf894b3221ca304bb9025ad95d066fa6f6c2ec Mon Sep 17 00:00:00 2001 From: Skulderlock <78735770+SkulderLock@users.noreply.github.com> Date: Sat, 23 Sep 2023 11:54:18 +0200 Subject: [PATCH 63/97] =?UTF-8?q?=E2=9C=A8=20New=20exception=20handler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/GlobalExceptionHandler.java | 11 ++++--- .../mealmaestro/config/SecurityConfig.java | 31 +++++++++---------- .../exceptions/UserNotFoundException.java | 7 +++++ .../controllers/UserController.java | 3 +- .../mealmaestro/services/UserService.java | 5 +-- .../services/auth/AuthenticationService.java | 3 +- 6 files changed, 34 insertions(+), 26 deletions(-) create mode 100644 backend/src/main/java/fellowship/mealmaestro/config/exceptions/UserNotFoundException.java diff --git a/backend/src/main/java/fellowship/mealmaestro/config/GlobalExceptionHandler.java b/backend/src/main/java/fellowship/mealmaestro/config/GlobalExceptionHandler.java index 5042f310..bdb1a7ec 100644 --- a/backend/src/main/java/fellowship/mealmaestro/config/GlobalExceptionHandler.java +++ b/backend/src/main/java/fellowship/mealmaestro/config/GlobalExceptionHandler.java @@ -5,12 +5,13 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import fellowship.mealmaestro.config.exceptions.UserNotFoundException; + @RestControllerAdvice public class GlobalExceptionHandler { - // @ExceptionHandler(RuntimeException.class) - // public ResponseEntity handleUserNotFoundException(RuntimeException e) - // { - // return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - // } + @ExceptionHandler(UserNotFoundException.class) + public ResponseEntity handleUserNotFoundException(UserNotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } } diff --git a/backend/src/main/java/fellowship/mealmaestro/config/SecurityConfig.java b/backend/src/main/java/fellowship/mealmaestro/config/SecurityConfig.java index e2066b59..29f2f32f 100644 --- a/backend/src/main/java/fellowship/mealmaestro/config/SecurityConfig.java +++ b/backend/src/main/java/fellowship/mealmaestro/config/SecurityConfig.java @@ -22,34 +22,31 @@ public class SecurityConfig { private final AuthenticationProvider authenticationProvider; - public SecurityConfig(JwtAuthenticationFilter jwtAuthFilter, AuthenticationProvider authenticationProvider){ + public SecurityConfig(JwtAuthenticationFilter jwtAuthFilter, AuthenticationProvider authenticationProvider) { this.jwtAuthFilter = jwtAuthFilter; this.authenticationProvider = authenticationProvider; } - + @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{ + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .cors(cors -> cors.configurationSource(corsConfigurationSource())) - .csrf(csrf -> csrf.disable()) - .authorizeHttpRequests(authReq -> authReq - .requestMatchers("/register", "/authenticate") - .permitAll() - .anyRequest() - .authenticated() - ) - .sessionManagement(session -> session - .sessionCreationPolicy(SessionCreationPolicy.STATELESS) - ) - .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); - + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(authReq -> authReq + .requestMatchers("/register", "/authenticate", "/hello") + .permitAll() + .anyRequest() + .authenticated()) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); http.authenticationProvider(authenticationProvider); return http.build(); } @Bean - CorsConfigurationSource corsConfigurationSource(){ + CorsConfigurationSource corsConfigurationSource() { CorsConfiguration corsConfig = new CorsConfiguration(); corsConfig.setAllowedOrigins(Arrays.asList("*")); corsConfig.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); diff --git a/backend/src/main/java/fellowship/mealmaestro/config/exceptions/UserNotFoundException.java b/backend/src/main/java/fellowship/mealmaestro/config/exceptions/UserNotFoundException.java new file mode 100644 index 00000000..8c8aa3d9 --- /dev/null +++ b/backend/src/main/java/fellowship/mealmaestro/config/exceptions/UserNotFoundException.java @@ -0,0 +1,7 @@ +package fellowship.mealmaestro.config.exceptions; + +public class UserNotFoundException extends RuntimeException { + public UserNotFoundException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/UserController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/UserController.java index ed17d8a7..16414a49 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/UserController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/UserController.java @@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; +import fellowship.mealmaestro.config.exceptions.UserNotFoundException; import fellowship.mealmaestro.models.UpdateUserRequestModel; import fellowship.mealmaestro.models.auth.AuthenticationRequestModel; import fellowship.mealmaestro.models.auth.AuthenticationResponseModel; @@ -32,7 +33,7 @@ public UserController(AuthenticationService authenticationService, UserService u @PostMapping("/findByEmail") public UserModel findByEmail(@RequestBody UserModel user) { - return userService.findByEmail(user.getEmail()).orElseThrow(() -> new RuntimeException("User not found")); + return userService.findByEmail(user.getEmail()).orElseThrow(() -> new UserNotFoundException("User not found")); } @PostMapping("/register") diff --git a/backend/src/main/java/fellowship/mealmaestro/services/UserService.java b/backend/src/main/java/fellowship/mealmaestro/services/UserService.java index b2f14868..a2c78542 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/UserService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/UserService.java @@ -4,6 +4,7 @@ import org.springframework.stereotype.Service; +import fellowship.mealmaestro.config.exceptions.UserNotFoundException; import fellowship.mealmaestro.models.UpdateUserRequestModel; import fellowship.mealmaestro.models.neo4j.UserModel; import fellowship.mealmaestro.repositories.neo4j.UserRepository; @@ -28,7 +29,7 @@ public UserModel updateUser(UpdateUserRequestModel user, String token) { String authToken = token.substring(7); String email = jwtService.extractUserEmail(authToken); UserModel userModel = userRepository.findByEmail(email) - .orElseThrow(() -> new RuntimeException("User not found")); + .orElseThrow(() -> new UserNotFoundException("User not found")); userModel.setName(user.getUsername()); return userRepository.save(userModel); @@ -37,6 +38,6 @@ public UserModel updateUser(UpdateUserRequestModel user, String token) { public UserModel getUser(String token) { String authToken = token.substring(7); String email = jwtService.extractUserEmail(authToken); - return userRepository.findByEmail(email).orElseThrow(() -> new RuntimeException("User not found")); + return userRepository.findByEmail(email).orElseThrow(() -> new UserNotFoundException("User not found")); } } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/auth/AuthenticationService.java b/backend/src/main/java/fellowship/mealmaestro/services/auth/AuthenticationService.java index a1c8bea4..2f5c49a9 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/auth/AuthenticationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/auth/AuthenticationService.java @@ -10,6 +10,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import fellowship.mealmaestro.config.exceptions.UserNotFoundException; import fellowship.mealmaestro.models.auth.AuthenticationRequestModel; import fellowship.mealmaestro.models.auth.AuthenticationResponseModel; import fellowship.mealmaestro.models.auth.AuthorityRoleModel; @@ -76,7 +77,7 @@ public AuthenticationResponseModel authenticate(AuthenticationRequestModel reque request.getPassword())); var user = userRepository.findByEmail(request.getEmail()) - .orElseThrow(() -> new RuntimeException("User not found")); + .orElseThrow(() -> new UserNotFoundException("User not found")); var jwt = jwtService.generateToken(user); return new AuthenticationResponseModel(jwt); From c7e05b22a647f24c8606d0eee44f00b11e7bed30 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 13:53:52 +0200 Subject: [PATCH 64/97] actually fixed now --- .../mealmaestro/models/neo4j/ViewModel.java | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java index b07d1042..924be042 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java @@ -1,6 +1,9 @@ package fellowship.mealmaestro.models.neo4j; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.schema.Id; @@ -13,9 +16,9 @@ public class ViewModel { @GeneratedValue private Long id; - String[] Keys; - Double[] scoreValues; - Double[] nScoreValues; + private String[] Keys; + private List scoreValues; + private List nScoreValues; private Double max =0.0; private Double min =0.0; @@ -88,22 +91,27 @@ public void normalise() { nScoreValues = hashMapValuesToArray(nScoreMap); } - public static Double[] hashMapValuesToArray(HashMap hashMap) { - return hashMap.values().toArray(new Double[0]); + public static List hashMapValuesToArray(HashMap hashMap) { + List listFromHashMap = new ArrayList<>(); + for (Map.Entry entry : hashMap.entrySet()) { + Double value = entry.getValue(); + listFromHashMap.add(value); + } + return listFromHashMap; } public static String[] hashMapKeysToArray(HashMap hashMap) { return hashMap.keySet().toArray(new String[0]); } - public static HashMap arraysToHashMap(String[] keys, Double[] values) { - if (keys.length != values.length) { + public static HashMap arraysToHashMap(String[] keys, List nScoreValues2) { + if (keys.length != nScoreValues2.size()) { throw new IllegalArgumentException("Keys and values arrays must have the same length."); } HashMap hashMap = new HashMap<>(); for (int i = 0; i < keys.length; i++) { - hashMap.put(keys[i], values[i]); + hashMap.put(keys[i], nScoreValues2.get(i)); } return hashMap; } From 10d979ffdb11324fb5237b3d6beee53b5599c1d4 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 13:54:25 +0200 Subject: [PATCH 65/97] value tweaks --- .../fellowship/mealmaestro/services/HydrationService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java index cfbeb164..df828bd3 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java @@ -73,13 +73,13 @@ private static List parseCommaSeparatedString(String input) { private static Double getScoreValue(String entryType) { switch (entryType.toLowerCase()) { case "regenerated": - return -0.4; + return -0.2; case "like": return 1.0; case "dislike": - return -0.7; + return -0.5; case "save": return 0.7; From a79b84bf8e330392ed646711cc981e801293a101 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 14:40:25 +0200 Subject: [PATCH 66/97] mapped out recommendation --- .../services/RecommendationService.java | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java index e0ed7af4..a9608986 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java @@ -1,10 +1,38 @@ package fellowship.mealmaestro.services; +import java.util.List; + import org.springframework.beans.factory.annotation.Autowired; +import fellowship.mealmaestro.models.neo4j.FoodModel; +import fellowship.mealmaestro.models.neo4j.MealModel; +import fellowship.mealmaestro.models.neo4j.PantryModel; +import fellowship.mealmaestro.models.neo4j.ViewModel; import fellowship.mealmaestro.repositories.neo4j.UserRepository; public class RecommendationService { @Autowired - private UserRepository userRepository; + private UserService userService; + @Autowired + private PantryService pantryService; + @Autowired + private MealDatabaseService mealDatabaseService; + @Autowired + private MealManagementService mealManagementService; + + public MealModel getRecommendedMeal(String token){ + MealModel recMealModel = new MealModel(); + //get view and pantry + List pantryModel = pantryService.getPantry(token.substring(7)); + ViewModel viewModel = userService.getUser(token).getView(); + //get positive keys + + // compare view and pantry + + //use list to find db meal + + //query gpt + + return recMealModel; + } } From 60527bb10b5be976319979c9e357158d738534a0 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 14:50:11 +0200 Subject: [PATCH 67/97] function to get certain values added --- .../mealmaestro/models/neo4j/ViewModel.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java index 924be042..bba173b8 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java @@ -9,6 +9,8 @@ import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; +import kotlin.coroutines.ContinuationInterceptor.Key; + @Node("View") public class ViewModel { @@ -116,5 +118,19 @@ public static HashMap arraysToHashMap(String[] keys, List getPositiveNScores(Double minNValue) throws Exception{ + ListKeys = new ArrayList<>(); + if(this.Keys != null && this.Keys.length > 0){ + HashMap nScoreMap = arraysToHashMap(this.Keys, this.nScoreValues); + + for (Map.Entry entry : nScoreMap.entrySet()) { + if (entry.getValue() >= minNValue) { + Keys.add(entry.getKey()); + } + } + + return Keys; + } + else return null; + } } From 0e6d09e32847ab1df73900fdddb4d0f13b3e5595 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 14:50:27 +0200 Subject: [PATCH 68/97] Update ViewModel.java --- .../java/fellowship/mealmaestro/models/neo4j/ViewModel.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java index bba173b8..e1d927ce 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java @@ -9,8 +9,6 @@ import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; -import kotlin.coroutines.ContinuationInterceptor.Key; - @Node("View") public class ViewModel { From 0b7463aa337c095d6f590b793672553ed2fabdf5 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 14:54:55 +0200 Subject: [PATCH 69/97] pantry list function --- .../fellowship/mealmaestro/models/neo4j/PantryModel.java | 8 ++++++++ .../mealmaestro/services/RecommendationService.java | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/PantryModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/PantryModel.java index 0cce95da..5631efb5 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/PantryModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/PantryModel.java @@ -45,4 +45,12 @@ public String toString() { return csv; } + + public List getNameList() throws Exception{ + List list = new ArrayList<>(); + for (FoodModel foodModel : this.foods) { + list.add(foodModel.getName()); + } + return list; + } } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java index a9608986..28353e75 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java @@ -20,12 +20,13 @@ public class RecommendationService { @Autowired private MealManagementService mealManagementService; - public MealModel getRecommendedMeal(String token){ + public MealModel getRecommendedMeal(String token) throws Exception{ MealModel recMealModel = new MealModel(); //get view and pantry List pantryModel = pantryService.getPantry(token.substring(7)); ViewModel viewModel = userService.getUser(token).getView(); //get positive keys + List validIngredients = viewModel.getPositiveNScores(-0.1); // compare view and pantry From b473f544669ecc977b1dda81caada37c31fb624e Mon Sep 17 00:00:00 2001 From: Skulderlock <78735770+SkulderLock@users.noreply.github.com> Date: Sat, 23 Sep 2023 15:05:01 +0200 Subject: [PATCH 70/97] =?UTF-8?q?=E2=9C=A8=20qol=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/MealManagementService.java | 3 + .../services/SettingsServiceTest.java | 86 ------------------ .../pages/acc-profile/acc-profile.page.html | 4 +- .../pages/acc-profile/acc-profile.page.scss | 26 ++---- frontend/src/app/pages/login/login.page.html | 91 +++++++++++-------- 5 files changed, 64 insertions(+), 146 deletions(-) delete mode 100644 backend/src/test/java/fellowship/mealmaestro/services/SettingsServiceTest.java diff --git a/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java b/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java index 2ce340f5..2e789cf0 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java @@ -50,6 +50,9 @@ public MealModel generateMeal(String mealType, String token) { String imageUrl = ""; imageUrl = unsplashService.fetchPhoto(mealJson.get("name").asText()); + if (!imageUrl.contains(("https://"))) + imageUrl = "https://images.unsplash.com/photo-1546069901-ba9599a7e63c?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=640&q=80"; + ObjectNode mealObject = objectMapper.valueToTree(mealJson); mealObject.put("type", mealType); mealObject.put("image", imageUrl); diff --git a/backend/src/test/java/fellowship/mealmaestro/services/SettingsServiceTest.java b/backend/src/test/java/fellowship/mealmaestro/services/SettingsServiceTest.java deleted file mode 100644 index 0a4a027e..00000000 --- a/backend/src/test/java/fellowship/mealmaestro/services/SettingsServiceTest.java +++ /dev/null @@ -1,86 +0,0 @@ -package fellowship.mealmaestro.services; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.springframework.boot.test.context.SpringBootTest; - -import fellowship.mealmaestro.models.neo4j.SettingsModel; -import fellowship.mealmaestro.repositories.neo4j.SettingsRepository; -import fellowship.mealmaestro.services.auth.JwtService; - -import java.util.Arrays; - -@SpringBootTest - -public class SettingsServiceTest { - - @InjectMocks - SettingsService settingsService; - - @Mock - JwtService jwtService; - - @Mock - SettingsRepository settingsRepository; - - @BeforeEach - public void setup() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void testGetSettings() { - SettingsModel settingsModel = new SettingsModel(); - settingsModel.setGoal("Lose Weight"); - settingsModel.setShoppingInterval("Weekly"); - settingsModel.setFoodPreferences(Arrays.asList("Vegetarian")); - settingsModel.setCalorieAmount(2000); - settingsModel.setBudgetRange("Low"); - settingsModel.setProtein(40); - settingsModel.setCarbs(40); - settingsModel.setFat(20); - settingsModel.setAllergies(Arrays.asList("Peanuts")); - settingsModel.setCookingTime("30 minutes"); - settingsModel.setUserHeight(180); - settingsModel.setUserWeight(70); - settingsModel.setUserBMI(22); - - when(jwtService.extractUserEmail("validToken")).thenReturn("test@example.com"); - when(settingsService.getSettings("test@example.com")).thenReturn(settingsModel); - - SettingsModel result = settingsService.getSettings("validToken"); - - assertEquals(settingsModel, result); - } - - @Test - public void testUpdateSettings() { - SettingsModel settingsModel = new SettingsModel(); - settingsModel.setGoal("Gain Weight"); - settingsModel.setShoppingInterval("Monthly"); - settingsModel.setFoodPreferences(Arrays.asList("Vegan")); - settingsModel.setCalorieAmount(3000); - settingsModel.setBudgetRange("High"); - settingsModel.setProtein(30); - settingsModel.setCarbs(50); - settingsModel.setFat(20); - settingsModel.setAllergies(Arrays.asList("Dairy")); - settingsModel.setCookingTime("45 minutes"); - settingsModel.setUserHeight(175); - settingsModel.setUserWeight(75); - settingsModel.setUserBMI(24); - - when(jwtService.extractUserEmail("validToken")).thenReturn("test@example.com"); - - settingsService.updateSettings(settingsModel, "validToken"); - - verify(settingsService).updateSettings(settingsModel, "test@example.com"); - } -} diff --git a/frontend/src/app/pages/acc-profile/acc-profile.page.html b/frontend/src/app/pages/acc-profile/acc-profile.page.html index 6f9e50de..046a7066 100644 --- a/frontend/src/app/pages/acc-profile/acc-profile.page.html +++ b/frontend/src/app/pages/acc-profile/acc-profile.page.html @@ -25,9 +25,9 @@ Logout - Delete Account + > -->
- +
-
+
- - - -
- Please enter a valid email. -
- - -
- Please enter a password. -
-
- - - Login + + +
+ Please enter a valid email. +
+ + +
+ Please enter a password. +
- -
+ + + + Login + + +
- Forget Password? + - + -
- \ No newline at end of file + From 7762403d7726db7c12af6dd1d278316ed5536ed1 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 15:05:23 +0200 Subject: [PATCH 71/97] Common items Function added --- .../services/RecommendationService.java | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java index 28353e75..6bbe903c 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java @@ -1,5 +1,6 @@ package fellowship.mealmaestro.services; +import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; @@ -20,20 +21,42 @@ public class RecommendationService { @Autowired private MealManagementService mealManagementService; + private final Double MIN_VALUE = -0.01; + public MealModel getRecommendedMeal(String token) throws Exception{ MealModel recMealModel = new MealModel(); //get view and pantry - List pantryModel = pantryService.getPantry(token.substring(7)); - ViewModel viewModel = userService.getUser(token).getView(); - //get positive keys - List validIngredients = viewModel.getPositiveNScores(-0.1); - + List pantryItems = userService.getUser(token).getPantry().getNameList(); + List validIngredients = userService.getUser(token).getView().getPositiveNScores(MIN_VALUE); + // compare view and pantry + //use list to find db meal + //query gpt return recMealModel; } + + public static List findCommonItems(List list1, List list2) { + List commonItems = new ArrayList<>(); + + for (String item1 : list1) { + for (String item2 : list2) { + if (item2.contains(item1)) { + commonItems.add(item1); + break; + } + if (item1.contains(item2)) { + commonItems.add(item2); + break; + } + } + } + + return commonItems; + + } } From 378339ad5b7f48ea922e356c2c3b05323d5776e8 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 15:05:45 +0200 Subject: [PATCH 72/97] Update RecommendationService.java --- .../services/RecommendationService.java | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java index 6bbe903c..87a50414 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java @@ -20,43 +20,40 @@ public class RecommendationService { private MealDatabaseService mealDatabaseService; @Autowired private MealManagementService mealManagementService; - + private final Double MIN_VALUE = -0.01; - public MealModel getRecommendedMeal(String token) throws Exception{ + public MealModel getRecommendedMeal(String token) throws Exception { MealModel recMealModel = new MealModel(); - //get view and pantry + // get view and pantry List pantryItems = userService.getUser(token).getPantry().getNameList(); - List validIngredients = userService.getUser(token).getView().getPositiveNScores(MIN_VALUE); - - // compare view and pantry - + List validIngredients = userService.getUser(token).getView().getPositiveNScores(MIN_VALUE); - //use list to find db meal + // compare view and pantry + // use list to find db meal - //query gpt + // query gpt return recMealModel; } public static List findCommonItems(List list1, List list2) { List commonItems = new ArrayList<>(); - + for (String item1 : list1) { for (String item2 : list2) { if (item2.contains(item1)) { commonItems.add(item1); - break; + break; } if (item1.contains(item2)) { commonItems.add(item2); - break; + break; } } } return commonItems; - } } From fad123f1c109408d3d4aff63a6aed9f36aa9d816 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 15:08:06 +0200 Subject: [PATCH 73/97] ingredient testing on regen --- .../services/RecommendationService.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java index 87a50414..f721fd08 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java @@ -14,8 +14,7 @@ public class RecommendationService { @Autowired private UserService userService; - @Autowired - private PantryService pantryService; + @Autowired private MealDatabaseService mealDatabaseService; @Autowired @@ -25,12 +24,9 @@ public class RecommendationService { public MealModel getRecommendedMeal(String token) throws Exception { MealModel recMealModel = new MealModel(); - // get view and pantry - List pantryItems = userService.getUser(token).getPantry().getNameList(); - List validIngredients = userService.getUser(token).getView().getPositiveNScores(MIN_VALUE); - - // compare view and pantry - + // get best items that are available in pantry + List bestAvailableIngredients = findCommonItems(userService.getUser(token).getPantry().getNameList(), userService.getUser(token).getView().getPositiveNScores(MIN_VALUE)); + System.out.println("Valid Ingredients" + bestAvailableIngredients); // use list to find db meal // query gpt @@ -38,7 +34,7 @@ public MealModel getRecommendedMeal(String token) throws Exception { return recMealModel; } - public static List findCommonItems(List list1, List list2) { + private static List findCommonItems(List list1, List list2) { List commonItems = new ArrayList<>(); for (String item1 : list1) { From 1b5583630d63c5e79d94b535864e938b7838222e Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 15:12:00 +0200 Subject: [PATCH 74/97] Forgot @servvice smh --- .../controllers/MealManagementController.java | 12 ++++++++++-- .../mealmaestro/services/RecommendationService.java | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java index d00c4619..b46c3a0d 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java @@ -22,6 +22,7 @@ import fellowship.mealmaestro.services.LogService; import fellowship.mealmaestro.services.MealDatabaseService; import fellowship.mealmaestro.services.MealManagementService; +import fellowship.mealmaestro.services.RecommendationService; import jakarta.validation.Valid; @RestController @@ -32,6 +33,8 @@ public class MealManagementController { private MealDatabaseService mealDatabaseService; @Autowired private LogService logService; + @Autowired + private RecommendationService recommendationService; @PostMapping("/getMealPlanForDay") public ResponseEntity> dailyMeals(@Valid @RequestBody DateModel request, @@ -124,9 +127,14 @@ public ResponseEntity regenerate(@RequestBody RegenerateMealRequest r throws JsonMappingException, JsonProcessingException { logService.logMeal(token, request.getMeal(), "regenerate"); - + try { + recommendationService.getRecommendedMeal(token); + } catch (Exception e) { + System.out.println("Recommendation Error"); + } + token = token.substring(7); - + // Try find an appropriate meal in the database Optional replacementMeal = mealDatabaseService.findMealTypeForUser(request.getMeal().getType(), token); diff --git a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java index f721fd08..268c0884 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java @@ -4,6 +4,7 @@ import java.util.List; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; import fellowship.mealmaestro.models.neo4j.FoodModel; import fellowship.mealmaestro.models.neo4j.MealModel; @@ -11,6 +12,7 @@ import fellowship.mealmaestro.models.neo4j.ViewModel; import fellowship.mealmaestro.repositories.neo4j.UserRepository; +@Service public class RecommendationService { @Autowired private UserService userService; From 9001bb1a0dc529097adbb2dd88f7ec30068d43e8 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 15:20:14 +0200 Subject: [PATCH 75/97] add meal passing for mealgenerationservice --- .../controllers/MealManagementController.java | 2 +- .../mealmaestro/services/RecommendationService.java | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java index b46c3a0d..270ecf23 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java @@ -128,7 +128,7 @@ public ResponseEntity regenerate(@RequestBody RegenerateMealRequest r logService.logMeal(token, request.getMeal(), "regenerate"); try { - recommendationService.getRecommendedMeal(token); + recommendationService.getRecommendedMeal(request.getMeal(),token); } catch (Exception e) { System.out.println("Recommendation Error"); } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java index 268c0884..461fd12f 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java @@ -24,15 +24,21 @@ public class RecommendationService { private final Double MIN_VALUE = -0.01; - public MealModel getRecommendedMeal(String token) throws Exception { - MealModel recMealModel = new MealModel(); + public MealModel getRecommendedMeal(MealModel meal, String token) throws Exception { + MealModel recMealModel = null; // get best items that are available in pantry List bestAvailableIngredients = findCommonItems(userService.getUser(token).getPantry().getNameList(), userService.getUser(token).getView().getPositiveNScores(MIN_VALUE)); System.out.println("Valid Ingredients" + bestAvailableIngredients); + // protection + if(bestAvailableIngredients == null || bestAvailableIngredients.isEmpty()){ + throw new IllegalArgumentException("no valid ingredients"); + } // use list to find db meal // query gpt + if(recMealModel == null){ + } return recMealModel; } From 05b7fa0fb74353810d6fbdd6943e25476cc08c54 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 15:23:14 +0200 Subject: [PATCH 76/97] generateFromIngredients added --- .../services/MealManagementService.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java b/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java index 4c51817e..884e7c12 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java @@ -2,6 +2,7 @@ import java.io.File; import java.io.IOException; +import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -60,6 +61,38 @@ public MealModel generateMeal(String mealType, String token) { } } + public MealModel generateMealFromIngredients(String mealType, List AvailableIngredients, String token) throws IOException { + MealModel defaultMeal = new MealModel("Bread", "1. Toast the bread", "Delicious Bread", + "https://images.unsplash.com/photo-1598373182133-52452f7691ef?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80", + "Bread", "5 minutes"); + defaultMeal.setType("breakfast"); + try { + JsonNode mealJson = objectMapper.readTree(openaiApiService.fetchMealResponseFromIngredients(mealType, AvailableIngredients, token)); + int i = 0; + if (!validate(mealJson)) { + for (i = 0; i < 4; i++) { + mealJson = objectMapper.readTree(openaiApiService.fetchMealResponseFromIngredients(mealType, AvailableIngredients,token)); + if (validate(mealJson)) + break; + } + return defaultMeal; + } + + String imageUrl = ""; + imageUrl = unsplashService.fetchPhoto(mealJson.get("name").asText()); + + ObjectNode mealObject = objectMapper.valueToTree(mealJson); + mealObject.put("type", mealType); + mealObject.put("image", imageUrl); + + MealModel mealModel = objectMapper.treeToValue(mealObject, MealModel.class); + return mealModel; + } catch (JsonProcessingException e) { + System.out.println(e.getMessage()); + return defaultMeal; + } + } + public boolean validate(JsonNode data) { try { File schemaFile = new File("src\\main\\resources\\MealSchema.json"); From cbf9584cf6c01ba6bb7ffcfe560d7dd05f920276 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 15:59:26 +0200 Subject: [PATCH 77/97] Added overloaded Functions to include Liked Ingredeints --- .../services/MealManagementService.java | 8 +-- .../services/OpenaiApiService.java | 69 +++++++++++++++++++ .../services/OpenaiPromptBuilder.java | 65 +++++++++++++++++ 3 files changed, 137 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java b/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java index 884e7c12..7a08e26c 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/MealManagementService.java @@ -2,8 +2,6 @@ import java.io.File; import java.io.IOException; -import java.util.List; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -61,17 +59,17 @@ public MealModel generateMeal(String mealType, String token) { } } - public MealModel generateMealFromIngredients(String mealType, List AvailableIngredients, String token) throws IOException { + public MealModel generateMealFromIngredients(String mealType, String availableIngredients, String token) throws IOException { MealModel defaultMeal = new MealModel("Bread", "1. Toast the bread", "Delicious Bread", "https://images.unsplash.com/photo-1598373182133-52452f7691ef?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80", "Bread", "5 minutes"); defaultMeal.setType("breakfast"); try { - JsonNode mealJson = objectMapper.readTree(openaiApiService.fetchMealResponseFromIngredients(mealType, AvailableIngredients, token)); + JsonNode mealJson = objectMapper.readTree(openaiApiService.fetchMealResponseFromIngredients(mealType, availableIngredients, token)); int i = 0; if (!validate(mealJson)) { for (i = 0; i < 4; i++) { - mealJson = objectMapper.readTree(openaiApiService.fetchMealResponseFromIngredients(mealType, AvailableIngredients,token)); + mealJson = objectMapper.readTree(openaiApiService.fetchMealResponseFromIngredients(mealType, availableIngredients,token)); if (validate(mealJson)) break; } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java b/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java index c25e558b..557c24e0 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/OpenaiApiService.java @@ -122,4 +122,73 @@ public String getJSONResponse(String Type, String token) throws JsonProcessingEx } } + public String fetchMealResponseFromIngredients(String type, String availableIngredients, String token)throws JsonMappingException, JsonProcessingException { + + String jsonResponse = getJSONResponse(type, token); + if (jsonResponse.equals("Timeout")) { + jsonResponse = getJSONResponse(type, token); + } + if (jsonResponse.equals("Error") || jsonResponse.equals("Timeout")) { + return "{\"error\":\"error\"}"; + } + + JsonNode jsonNode = jsonMapper.readTree(jsonResponse); + + JsonNode contentNode = jsonNode + .path("choices") + .get(0) + .path("message") + .path("content"); + + String text = contentNode.asText(); + + text = text.replace("\\\"", "\""); + text = text.replace("\n", ""); + text = text.replace("/r/n", "\\r\\n"); + int index = text.indexOf('{'); + int lastIndex = text.lastIndexOf('}') + 1; + if (index != -1 && lastIndex != -1 && index < lastIndex) { + text = text.substring(index, lastIndex); + } + + return text; + } + + public String getJSONResponse(String Type, String token, String availableIngredients) throws JsonProcessingException { + + OpenAIChatRequest prompt; + String jsonRequest; + + prompt = pBuilder.buildPrompt(Type, token,availableIngredients); + jsonRequest = jsonMapper.writeValueAsString(prompt); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Authorization", "Bearer " + API_KEY); + + System.out.println("Sending request to OpenAI"); + + try { + String response = webClient.post() + .uri(OPENAI_URL) + .contentType(MediaType.APPLICATION_JSON) + .headers(h -> h.setAll(headers.toSingleValueMap())) + .body(Mono.just(jsonRequest), String.class) + .retrieve() + .bodyToMono(String.class) + .timeout(Duration.ofSeconds(30)) + .block(); + + return response; + } catch (RuntimeException e) { + if (e.getCause() instanceof TimeoutException) { + System.out.println("Timeout"); + return "Timeout"; + } else { + System.out.println("Error"); + return "Error"; + } + } + } + } diff --git a/backend/src/main/java/fellowship/mealmaestro/services/OpenaiPromptBuilder.java b/backend/src/main/java/fellowship/mealmaestro/services/OpenaiPromptBuilder.java index e3673f0c..78bbd981 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/OpenaiPromptBuilder.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/OpenaiPromptBuilder.java @@ -103,4 +103,69 @@ public Message buildUserMessage(String type, String token) { return userMessage; } + + public OpenAIChatRequest buildPrompt(String type, String token, String fromIngredients) throws JsonProcessingException { + + OpenAIChatRequest request = new OpenAIChatRequest(); + request.setModel("gpt-3.5-turbo"); + + OpenAIChatRequest.Message systemMessage = buildSystemMessage(); + OpenAIChatRequest.Message userMessage = buildUserMessage(type, token, fromIngredients); + + request.setMessages(List.of(systemMessage, userMessage)); + + return request; + } + + public Message buildUserMessage(String type, String token, String fromIngredients) { + String email = jwtService.extractUserEmail(token); + + UserModel user = userRepository.findByEmail(email).get(); + String pantryFoods = user.getPantry().toString(); + String settings = user.getSettings().toString(); + + double random = rand.nextDouble(); + + OpenAIChatRequest.Message userMessage = new OpenAIChatRequest.Message(); + + System.out.println("1st random: " + random); + + if (pantryFoods.equals("")) { + if (random < 0.3) { + pantryFoods = "I have no food in my pantry"; + } else if (random < 0.6) { + pantryFoods = "There is no food in my pantry"; + } else { + pantryFoods = "I haven't got any food in my pantry"; + } + } else { + pantryFoods = "I have the following foods in my pantry: " + pantryFoods + " And These are my favourite: " + fromIngredients; + } + + random = rand.nextDouble(); + System.out.println("2nd random: " + random); + + if (settings.equals("")) { + if (random < 0.3) { + settings = "You can make whatever unique meal you want."; + } else if (random < 0.6) { + settings = "You can make whatever meal you want."; + } else { + settings = "You can make whatever meal you want, as long as it is " + type + "."; + } + } else { + settings = "Some other useful information about me: " + settings + "."; + } + + random = rand.nextDouble(); + System.out.println("3rd random: " + random); + userMessage.setRole("user"); + if (random < 0.5) { + userMessage.setContent("I want to cook a " + type + " meal. " + pantryFoods + ". " + settings); + } else { + userMessage.setContent("I want to cook a " + type + " meal. " + settings + ". " + pantryFoods); + } + + return userMessage; + } } From 1430eaf7cc9e0ffab73e62f6de34ff17b66d3303 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 16:04:59 +0200 Subject: [PATCH 78/97] gpt now gets queried with liked meals, not tested --- .../mealmaestro/services/RecommendationService.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java index 461fd12f..a8ed97da 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java @@ -6,17 +6,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import fellowship.mealmaestro.models.neo4j.FoodModel; import fellowship.mealmaestro.models.neo4j.MealModel; -import fellowship.mealmaestro.models.neo4j.PantryModel; -import fellowship.mealmaestro.models.neo4j.ViewModel; -import fellowship.mealmaestro.repositories.neo4j.UserRepository; @Service public class RecommendationService { @Autowired private UserService userService; - @Autowired private MealDatabaseService mealDatabaseService; @Autowired @@ -24,20 +19,20 @@ public class RecommendationService { private final Double MIN_VALUE = -0.01; - public MealModel getRecommendedMeal(MealModel meal, String token) throws Exception { + public MealModel getRecommendedMeal(String mealType, String token) throws Exception { MealModel recMealModel = null; // get best items that are available in pantry List bestAvailableIngredients = findCommonItems(userService.getUser(token).getPantry().getNameList(), userService.getUser(token).getView().getPositiveNScores(MIN_VALUE)); System.out.println("Valid Ingredients" + bestAvailableIngredients); // protection if(bestAvailableIngredients == null || bestAvailableIngredients.isEmpty()){ - throw new IllegalArgumentException("no valid ingredients"); + throw new IllegalArgumentException("No valid ingredients in pantry, Could be empty or no matches"); } // use list to find db meal // query gpt if(recMealModel == null){ - + recMealModel = mealManagementService.generateMealFromIngredients(mealType, token, String.join(", ", bestAvailableIngredients)); } return recMealModel; } From 34ac44616e650949a658adad091cb5226c81398e Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 16:06:01 +0200 Subject: [PATCH 79/97] forgot to change --- .../mealmaestro/controllers/MealManagementController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java index 270ecf23..0f8ac633 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java @@ -128,7 +128,7 @@ public ResponseEntity regenerate(@RequestBody RegenerateMealRequest r logService.logMeal(token, request.getMeal(), "regenerate"); try { - recommendationService.getRecommendedMeal(request.getMeal(),token); + recommendationService.getRecommendedMeal(request.getMeal().getType(),token); } catch (Exception e) { System.out.println("Recommendation Error"); } From 38105754fc89ade789642882ba728451b7b14a93 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 16:24:15 +0200 Subject: [PATCH 80/97] all implemented except for checking the database for valid meals --- .../controllers/MealManagementController.java | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java index 0f8ac633..2514e837 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java @@ -127,26 +127,35 @@ public ResponseEntity regenerate(@RequestBody RegenerateMealRequest r throws JsonMappingException, JsonProcessingException { logService.logMeal(token, request.getMeal(), "regenerate"); + MealModel recoMeal = null; try { - recommendationService.getRecommendedMeal(request.getMeal().getType(),token); + recoMeal = recommendationService.getRecommendedMeal(request.getMeal().getType(), token); } catch (Exception e) { - System.out.println("Recommendation Error"); + System.out.println(e); } - - token = token.substring(7); - - // Try find an appropriate meal in the database - Optional replacementMeal = mealDatabaseService.findMealTypeForUser(request.getMeal().getType(), - token); + + Optional replacementMeal = null; MealModel returnedMeal = null; - if (replacementMeal.isPresent()) { - returnedMeal = mealDatabaseService.replaceMeal(request, replacementMeal.get(), token); + if (recoMeal != null) { + replacementMeal = Optional.ofNullable(recoMeal); } else { - // If there is no replacement, generate a new meal - MealModel newMeal = mealManagementService.generateMeal(request.getMeal().getType(), token); - returnedMeal = mealDatabaseService.replaceMeal(request, newMeal, token); + token = token.substring(7); + + // Try find an appropriate meal in the database + replacementMeal = mealDatabaseService.findMealTypeForUser(request.getMeal().getType(),token); } + if(recoMeal != null) + { + returnedMeal = recoMeal; + } else + if (replacementMeal.isPresent()) { + returnedMeal = mealDatabaseService.replaceMeal(request, replacementMeal.get(), token); + } else { + // If there is no replacement, generate a new meal + MealModel newMeal = mealManagementService.generateMeal(request.getMeal().getType(), token); + returnedMeal = mealDatabaseService.replaceMeal(request, newMeal, token); + } return ResponseEntity.ok(returnedMeal); } From 82b966aee7420e7d9f6e80eaeffde78a7d67f6c6 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 16:28:51 +0200 Subject: [PATCH 81/97] to lower case trim to ensure less duplicates --- .../java/fellowship/mealmaestro/services/HydrationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java index df828bd3..f12a2896 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java @@ -59,7 +59,7 @@ public void pollLogs() { private static String trimCharacters(String input) { String regex = "[0-9\\s]+"; String result = input.replaceAll(regex, ""); - return result; + return result.toLowerCase(); } // convert From 6e86bba9e0a7956ebda295d24208dd24fd8df6d8 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 16:29:31 +0200 Subject: [PATCH 82/97] to lower case for comparisons --- .../mealmaestro/services/RecommendationService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java index a8ed97da..31d0b13b 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java @@ -42,11 +42,11 @@ private static List findCommonItems(List list1, List lis for (String item1 : list1) { for (String item2 : list2) { - if (item2.contains(item1)) { + if (item2.toLowerCase().contains(item1.toLowerCase())) { commonItems.add(item1); break; } - if (item1.contains(item2)) { + if (item1.toLowerCase().contains(item2.toLowerCase())) { commonItems.add(item2); break; } From fc0cfcde91c274f54e742180fe197f568abf7b76 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 16:33:00 +0200 Subject: [PATCH 83/97] better trimming --- .../java/fellowship/mealmaestro/services/HydrationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java index f12a2896..96a8c35f 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java @@ -57,7 +57,7 @@ public void pollLogs() { // helper functions to be done // trim private static String trimCharacters(String input) { - String regex = "[0-9\\s]+"; + String regex = "\\d+(?:/\\d+)?\\s*(?:tablespoon|tablespoons|teaspoon|teaspoons|cup|cups|ounce|ounces|gram|grams|milliliter|milliliters|liter|liters|pound|pounds|kg|kilo|kilogram|kilograms|gallon|gallons)?"; String result = input.replaceAll(regex, ""); return result.toLowerCase(); } From 95e7a38c3944381cd31824d7f82e74ef7f89f5fa Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 16:41:05 +0200 Subject: [PATCH 84/97] cleanup and better regex matching --- .../fellowship/mealmaestro/services/HydrationService.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java index 96a8c35f..df0e1c75 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java @@ -10,6 +10,7 @@ import fellowship.mealmaestro.models.neo4j.relationships.HasLogEntry; import fellowship.mealmaestro.repositories.neo4j.UserRepository; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -57,16 +58,14 @@ public void pollLogs() { // helper functions to be done // trim private static String trimCharacters(String input) { - String regex = "\\d+(?:/\\d+)?\\s*(?:tablespoon|tablespoons|teaspoon|teaspoons|cup|cups|ounce|ounces|gram|grams|milliliter|milliliters|liter|liters|pound|pounds|kg|kilo|kilogram|kilograms|gallon|gallons)?"; + String regex = "\\d+(?:/\\d+)?(?:\\s*(?:tablespoon|tablespoons|teaspoon|teaspoons|cup|cups|ounce|ounces|gram|grams|milliliter|milliliters|liter|liters|pound|pounds|kg|kilo|kilogram|kilograms|gallon|gallons))?"; String result = input.replaceAll(regex, ""); return result.toLowerCase(); } // convert private static List parseCommaSeparatedString(String input) { - String[] elements = input.split(","); - - return Arrays.asList(elements); + return Arrays.asList(input.split(",")); } // scores From 92fa9d6bb33d1e1b63a989860389b3fd41bd15b7 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 16:42:07 +0200 Subject: [PATCH 85/97] cleanup and trimming whitespace on list creation --- .../services/HydrationService.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java index df0e1c75..5521d88f 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java @@ -18,20 +18,20 @@ public class HydrationService { @Autowired private UserRepository userRepository; + @Transactional - @Scheduled(fixedRate = 1 * 60 * 1000) + @Scheduled(fixedRate = 1 * 60 * 1000) public void pollLogs() { List userList = userRepository.findUsersWithNewLogEntries(); // per user for (UserModel nuser : userList) { UserModel user = userRepository.findByEmail(nuser.getEmail()).get(); ViewModel viewModel = user.getView(); - if(viewModel == null) - { + if (viewModel == null) { viewModel = new ViewModel(); } - for (int i = 0; i < user.getEntries().size();i++) { - HasLogEntry entry = user.getEntries().remove(i); + for (int i = 0; i < user.getEntries().size(); i++) { + HasLogEntry entry = user.getEntries().remove(i); String ingredientString = entry.getMeal().getIngredients(); // trim ingredient list ingredientString = trimCharacters(ingredientString); @@ -65,7 +65,15 @@ private static String trimCharacters(String input) { // convert private static List parseCommaSeparatedString(String input) { - return Arrays.asList(input.split(",")); + // return Arrays.asList(input.split(",")); + String[] elements = input.split(","); + + List result = new ArrayList<>(); + for (String element : elements) { + result.add(element.trim()); + } + + return result; } // scores @@ -73,7 +81,7 @@ private static Double getScoreValue(String entryType) { switch (entryType.toLowerCase()) { case "regenerated": return -0.2; - + case "like": return 1.0; From 59171e64e91d4ac9c9a3cd668cb16ebd0c0d3709 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 16:43:10 +0200 Subject: [PATCH 86/97] cleanup --- .../java/fellowship/mealmaestro/services/HydrationService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java index 5521d88f..f0609c96 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/HydrationService.java @@ -11,7 +11,6 @@ import fellowship.mealmaestro.repositories.neo4j.UserRepository; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; @Service From 909a7ed4d724adaa6ece2a62c453a49143c04ace Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 16:57:02 +0200 Subject: [PATCH 87/97] added function and variable to find a meal in the database the user can make, and will hopefully like --- .../services/MealDatabaseService.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/MealDatabaseService.java b/backend/src/main/java/fellowship/mealmaestro/services/MealDatabaseService.java index da4f2316..a98c8cdc 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/MealDatabaseService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/MealDatabaseService.java @@ -151,6 +151,27 @@ public Optional findMealForUser(String mealName, String token) { return Optional.empty(); } + + public Optional findMealUserLikes(String type, String token, String likedAndAvailableIngredients) { + String email = jwtService.extractUserEmail(token); + + UserModel user = userRepository.findByEmail(email).get(); + List randomMeals = mealRepository.get100RandomMeals(); + + // if meal with meal type is present in randomMeals, return it + for (MealModel meal : randomMeals) { + if (meal.getType().equals(type)) { + if (canMakeMeal(user.getPantry().getFoods(), meal.getIngredients())) + //% matched + if(mealPercentageMatched(likedAndAvailableIngredients, meal.getIngredients())) + { + return Optional.of(meal); + } + } + } + + return Optional.empty(); + } public boolean canMakeMeal(List pantryItems, String ingredients) { String[] ingredientsArray = ingredients.split(","); @@ -169,6 +190,28 @@ public boolean canMakeMeal(List pantryItems, String ingredients) { return true; } + private final Double PERCENTAGE_TO_MATCH = 0.4; + + public boolean mealPercentageMatched(String likedIngredients, String mealIngredients) { + String[] likedIngredientsArray = likedIngredients.split(","); + String[] mealIngredientsArray = mealIngredients.split(","); + int totalLikedIngredients = likedIngredientsArray.length; + int matchingIngredientsCount = 0; + + for (String likedIngredient : likedIngredientsArray) { + for (String mealIngredient : mealIngredientsArray) { + if (mealIngredient.trim().equalsIgnoreCase(likedIngredient.trim())) { + matchingIngredientsCount++; + break; + } + } + } + + double percentageMatched = (double) matchingIngredientsCount / totalLikedIngredients; + + return percentageMatched >= PERCENTAGE_TO_MATCH; + } + @Transactional public MealModel replaceMeal(RegenerateMealRequest request, MealModel newMeal, String token) { String email = jwtService.extractUserEmail(token); From 908d597cdd1b1368addb5c05949fb2be875f68c0 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 17:01:02 +0200 Subject: [PATCH 88/97] done, time for testing --- .../mealmaestro/services/RecommendationService.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java index 31d0b13b..60319d59 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java @@ -29,8 +29,12 @@ public MealModel getRecommendedMeal(String mealType, String token) throws Except throw new IllegalArgumentException("No valid ingredients in pantry, Could be empty or no matches"); } // use list to find db meal - + java.util.Optional recMealFromDatabase = mealDatabaseService.findMealUserLikes(mealType, token, String.join(", ", bestAvailableIngredients)); // query gpt + if(recMealFromDatabase.isPresent()) + { + return recMealFromDatabase.get(); + } if(recMealModel == null){ recMealModel = mealManagementService.generateMealFromIngredients(mealType, token, String.join(", ", bestAvailableIngredients)); } From 4c8db1a2615eec6f0e79098229ce5969e7967b19 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sat, 23 Sep 2023 17:06:52 +0200 Subject: [PATCH 89/97] fixed some dumb logic error of mine --- .../controllers/MealManagementController.java | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java index 2514e837..4b8909a6 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java @@ -143,19 +143,16 @@ public ResponseEntity regenerate(@RequestBody RegenerateMealRequest r token = token.substring(7); // Try find an appropriate meal in the database - replacementMeal = mealDatabaseService.findMealTypeForUser(request.getMeal().getType(),token); + replacementMeal = mealDatabaseService.findMealTypeForUser(request.getMeal().getType(), token); + } + + if (replacementMeal.isPresent()) { + returnedMeal = mealDatabaseService.replaceMeal(request, replacementMeal.get(), token); + } else { + // If there is no replacement, generate a new meal + MealModel newMeal = mealManagementService.generateMeal(request.getMeal().getType(), token); + returnedMeal = mealDatabaseService.replaceMeal(request, newMeal, token); } - if(recoMeal != null) - { - returnedMeal = recoMeal; - } else - if (replacementMeal.isPresent()) { - returnedMeal = mealDatabaseService.replaceMeal(request, replacementMeal.get(), token); - } else { - // If there is no replacement, generate a new meal - MealModel newMeal = mealManagementService.generateMeal(request.getMeal().getType(), token); - returnedMeal = mealDatabaseService.replaceMeal(request, newMeal, token); - } return ResponseEntity.ok(returnedMeal); } From 86781288e0a975670cf2dedb91691c078ab3711f Mon Sep 17 00:00:00 2001 From: Skulderlock <78735770+SkulderLock@users.noreply.github.com> Date: Sat, 23 Sep 2023 19:20:42 +0200 Subject: [PATCH 90/97] =?UTF-8?q?=F0=9F=90=9B=20minor=20change?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/app/pages/pantry/pantry.page.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/pages/pantry/pantry.page.ts b/frontend/src/app/pages/pantry/pantry.page.ts index 7927003b..0848a7b5 100644 --- a/frontend/src/app/pages/pantry/pantry.page.ts +++ b/frontend/src/app/pages/pantry/pantry.page.ts @@ -303,7 +303,13 @@ export class PantryPage implements OnInit, ViewWillEnter { calculateTotalPrice() { this.totalShoppingPrice = 0; this.shoppingItems.forEach((item) => { - if (item.price) { + if ( + item.price && + item.price !== undefined && + item.price !== null && + item.price !== 0 && + item.price !== -1 + ) { this.totalShoppingPrice += item.price; } }); From 50eca2023fdb7d307e9390b71b2a184ad589d42e Mon Sep 17 00:00:00 2001 From: Skulderlock <78735770+SkulderLock@users.noreply.github.com> Date: Sat, 23 Sep 2023 19:23:23 +0200 Subject: [PATCH 91/97] =?UTF-8?q?=F0=9F=8D=B1=20App=20Icon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppIcon.appiconset/AppIcon-512@2x.png | Bin 110522 -> 135540 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png b/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png index adf6ba01dbe256605c5152ac1fd78ae99aaa2a8d..9048530e848c27d1931455664fbdc23774d191b3 100644 GIT binary patch literal 135540 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 C`SD)> From 53c2f3a0533b0fea7116ce38f3784ae10e01d03e Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sun, 24 Sep 2023 10:07:21 +0200 Subject: [PATCH 92/97] Optimise view model --- .../fellowship/mealmaestro/config/Neo4jConfig.java | 2 +- .../mealmaestro/models/neo4j/ViewModel.java | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/config/Neo4jConfig.java b/backend/src/main/java/fellowship/mealmaestro/config/Neo4jConfig.java index f5ac5198..5501a7e6 100644 --- a/backend/src/main/java/fellowship/mealmaestro/config/Neo4jConfig.java +++ b/backend/src/main/java/fellowship/mealmaestro/config/Neo4jConfig.java @@ -38,7 +38,7 @@ public Driver neo4jDriver() { username = "No DB Username Found"; password = "No DB Password Found"; } - + return GraphDatabase.driver(uri, AuthTokens.basic(username, password)); } } \ No newline at end of file diff --git a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java index e1d927ce..ae2cf15b 100644 --- a/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java +++ b/backend/src/main/java/fellowship/mealmaestro/models/neo4j/ViewModel.java @@ -20,8 +20,8 @@ public class ViewModel { private List scoreValues; private List nScoreValues; - private Double max =0.0; - private Double min =0.0; + private Double max = Double.MIN_VALUE; + private Double min = Double.MAX_VALUE; public ViewModel() { } @@ -81,6 +81,16 @@ public Double normalise(Double Score) { public void normalise() { // return 2 * ((Score - min) / (max - min)) - 1; + this.max = Double.MIN_VALUE; + this.min = Double.MAX_VALUE; + for (Double score : scoreValues) { + if(score < this.min){ + this.min = score; + } + if(score > this.max){ + this.max = score; + } + } HashMap ScoreMap = new HashMap<>(); HashMap nScoreMap = new HashMap<>(); for (String ingredient : ScoreMap.keySet()) { From 2963c84b23cb7ace64f460aeab3d80f4db2c07c5 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sun, 24 Sep 2023 10:10:22 +0200 Subject: [PATCH 93/97] cleanup --- .../fellowship/mealmaestro/config/GlobalExceptionHandler.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/config/GlobalExceptionHandler.java b/backend/src/main/java/fellowship/mealmaestro/config/GlobalExceptionHandler.java index 5042f310..85e49341 100644 --- a/backend/src/main/java/fellowship/mealmaestro/config/GlobalExceptionHandler.java +++ b/backend/src/main/java/fellowship/mealmaestro/config/GlobalExceptionHandler.java @@ -1,8 +1,5 @@ package fellowship.mealmaestro.config; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice From 7517c03a38e8f062721dd7349226d6cd6e3e2ce0 Mon Sep 17 00:00:00 2001 From: skitsbi <88lerouxt@gmail.com> Date: Sun, 24 Sep 2023 10:13:11 +0200 Subject: [PATCH 94/97] last tweak for the day --- .../fellowship/mealmaestro/services/RecommendationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java index 60319d59..4305e384 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/RecommendationService.java @@ -17,7 +17,7 @@ public class RecommendationService { @Autowired private MealManagementService mealManagementService; - private final Double MIN_VALUE = -0.01; + private final Double MIN_VALUE = -0.2; public MealModel getRecommendedMeal(String mealType, String token) throws Exception { MealModel recMealModel = null; From 9f9ce1d58cc17e68c6718b60bc212e2483639306 Mon Sep 17 00:00:00 2001 From: Skulderlock <78735770+SkulderLock@users.noreply.github.com> Date: Sun, 24 Sep 2023 11:06:36 +0200 Subject: [PATCH 95/97] =?UTF-8?q?=F0=9F=90=9B=20fix=20for=20building?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/build.gradle | 1 - .../mealmaestro/services/webscraping/CheckersScraper.java | 8 ++++---- .../services/webscraping/WebscrapeService.java | 5 ++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/backend/build.gradle b/backend/build.gradle index 2cf073f5..d936900e 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -26,7 +26,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-logging' - implementation 'org.seleniumhq.selenium:selenium-java:4.12.1' implementation 'org.jsoup:jsoup:1.16.1' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' diff --git a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java index 3c6ab80b..c5548bd8 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/CheckersScraper.java @@ -112,7 +112,7 @@ public void handleCategoryLink(ToVisitLinkModel link) { Document doc; if (link.getLink().startsWith("/")) { doc = Jsoup.connect("http://www.checkers.co.za" + link.getLink()).userAgent( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.76") + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36") .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8") .header("Accept-Encoding", "gzip, deflate, br") @@ -120,7 +120,7 @@ public void handleCategoryLink(ToVisitLinkModel link) { .get(); } else { doc = Jsoup.connect(link.getLink()).userAgent( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.76") + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36") .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8") .header("Accept-Encoding", "gzip, deflate, br") @@ -203,7 +203,7 @@ public void handleProductLink(ToVisitLinkModel link) { Document doc; if (link.getLink().startsWith("/")) { doc = Jsoup.connect("http://www.checkers.co.za" + link.getLink()).userAgent( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36") + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36") .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8") .header("Accept-Encoding", "gzip, deflate, br") @@ -211,7 +211,7 @@ public void handleProductLink(ToVisitLinkModel link) { .get(); } else { doc = Jsoup.connect(link.getLink()).userAgent( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36") + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36") .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8") .header("Accept-Encoding", "gzip, deflate, br") diff --git a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java index 858f93b2..82273810 100644 --- a/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java +++ b/backend/src/main/java/fellowship/mealmaestro/services/webscraping/WebscrapeService.java @@ -29,13 +29,12 @@ public WebscrapeService(CheckersScraper checkersScraper, TaskScheduler taskSched @PostConstruct public void init() { - System.out.println("WebscrapeService init"); - startScraping(); + // startScraping(); } private LocalDateTime getStartTime() { LocalDateTime now = LocalDateTime.now(); - LocalDateTime next6AM = now.withHour(13).withMinute(55).withSecond(0); + LocalDateTime next6AM = now.withHour(6).withMinute(0).withSecond(0); if (now.isAfter(next6AM) || now.isEqual(next6AM)) { return next6AM.plusDays(1); From 94568d8a9371501afaa23c9f57e14abc71a50d9b Mon Sep 17 00:00:00 2001 From: Skulderlock <78735770+SkulderLock@users.noreply.github.com> Date: Sun, 24 Sep 2023 11:31:39 +0200 Subject: [PATCH 96/97] =?UTF-8?q?=F0=9F=90=9B=20merge=20conflict=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mealmaestro/config/GlobalExceptionHandler.java | 3 +++ .../mealmaestro/controllers/MealManagementController.java | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/config/GlobalExceptionHandler.java b/backend/src/main/java/fellowship/mealmaestro/config/GlobalExceptionHandler.java index eebdd1ba..bdb1a7ec 100644 --- a/backend/src/main/java/fellowship/mealmaestro/config/GlobalExceptionHandler.java +++ b/backend/src/main/java/fellowship/mealmaestro/config/GlobalExceptionHandler.java @@ -1,5 +1,8 @@ package fellowship.mealmaestro.config; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import fellowship.mealmaestro.config.exceptions.UserNotFoundException; diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java index 21e9955c..b713bb31 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/MealManagementController.java @@ -29,14 +29,18 @@ public class MealManagementController { private final MealManagementService mealManagementService; private final MealDatabaseService mealDatabaseService; + private final RecommendationService recommendationService; + private final LogService logService; public MealManagementController(MealManagementService mealManagementService, - MealDatabaseService mealDatabaseService) { + MealDatabaseService mealDatabaseService, RecommendationService recommendationService, + LogService logService) { this.mealManagementService = mealManagementService; this.mealDatabaseService = mealDatabaseService; + this.recommendationService = recommendationService; + this.logService = logService; } - @PostMapping("/getMealPlanForDay") public ResponseEntity> dailyMeals(@Valid @RequestBody DateModel request, @RequestHeader("Authorization") String token) { From ae0182a3039fdbc1cf427a2f2c05e2230fe9ae77 Mon Sep 17 00:00:00 2001 From: Skulderlock <78735770+SkulderLock@users.noreply.github.com> Date: Sun, 24 Sep 2023 12:03:37 +0200 Subject: [PATCH 97/97] =?UTF-8?q?=E2=9C=85=20update=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/ProductController.java | 12 +---------- .../src/app/pages/pantry/pantry.page.spec.ts | 21 +++++++++++-------- .../barcode-api/barcode-api.service.spec.ts | 6 ++++-- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/backend/src/main/java/fellowship/mealmaestro/controllers/ProductController.java b/backend/src/main/java/fellowship/mealmaestro/controllers/ProductController.java index 3bcffa10..22251ea4 100644 --- a/backend/src/main/java/fellowship/mealmaestro/controllers/ProductController.java +++ b/backend/src/main/java/fellowship/mealmaestro/controllers/ProductController.java @@ -1,7 +1,6 @@ package fellowship.mealmaestro.controllers; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; @@ -10,17 +9,14 @@ import fellowship.mealmaestro.models.mongo.findBarcodeRequest; import fellowship.mealmaestro.models.mongo.FoodModelM; import fellowship.mealmaestro.services.BarcodeService; -import fellowship.mealmaestro.services.webscraping.CheckersScraper; @RestController public class ProductController { private final BarcodeService barcodeService; - private final CheckersScraper checkersScraper; - public ProductController(BarcodeService barcodeService, CheckersScraper checkersScraper) { + public ProductController(BarcodeService barcodeService) { this.barcodeService = barcodeService; - this.checkersScraper = checkersScraper; } @PostMapping("/findProduct") @@ -40,10 +36,4 @@ public ResponseEntity addProduct(@RequestBody FoodModelM product, } return ResponseEntity.ok(barcodeService.addProduct(product)); } - - @GetMapping("/loc") - public ResponseEntity loc() { - checkersScraper.getLocLinks(); - return ResponseEntity.ok("loc"); - } } diff --git a/frontend/src/app/pages/pantry/pantry.page.spec.ts b/frontend/src/app/pages/pantry/pantry.page.spec.ts index 474e5a82..64e9593a 100644 --- a/frontend/src/app/pages/pantry/pantry.page.spec.ts +++ b/frontend/src/app/pages/pantry/pantry.page.spec.ts @@ -6,6 +6,7 @@ import { of } from 'rxjs'; import { FoodItemI } from '../../models/interfaces'; import { AuthenticationService, + BarcodeApiService, PantryApiService, ShoppingListApiService, } from '../../services/services'; @@ -17,6 +18,7 @@ describe('PantryPage', () => { let mockPantryService: jasmine.SpyObj; let mockShoppingListService: jasmine.SpyObj; let mockAuthService: jasmine.SpyObj; + let mockBarcodeService: jasmine.SpyObj; let mockItems: FoodItemI[]; beforeEach(async () => { @@ -31,11 +33,15 @@ describe('PantryPage', () => { 'deleteShoppingListItem', ]); mockAuthService = jasmine.createSpyObj('AuthenticationService', ['logout']); + mockBarcodeService = jasmine.createSpyObj('BarcodeApiService', [ + 'findProduct', + ]); mockItems = [ { name: 'test', quantity: 1, unit: 'pcs', + price: 2, }, { name: 'test2', @@ -53,6 +59,10 @@ describe('PantryPage', () => { body: mockItems[0], status: 200, }); + const barcodeResponse = new HttpResponse({ + body: mockItems[0], + status: 200, + }); mockPantryService.getPantryItems.and.returnValue(of(itemsResponse)); mockPantryService.addToPantry.and.returnValue(of(itemResponse)); @@ -64,6 +74,7 @@ describe('PantryPage', () => { mockShoppingListService.deleteShoppingListItem.and.returnValue( of(emptyResponse) ); + mockBarcodeService.findProduct.and.returnValue(of(barcodeResponse)); await TestBed.configureTestingModule({ imports: [IonicModule, PantryPage], @@ -71,6 +82,7 @@ describe('PantryPage', () => { { provide: PantryApiService, useValue: mockPantryService }, { provide: ShoppingListApiService, useValue: mockShoppingListService }, { provide: AuthenticationService, useValue: mockAuthService }, + { provide: BarcodeApiService, useValue: mockBarcodeService }, ], }).compileComponents(); @@ -83,15 +95,6 @@ describe('PantryPage', () => { expect(component).toBeTruthy(); }); - it('#ngOnInit should call getPantryItems and getShoppingListItems', () => { - component.ngOnInit(); - expect(mockPantryService.getPantryItems).toHaveBeenCalled(); - expect(mockShoppingListService.getShoppingListItems).toHaveBeenCalled(); - - expect(component.pantryItems).toEqual(mockItems); - expect(component.shoppingItems).toEqual(mockItems); - }); - it('#addItemToPantry should call addToPantry', () => { component.addItemToPantry({ detail: { role: 'confirm', data: mockItems[0] }, diff --git a/frontend/src/app/services/barcode-api/barcode-api.service.spec.ts b/frontend/src/app/services/barcode-api/barcode-api.service.spec.ts index e07f0099..3a5ffe56 100644 --- a/frontend/src/app/services/barcode-api/barcode-api.service.spec.ts +++ b/frontend/src/app/services/barcode-api/barcode-api.service.spec.ts @@ -1,13 +1,15 @@ import { TestBed } from '@angular/core/testing'; import { BarcodeApiService } from './barcode-api.service'; +import { HttpClient } from '@angular/common/http'; describe('BarcodeApiService', () => { let service: BarcodeApiService; + let httpClientSpy: jasmine.SpyObj; beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(BarcodeApiService); + service = new BarcodeApiService(httpClientSpy as any); + httpClientSpy = jasmine.createSpyObj('HttpClient', ['post']); }); it('should be created', () => {