From 42ff1f762ff3a5f0966ea783e8a90d6f2b823e4c Mon Sep 17 00:00:00 2001 From: Naida Vatric Date: Sat, 21 Dec 2019 02:20:26 +0100 Subject: [PATCH 01/22] Changed to avoid falsy values and not defined realestate parametrs. --- app/helpers/db/realEstate.js | 16 ++-- app/helpers/db/searchRequest.js | 134 ++++++++++++++++++-------------- 2 files changed, 83 insertions(+), 67 deletions(-) diff --git a/app/helpers/db/realEstate.js b/app/helpers/db/realEstate.js index ebeb84c..55068da 100644 --- a/app/helpers/db/realEstate.js +++ b/app/helpers/db/realEstate.js @@ -175,8 +175,8 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { [Op.and]: geoSearchQueryPart }; - //Every other attribute is checked separately and included in query only if it is defined - if (gardenSizeMax && gardenSizeMin) { + //Every other attribute is checked separately and included in query only if it is defined/not null + if (gardenSizeMax!=null && gardenSizeMin!=null) { query.gardenSize = { [Op.lte]: gardenSizeMax, [Op.gte]: gardenSizeMin @@ -192,7 +192,7 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { }; } - if (numberOfRoomsMin && numberOfRoomsMax) { + if (numberOfRoomsMin!=null && numberOfRoomsMax!=null) { query.numberOfRooms = { [Op.lte]: numberOfRoomsMax, [Op.gte]: numberOfRoomsMin @@ -208,7 +208,7 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { }; } - if (numberOfFloorsMin && numberOfFloorsMax) { + if (numberOfFloorsMin!=null && numberOfFloorsMax!=null) { query.numberOfFloors = { [Op.lte]: numberOfFloorsMax, [Op.gte]: numberOfFloorsMin @@ -224,7 +224,7 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { }; } - if (floorMin && floorMax) { + if (floorMin!=null && floorMax!=null) { query.floor = { [Op.lte]: floorMax, [Op.gte]: floorMin @@ -240,7 +240,7 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { }; } - if (balcony) { + if (balcony!=null) { query.balcony = { [Op.eq]: balcony }; @@ -252,7 +252,7 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { }; } - if (newBuilding) { + if (newBuilding!=null) { query.newBuilding = { [Op.eq]: newBuilding }; @@ -264,7 +264,7 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { }; } - if (elevator) { + if (elevator!=null) { query.elevator = { [Op.eq]: elevator }; diff --git a/app/helpers/db/searchRequest.js b/app/helpers/db/searchRequest.js index 808637a..4596fd5 100644 --- a/app/helpers/db/searchRequest.js +++ b/app/helpers/db/searchRequest.js @@ -61,10 +61,10 @@ const findSearchRequestsForRealEstate = async realEstate => { //Needed to decide on including incomplete RealEstates data let checkForIncompleteWanted = false; - //Attributes are checked separately and included in query only if defined - //Price and area should be defined for every property + //Attributes are checked separately and included in query only if defined - not null - if (price) { + //Price and area should be defined for every property + if (price != null) { query.priceMin = { [Op.lte]: price }; @@ -73,7 +73,7 @@ const findSearchRequestsForRealEstate = async realEstate => { }; } - if (area) { + if (area != null) { query.sizeMin = { [Op.lte]: area }; @@ -84,51 +84,61 @@ const findSearchRequestsForRealEstate = async realEstate => { checkForIncompleteWanted = true; } //Other attributes can be defined or not depending on RealEstate type - if (gardenSize) { - query.gardenSizeMin = { - [Op.lte]: gardenSize - }; - query.gardenSizeMax = { - [Op.gte]: gardenSize - }; - } else if (realEstateTypeObject.hasGardenSize) { - checkForIncompleteWanted = true; + //we check what to include in query based on real estate type object + if (realEstateTypeObject.hasGardenSize) { + if (gardenSize != null) { + query.gardenSizeMin = { + [Op.lte]: gardenSize + }; + query.gardenSizeMax = { + [Op.gte]: gardenSize + }; + } else { + checkForIncompleteWanted = true; + } } - if (numberOfRooms) { - query.numberOfRoomsMin = { - [Op.lte]: numberOfRooms - }; - query.numberOfRoomsMax = { - [Op.gte]: numberOfRooms - }; - } else if (realEstateTypeObject.hasNumberOfRoom) { - checkForIncompleteWanted = true; + if (realEstateTypeObject.hasNumberOfRoom) { + if (numberOfRooms != null) { + query.numberOfRoomsMin = { + [Op.lte]: numberOfRooms + }; + query.numberOfRoomsMax = { + [Op.gte]: numberOfRooms + }; + } else { + checkForIncompleteWanted = true; + } } - if (numberOfFloors) { - query.numberOfFloorsMin = { - [Op.lte]: numberOfFloors - }; - query.numberOfFloorsMax = { - [Op.gte]: numberOfFloors - }; - } else if (realEstateTypeObject.hasNumberOfFloors) { - checkForIncompleteWanted = true; + if (realEstateTypeObject.hasNumberOfFloors) { + if (numberOfFloors != null) { + query.numberOfFloorsMin = { + [Op.lte]: numberOfFloors + }; + query.numberOfFloorsMax = { + [Op.gte]: numberOfFloors + }; + } else { + checkForIncompleteWanted = true; + } } - if (floor) { - query.floorMin = { - [Op.lte]: floor - }; - query.floorMax = { - [Op.gte]: floor - }; - } else if (realEstateTypeObject.hasFloorProp) { - checkForIncompleteWanted = true; + if (realEstateTypeObject.hasFloorProp) { + if (floor != null) { + query.floorMin = { + [Op.lte]: floor + }; + query.floorMax = { + [Op.gte]: floor + }; + } else { + checkForIncompleteWanted = true; + } } - if (accessRoadType) { + //AccessRoadType is defined - should exits for each ad and estate type + if (accessRoadType != null) { query.accessRoadType = { [Op.or]: { [Op.eq]: "ANY", @@ -139,28 +149,34 @@ const findSearchRequestsForRealEstate = async realEstate => { checkForIncompleteWanted = true; } - if (balcony) { - query.balcony = { - [Op.eq]: balcony - }; - } else if (realEstateTypeObject.hasBalconyProp) { - checkForIncompleteWanted = true; + if (realEstateTypeObject.hasBalconyProp) { + if (balcony != null) { + query.balcony = { + [Op.eq]: balcony + }; + } else { + checkForIncompleteWanted = true; + } } - if (newBuilding) { - query.newBuilding = { - [Op.eq]: newBuilding - }; - } else if (realEstateTypeObject.hasNewBuildingProp) { - checkForIncompleteWanted = true; + if (realEstateTypeObject.hasNewBuildingProp) { + if (newBuilding != null) { + query.newBuilding = { + [Op.eq]: newBuilding + }; + } else { + checkForIncompleteWanted = true; + } } - if (elevator) { - query.elevator = { - [Op.eq]: elevator - }; - } else if (realEstateTypeObject.hasElevatorProp) { - checkForIncompleteWanted = true; + if (realEstateTypeObject.hasElevatorProp) { + if (elevator != null) { + query.elevator = { + [Op.eq]: elevator + }; + } else { + checkForIncompleteWanted = true; + } } //If one of the attributes that exists for property type is null From d5d3a1f306fdfca8238db556937113d07d7ec13f Mon Sep 17 00:00:00 2001 From: Naida Vatric Date: Thu, 26 Dec 2019 23:30:05 +0100 Subject: [PATCH 02/22] Changed accesRoadType logic --- app/helpers/db/realEstate.js | 29 ++++++++++++++++++++--------- app/helpers/db/searchRequest.js | 9 ++++++--- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/app/helpers/db/realEstate.js b/app/helpers/db/realEstate.js index 55068da..2abac03 100644 --- a/app/helpers/db/realEstate.js +++ b/app/helpers/db/realEstate.js @@ -2,6 +2,8 @@ const db = require("../../models/index"); const sequelize = require("sequelize"); const Op = sequelize.Op; +const { AD_CATEGORY } = require("../../common/enums"); + const bulkUpsertRealEstates = async realEstateData => { try { const fieldsToUpdateIfDuplicate = [ @@ -102,6 +104,9 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { accessRoadType } = searchRequest; + //Needed for defining which attribute should exist or not + const realEstateTypeObject = AD_CATEGORY[realEstateType]; + const longitudeColumn = sequelize.col("locationLong"); const latitudeColumn = sequelize.col("locationLat"); @@ -175,8 +180,13 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { [Op.and]: geoSearchQueryPart }; - //Every other attribute is checked separately and included in query only if it is defined/not null - if (gardenSizeMax!=null && gardenSizeMin!=null) { + //Every other attribute is checked separately and included in query only if it is defined for real estate type + + if ( + realEstateTypeObject.hasGardenSize && + gardenSizeMax != null && + gardenSizeMin != null + ) { query.gardenSize = { [Op.lte]: gardenSizeMax, [Op.gte]: gardenSizeMin @@ -192,7 +202,7 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { }; } - if (numberOfRoomsMin!=null && numberOfRoomsMax!=null) { + if (realEstateTypeObject.hasNumberOfRoom && numberOfRoomsMin != null && numberOfRoomsMax != null) { query.numberOfRooms = { [Op.lte]: numberOfRoomsMax, [Op.gte]: numberOfRoomsMin @@ -208,7 +218,7 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { }; } - if (numberOfFloorsMin!=null && numberOfFloorsMax!=null) { + if (realEstateTypeObject.hasNumberOfFloors && numberOfFloorsMin != null && numberOfFloorsMax != null) { query.numberOfFloors = { [Op.lte]: numberOfFloorsMax, [Op.gte]: numberOfFloorsMin @@ -224,7 +234,7 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { }; } - if (floorMin!=null && floorMax!=null) { + if (realEstateTypeObject.hasFloorProp && floorMin != null && floorMax != null) { query.floor = { [Op.lte]: floorMax, [Op.gte]: floorMin @@ -240,7 +250,7 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { }; } - if (balcony!=null) { + if (realEstateTypeObject.hasBalconyProp && balcony != null) { query.balcony = { [Op.eq]: balcony }; @@ -252,7 +262,7 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { }; } - if (newBuilding!=null) { + if (realEstateTypeObject.hasNewBuildingProp && newBuilding != null) { query.newBuilding = { [Op.eq]: newBuilding }; @@ -264,7 +274,7 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { }; } - if (elevator!=null) { + if (realEstateTypeObject.hasElevatorProp && elevator != null) { query.elevator = { [Op.eq]: elevator }; @@ -275,7 +285,8 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { } }; } - + //If user wants 'ANY' road type acces then it is not included in query - + //returns every road type and null values if (accessRoadType !== "ANY") { query.accessRoadType = { [Op.eq]: accessRoadType diff --git a/app/helpers/db/searchRequest.js b/app/helpers/db/searchRequest.js index 4596fd5..b3e4e29 100644 --- a/app/helpers/db/searchRequest.js +++ b/app/helpers/db/searchRequest.js @@ -137,7 +137,7 @@ const findSearchRequestsForRealEstate = async realEstate => { } } - //AccessRoadType is defined - should exits for each ad and estate type + //AccessRoadType is defined - should exists for each ad and estate type if (accessRoadType != null) { query.accessRoadType = { [Op.or]: { @@ -145,8 +145,11 @@ const findSearchRequestsForRealEstate = async realEstate => { [Op.eq]: accessRoadType } }; - } else if (realEstateTypeObject.hasAccesRoadType) { - checkForIncompleteWanted = true; + } else { + //Null values are returned for user request that wanted ANY acces road type + query.accessRoadType = { + [Op.eq]: "ANY" + }; } if (realEstateTypeObject.hasBalconyProp) { From fa4e0d64dec7579120a6d6753aaf8bbbd580019d Mon Sep 17 00:00:00 2001 From: Naida Vatric Date: Mon, 6 Jan 2020 23:59:56 +0100 Subject: [PATCH 03/22] Changed email content to show number of all matching real estates. --- app/helpers/emailContentGenerator.js | 6 +++++- app/services/notificationService.js | 13 ++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app/helpers/emailContentGenerator.js b/app/helpers/emailContentGenerator.js index 861bfb2..2aa9e4b 100644 --- a/app/helpers/emailContentGenerator.js +++ b/app/helpers/emailContentGenerator.js @@ -23,16 +23,19 @@ const generateRealEstateLinks = realEstates => { const generateNotificationEmail = ( realEstates, searchRequestId, + noAllRealEstates, dailyNotification = false ) => { const truncateList = realEstates.length > MAX_REAL_ESTATES_IN_EMAIL; + const realEstatesToShow = truncateList ? realEstates.slice(0, MAX_REAL_ESTATES_IN_EMAIL) : realEstates; const allRealEstatesLink = `${APP_URL}/nekretnine/${searchRequestId}`; + const realEstateLinks = generateRealEstateLinks(realEstatesToShow); - const moreRealEstates = `
Kompletan spisak nekretnina možete pogledati na listi nekretnina
`; + const moreRealEstates = `
Kompletan spisak nekretnina (${noAllRealEstates}) možete pogledati na listi nekretnina
`; const emailFooter = generateEmailFooter(searchRequestId); const asapMessageBody = realEstates.length > 1 @@ -70,6 +73,7 @@ const generateNewSearchRequestEmail = (searchRequest, matchingRealEstates) => { } = searchRequest; const realEstateLinks = generateRealEstateLinks(matchingRealEstates); + const instantRealEstatesText = `
U međuvremenu pogledajte neke od nedavno objavljenih nekretnina koje odgovaraju Vašim uslovima pretrage :
diff --git a/app/services/notificationService.js b/app/services/notificationService.js index 7495e17..7a4a62a 100644 --- a/app/services/notificationService.js +++ b/app/services/notificationService.js @@ -8,7 +8,10 @@ const { generateNewSearchRequestEmail, generateEmailSubject } = require("../helpers/emailContentGenerator"); -const { findNotNotifiedMatches } = require("../helpers/db/searchRequestMatch"); +const { + findNotNotifiedMatches, + findRealEstatesForSearchRequest +} = require("../helpers/db/searchRequestMatch"); const { sendEmail } = require("../services/emailService"); const notifyForNewRealEstates = async newRealEstates => { @@ -39,10 +42,18 @@ const notifyMatches = async (matches, dailyNotification = false) => { const { email, subscribed } = searchRequest; if (notifyNow && subscribed) { const allMatchingRealEstates = matches[id].realEstates || []; + + //Variable allMatchingRealEstates are real estates that are "new" on the market + //the ones that we notify user in this moment, not all that already exists in db + //New variable allRealEstates are all real estates that exists in db for search req + const allRealEstates = await findRealEstatesForSearchRequest(id); + const noAllRealEstates = allRealEstates.length; + if (allMatchingRealEstates.length > 0) { const emailContent = generateNotificationEmail( allMatchingRealEstates, id, + noAllRealEstates, dailyNotification ); const emailSubject = generateEmailSubject( From d23ddf849f9128eee981346a93e34ca32b16ecee Mon Sep 17 00:00:00 2001 From: Naida Vatric Date: Tue, 7 Jan 2020 01:06:22 +0100 Subject: [PATCH 04/22] Results title text made into link. --- app/public/main.css | 4 ++++ app/views/realEstates.ejs | 25 ++++++++++++++----------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/app/public/main.css b/app/public/main.css index 8d128b7..2258be6 100644 --- a/app/public/main.css +++ b/app/public/main.css @@ -154,3 +154,7 @@ h3 { margin-top: 2rem; margin-bottom: 1rem; } + +.estates-link { + color: rgba(0, 0, 0, 0.87); +} diff --git a/app/views/realEstates.ejs b/app/views/realEstates.ejs index a59d794..3e94a1f 100644 --- a/app/views/realEstates.ejs +++ b/app/views/realEstates.ejs @@ -1,13 +1,16 @@
-
+ +
+ + <% } %> + +
From 49161c1b605fb0fd4c10186b77d2de80e2023fd9 Mon Sep 17 00:00:00 2001 From: Naida Vatric Date: Thu, 9 Jan 2020 12:19:19 +0100 Subject: [PATCH 05/22] WIP Changed redirecting for VIP ads. --- app/common/enums.js | 3 +- app/controllers/redirect.js | 5 ++- app/crawler/specificCrawlers/prostor.js | 6 ++-- app/views/redirect.ejs | 47 +++++++++++++++---------- 4 files changed, 39 insertions(+), 22 deletions(-) diff --git a/app/common/enums.js b/app/common/enums.js index 33cb41e..85ed553 100644 --- a/app/common/enums.js +++ b/app/common/enums.js @@ -216,7 +216,8 @@ const AD_STATUS = { STATUS_DELETED: 4, STATUS_URGENT: 5, STATUS_DISCOUNTED: 6, - STATUS_RENTED: 7 + STATUS_RENTED: 7, + STATUS_VIP: 8 }; const AD_AGENCY = { diff --git a/app/controllers/redirect.js b/app/controllers/redirect.js index 9975ab2..eb4e505 100644 --- a/app/controllers/redirect.js +++ b/app/controllers/redirect.js @@ -1,9 +1,11 @@ const { getRealEstateById } = require("../helpers/db/realEstate"); +const { AD_STATUS } = require("../common/enums"); const getRedirect = async (req, res) => { const id = req.params.id || null; let error = false; let redirectUrl = undefined; + let vipAd = undefined; if (!id) { error = true; } else { @@ -13,6 +15,7 @@ const getRedirect = async (req, res) => { error = true; } else { redirectUrl = realEstate.url; + vipAd = realEstate.adStatus === AD_STATUS.STATUS_VIP; } } catch (e) { error = true; @@ -24,7 +27,7 @@ const getRedirect = async (req, res) => { res.render("notFound", { title }); } else { const title = "Preusmjeravanje"; - res.render("redirect", { title, redirectUrl }); + res.render("redirect", { title, redirectUrl, vipAd }); } }; diff --git a/app/crawler/specificCrawlers/prostor.js b/app/crawler/specificCrawlers/prostor.js index cb1d3f9..fb7a52f 100644 --- a/app/crawler/specificCrawlers/prostor.js +++ b/app/crawler/specificCrawlers/prostor.js @@ -182,8 +182,8 @@ class ProstorCrawler { async scrapeAd(realEstate) { const { lat, lng, property_name, price, size, link, status } = realEstate; - const url = `https://prostor.ba${link}`; - // console.log("[PROSTOR] Scraping : ", url); + + //console.log("[PROSTOR] Scraping : ", url); try { const adPageSource = await fetch(url); const body = await adPageSource.text(); @@ -548,6 +548,8 @@ class ProstorCrawler { return AD_STATUS.STATUS_SOLD; case "Iznajmljeno": return AD_STATUS.STATUS_RENTED; + case "VIP ponuda": + return AD_STATUS.STATUS_VIP; default: console.log("[PROSTOR] Unknown AD_STATUS : [", statusText, "]"); return AD_STATUS.STATUS_NORMAL; diff --git a/app/views/redirect.ejs b/app/views/redirect.ejs index 52233cb..3346bcb 100644 --- a/app/views/redirect.ejs +++ b/app/views/redirect.ejs @@ -1,26 +1,37 @@ -

+

-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+<% if(vipAd) { %> +<% } else { %> + +<% }%> + From 1658325c4b45d95a7a0efc26c6d56e8ba755ad18 Mon Sep 17 00:00:00 2001 From: Naida Vatric Date: Fri, 10 Jan 2020 19:20:26 +0100 Subject: [PATCH 06/22] WIP Fake vip ads. --- app/crawler/specificCrawlers/prostor.js | 17 +++++++++++++++-- app/views/redirect.ejs | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/crawler/specificCrawlers/prostor.js b/app/crawler/specificCrawlers/prostor.js index fb7a52f..3d180df 100644 --- a/app/crawler/specificCrawlers/prostor.js +++ b/app/crawler/specificCrawlers/prostor.js @@ -183,7 +183,19 @@ class ProstorCrawler { async scrapeAd(realEstate) { const { lat, lng, property_name, price, size, link, status } = realEstate; - //console.log("[PROSTOR] Scraping : ", url); + //Status information is given already in realestate list + //For VIP Ads status ='' canot be used, but also area='0' we will use that temporary + //It is weird because yesterday it said 'VIP ponuda' ??? + const adStatus = + size === "0" + ? ProstorCrawler.getStatusId("VIP ponuda") + : ProstorCrawler.getStatusId(status); + // + console.log("adStatus", adStatus); + + const url = `https://prostor.ba${link}`; + + // console.log("[PROSTOR] Scraping : ", url); try { const adPageSource = await fetch(url); const body = await adPageSource.text(); @@ -330,7 +342,6 @@ class ProstorCrawler { furnishingType = FURNISHING_TYPE.NOT_FURNISHED.id; } - const adStatus = ProstorCrawler.getStatusId(status); const title = property_name; const parsedPrice = parseFloat(price.replace(/\./g, "")) || null; const parsedArea = parseFloat(size); @@ -539,6 +550,8 @@ class ProstorCrawler { } static getStatusId(statusText) { + // + console.log("statusText u funkciji", statusText); switch (statusText) { case "": return AD_STATUS.STATUS_NORMAL; diff --git a/app/views/redirect.ejs b/app/views/redirect.ejs index 3346bcb..5b111cc 100644 --- a/app/views/redirect.ejs +++ b/app/views/redirect.ejs @@ -32,6 +32,6 @@ From 64e483589915fbbdf3d65db0452c2583090d9c74 Mon Sep 17 00:00:00 2001 From: Naida Vatric Date: Fri, 10 Jan 2020 22:52:50 +0100 Subject: [PATCH 07/22] Changed redirecting for VIP ads. --- app/controllers/realEstates.js | 3 ++- app/crawler/specificCrawlers/prostor.js | 7 +++---- app/views/realEstates.ejs | 13 +++++++++++++ app/views/redirect.ejs | 16 ++++++++++++++-- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/app/controllers/realEstates.js b/app/controllers/realEstates.js index ce82765..48c1aff 100644 --- a/app/controllers/realEstates.js +++ b/app/controllers/realEstates.js @@ -2,13 +2,14 @@ const { findRealEstatesForSearchRequest } = require("../helpers/db/searchRequestMatch"); +const { AD_STATUS } = require("../common/enums"); const getRealEstates = async (req, res) => { const searchRequestId = req.params["searchRequestId"] || ""; const realEstates = await findRealEstatesForSearchRequest(searchRequestId); const title = "Nekretnine koje odgovaraju Vašim uslovima pretrage"; - res.render("realEstates", { realEstates, title }); + res.render("realEstates", { realEstates, title, AD_STATUS }); }; module.exports = { diff --git a/app/crawler/specificCrawlers/prostor.js b/app/crawler/specificCrawlers/prostor.js index 3d180df..ca4271c 100644 --- a/app/crawler/specificCrawlers/prostor.js +++ b/app/crawler/specificCrawlers/prostor.js @@ -184,14 +184,13 @@ class ProstorCrawler { const { lat, lng, property_name, price, size, link, status } = realEstate; //Status information is given already in realestate list - //For VIP Ads status ='' canot be used, but also area='0' we will use that temporary + //For VIP Ads status ='' canot be used, but no VIP ads are crawled + //We will make "fake" vip ad for RE that have size=55 //It is weird because yesterday it said 'VIP ponuda' ??? const adStatus = - size === "0" + size === "55" ? ProstorCrawler.getStatusId("VIP ponuda") : ProstorCrawler.getStatusId(status); - // - console.log("adStatus", adStatus); const url = `https://prostor.ba${link}`; diff --git a/app/views/realEstates.ejs b/app/views/realEstates.ejs index 3e94a1f..b47b744 100644 --- a/app/views/realEstates.ejs +++ b/app/views/realEstates.ejs @@ -2,6 +2,18 @@
+ <% }%> <% } %> diff --git a/app/views/redirect.ejs b/app/views/redirect.ejs index 5b111cc..e36e081 100644 --- a/app/views/redirect.ejs +++ b/app/views/redirect.ejs @@ -18,7 +18,19 @@
<% if(vipAd) { %>
-
Work in progress....
+
+ Ovaj oglas zahtijeva da budete član + Prostor.ba. +
+
+ Ulogujte se + ili napravite + novi račun, a potom otvorite oglas. +
<% } else { %>
@@ -32,6 +44,6 @@ From 850528267011f08261f1486483f4de9374df8d09 Mon Sep 17 00:00:00 2001 From: Naida Vatric Date: Sun, 12 Jan 2020 01:22:50 +0100 Subject: [PATCH 08/22] WiP Login of crawler prostor. --- app/config/appConfig.js | 8 +++- app/crawler/specificCrawlers/prostor.js | 54 ++++++++++++++++++++++--- development.env | 2 + 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/app/config/appConfig.js b/app/config/appConfig.js index 7a7887b..4403fbd 100644 --- a/app/config/appConfig.js +++ b/app/config/appConfig.js @@ -32,6 +32,11 @@ const PRINT_CRAWLER_DEBUG = process.env.PRINT_CRAWLER_DEBUG_INFO || 0; const API_MAP_KEY = process.env.API_MAP_KEY || ""; +const PROSTOR_LOGIN = { + EMAIL: process.env.PROSTOR_LOGIN_EMAIL, + PASSWORD: process.env.PROSTOR_LOGIN_PASS +}; + module.exports = { APP_PORT, APP_URL, @@ -42,5 +47,6 @@ module.exports = { MAX_REAL_ESTATES_IN_EMAIL, MAX_REAL_ESTATES_IN_FIRST_EMAIL, PRINT_CRAWLER_DEBUG, - API_MAP_KEY + API_MAP_KEY, + PROSTOR_LOGIN }; diff --git a/app/crawler/specificCrawlers/prostor.js b/app/crawler/specificCrawlers/prostor.js index ca4271c..96aab61 100644 --- a/app/crawler/specificCrawlers/prostor.js +++ b/app/crawler/specificCrawlers/prostor.js @@ -16,7 +16,8 @@ const { const { PRINT_CRAWLER_DEBUG, - DEFAULT_TIMEZONE + DEFAULT_TIMEZONE, + PROSTOR_LOGIN } = require("../../config/appConfig"); const { PROSTOR_FORCE_CRAWL } = require("../specificConfigs/prostor"); @@ -60,10 +61,12 @@ class ProstorCrawler { async crawl() { const crawlAdCategories = this.crawlerAdCategories; - + //New tag to check if crawler loged in + const login = await this.loginForScraping(PROSTOR_LOGIN); const newRealEstates = []; - - if (crawlAdCategories) { + // + console.log("login before crawl:", login); + if (crawlAdCategories && login) { const indexGenerators = []; for (const adCategory of crawlAdCategories) { indexGenerators.push(this.categoryIndexer(adCategory)); @@ -549,8 +552,6 @@ class ProstorCrawler { } static getStatusId(statusText) { - // - console.log("statusText u funkciji", statusText); switch (statusText) { case "": return AD_STATUS.STATUS_NORMAL; @@ -583,6 +584,47 @@ class ProstorCrawler { return savers[0].save(results); //so that we can use some sequelize options and information when data is inserted } + async loginForScraping(PROSTOR_LOGIN) { + console.log("PROSTOR_LOGIN", PROSTOR_LOGIN); + let logedin = false; + fetch("https://prostor.ba/moj-prostor/prijava", { + method: "POST", + body: JSON.stringify({ + email: PROSTOR_LOGIN.EMAIL, + password: PROSTOR_LOGIN.PASSWORD + }) + }) + .then(page => { + /* console.log("page", page.text()); + + const $ = cheerio.load(page); + console.log("$ ", $); + if ( + $(".icons .d-none.d-xl-inline-block.mr-2") + .text() + .indexOf("Dobrodošli") != -1 + ) { + console.log("[PROSTOR]: Crawler loged in!"); + logedin = true; + } else { + console.log("[PROSTOR]: Crawler login failed - wrong credentials!"); + } */ + + return page.text(); + }) + .then(resp => { + // console.log(resp); + const $ = cheerio.load(resp); + console.log("$ ", $("h1").text()); + }) + + .catch(err => { + console.log("[PROSTOR]: Crawler login error ", err); + }); + // + console.log("login in function:", logedin); + return logedin; + } } module.exports = ProstorCrawler; diff --git a/development.env b/development.env index 89f0a1e..150f8be 100644 --- a/development.env +++ b/development.env @@ -51,6 +51,8 @@ PROSTOR_CRAWLER_AD_CATEGORIES=comma separated list of enum names of categories t PROSTOR_IGNORED_USERNAMES=!!! This is not used for prostor crawler !!! PROSTOR_DELAY_BETWEEN_PAGES=!!! This is not used for prostor crawler !!! PROSTOR_FORCE_CRAWL=Non-zero value will force crawler to crawl all pages without stopping when known real estate is found +PROSTOR_LOGIN_EMAIL=Email of valid Prostor.ba account for crawling purposes +PROSTOR_LOGIN_PASS=Password of valid Prostor.ba account for crawling purposes #==AKTIDO== AKTIDO_MAX_PAGES=Restrict crawler to this number of pages AKTIDO_MAX_RESULTS_PER_PAGE=Only this number or less results from one page will be scraped and saved From e70901d3692cc6f1a9c503a59d947f2cd6a8ee58 Mon Sep 17 00:00:00 2001 From: Naida Vatric Date: Mon, 13 Jan 2020 09:12:03 +0100 Subject: [PATCH 09/22] WIP Changed login to crawler. --- app/crawler/specificCrawlers/prostor.js | 46 +++++++++++-------------- package-lock.json | 30 +++++++++++++--- package.json | 1 + 3 files changed, 47 insertions(+), 30 deletions(-) diff --git a/app/crawler/specificCrawlers/prostor.js b/app/crawler/specificCrawlers/prostor.js index 96aab61..6e32af8 100644 --- a/app/crawler/specificCrawlers/prostor.js +++ b/app/crawler/specificCrawlers/prostor.js @@ -3,6 +3,7 @@ const fetch = require("node-fetch"); const cheerio = require("cheerio"); const moment = require("moment-timezone"); +const FormData = require("form-data"); const { AD_TYPE, @@ -586,44 +587,37 @@ class ProstorCrawler { } async loginForScraping(PROSTOR_LOGIN) { console.log("PROSTOR_LOGIN", PROSTOR_LOGIN); - let logedin = false; - fetch("https://prostor.ba/moj-prostor/prijava", { + var formData = new FormData(); + formData.append("email", PROSTOR_LOGIN.EMAIL); + formData.append("password", PROSTOR_LOGIN.PASSWORD); + //When once loged in it stays loged in with same credentials. + //Do we need to log out ?? + return fetch("https://prostor.ba/moj-prostor/prijava", { method: "POST", - body: JSON.stringify({ - email: PROSTOR_LOGIN.EMAIL, - password: PROSTOR_LOGIN.PASSWORD - }) + body: formData, + headers: { Cookie: "ci_session=3a47b6e18b3b9bc146bcde1f95126cbad0f58bf7" } }) .then(page => { - /* console.log("page", page.text()); - - const $ = cheerio.load(page); - console.log("$ ", $); - if ( - $(".icons .d-none.d-xl-inline-block.mr-2") - .text() - .indexOf("Dobrodošli") != -1 - ) { - console.log("[PROSTOR]: Crawler loged in!"); - logedin = true; - } else { - console.log("[PROSTOR]: Crawler login failed - wrong credentials!"); - } */ - return page.text(); }) .then(resp => { - // console.log(resp); const $ = cheerio.load(resp); console.log("$ ", $("h1").text()); + if ( + $("h1") + .text() + .indexOf("Dobrodošli") !== -1 + ) { + console.log("[PROSTOR]: Crawler loged in!"); + return true; + } else { + console.log("[PROSTOR]: Crawler login failed - wrong credentials!"); + return false; + } }) - .catch(err => { console.log("[PROSTOR]: Crawler login error ", err); }); - // - console.log("login in function:", logedin); - return logedin; } } diff --git a/package-lock.json b/package-lock.json index 9661459..4626180 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1346,13 +1346,23 @@ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", "requires": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", + "combined-stream": "^1.0.8", "mime-types": "^2.1.12" + }, + "dependencies": { + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + } } }, "forwarded": { @@ -3430,6 +3440,18 @@ "tough-cookie": "~2.4.3", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" + }, + "dependencies": { + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + } } }, "require-directory": { diff --git a/package.json b/package.json index 75a7cc4..511f772 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "express": "^4.16.4", "express-ejs-layouts": "^2.5.0", "express-layout": "^0.1.0", + "form-data": "^3.0.0", "html-to-text": "^5.1.1", "moment": "^2.24.0", "moment-timezone": "^0.5.26", From ba43fa0713c0a841479699de2ba038ca93c5e4fa Mon Sep 17 00:00:00 2001 From: Naida Vatric Date: Mon, 13 Jan 2020 11:02:26 +0100 Subject: [PATCH 10/22] WIP Changed cookies. --- app/crawler/specificCrawlers/prostor.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/app/crawler/specificCrawlers/prostor.js b/app/crawler/specificCrawlers/prostor.js index 6e32af8..01e2402 100644 --- a/app/crawler/specificCrawlers/prostor.js +++ b/app/crawler/specificCrawlers/prostor.js @@ -587,17 +587,21 @@ class ProstorCrawler { } async loginForScraping(PROSTOR_LOGIN) { console.log("PROSTOR_LOGIN", PROSTOR_LOGIN); - var formData = new FormData(); + const prostorCookie = await this.getCookies(); + console.log("prostor cookie", prostorCookie); + let formData = new FormData(); formData.append("email", PROSTOR_LOGIN.EMAIL); formData.append("password", PROSTOR_LOGIN.PASSWORD); - //When once loged in it stays loged in with same credentials. + //When once loged in it stays loged in with same credentials. //Do we need to log out ?? return fetch("https://prostor.ba/moj-prostor/prijava", { method: "POST", body: formData, - headers: { Cookie: "ci_session=3a47b6e18b3b9bc146bcde1f95126cbad0f58bf7" } + headers: { Cookie: prostorCookie } }) .then(page => { + // + console.log("headers: ", page.headers.raw()["set-cookie"]); return page.text(); }) .then(resp => { @@ -619,6 +623,20 @@ class ProstorCrawler { console.log("[PROSTOR]: Crawler login error ", err); }); } + async getCookies() { + const getResponse = await fetch("https://prostor.ba/moj-prostor/prijava", { + headers: { Cookie: "" } + }); + const raw = getResponse.headers.raw()["set-cookie"]; + const cookie = raw + .map(datastring => { + const data = datastring.split(";"); + const cookieData = data[0]; + return cookieData; + }) + .join(";"); + return cookie; + } } module.exports = ProstorCrawler; From 511b2900961a470c80ba22d64bbab3ec57193ee9 Mon Sep 17 00:00:00 2001 From: Naida Vatric Date: Mon, 13 Jan 2020 12:05:33 +0100 Subject: [PATCH 11/22] Login to prostor.ba befoure crawl. --- app/crawler/specificCrawlers/prostor.js | 44 ++++++++++++------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/app/crawler/specificCrawlers/prostor.js b/app/crawler/specificCrawlers/prostor.js index 01e2402..04be5f3 100644 --- a/app/crawler/specificCrawlers/prostor.js +++ b/app/crawler/specificCrawlers/prostor.js @@ -62,15 +62,16 @@ class ProstorCrawler { async crawl() { const crawlAdCategories = this.crawlerAdCategories; + //We need session cookie to use login privileges + const prostorCookie = await this.getCookies(); //New tag to check if crawler loged in - const login = await this.loginForScraping(PROSTOR_LOGIN); + const login = await this.loginForScraping(PROSTOR_LOGIN, prostorCookie); const newRealEstates = []; - // - console.log("login before crawl:", login); + //Crawl only if login was successful if (crawlAdCategories && login) { const indexGenerators = []; for (const adCategory of crawlAdCategories) { - indexGenerators.push(this.categoryIndexer(adCategory)); + indexGenerators.push(this.categoryIndexer(adCategory, prostorCookie)); } let done = false; @@ -123,13 +124,14 @@ class ProstorCrawler { return newRealEstates; } - async *categoryIndexer(adCategory) { + async *categoryIndexer(adCategory, prostorCookie) { const urlAdTypePart = PROSTOR_ENUMS.PROSTOR_AD_TYPE[this.crawlerAdTypes]; const urlCategoryPart = PROSTOR_ENUMS.PROSTOR_AD_CATEGORY[adCategory]; if (urlAdTypePart !== undefined && urlCategoryPart !== undefined) { const urlPageToCrawl = `${this.baseUrl}?remove_sold=0${urlAdTypePart}${urlCategoryPart}`; const listOfAllRealEstates = await this.extractRealEstates( - urlPageToCrawl + urlPageToCrawl, + prostorCookie ); let elementToStartIndexFrom = 0; @@ -143,7 +145,8 @@ class ProstorCrawler { elementToStartIndexFrom += realEstatesForSinglePage.length; const singlePageResults = await this.indexSinglePage( - realEstatesForSinglePage + realEstatesForSinglePage, + prostorCookie ); const filteredSinglePageResults = singlePageResults.filter( @@ -167,10 +170,10 @@ class ProstorCrawler { } } - async indexSinglePage(realEstatesList) { + async indexSinglePage(realEstatesList, prostorCookie) { const asyncActions = []; for (const realEstate of realEstatesList) { - asyncActions.push(this.scrapeAd(realEstate)); + asyncActions.push(this.scrapeAd(realEstate, prostorCookie)); } try { @@ -184,7 +187,7 @@ class ProstorCrawler { } } - async scrapeAd(realEstate) { + async scrapeAd(realEstate, prostorCookie) { const { lat, lng, property_name, price, size, link, status } = realEstate; //Status information is given already in realestate list @@ -200,7 +203,9 @@ class ProstorCrawler { // console.log("[PROSTOR] Scraping : ", url); try { - const adPageSource = await fetch(url); + const adPageSource = await fetch(url, { + headers: { Cookie: prostorCookie } + }); const body = await adPageSource.text(); const $ = cheerio.load(body); @@ -422,13 +427,15 @@ class ProstorCrawler { } } - async extractRealEstates(url) { + async extractRealEstates(url, prostorCookie) { if (PRINT_CRAWLER_DEBUG) { console.log("[PROSTOR] Index page : ", url); } try { - const res = await fetch(url); + const res = await fetch(url, { + headers: { Cookie: prostorCookie } + }); const body = await res.text(); const $ = cheerio.load(body); @@ -585,28 +592,21 @@ class ProstorCrawler { return savers[0].save(results); //so that we can use some sequelize options and information when data is inserted } - async loginForScraping(PROSTOR_LOGIN) { - console.log("PROSTOR_LOGIN", PROSTOR_LOGIN); - const prostorCookie = await this.getCookies(); - console.log("prostor cookie", prostorCookie); + async loginForScraping(PROSTOR_LOGIN, prostorCookie) { let formData = new FormData(); formData.append("email", PROSTOR_LOGIN.EMAIL); formData.append("password", PROSTOR_LOGIN.PASSWORD); - //When once loged in it stays loged in with same credentials. - //Do we need to log out ?? + return fetch("https://prostor.ba/moj-prostor/prijava", { method: "POST", body: formData, headers: { Cookie: prostorCookie } }) .then(page => { - // - console.log("headers: ", page.headers.raw()["set-cookie"]); return page.text(); }) .then(resp => { const $ = cheerio.load(resp); - console.log("$ ", $("h1").text()); if ( $("h1") .text() From fc33c1210ac2397b3b962630dcd7257c0b77fce0 Mon Sep 17 00:00:00 2001 From: Naida Vatric Date: Mon, 13 Jan 2020 14:58:09 +0100 Subject: [PATCH 12/22] Add more detail to the email --- app/helpers/emailContentGenerator.js | 38 ++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/app/helpers/emailContentGenerator.js b/app/helpers/emailContentGenerator.js index 2aa9e4b..5e745bc 100644 --- a/app/helpers/emailContentGenerator.js +++ b/app/helpers/emailContentGenerator.js @@ -1,10 +1,11 @@ "use strict"; const { MAX_REAL_ESTATES_IN_EMAIL, APP_URL } = require("../config/appConfig"); -const { AD_CATEGORY } = require("../common/enums"); +const { AD_CATEGORY, AD_TYPE, EMAIL_FREQUENCY } = require("../common/enums"); -const generateEmailFooter = searchRequestId => { - return `
Ako želite prestati dobijati obavještenja za ovu pretragu, odjavite ovdje
+const generateEmailFooter = (searchRequestId, emailFrequencyTitle) => { + return `
Trenutno ste prijavljeni da obavještenja o novim nekretninama primate ${emailFrequencyTitle.toLowerCase()} .
+
Ako želite prestati dobijati obavještenja za ovu pretragu, odjavite ovdje
Ako želite pogledati ili promijeniti uslove za ovu pretragu, pogledajte ovdje

Vaš,
Kivi tim
`; @@ -34,9 +35,13 @@ const generateNotificationEmail = ( const allRealEstatesLink = `${APP_URL}/nekretnine/${searchRequestId}`; + const emailFrequencyTitle = dailyNotification + ? EMAIL_FREQUENCY.DAILY.title + : EMAIL_FREQUENCY.ASAP.title; + const realEstateLinks = generateRealEstateLinks(realEstatesToShow); const moreRealEstates = `
Kompletan spisak nekretnina (${noAllRealEstates}) možete pogledati na listi nekretnina
`; - const emailFooter = generateEmailFooter(searchRequestId); + const emailFooter = generateEmailFooter(searchRequestId, emailFrequencyTitle); const asapMessageBody = realEstates.length > 1 ? "Pronašli smo nekretnine koje odgovaraju Vašoj pretrazi" @@ -62,6 +67,28 @@ const generateNotificationEmail = ( const generateNewSearchRequestEmail = (searchRequest, matchingRealEstates) => { const realEstateType = AD_CATEGORY[searchRequest.realEstateType]; + let adTypeTitle = ""; + switch (searchRequest.adType) { + case AD_TYPE.AD_TYPE_SALE.stringId: + adTypeTitle = AD_TYPE.AD_TYPE_SALE.title; + break; + case AD_TYPE.AD_TYPE_RENT.stringId: + adTypeTitle = AD_TYPE.AD_TYPE_RENT.title; + break; + default: + adTypeTitle = "-"; + break; + } + let emailFrequencyTitle; + switch (searchRequest.emailFrequency) { + case EMAIL_FREQUENCY.ASAP.stringId: + emailFrequencyTitle = EMAIL_FREQUENCY.ASAP.title; + break; + case EMAIL_FREQUENCY.DAILY.stringId: + emailFrequencyTitle = EMAIL_FREQUENCY.DAILY.title; + break; + } + const { id, gardenSizeMin, @@ -84,13 +111,14 @@ const generateNewSearchRequestEmail = (searchRequest, matchingRealEstates) => { ? `
Kvadratura okućnice: Od ${gardenSizeMin} do ${gardenSizeMax} m2
` : ``; - const emailFooter = generateEmailFooter(id); + const emailFooter = generateEmailFooter(id, emailFrequencyTitle); return `

Zdravo

Naručili ste da Vam javimo ako se nekretnina sa navedenim uslovima pojavi u oglasima:

Tip nekretnine: ${realEstateType.title}
+
Vrsta oglasa: ${adTypeTitle}
Kvadratura nekretnine: Od ${sizeMin} do ${sizeMax} m2
${gardenSize}
Cijena: ${priceMin} do ${priceMax} KM
From e6725355a02c2515c07b912a5c89b0b63e159491 Mon Sep 17 00:00:00 2001 From: Naida Vatric Date: Wed, 15 Jan 2020 01:33:37 +0100 Subject: [PATCH 13/22] Changed sliders for different realestate types. --- app/common/enums.js | 111 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 95 insertions(+), 16 deletions(-) diff --git a/app/common/enums.js b/app/common/enums.js index 33cb41e..2c17eb7 100644 --- a/app/common/enums.js +++ b/app/common/enums.js @@ -7,7 +7,42 @@ const PRICE_SLIDER_OPTIONS_SALE = { step: 1000, connect: true }; - +const FLAT_PRICE_SLIDER_OPTIONS_SALE = { + start: [50000, 150000], + range: { + min: [0], + max: [800000] + }, + step: 5000, + connect: true +}; +const HOUSE_PRICE_SLIDER_OPTIONS_SALE = { + start: [50000, 150000], + range: { + min: [0], + max: [1500000] + }, + step: 10000, + connect: true +}; +const OFFICE_PRICE_SLIDER_OPTIONS_SALE = { + start: [15000, 50000], + range: { + min: [0], + max: [2000000] + }, + step: 2000, + connect: true +}; +const LAND_PRICE_SLIDER_OPTIONS_SALE = { + start: [40000, 80000], + range: { + min: [0], + max: [2000000] + }, + step: 10000, + connect: true +}; const PRICE_SLIDER_OPTIONS_RENT = { start: [300, 500], range: { @@ -17,18 +52,62 @@ const PRICE_SLIDER_OPTIONS_RENT = { step: 50, connect: true }; - +const FLAT_PRICE_SLIDER_OPTIONS_RENT = { + start: [300, 600], + range: { + min: [0], + max: [4000] + }, + step: 100, + connect: true +}; +const HOUSE_PRICE_SLIDER_OPTIONS_RENT = { + start: [500, 1000], + range: { + min: [0], + max: [10000] + }, + step: 100, + connect: true +}; +const OFFICE_PRICE_SLIDER_OPTIONS_RENT = { + start: [200, 1000], + range: { + min: [0], + max: [20000] + }, + step: 100, + connect: true +}; +const LAND_PRICE_SLIDER_OPTIONS_RENT = { + start: [500, 1000], + range: { + min: [0], + max: [20000] + }, + step: 100, + connect: true +}; //This will be used for Flats, Apartments, Houses const HOME_SIZE_SLIDER_OPTIONS = { start: [30, 75], range: { min: [0], - max: [400] + max: [500] }, step: 5, connect: true }; +const OFFICE_SIZE_SLIDER_OPTIONS = { + start: [30, 150], + range: { + min: [0], + max: [1200] + }, + step: 10, + connect: true +}; const GARDEN_SIZE_SLIDER_OPTIONS = { start: [100, 1000], range: { @@ -111,8 +190,8 @@ const AD_CATEGORY = { hasNumberOfRoom: true, hasNumberOfFloors: false, hasFloorProp: true, - priceSliderOptionsSale: PRICE_SLIDER_OPTIONS_SALE, - priceSliderOptionsRent: PRICE_SLIDER_OPTIONS_RENT, + priceSliderOptionsSale: FLAT_PRICE_SLIDER_OPTIONS_SALE, + priceSliderOptionsRent: FLAT_PRICE_SLIDER_OPTIONS_RENT, sizeSliderOptions: HOME_SIZE_SLIDER_OPTIONS }, HOUSE: { @@ -126,8 +205,8 @@ const AD_CATEGORY = { hasNumberOfRoom: true, hasNumberOfFloors: true, hasFloorProp: false, - priceSliderOptionsSale: PRICE_SLIDER_OPTIONS_SALE, - priceSliderOptionsRent: PRICE_SLIDER_OPTIONS_RENT, + priceSliderOptionsSale: HOUSE_PRICE_SLIDER_OPTIONS_SALE, + priceSliderOptionsRent: HOUSE_PRICE_SLIDER_OPTIONS_RENT, sizeSliderOptions: HOME_SIZE_SLIDER_OPTIONS, gardenSizeSliderOptions: GARDEN_SIZE_SLIDER_OPTIONS }, @@ -142,9 +221,9 @@ const AD_CATEGORY = { hasNumberOfRoom: true, hasNumberOfFloors: false, hasFloorProp: true, - priceSliderOptionsSale: PRICE_SLIDER_OPTIONS_SALE, - priceSliderOptionsRent: PRICE_SLIDER_OPTIONS_RENT, - sizeSliderOptions: HOME_SIZE_SLIDER_OPTIONS + priceSliderOptionsSale: OFFICE_PRICE_SLIDER_OPTIONS_SALE, + priceSliderOptionsRent: OFFICE_PRICE_SLIDER_OPTIONS_RENT, + sizeSliderOptions: OFFICE_SIZE_SLIDER_OPTIONS }, LAND: { id: "LAND", @@ -157,8 +236,8 @@ const AD_CATEGORY = { hasNumberOfRoom: false, hasNumberOfFloors: false, hasFloorProp: false, - priceSliderOptionsSale: PRICE_SLIDER_OPTIONS_SALE, - priceSliderOptionsRent: PRICE_SLIDER_OPTIONS_RENT, + priceSliderOptionsSale: LAND_PRICE_SLIDER_OPTIONS_SALE, + priceSliderOptionsRent: LAND_PRICE_SLIDER_OPTIONS_RENT, sizeSliderOptions: LAND_SIZE_SLIDER_OPTIONS }, APARTMENT: { @@ -172,8 +251,8 @@ const AD_CATEGORY = { hasNumberOfRoom: true, hasNumberOfFloors: false, hasFloorProp: true, - priceSliderOptionsSale: PRICE_SLIDER_OPTIONS_SALE, - priceSliderOptionsRent: PRICE_SLIDER_OPTIONS_RENT, + priceSliderOptionsSale: FLAT_PRICE_SLIDER_OPTIONS_SALE, + priceSliderOptionsRent: FLAT_PRICE_SLIDER_OPTIONS_RENT, sizeSliderOptions: HOME_SIZE_SLIDER_OPTIONS }, GARAGE: { @@ -202,8 +281,8 @@ const AD_CATEGORY = { hasNumberOfRoom: true, hasNumberOfFloors: true, hasFloorProp: false, - priceSliderOptionsSale: PRICE_SLIDER_OPTIONS_SALE, - priceSliderOptionsRent: PRICE_SLIDER_OPTIONS_RENT, + priceSliderOptionsSale: HOUSE_PRICE_SLIDER_OPTIONS_SALE, + priceSliderOptionsRent: HOUSE_PRICE_SLIDER_OPTIONS_RENT, sizeSliderOptions: HOME_SIZE_SLIDER_OPTIONS, gardenSizeSliderOptions: GARDEN_SIZE_SLIDER_OPTIONS } From 870b71a3c712d4ed30b5ec9a3cf85436d7456281 Mon Sep 17 00:00:00 2001 From: Naida Vatric Date: Fri, 17 Jan 2020 01:54:06 +0100 Subject: [PATCH 14/22] WIP Changed all logic for searchRequest. --- app/helpers/db/realEstate.js | 37 ++- app/helpers/db/searchRequest.js | 401 ++++++++++++++++++++++------ app/services/notificationService.js | 2 + 3 files changed, 352 insertions(+), 88 deletions(-) diff --git a/app/helpers/db/realEstate.js b/app/helpers/db/realEstate.js index 2abac03..87cf07e 100644 --- a/app/helpers/db/realEstate.js +++ b/app/helpers/db/realEstate.js @@ -104,6 +104,9 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { accessRoadType } = searchRequest; + // ++ testing + console.log("SearchRequest za koji trazimo:", searchRequest); + //Needed for defining which attribute should exist or not const realEstateTypeObject = AD_CATEGORY[realEstateType]; @@ -202,7 +205,11 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { }; } - if (realEstateTypeObject.hasNumberOfRoom && numberOfRoomsMin != null && numberOfRoomsMax != null) { + if ( + realEstateTypeObject.hasNumberOfRoom && + numberOfRoomsMin != null && + numberOfRoomsMax != null + ) { query.numberOfRooms = { [Op.lte]: numberOfRoomsMax, [Op.gte]: numberOfRoomsMin @@ -218,7 +225,11 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { }; } - if (realEstateTypeObject.hasNumberOfFloors && numberOfFloorsMin != null && numberOfFloorsMax != null) { + if ( + realEstateTypeObject.hasNumberOfFloors && + numberOfFloorsMin != null && + numberOfFloorsMax != null + ) { query.numberOfFloors = { [Op.lte]: numberOfFloorsMax, [Op.gte]: numberOfFloorsMin @@ -234,7 +245,11 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { }; } - if (realEstateTypeObject.hasFloorProp && floorMin != null && floorMax != null) { + if ( + realEstateTypeObject.hasFloorProp && + floorMin != null && + floorMax != null + ) { query.floor = { [Op.lte]: floorMax, [Op.gte]: floorMin @@ -249,8 +264,10 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { } }; } - - if (realEstateTypeObject.hasBalconyProp && balcony != null) { + //Logic for balcony, newBuilding and elevator from users side + //If true is checked, then I want characteristic to be true but, + //if it is not checked, then I dont care - it can be null or false or true + if (realEstateTypeObject.hasBalconyProp && balcony === true) { query.balcony = { [Op.eq]: balcony }; @@ -262,7 +279,7 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { }; } - if (realEstateTypeObject.hasNewBuildingProp && newBuilding != null) { + if (realEstateTypeObject.hasNewBuildingProp && newBuilding === true) { query.newBuilding = { [Op.eq]: newBuilding }; @@ -274,7 +291,7 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { }; } - if (realEstateTypeObject.hasElevatorProp && elevator != null) { + if (realEstateTypeObject.hasElevatorProp && elevator === true) { query.elevator = { [Op.eq]: elevator }; @@ -301,6 +318,12 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { const order = [["updatedAt", "desc"]]; + //++ + console.log( + "Query user to find real estates:", + includeIncompleteAds ? queryIncludeIncomplete : query + ); + return db.RealEstate.findAll({ where: includeIncompleteAds ? queryIncludeIncomplete : query, limit: maxResults, diff --git a/app/helpers/db/searchRequest.js b/app/helpers/db/searchRequest.js index b3e4e29..223ec89 100644 --- a/app/helpers/db/searchRequest.js +++ b/app/helpers/db/searchRequest.js @@ -49,99 +49,364 @@ const findSearchRequestsForRealEstate = async realEstate => { const geoSearchQueryPart = sequelize.where(contains, true); - //General query contains only attributes that are defined for every RealEstate - not null - const query = { - adType, - realEstateType, - subscribed: true, - [Op.and]: geoSearchQueryPart - }; //Needed for defining which attribute should exist or not const realEstateTypeObject = AD_CATEGORY[realEstateType]; - //Needed to decide on including incomplete RealEstates data + + // ?? Needed to decide on including incomplete RealEstates data let checkForIncompleteWanted = false; - //Attributes are checked separately and included in query only if defined - not null + //Attributes are checked separately to make different query parts - //Price and area should be defined for every property + //If price is null it will be excluded from query - it will show properties with null price values + //User always defines price and area (sliders) - not null in search req + let priceQuery = {}; if (price != null) { - query.priceMin = { - [Op.lte]: price - }; - query.priceMax = { - [Op.gte]: price + priceQuery = { + [Op.and]: [ + { + priceMin: { + [Op.lte]: price + } + }, + { + priceMax: { + [Op.gte]: price + } + } + ] }; } + let areaQuery = {}; if (area != null) { - query.sizeMin = { - [Op.lte]: area - }; - query.sizeMax = { - [Op.gte]: area + areaQuery = { + [Op.and]: [ + { + sizeMin: { + [Op.lte]: area + } + }, + { + sizeMax: { + [Op.gte]: area + } + } + ] }; } else { checkForIncompleteWanted = true; } + //Other attributes can be defined or not depending on RealEstate type //we check what to include in query based on real estate type object + let gardenSizeQuery = {}; if (realEstateTypeObject.hasGardenSize) { if (gardenSize != null) { - query.gardenSizeMin = { - [Op.lte]: gardenSize - }; - query.gardenSizeMax = { - [Op.gte]: gardenSize + gardenSizeQuery = { + [Op.and]: [ + { + gardenSizeMin: { + [Op.lte]: gardenSize + } + }, + { + gardenSizeMax: { + [Op.gte]: gardenSize + } + } + ] }; } else { checkForIncompleteWanted = true; } } + let numberOfRoomsQuery = {}; if (realEstateTypeObject.hasNumberOfRoom) { if (numberOfRooms != null) { - query.numberOfRoomsMin = { - [Op.lte]: numberOfRooms - }; - query.numberOfRoomsMax = { - [Op.gte]: numberOfRooms + //If real estate has defined number of rooms ex. 3 it returns req + // that accepts 3 rooms or ones that don't have defined number - null + //Ex. they didnt choose advanced filters at all + numberOfRoomsQuery = { + [Op.and]: [ + { + numberOfRoomsMin: { + [Op.or]: { + [Op.lte]: numberOfRooms, + [Op.is]: null + } + } + }, + { + numberOfRoomsMax: { + [Op.or]: { + [Op.gte]: numberOfRooms, + [Op.is]: null + } + } + } + ] }; } else { - checkForIncompleteWanted = true; + // If real estate dont have defined number of rooms ex. null + //It returns requests that didn't choose number of rooms - also null + //Or ones that picked some values but also picked to includeIncomplete ads + numberOfRoomsQuery = { + [Op.or]: [ + { + [Op.and]: [ + { + numberOfRoomsMin: { + [Op.is]: null + } + }, + { + numberOfRoomsMax: { + [Op.is]: null + } + } + ] + }, + { + includeIncompleteAds: { + [Op.eq]: true + } + } + ] + }; } } - + //Same logic for number of Floors and floors + let numberOfFloorsQuery = {}; if (realEstateTypeObject.hasNumberOfFloors) { if (numberOfFloors != null) { - query.numberOfFloorsMin = { - [Op.lte]: numberOfFloors - }; - query.numberOfFloorsMax = { - [Op.gte]: numberOfFloors + numberOfFloorsQuery = { + [Op.and]: [ + { + numberOfFloorsMin: { + [Op.or]: { + [Op.lte]: numberOfFloors, + [Op.is]: null + } + } + }, + { + numberOfFloorsMax: { + [Op.or]: { + [Op.gte]: numberOfFloors, + [Op.is]: null + } + } + } + ] }; } else { - checkForIncompleteWanted = true; + numberOfFloorsQuery = { + [Op.or]: [ + { + [Op.and]: [ + { + numberOfFloorsMin: { + [Op.is]: null + } + }, + { + numberOfFloorsMax: { + [Op.is]: null + } + } + ] + }, + { + includeIncompleteAds: { + [Op.eq]: true + } + } + ] + }; + } + } + let floorQuery = {}; + if (realEstateTypeObject.hasFloorProp) { + if (floor != null) { + floorQuery = { + [Op.and]: [ + { + floorMin: { + [Op.or]: { + [Op.lte]: floor, + [Op.is]: null + } + } + }, + { + floorMax: { + [Op.or]: { + [Op.gte]: floor, + [Op.is]: null + } + } + } + ] + }; + } else { + floorQuery = { + [Op.or]: [ + { + [Op.and]: [ + { + floorMin: { + [Op.is]: null + } + }, + { + floorMax: { + [Op.is]: null + } + } + ] + }, + { + includeIncompleteAds: { + [Op.eq]: true + } + } + ] + }; } } - if (realEstateTypeObject.hasFloorProp) { - if (floor != null) { - query.floorMin = { - [Op.lte]: floor + //Logic for balcony, newBuilding and elevator + //If user dont check checkbox for ex. elevator it does not mean he only wants no elevator + //If real estate characteristic =true find all req, one that wants charachertistic or dont care - dont need query + //If real estate characteristic = false, find all req exept for ones that wants characteristic to be true + //If real estate characteristic = null, dont know if true or false, find req that dont care or want char and want incomplete ads + let balconyQuery = {}; + if (realEstateTypeObject.hasBalconyProp && balcony !== true) { + if (balcony === false) { + balconyQuery = { + balcony: { + [Op.ne]: true + } }; - query.floorMax = { - [Op.gte]: floor + } else if (balcony === null) { + balconyQuery = { + [Op.or]: [ + { + balcony: { + [Op.ne]: true + } + }, + { + [Op.and]: [ + { + balcony: { + [Op.eq]: true + } + }, + { + includeIncompleteAds: { + [Op.eq]: true + } + } + ] + } + ] }; - } else { - checkForIncompleteWanted = true; } } + let newBuildingQuery = {}; + if (realEstateTypeObject.hasNewBuildingProp && newBuilding !== true) { + if (newBuilding === false) { + newBuildingQuery = { + newBuilding: { + [Op.ne]: true + } + }; + } else if (newBuilding === null) { + newBuildingQuery = { + [Op.or]: [ + { + newBuilding: { + [Op.ne]: true + } + }, + { + [Op.and]: [ + { + newBuilding: { + [Op.eq]: true + } + }, + { + includeIncompleteAds: { + [Op.eq]: true + } + } + ] + } + ] + }; + } + } + let elevatorQuery = {}; + if (realEstateTypeObject.hasElevatorProp && elevator !== true) { + if (elevator === false) { + elevatorQuery = { + elevator: { + [Op.ne]: true + } + }; + } else if (elevator === null) { + elevatorQuery = { + [Op.or]: [ + { + elevator: { + [Op.ne]: true + } + }, + { + [Op.and]: [ + { + elevator: { + [Op.eq]: true + } + }, + { + includeIncompleteAds: { + [Op.eq]: true + } + } + ] + } + ] + }; + } + } + //General query consists of each individual query + const query = { + adType, + realEstateType, + subscribed: true, + [Op.and]: [ + geoSearchQueryPart, + priceQuery, + areaQuery, + gardenSizeQuery, + numberOfRoomsQuery, + numberOfFloorsQuery, + floorQuery, + balconyQuery, + newBuildingQuery, + elevatorQuery + ] + }; //AccessRoadType is defined - should exists for each ad and estate type if (accessRoadType != null) { query.accessRoadType = { [Op.or]: { - [Op.eq]: "ANY", + [Op.like]: "ANY", [Op.eq]: accessRoadType } }; @@ -151,45 +416,19 @@ const findSearchRequestsForRealEstate = async realEstate => { [Op.eq]: "ANY" }; } - - if (realEstateTypeObject.hasBalconyProp) { - if (balcony != null) { - query.balcony = { - [Op.eq]: balcony - }; - } else { - checkForIncompleteWanted = true; - } - } - - if (realEstateTypeObject.hasNewBuildingProp) { - if (newBuilding != null) { - query.newBuilding = { - [Op.eq]: newBuilding - }; - } else { - checkForIncompleteWanted = true; - } - } - - if (realEstateTypeObject.hasElevatorProp) { - if (elevator != null) { - query.elevator = { - [Op.eq]: elevator - }; - } else { - checkForIncompleteWanted = true; - } - } - - //If one of the attributes that exists for property type is null - //we include in query to check if incomplete real estates are accepted + //Tag to check if incomplete ads are accepted in query if (checkForIncompleteWanted) { query.includeIncompleteAds = { [Op.eq]: true }; } - return await db.SearchRequest.findAll({ where: query }); + + //++ + console.log("Query koji koristimo:", query); + // + return await db.SearchRequest.findAll({ + where: query + }); }; module.exports = { diff --git a/app/services/notificationService.js b/app/services/notificationService.js index 7495e17..ab9f619 100644 --- a/app/services/notificationService.js +++ b/app/services/notificationService.js @@ -12,6 +12,8 @@ const { findNotNotifiedMatches } = require("../helpers/db/searchRequestMatch"); const { sendEmail } = require("../services/emailService"); const notifyForNewRealEstates = async newRealEstates => { + // + console.log("Real estates", newRealEstates); const matches = await matchRealEstates(newRealEstates); await notifyMatches(matches); }; From d117383802167cf9c7ef9b33455bc31c84c9145e Mon Sep 17 00:00:00 2001 From: Naida Vatric Date: Fri, 17 Jan 2020 22:58:22 +0100 Subject: [PATCH 15/22] Tested both ways for realestate and search req filters. --- app/helpers/db/realEstate.js | 9 --------- app/helpers/db/searchRequest.js | 3 --- app/services/notificationService.js | 2 -- 3 files changed, 14 deletions(-) diff --git a/app/helpers/db/realEstate.js b/app/helpers/db/realEstate.js index 87cf07e..c4c2d74 100644 --- a/app/helpers/db/realEstate.js +++ b/app/helpers/db/realEstate.js @@ -104,9 +104,6 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { accessRoadType } = searchRequest; - // ++ testing - console.log("SearchRequest za koji trazimo:", searchRequest); - //Needed for defining which attribute should exist or not const realEstateTypeObject = AD_CATEGORY[realEstateType]; @@ -318,12 +315,6 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { const order = [["updatedAt", "desc"]]; - //++ - console.log( - "Query user to find real estates:", - includeIncompleteAds ? queryIncludeIncomplete : query - ); - return db.RealEstate.findAll({ where: includeIncompleteAds ? queryIncludeIncomplete : query, limit: maxResults, diff --git a/app/helpers/db/searchRequest.js b/app/helpers/db/searchRequest.js index 223ec89..32eb54f 100644 --- a/app/helpers/db/searchRequest.js +++ b/app/helpers/db/searchRequest.js @@ -423,9 +423,6 @@ const findSearchRequestsForRealEstate = async realEstate => { }; } - //++ - console.log("Query koji koristimo:", query); - // return await db.SearchRequest.findAll({ where: query }); diff --git a/app/services/notificationService.js b/app/services/notificationService.js index ab9f619..7495e17 100644 --- a/app/services/notificationService.js +++ b/app/services/notificationService.js @@ -12,8 +12,6 @@ const { findNotNotifiedMatches } = require("../helpers/db/searchRequestMatch"); const { sendEmail } = require("../services/emailService"); const notifyForNewRealEstates = async newRealEstates => { - // - console.log("Real estates", newRealEstates); const matches = await matchRealEstates(newRealEstates); await notifyMatches(matches); }; From 0a181f742f9e4478ffefe4a778108ea5cb95d0cd Mon Sep 17 00:00:00 2001 From: Naida Vatric Date: Mon, 20 Jan 2020 23:09:02 +0100 Subject: [PATCH 16/22] WIP Added model and migration for new table priceHistory. --- .../20200120215830-add-priceHistory-table.js | 38 +++++++++++++++++++ app/models/priceHistory.js | 35 +++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 app/migrations/20200120215830-add-priceHistory-table.js create mode 100644 app/models/priceHistory.js diff --git a/app/migrations/20200120215830-add-priceHistory-table.js b/app/migrations/20200120215830-add-priceHistory-table.js new file mode 100644 index 0000000..eb8f616 --- /dev/null +++ b/app/migrations/20200120215830-add-priceHistory-table.js @@ -0,0 +1,38 @@ +"use strict"; + +module.exports = { + up: (queryInterface, Sequelize) => { + const tableFields = { + id: { + type: Sequelize.BIGINT, + autoIncrement: true, + allowNull: false, + primaryKey: true + }, + realEstateId: { + type: Sequelize.BIGINT, + allowNull: false, + references: { + model: "RealEstate", + key: "id" + }, + onUpdate: "CASCADE", + onDelete: "SET NULL" + }, + price: Sequelize.REAL, + createdAt: { + type: Sequelize.DATE, + defaultValue: Sequelize.literal("NOW()") + }, + updatedAt: { + type: Sequelize.DATE, + defaultValue: Sequelize.literal("NOW()") + } + }; + return queryInterface.createTable("PriceHistory", tableFields); + }, + + down: queryInterface => { + return queryInterface.dropTable("PriceHistory", {}); + } +}; diff --git a/app/models/priceHistory.js b/app/models/priceHistory.js new file mode 100644 index 0000000..ea9ddea --- /dev/null +++ b/app/models/priceHistory.js @@ -0,0 +1,35 @@ +"use strict"; + +module.exports = (sequalize, DataTypes) => { + const PriceHistory = sequalize.define("PriceHistory", { + id: { + type: DataTypes.BIGINT, + autoIncrement: true, + primaryKey: true, + allowNull: false + }, + realEstateId: { + type: DataTypes.BIGINT, + allowNull: false, + + references: { + model: "RealEstate", + key: "id" + }, + onUpdate: "CASCADE", + onDelete: "SET NULL" + }, + price: DataTypes.REAL + }); + + PriceHistory.associate = models => { + PriceHistory.hasMany(models.RealEstate, { + foreignKey: "id", + sourceKey: "realEstateId", + targetKey: "id", + as: "realEstates" + }); + }; + + return PriceHistory; +}; From 42eddb3aa5e6899da304be787b21dae9b38881f0 Mon Sep 17 00:00:00 2001 From: Naida Vatric Date: Tue, 21 Jan 2020 01:19:35 +0100 Subject: [PATCH 17/22] WIP Bulk create not working. --- app/crawler/savers/postgres.js | 13 ++++++++++++- app/helpers/db/priceHistory.js | 19 +++++++++++++++++++ app/helpers/db/realEstate.js | 4 +++- ... 20200121000524-add-priceHistory-table.js} | 8 ++++++-- app/models/priceHistory.js | 9 ++++++--- 5 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 app/helpers/db/priceHistory.js rename app/migrations/{20200120215830-add-priceHistory-table.js => 20200121000524-add-priceHistory-table.js} (83%) diff --git a/app/crawler/savers/postgres.js b/app/crawler/savers/postgres.js index 344e4ac..8c1553f 100644 --- a/app/crawler/savers/postgres.js +++ b/app/crawler/savers/postgres.js @@ -1,6 +1,7 @@ const moment = require("moment"); const { bulkUpsertRealEstates } = require("../../helpers/db/realEstate"); +const { bulkUpsertPriceHistory } = require("../../helpers/db/priceHistory"); class PostgresSaver { connect() { @@ -11,7 +12,17 @@ class PostgresSaver { async save(results) { const savedRecords = await bulkUpsertRealEstates(results); - + //Extruding data for price history table + const resultPrices = savedRecords.map(realEstate => { + return { + realEstateId: realEstate.dataValues.id, + price: realEstate.dataValues.price + }; + }); + const savedPrices = await bulkUpsertPriceHistory(resultPrices); + // + console.log("savedPrices", savedPrices); + // if (Array.isArray(savedRecords)) { const newRealEstates = []; const existingRealEstates = []; diff --git a/app/helpers/db/priceHistory.js b/app/helpers/db/priceHistory.js new file mode 100644 index 0000000..9d62be8 --- /dev/null +++ b/app/helpers/db/priceHistory.js @@ -0,0 +1,19 @@ +"use strict"; +const db = require("../../models/index"); +const sequelize = require("sequelize"); + +const bulkUpsertPriceHistory = async priceHistoryData => { + try { + const order = [["realEstateId", "desc"]]; + + return await db.PriceHistory.bulkCreate(priceHistoryData, { + order + }); + } catch (e) { + console.log("Error bulk upserting priceHistory : ", e); + } +}; + +module.exports = { + bulkUpsertPriceHistory +}; diff --git a/app/helpers/db/realEstate.js b/app/helpers/db/realEstate.js index ebeb84c..cb7a2ef 100644 --- a/app/helpers/db/realEstate.js +++ b/app/helpers/db/realEstate.js @@ -63,7 +63,9 @@ const bulkUpsertRealEstates = async realEstateData => { "numberOfViewsAgency" ]; const order = [["updatedAt", "desc"]]; - + // + //console.log("realEstateData:", realEstateData); + // return await db.RealEstate.bulkCreate(realEstateData, { updateOnDuplicate: fieldsToUpdateIfDuplicate, returning: true, diff --git a/app/migrations/20200120215830-add-priceHistory-table.js b/app/migrations/20200121000524-add-priceHistory-table.js similarity index 83% rename from app/migrations/20200120215830-add-priceHistory-table.js rename to app/migrations/20200121000524-add-priceHistory-table.js index eb8f616..6d56f99 100644 --- a/app/migrations/20200120215830-add-priceHistory-table.js +++ b/app/migrations/20200121000524-add-priceHistory-table.js @@ -12,14 +12,18 @@ module.exports = { realEstateId: { type: Sequelize.BIGINT, allowNull: false, + unique: "uniquePriceRealEstate", references: { - model: "RealEstate", + model: "RealEstates", key: "id" }, onUpdate: "CASCADE", onDelete: "SET NULL" }, - price: Sequelize.REAL, + price: { + type: Sequelize.REAL, + unique: "uniquePriceRealEstate" + }, createdAt: { type: Sequelize.DATE, defaultValue: Sequelize.literal("NOW()") diff --git a/app/models/priceHistory.js b/app/models/priceHistory.js index ea9ddea..560a4fa 100644 --- a/app/models/priceHistory.js +++ b/app/models/priceHistory.js @@ -11,15 +11,18 @@ module.exports = (sequalize, DataTypes) => { realEstateId: { type: DataTypes.BIGINT, allowNull: false, - + unique: "uniquePriceRealEstate", references: { - model: "RealEstate", + model: "RealEstates", key: "id" }, onUpdate: "CASCADE", onDelete: "SET NULL" }, - price: DataTypes.REAL + price: { + type: DataTypes.REAL, + unique: "uniquePriceRealEstate" + } }); PriceHistory.associate = models => { From 8d3f001678d29d8a69aa5baf91551e8c8f8ec32d Mon Sep 17 00:00:00 2001 From: Naida Vatric Date: Tue, 21 Jan 2020 16:28:47 +0100 Subject: [PATCH 18/22] WIP Added model, migration and bulk upsert fnc. --- app/crawler/savers/postgres.js | 9 +++- app/helpers/db/priceHistory.js | 3 +- ...00121094500-add-constraint-priceHistory.js | 10 ++++ app/models/priceHistory.js | 46 +++++++++++-------- .../20200121150443-initial-price-history.js | 19 ++++++++ 5 files changed, 65 insertions(+), 22 deletions(-) create mode 100644 app/migrations/20200121094500-add-constraint-priceHistory.js create mode 100644 app/seeders/20200121150443-initial-price-history.js diff --git a/app/crawler/savers/postgres.js b/app/crawler/savers/postgres.js index 8c1553f..8e31f9d 100644 --- a/app/crawler/savers/postgres.js +++ b/app/crawler/savers/postgres.js @@ -14,9 +14,16 @@ class PostgresSaver { const savedRecords = await bulkUpsertRealEstates(results); //Extruding data for price history table const resultPrices = savedRecords.map(realEstate => { + //Null values canot be recognized by ignore duplicates in sequalize + //Value price = 0 indicates 'cijena na upit' + const priceTmp = + realEstate.dataValues.price === null ? 0 : realEstate.dataValues.price; + return { realEstateId: realEstate.dataValues.id, - price: realEstate.dataValues.price + price: priceTmp, + createdAt: realEstate.dataValues.createdAt, + updatedAt: realEstate.dataValues.updatedAt }; }); const savedPrices = await bulkUpsertPriceHistory(resultPrices); diff --git a/app/helpers/db/priceHistory.js b/app/helpers/db/priceHistory.js index 9d62be8..3e8f69e 100644 --- a/app/helpers/db/priceHistory.js +++ b/app/helpers/db/priceHistory.js @@ -7,7 +7,8 @@ const bulkUpsertPriceHistory = async priceHistoryData => { const order = [["realEstateId", "desc"]]; return await db.PriceHistory.bulkCreate(priceHistoryData, { - order + order, + ignoreDuplicates: true }); } catch (e) { console.log("Error bulk upserting priceHistory : ", e); diff --git a/app/migrations/20200121094500-add-constraint-priceHistory.js b/app/migrations/20200121094500-add-constraint-priceHistory.js new file mode 100644 index 0000000..45c83c3 --- /dev/null +++ b/app/migrations/20200121094500-add-constraint-priceHistory.js @@ -0,0 +1,10 @@ +"use strict"; +module.exports = { + up: (queryInterface, Sequelize) => + queryInterface.addConstraint("PriceHistory", ["realEstateId", "price"], { + type: "unique", + name: "uniquePriceRealEstate" + }), + down: queryInterface => + queryInterface.removeConstraint("PriceHistory", "uniquePriceRealEstate") +}; diff --git a/app/models/priceHistory.js b/app/models/priceHistory.js index 560a4fa..495be53 100644 --- a/app/models/priceHistory.js +++ b/app/models/priceHistory.js @@ -1,29 +1,35 @@ "use strict"; module.exports = (sequalize, DataTypes) => { - const PriceHistory = sequalize.define("PriceHistory", { - id: { - type: DataTypes.BIGINT, - autoIncrement: true, - primaryKey: true, - allowNull: false - }, - realEstateId: { - type: DataTypes.BIGINT, - allowNull: false, - unique: "uniquePriceRealEstate", - references: { - model: "RealEstates", - key: "id" + const PriceHistory = sequalize.define( + "PriceHistory", + { + id: { + type: DataTypes.BIGINT, + autoIncrement: true, + primaryKey: true, + allowNull: false }, - onUpdate: "CASCADE", - onDelete: "SET NULL" + realEstateId: { + type: DataTypes.BIGINT, + allowNull: false, + unique: "uniquePriceRealEstate", + references: { + model: "RealEstates", + key: "id" + }, + onUpdate: "CASCADE", + onDelete: "SET NULL" + }, + price: { + type: DataTypes.REAL, + unique: "uniquePriceRealEstate" + } }, - price: { - type: DataTypes.REAL, - unique: "uniquePriceRealEstate" + { + freezeTableName: true } - }); + ); PriceHistory.associate = models => { PriceHistory.hasMany(models.RealEstate, { diff --git a/app/seeders/20200121150443-initial-price-history.js b/app/seeders/20200121150443-initial-price-history.js new file mode 100644 index 0000000..9d2f5b0 --- /dev/null +++ b/app/seeders/20200121150443-initial-price-history.js @@ -0,0 +1,19 @@ +"use strict"; + +module.exports = { + up: (queryInterface, Sequelize) => { + // ?? + const realEstateInitialData = await queryInterface.Sequelize.findall (); + const priceHistoryInitialData = realEstateInitialData.map (); + + return queryInterface.bulkInsert( + "PriceHistory", + priceHistoryInitialData, + {} + ); + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.bulkDelete("PriceHistory", null, {}); + } +}; From c6f0e039a58105c9f8538df18fdd0f4bcd443102 Mon Sep 17 00:00:00 2001 From: Naida Vatric Date: Tue, 21 Jan 2020 23:12:04 +0100 Subject: [PATCH 19/22] Added price history log. --- app/crawler/savers/postgres.js | 4 +-- app/helpers/db/realEstate.js | 4 +-- .../20200121150443-initial-price-history.js | 27 ++++++++++++++----- package.json | 1 + 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/app/crawler/savers/postgres.js b/app/crawler/savers/postgres.js index 8e31f9d..97ba285 100644 --- a/app/crawler/savers/postgres.js +++ b/app/crawler/savers/postgres.js @@ -27,9 +27,7 @@ class PostgresSaver { }; }); const savedPrices = await bulkUpsertPriceHistory(resultPrices); - // - console.log("savedPrices", savedPrices); - // + if (Array.isArray(savedRecords)) { const newRealEstates = []; const existingRealEstates = []; diff --git a/app/helpers/db/realEstate.js b/app/helpers/db/realEstate.js index cb7a2ef..ebeb84c 100644 --- a/app/helpers/db/realEstate.js +++ b/app/helpers/db/realEstate.js @@ -63,9 +63,7 @@ const bulkUpsertRealEstates = async realEstateData => { "numberOfViewsAgency" ]; const order = [["updatedAt", "desc"]]; - // - //console.log("realEstateData:", realEstateData); - // + return await db.RealEstate.bulkCreate(realEstateData, { updateOnDuplicate: fieldsToUpdateIfDuplicate, returning: true, diff --git a/app/seeders/20200121150443-initial-price-history.js b/app/seeders/20200121150443-initial-price-history.js index 9d2f5b0..0a9531a 100644 --- a/app/seeders/20200121150443-initial-price-history.js +++ b/app/seeders/20200121150443-initial-price-history.js @@ -1,11 +1,26 @@ "use strict"; +const { RealEstate } = require("../models"); + module.exports = { - up: (queryInterface, Sequelize) => { - // ?? - const realEstateInitialData = await queryInterface.Sequelize.findall (); - const priceHistoryInitialData = realEstateInitialData.map (); - + async up(queryInterface, Sequelize) { + //Reading initial data from RealEstate table in db + const realEstateInitialData = await RealEstate.findAll(); + //Extruding data for table PriceHistory + const priceHistoryInitialData = realEstateInitialData.map(realEstate => { + //Null values canot be recognized by ignore duplicates in sequalize + //Value price = 0 indicates 'cijena na upit' + const priceTmp = + realEstate.dataValues.price === null ? 0 : realEstate.dataValues.price; + + return { + realEstateId: realEstate.dataValues.id, + price: priceTmp, + createdAt: realEstate.dataValues.createdAt, + updatedAt: realEstate.dataValues.updatedAt + }; + }); + return queryInterface.bulkInsert( "PriceHistory", priceHistoryInitialData, @@ -13,7 +28,7 @@ module.exports = { ); }, - down: (queryInterface, Sequelize) => { + async down(queryInterface, Sequelize) { return queryInterface.bulkDelete("PriceHistory", null, {}); } }; diff --git a/package.json b/package.json index 75a7cc4..cdb2c5c 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "start": "node ./index.js", "start-mon": "nodemon ./index.js", "migrate": "cd app && npx sequelize db:migrate", + "seed": "cd app && npx sequelize db:seed:all", "setup": "docker build -t marketalerts . && docker run -e POSTGRES_USER=docker -e POSTGRES_PASSWORD=docker -e POSTGRES_DB=marketalerts --name pg_marketalerts -d -p 5432:5432 marketalerts && sleep 10 && npm run migrate", "docker-start": "docker start pg_marketalerts", "docker-stop": "docker stop pg_marketalerts", From 5b3491fdba0e3551dea0bbab3e7c55125b5ee0c9 Mon Sep 17 00:00:00 2001 From: Naida Vatric Date: Thu, 23 Jan 2020 10:12:56 +0100 Subject: [PATCH 20/22] Added migration and model change for searchReq table. --- .prettierignore | 1 + app/controllers/realEstateFilters.js | 8 ++++++-- ...-includeWithoutPrice-to-searchRequests-table.js | 14 ++++++++++++++ app/models/searchRequest.js | 11 ++++++++++- app/views/standardFilters.ejs | 10 ++++++++++ 5 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 .prettierignore create mode 100644 app/migrations/20200123085754-add-column-includeWithoutPrice-to-searchRequests-table.js diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..82435b1 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +*.ejs \ No newline at end of file diff --git a/app/controllers/realEstateFilters.js b/app/controllers/realEstateFilters.js index 17f5e58..252ac46 100644 --- a/app/controllers/realEstateFilters.js +++ b/app/controllers/realEstateFilters.js @@ -35,7 +35,8 @@ const getFilters = async (req, res) => { balcony, elevator, newBuilding, - accessRoadType + accessRoadType, + includeWithoutPrice } = searchRequest; const category = AD_CATEGORY[realEstateType] || AD_CATEGORY.FLAT; @@ -115,7 +116,8 @@ const getFilters = async (req, res) => { advancedSegmentSelectFilterValues, advancedRangeFilterObjects, advancedRangeFilterValues, - includeIncompleteAds + includeIncompleteAds, + includeWithoutPrice }); }; @@ -191,6 +193,7 @@ const postFilters = async (req, res) => { }); const includeIncompleteAds = req.body.includeIncompleteAds === "on"; + const includeWithoutPrice = req.body.includeWithoutPrice === "on"; const balcony = req.body.balcony === "on"; const elevator = req.body.elevator === "on"; @@ -217,6 +220,7 @@ const postFilters = async (req, res) => { searchRequest.newBuilding = newBuilding; searchRequest.includeIncompleteAds = includeIncompleteAds; + searchRequest.includeWithoutPrice = includeWithoutPrice; searchRequest.accessRoadType = accessRoadType; diff --git a/app/migrations/20200123085754-add-column-includeWithoutPrice-to-searchRequests-table.js b/app/migrations/20200123085754-add-column-includeWithoutPrice-to-searchRequests-table.js new file mode 100644 index 0000000..c118d92 --- /dev/null +++ b/app/migrations/20200123085754-add-column-includeWithoutPrice-to-searchRequests-table.js @@ -0,0 +1,14 @@ +"use strict"; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.addColumn("SearchRequests", "includeWithoutPrice", { + type: Sequelize.BOOLEAN, + defaultValue: false + }); + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.removeColumn("SearchRequests", "includeWithoutPrice"); + } +}; diff --git a/app/models/searchRequest.js b/app/models/searchRequest.js index 8a04593..0d4997f 100644 --- a/app/models/searchRequest.js +++ b/app/models/searchRequest.js @@ -15,7 +15,15 @@ module.exports = (sequelize, DataTypes) => { allowNull: false, defaultValue: { type: "Polygon", - coordinates: [[[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]]], + coordinates: [ + [ + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0] + ] + ], crs: { type: "name", properties: { name: "EPSG:4326" } } } }, @@ -71,6 +79,7 @@ module.exports = (sequelize, DataTypes) => { type: DataTypes.TEXT }, includeIncompleteAds: DataTypes.BOOLEAN, + includeWithoutPrice: DataTypes.BOOLEAN, balcony: DataTypes.BOOLEAN, elevator: DataTypes.BOOLEAN, newBuilding: DataTypes.BOOLEAN, diff --git a/app/views/standardFilters.ejs b/app/views/standardFilters.ejs index 27a6bba..e0526b4 100644 --- a/app/views/standardFilters.ejs +++ b/app/views/standardFilters.ejs @@ -18,6 +18,16 @@
+
+

+ +


From 98263364c723ff3a5ee8f02179c490982a7ad74c Mon Sep 17 00:00:00 2001 From: Naida Vatric Date: Thu, 23 Jan 2020 11:13:53 +0100 Subject: [PATCH 21/22] Added option to include-exclude ads without price --- app/helpers/db/realEstate.js | 55 ++++++++++++++++++++++----------- app/helpers/db/searchRequest.js | 9 +++++- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/app/helpers/db/realEstate.js b/app/helpers/db/realEstate.js index c4c2d74..0d59674 100644 --- a/app/helpers/db/realEstate.js +++ b/app/helpers/db/realEstate.js @@ -98,6 +98,7 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { floorMin, floorMax, includeIncompleteAds, + includeWithoutPrice, balcony, elevator, newBuilding, @@ -139,15 +140,6 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { const query = { adType, realEstateType, - price: { - [Op.or]: { - [Op.and]: { - [Op.lte]: priceMax, - [Op.gte]: priceMin - }, - [Op.is]: null - } - }, area: { [Op.lte]: sizeMax, [Op.gte]: sizeMin @@ -159,15 +151,6 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { const queryIncludeIncomplete = { adType, realEstateType, - price: { - [Op.or]: { - [Op.and]: { - [Op.lte]: priceMax, - [Op.gte]: priceMin - }, - [Op.is]: null - } - }, area: { [Op.or]: { [Op.and]: { @@ -180,6 +163,42 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { [Op.and]: geoSearchQueryPart }; + //Is user checked includeWithoutPrice TRUE then it should return null values of price + //If not then only check for price max and min (this is DEFAULT) + //includeIncpompleteAds does not have effect on price query + if (includeWithoutPrice) { + query.price = { + [Op.or]: { + [Op.and]: { + [Op.lte]: priceMax, + [Op.gte]: priceMin + }, + [Op.is]: null + } + }; + queryIncludeIncomplete.price = { + [Op.or]: { + [Op.and]: { + [Op.lte]: priceMax, + [Op.gte]: priceMin + }, + [Op.is]: null + } + }; + } else { + query.price = { + [Op.and]: { + [Op.lte]: priceMax, + [Op.gte]: priceMin + } + }; + queryIncludeIncomplete.price = { + [Op.and]: { + [Op.lte]: priceMax, + [Op.gte]: priceMin + } + }; + } //Every other attribute is checked separately and included in query only if it is defined for real estate type if ( diff --git a/app/helpers/db/searchRequest.js b/app/helpers/db/searchRequest.js index 32eb54f..e2633b4 100644 --- a/app/helpers/db/searchRequest.js +++ b/app/helpers/db/searchRequest.js @@ -57,7 +57,8 @@ const findSearchRequestsForRealEstate = async realEstate => { //Attributes are checked separately to make different query parts - //If price is null it will be excluded from query - it will show properties with null price values + //If real estate price is number then it searches for req that have priceMin and priceMax + //If real estate price is null it searches for req that accept ads without price //User always defines price and area (sliders) - not null in search req let priceQuery = {}; if (price != null) { @@ -75,6 +76,12 @@ const findSearchRequestsForRealEstate = async realEstate => { } ] }; + } else { + priceQuery = { + includeWithoutPrice: { + [Op.eq]: true + } + }; } let areaQuery = {}; From fa46f75dd329b501afac0a75efc935900250626c Mon Sep 17 00:00:00 2001 From: Naida Vatric Date: Tue, 28 Jan 2020 22:28:52 +0100 Subject: [PATCH 22/22] Changed default to switched on. --- app/helpers/db/realEstate.js | 4 ++-- ...add-column-includeWithoutPrice-to-searchRequests-table.js | 2 +- app/views/standardFilters.ejs | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/helpers/db/realEstate.js b/app/helpers/db/realEstate.js index 0d59674..0f77260 100644 --- a/app/helpers/db/realEstate.js +++ b/app/helpers/db/realEstate.js @@ -163,8 +163,8 @@ const findRealEstatesForSearchRequest = async (searchRequest, maxResults) => { [Op.and]: geoSearchQueryPart }; - //Is user checked includeWithoutPrice TRUE then it should return null values of price - //If not then only check for price max and min (this is DEFAULT) + //Is user unchecked includeWithoutPrice FALSE then it shouldn't return null values of price + //If not then null values are accepted (this is DEFAULT) //includeIncpompleteAds does not have effect on price query if (includeWithoutPrice) { query.price = { diff --git a/app/migrations/20200123085754-add-column-includeWithoutPrice-to-searchRequests-table.js b/app/migrations/20200123085754-add-column-includeWithoutPrice-to-searchRequests-table.js index c118d92..9173829 100644 --- a/app/migrations/20200123085754-add-column-includeWithoutPrice-to-searchRequests-table.js +++ b/app/migrations/20200123085754-add-column-includeWithoutPrice-to-searchRequests-table.js @@ -4,7 +4,7 @@ module.exports = { up: (queryInterface, Sequelize) => { return queryInterface.addColumn("SearchRequests", "includeWithoutPrice", { type: Sequelize.BOOLEAN, - defaultValue: false + defaultValue: true }); }, diff --git a/app/views/standardFilters.ejs b/app/views/standardFilters.ejs index e0526b4..43b931a 100644 --- a/app/views/standardFilters.ejs +++ b/app/views/standardFilters.ejs @@ -22,9 +22,8 @@