|
|
@@ -1,131 +1,10 @@
|
|
1
|
1
|
const Schmervice = require('@hapipal/schmervice')
|
|
2
|
2
|
const haversine = require('haversine')
|
|
3
|
|
-const config = require('../../db/data-generator/config.json')
|
|
4
|
|
-
|
|
5
|
|
-const _isScorableResponse = res_key_id => {
|
|
6
|
|
- let isScorable = false
|
|
7
|
|
- if(config.resKeys.includes(res_key_id)) {
|
|
8
|
|
- isScorable = true
|
|
9
|
|
- }
|
|
10
|
|
- return isScorable
|
|
11
|
|
-}
|
|
12
|
|
-
|
|
13
|
|
-const scoreResponses = (seeker, potentialMatch, prescoreLookup) => {
|
|
14
|
|
- if (seeker.responses.length != potentialMatch.responses.length)
|
|
15
|
|
- return {
|
|
16
|
|
- error: `complete responses for profile: ${seeker.profile_id} unqeual to profile: ${potentialMatch.profile_id} | ${seeker.responses.length}:${potentialMatch.responses.length}`,
|
|
17
|
|
- }
|
|
18
|
|
-
|
|
19
|
|
- const aRes = seeker.responses.filter(
|
|
20
|
|
- res => _isScorableResponse(res.response_key_id)
|
|
21
|
|
- )
|
|
22
|
|
- const bRes = potentialMatch.responses.filter(
|
|
23
|
|
- res => _isScorableResponse(res.response_key_id)
|
|
24
|
|
- )
|
|
25
|
|
-
|
|
26
|
|
- const composite = []
|
|
27
|
|
- while (aRes.length + bRes.length > 0) {
|
|
28
|
|
- const mKey = resList => {
|
|
29
|
|
- let el = resList.shift()
|
|
30
|
|
- let pair = el.val
|
|
31
|
|
- el = resList.shift()
|
|
32
|
|
- return `${pair}:${el.val}`
|
|
33
|
|
- }
|
|
34
|
|
- composite.push(prescoreLookup[mKey(aRes)][mKey(bRes)])
|
|
35
|
|
- }
|
|
36
|
|
- const scoreAvg = composite.reduce((a, b) => a + b) / composite.length
|
|
37
|
|
- return {
|
|
38
|
|
- total: Math.round(scoreAvg),
|
|
39
|
|
- aspects: composite,
|
|
40
|
|
- }
|
|
41
|
|
-}
|
|
42
|
|
-const filterByDistance = (profileList, max) => {
|
|
43
|
|
- return profileList.filter(profile => {
|
|
44
|
|
- const profileDistance = Math.floor(parseFloat(profile.distance) * 100)
|
|
45
|
|
- const adjustedMaxDistance = Math.floor(parseFloat(max) * 100)
|
|
46
|
|
- return profileDistance <= adjustedMaxDistance
|
|
47
|
|
- })
|
|
48
|
|
-}
|
|
49
|
|
-const scoreAll = (profileList, userProfile, prescoreLookup) => {
|
|
50
|
|
- return profileList.map(profile => {
|
|
51
|
|
- return {
|
|
52
|
|
- // Uncomment to return the whole profile
|
|
53
|
|
- // ...profile,
|
|
54
|
|
- profile_id: profile.profile_id,
|
|
55
|
|
- score: scoreResponses(userProfile, profile, prescoreLookup),
|
|
56
|
|
- distance: profile.distance,
|
|
57
|
|
- }
|
|
58
|
|
- })
|
|
59
|
|
-}
|
|
60
|
|
-/**
|
|
61
|
|
- * Grab the zip code string
|
|
62
|
|
- */
|
|
63
|
|
-const getZipCodeFromProfile = profile => {
|
|
64
|
|
- // There should only be one zip code entry per profile
|
|
65
|
|
- let zipRes = profile.responses.find(
|
|
66
|
|
- res => res.response_key_id == 7
|
|
67
|
|
- )
|
|
68
|
|
- return zipRes.val
|
|
69
|
|
-}
|
|
70
|
|
-
|
|
71
|
|
-const makeScoreLookup = (aspects, labels) => {
|
|
72
|
|
- const labelLookup = {}
|
|
73
|
|
- labels.forEach(label => (labelLookup[label.aspect_id] = label))
|
|
74
|
|
-
|
|
75
|
|
- const scoreLookup = {}
|
|
76
|
|
- aspects.forEach(aspect => {
|
|
77
|
|
- const key = labelLookup[aspect.aspect_id]
|
|
78
|
|
- scoreLookup[`${key.a}:${key.b}`] = {}
|
|
79
|
|
- Object.keys(aspect).forEach(aspect_id => {
|
|
80
|
|
- if (!labelLookup[aspect_id]) return
|
|
81
|
|
- const comp = labelLookup[aspect_id]
|
|
82
|
|
- const score = aspect[aspect_id]
|
|
83
|
|
- scoreLookup[`${key.a}:${key.b}`][`${comp.a}:${comp.b}`] = score
|
|
84
|
|
- })
|
|
85
|
|
- })
|
|
86
|
|
- return scoreLookup
|
|
87
|
|
-}
|
|
88
|
|
-
|
|
89
|
|
-/**
|
|
90
|
|
- * Class to hold our retrieved profile information
|
|
91
|
|
- * in a convenient wrapper
|
|
92
|
|
- * !: This needs to match the responseSchema in profiles.js
|
|
93
|
|
- */
|
|
94
|
|
-class CompleteProfile {
|
|
95
|
|
- constructor(profile, type) {
|
|
96
|
|
- this.user_id = profile.user_id // int user_id
|
|
97
|
|
- this.profile_id = profile.profile_id // int profile_id
|
|
98
|
|
- this.user_name = profile.user.user_name // string user_name
|
|
99
|
|
- this.user_email = profile.user.user_email
|
|
100
|
|
- this.responses = []
|
|
101
|
|
- this.user_type = type
|
|
102
|
|
- this.tags = profile.tags.filter(t => t.category != 'reveal')
|
|
103
|
|
- this.reveal = profile.tags.filter(t => t.category == 'reveal')
|
|
104
|
|
-
|
|
105
|
|
- // TODO: generalize this for multiple images, and languages
|
|
106
|
|
- this.profile_description = ''
|
|
107
|
|
- this.profile_media = []
|
|
108
|
|
- this.profile_languages = []
|
|
109
|
|
- this.profile_prefs = {}
|
|
110
|
|
-
|
|
111
|
|
- // TODO: filter these correctly
|
|
112
|
|
- if (profile?.responses?.length) {
|
|
113
|
|
- // [] of all "profile" responses
|
|
114
|
|
- this.responses = profile.responses
|
|
115
|
|
- // image, language, duration, presence, blurb, urgency, role, pronouns, distance
|
|
116
|
|
- const prefs = ['zipcode', 'duration', 'presence', 'urgency', 'role', 'pronouns', 'distance']
|
|
117
|
|
- const prefsKeys = config.prefKeys
|
|
118
|
|
- prefs.forEach((pref, i) => {
|
|
119
|
|
- this.profile_prefs[pref] = this.responses.filter(
|
|
120
|
|
- r => r.response_key_id === prefsKeys[i]
|
|
121
|
|
- )[0]
|
|
122
|
|
- })
|
|
123
|
|
- this.profile_description = this.responses.filter(r => r.response_key_id === config.blurbKey).map(r => r.val)[0]
|
|
124
|
|
- this.profile_media = this.responses.filter(r => r.response_key_id === config.mediaKey).map(r => r.val)
|
|
125
|
|
- this.profile_languages = this.responses.filter(r => r.response_key_id === config.langKey).map(r => r.val)
|
|
126
|
|
- }
|
|
127
|
|
- }
|
|
128
|
|
-}
|
|
|
3
|
+const config = require('../../../db/data-generator/config.json')
|
|
|
4
|
+const profiler = require('./profiler')
|
|
|
5
|
+const scoring = require('./scorer')
|
|
|
6
|
+const zipcoder = require('./zipcoder')
|
|
|
7
|
+const tagger = require('./tagger')
|
|
129
|
8
|
|
|
130
|
9
|
module.exports = class ProfileService extends Schmervice.Service {
|
|
131
|
10
|
constructor(...args) {
|
|
|
@@ -141,7 +20,7 @@ module.exports = class ProfileService extends Schmervice.Service {
|
|
141
|
20
|
const { Aspect, AspectLabel } = this.server.models()
|
|
142
|
21
|
const aspects = await Aspect.query()
|
|
143
|
22
|
const labels = await AspectLabel.query()
|
|
144
|
|
- this.scoreLookup = makeScoreLookup(aspects, labels)
|
|
|
23
|
+ this.scoreLookup = scoring.makeScoreLookup(aspects, labels)
|
|
145
|
24
|
}
|
|
146
|
25
|
}
|
|
147
|
26
|
async _setTagLookup() {
|
|
|
@@ -186,13 +65,8 @@ module.exports = class ProfileService extends Schmervice.Service {
|
|
186
|
65
|
.withGraphFetched('responses')
|
|
187
|
66
|
.withGraphFetched('user')
|
|
188
|
67
|
|
|
189
|
|
- if(matchingProfile?.tags.length){
|
|
190
|
|
- matchingProfile.tags = matchingProfile.tags.map(
|
|
191
|
|
- tag => this.tagLookup[tag.tag_id],
|
|
192
|
|
- )
|
|
193
|
|
- }
|
|
194
|
|
-
|
|
195
|
|
- return new CompleteProfile(matchingProfile)
|
|
|
68
|
+ tagger.setProfileTags(matchingProfile, matchingProfile, this.tagLookup)
|
|
|
69
|
+ return new profiler.CompleteProfile(matchingProfile)
|
|
196
|
70
|
}
|
|
197
|
71
|
|
|
198
|
72
|
async getCompleteProfilesFor(userId, type) {
|
|
|
@@ -208,15 +82,8 @@ module.exports = class ProfileService extends Schmervice.Service {
|
|
208
|
82
|
// CHECKTHIS: Added this because we added user.user_name to CompleteProfile
|
|
209
|
83
|
// so without this, we get undefined user_name
|
|
210
|
84
|
.withGraphFetched('user')
|
|
211
|
|
-
|
|
212
|
|
- profilesEntries.forEach(profile => {
|
|
213
|
|
- profile.tags = profile.tags.map(tag => this.tagLookup[tag.tag_id])
|
|
214
|
|
- })
|
|
215
|
|
-
|
|
216
|
|
- //** Get responses asociated with each profile_id */
|
|
217
|
|
- return profilesEntries.map(profile => {
|
|
218
|
|
- return new CompleteProfile(profile, type)
|
|
219
|
|
- })
|
|
|
85
|
+
|
|
|
86
|
+ return profiler.makeCompleteProfilesFromProfile(profilesEntries, type, this.tagLookup)
|
|
220
|
87
|
}
|
|
221
|
88
|
|
|
222
|
89
|
async getProfilesFor(profileIdArray, type, includeResponses = true) {
|
|
|
@@ -234,24 +101,13 @@ module.exports = class ProfileService extends Schmervice.Service {
|
|
234
|
101
|
// taking the info from profilesEntries
|
|
235
|
102
|
// to repack into completeProfiles
|
|
236
|
103
|
// in same order as profileIdArray
|
|
237
|
|
- const completeProfiles = []
|
|
238
|
|
- profileIdArray.forEach(pid => {
|
|
239
|
|
- profilesEntries.forEach(entry => {
|
|
240
|
|
- if (entry.profile_id == pid) {
|
|
241
|
|
- const complete = new CompleteProfile(entry, type)
|
|
242
|
|
- if (!includeResponses) {
|
|
243
|
|
- delete complete['responses']
|
|
244
|
|
- }
|
|
245
|
|
- if (entry?.tags?.length) {
|
|
246
|
|
- complete.tags = entry.tags.map(
|
|
247
|
|
- tag => this.tagLookup[tag.tag_id],
|
|
248
|
|
- )
|
|
249
|
|
- }
|
|
250
|
|
- completeProfiles.push(complete)
|
|
251
|
|
- }
|
|
252
|
|
- })
|
|
253
|
|
- })
|
|
254
|
|
- return completeProfiles
|
|
|
104
|
+ return profiler.makeCompleteProfiles(
|
|
|
105
|
+ profileIdArray,
|
|
|
106
|
+ profilesEntries,
|
|
|
107
|
+ type,
|
|
|
108
|
+ includeResponses,
|
|
|
109
|
+ this.tagLookup
|
|
|
110
|
+ )
|
|
255
|
111
|
}
|
|
256
|
112
|
|
|
257
|
113
|
/**
|
|
|
@@ -274,7 +130,7 @@ module.exports = class ProfileService extends Schmervice.Service {
|
|
274
|
130
|
* DUPLICATE:See saveResponseForProfile() line 343
|
|
275
|
131
|
*/
|
|
276
|
132
|
let convertedResponse = responseToSave
|
|
277
|
|
- if(_isScorableResponse(responseToSave.response_key_id)) {
|
|
|
133
|
+ if(scoring._isScorableResponse(responseToSave.response_key_id)) {
|
|
278
|
134
|
// Convert -3 to 0, 0 to 3, 3 to 6
|
|
279
|
135
|
const offset = (config.scoreVals.length - 1) / 2
|
|
280
|
136
|
const indexFromInput = parseInt(responseToSave.val) + offset
|
|
|
@@ -352,7 +208,7 @@ module.exports = class ProfileService extends Schmervice.Service {
|
|
352
|
208
|
* of the generated possible prescore array in config
|
|
353
|
209
|
*/
|
|
354
|
210
|
let convertedResponse = responseToSave
|
|
355
|
|
- if(_isScorableResponse(responseToSave.response_key_id)) {
|
|
|
211
|
+ if(scoring._isScorableResponse(responseToSave.response_key_id)) {
|
|
356
|
212
|
// Convert -3 to 0, 0 to 3, 3 to 6
|
|
357
|
213
|
const offset = (config.scoreVals.length - 1) / 2
|
|
358
|
214
|
const indexFromInput = parseInt(responseToSave.val) + offset
|
|
|
@@ -397,7 +253,7 @@ module.exports = class ProfileService extends Schmervice.Service {
|
|
397
|
253
|
.withGraphFetched('user')
|
|
398
|
254
|
|
|
399
|
255
|
// Move unneeded responses
|
|
400
|
|
- const userZip = getZipCodeFromProfile(userProfile)
|
|
|
256
|
+ const userZip = zipcoder.getZipCodeFromProfile(userProfile)
|
|
401
|
257
|
|
|
402
|
258
|
// Find all Profiles that are NOT of our userProfile.type
|
|
403
|
259
|
// ie. If userProfile.type == seeker, then find: poster
|
|
|
@@ -411,12 +267,12 @@ module.exports = class ProfileService extends Schmervice.Service {
|
|
411
|
267
|
return profile.user.is_poster == isPosterOpposite
|
|
412
|
268
|
}).filter(profile => {
|
|
413
|
269
|
// Only include profiles that included zipcode response
|
|
414
|
|
- return getZipCodeFromProfile(profile) ? true : false
|
|
|
270
|
+ return zipcoder.getZipCodeFromProfile(profile) ? true : false
|
|
415
|
271
|
})
|
|
416
|
272
|
|
|
417
|
273
|
const profilePlusDistance = await Promise.all(
|
|
418
|
274
|
profileIdsOfOppositeType.map(async profile => {
|
|
419
|
|
- const targetZip = getZipCodeFromProfile(profile)
|
|
|
275
|
+ const targetZip = zipcoder.getZipCodeFromProfile(profile)
|
|
420
|
276
|
|
|
421
|
277
|
if (!userZip || !targetZip)
|
|
422
|
278
|
return { ...profile, distance: [9999, distanceUnit] }
|
|
|
@@ -432,11 +288,11 @@ module.exports = class ProfileService extends Schmervice.Service {
|
|
432
|
288
|
}
|
|
433
|
289
|
}),
|
|
434
|
290
|
)
|
|
435
|
|
- const distanceFilteredProfiles = filterByDistance(
|
|
|
291
|
+ const distanceFilteredProfiles = zipcoder.filterByDistance(
|
|
436
|
292
|
profilePlusDistance,
|
|
437
|
293
|
maxDistance,
|
|
438
|
294
|
)
|
|
439
|
|
- const scoredProfilesWithDistance = scoreAll(
|
|
|
295
|
+ const scoredProfilesWithDistance = scoring.scoreAll(
|
|
440
|
296
|
distanceFilteredProfiles,
|
|
441
|
297
|
userProfile,
|
|
442
|
298
|
this.scoreLookup,
|
|
|
@@ -455,15 +311,11 @@ module.exports = class ProfileService extends Schmervice.Service {
|
|
455
|
311
|
*/
|
|
456
|
312
|
async _latLonForZip(zipCode) {
|
|
457
|
313
|
const { ZipCode } = this.server.models()
|
|
458
|
|
-
|
|
459
|
314
|
const zipInfo = await ZipCode.query().findOne(
|
|
460
|
315
|
'zip_code_id',
|
|
461
|
316
|
parseInt(zipCode),
|
|
462
|
317
|
)
|
|
463
|
|
- if (!zipInfo) {
|
|
464
|
|
- console.error('zip:', zipCode)
|
|
465
|
|
- }
|
|
466
|
|
-
|
|
|
318
|
+ if (!zipInfo) { console.error('zip:', zipCode) }
|
|
467
|
319
|
return {
|
|
468
|
320
|
latitude: parseFloat(zipInfo.latitude),
|
|
469
|
321
|
longitude: parseFloat(zipInfo.longitude),
|
|
|
@@ -478,10 +330,8 @@ module.exports = class ProfileService extends Schmervice.Service {
|
|
478
|
330
|
*/
|
|
479
|
331
|
async _compareDistance(start_zip, end_zip, distanceUnit) {
|
|
480
|
332
|
if (!start_zip || !end_zip || isNaN(start_zip) || isNaN(end_zip)) return
|
|
481
|
|
-
|
|
482
|
333
|
const start = await this._latLonForZip(start_zip)
|
|
483
|
334
|
const end = await this._latLonForZip(end_zip)
|
|
484
|
|
-
|
|
485
|
335
|
return haversine(start, end, { unit: distanceUnit })
|
|
486
|
336
|
}
|
|
487
|
337
|
}
|