Skip to content

Commit 8d1dc64

Browse files
committed
paradigm endpoint updates
1 parent d66c50c commit 8d1dc64

File tree

11 files changed

+197
-108
lines changed

11 files changed

+197
-108
lines changed

api/controllers/rest/paradigmsController.js

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
import personRepo from '../../repos/personRepo.js';
2-
import { BadRequest } from '../../helpers/problem.js';
2+
import { BadRequest, NotFound } from '../../helpers/problem.js';
33

44
async function getParadigms(req, res) {
55
//get the search query from the query params
66
const { search } = req.query;
77
if (!search) {
88
throw new BadRequest(req, res, 'Search query is required');
99
}
10+
1011
const paradigms = await personRepo.personSearch(search, {
1112
excludeBanned: true,
1213
excludeUnconfirmedEmail: true,
1314
hasValidParadigm: true,
15+
hasJudged: true,
16+
limit: 50,
1417
include: {
15-
chapterJudges: {
18+
judges: {
1619
fields: ['id'],
1720
include: {
18-
chapter: {
21+
school: {
1922
fields: ['id','name'],
2023
},
2124
},
@@ -25,17 +28,26 @@ async function getParadigms(req, res) {
2528

2629
const results = paradigms.map(p => {
2730
const nameParts = [p.firstName, p.middleName, p.lastName].filter(Boolean);
31+
// Get all schools from Judges
32+
const schools = p.Judges
33+
? p.Judges
34+
.filter(j => j && j.School)
35+
.map(j => ({
36+
id: j.School.id,
37+
name: j.School.name,
38+
}))
39+
: [];
40+
41+
// Deduplicate schools by id
42+
const distinctSchools = Array.from(
43+
new Map(schools.map(s => [s.name, s])).values()
44+
);
45+
2846
return {
2947
id: p.id,
3048
name: nameParts.join(' '),
31-
chapters: p.ChapterJudges
32-
? p.ChapterJudges
33-
.filter(cj => cj && cj.Chapter)
34-
.map(cj => ({
35-
id: cj.Chapter.id,
36-
name: cj.Chapter.name,
37-
}))
38-
: [],
49+
tournJudged: p.Judges ? p.Judges.length : 0,
50+
schools: distinctSchools,
3951
};
4052
});
4153
res.json(results);
@@ -50,16 +62,16 @@ async function getParadigmByPersonId(req, res) {
5062
excludeBanned: true,
5163
excludeUnconfirmedEmail: true,
5264
hasValidParadigm: true,
53-
settings: ['paradigm'],
65+
settings: ['paradigm', 'paradigm_timestamp'],
5466
});
5567

5668
if (!person) {
57-
res.status(404).json({ message: 'Person not found or does not have a valid paradigm' });
58-
return;
69+
return NotFound(req, res, 'Person not found or does not have a valid paradigm');
5970
}
6071
res.json({
6172
id: person.id,
6273
name: [person.firstName, person.middleName, person.lastName].filter(Boolean).join(' '),
74+
lastReviewed: person.settings['paradigm_timestamp'] || null,
6375
paradigm: person.settings['paradigm'] || null,
6476

6577
});

api/controllers/user/chapter/school.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
describe ('getMySchoolsByTourn', () => {
1616

1717
beforeEach(async () => {
18-
await db.permission.create(testUserChapterPerm);
18+
await db.permission.upsert(testUserChapterPerm);
1919
await db.contact.create(testUserSchoolContact);
2020
});
2121
afterEach(async () => {

api/data/db.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ db.timeslot.hasMany(db.round, { as: 'rounds', foreignKey: 'timeslot' });
2323
// Ensure chapterJudge <-> person association exists
2424
db.chapterJudge.belongsTo(db.person, { as: 'person_person', foreignKey: 'person' });
2525
db.person.hasMany(db.chapterJudge, { as: 'chapter_judges', foreignKey: 'person' });
26+
// judge to school
27+
db.judge.belongsTo(db.school, { as: 'school_school', foreignKey: 'school' });
28+
db.school.hasMany(db.judge, { as: 'judges', foreignKey: 'school' });
29+
// person to judge
30+
db.person.hasMany(db.judge, { as: 'judges', foreignKey: 'person' });
31+
db.judge.belongsTo(db.person, { as: 'person_person', foreignKey: 'person' });
2632

2733
// By default Sequelize wants you to try...catch every single database call
2834
// for Reasons? Otherwise all your database errors just go unprinted and you

api/repos/judgeRepo.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import db from '../data/db.js';
2+
import { schoolInclude } from './schoolRepo.js';
23
import { toDomain, toPersistence, FIELD_MAP } from './mappers/judgeMapper.js';
34
import { resolveAttributesFromFields } from './utils/repoUtils.js';
45
import { withSettingsInclude } from './utils/settings.js';
@@ -10,6 +11,14 @@ function buildJudgeQuery(opts = {}) {
1011
include: [],
1112
};
1213

14+
if (opts.include?.school) {
15+
query.include.push({
16+
...schoolInclude(opts.include.school),
17+
as: 'school_school',
18+
required: false,
19+
});
20+
}
21+
1322
// Judge settings (same pattern as category)
1423
query.include.push(
1524
...withSettingsInclude({

api/repos/mappers/judgeMapper.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { toDomain as genericToDomain, toPersistence as genericToPersistence, toBool, fromBool } from './mapperUtils.js';
2-
2+
import { toDomain as schoolToDomain } from './schoolMapper.js';
33
export const FIELD_MAP = {
44
id: 'id',
55
code: 'code',
@@ -24,7 +24,14 @@ export const FIELD_MAP = {
2424
createdAt: { db: 'created_at', toDb: () => undefined },
2525
};
2626

27-
export const toDomain = dbRow => genericToDomain(dbRow, FIELD_MAP);
27+
export const toDomain = dbRow => {
28+
if (!dbRow) return null;
29+
const judge = genericToDomain(dbRow, FIELD_MAP);
30+
if (dbRow.school_school) {
31+
judge.School = schoolToDomain(dbRow.school_school);
32+
}
33+
return judge;
34+
};
2835
export const toPersistence = domainObj => genericToPersistence(domainObj, FIELD_MAP);
2936

3037
export default {

api/repos/mappers/personMapper.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { toDomain as genericToDomain, toPersistence as genericToPersistence, toBool, fromBool } from './mapperUtils.js';
22
import { toDomain as chapterJudgeToDomain } from './chapterJudgeMapper.js';
3+
import { toDomain as judgeToDomain } from './judgeMapper.js';
34

45
export const FIELD_MAP = {
56
id : 'id',
@@ -31,6 +32,9 @@ export const toDomain = dbRow => {
3132
if (dbRow.chapter_judges && Array.isArray(dbRow.chapter_judges)) {
3233
person.ChapterJudges = dbRow.chapter_judges.map(chapterJudgeToDomain);
3334
}
35+
if (dbRow.judges && Array.isArray(dbRow.judges)) {
36+
person.Judges = dbRow.judges.map(judgeToDomain);
37+
}
3438

3539
return person;
3640
};

api/repos/personRepo.js

Lines changed: 55 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
12
import db from '../data/db.js';
23
import { FIELD_MAP,toDomain,toPersistence } from './mappers/personMapper.js';
34
import { withSettingsInclude, saveSettings } from './utils/settings.js';
45
import { resolveAttributesFromFields } from './utils/repoUtils.js';
56
import { chapterJudgeInclude } from './chapterJudge.js';
7+
import { judgeInclude } from './judgeRepo.js';
68

79
async function buildPersonQuery(opts = {}) {
810

@@ -16,95 +18,68 @@ async function buildPersonQuery(opts = {}) {
1618
attributes.exclude.push('password');
1719
}
1820
}
21+
1922
const query = {
2023
where: {},
2124
attributes,
2225
include: [],
2326
};
2427

25-
// Exclude banned persons
28+
// Build tag filters for settings include
29+
let settingsTags = [];
30+
if (opts.settings === true) {
31+
settingsTags = true;
32+
} else if (Array.isArray(opts.settings)) {
33+
settingsTags = [...opts.settings];
34+
}
35+
36+
const settingsInclude = withSettingsInclude({
37+
model: db.personSetting,
38+
as: 'person_settings',
39+
settings: settingsTags,
40+
})[0];
41+
2642
if (opts.excludeBanned) {
27-
if (!query.where[db.Sequelize.Op.and]) {
28-
query.where[db.Sequelize.Op.and] = [];
29-
}
43+
query.where[db.Sequelize.Op.and] = query.where[db.Sequelize.Op.and] || [];
3044
query.where[db.Sequelize.Op.and].push(
31-
db.sequelize.where(
32-
db.sequelize.literal(`NOT EXISTS (
33-
SELECT 1 FROM person_setting banned
34-
WHERE banned.person = person.id
35-
AND banned.tag = 'banned'
36-
)`),
37-
db.Sequelize.Op.eq,
38-
db.sequelize.literal('1')
39-
)
45+
db.Sequelize.literal(`NOT EXISTS (SELECT 1 FROM person_setting ps WHERE ps.person = person.id AND ps.tag = 'banned')`)
4046
);
4147
}
4248

43-
// Exclude persons with unconfirmed emails
4449
if (opts.excludeUnconfirmedEmail) {
45-
if (!query.where[db.Sequelize.Op.and]) {
46-
query.where[db.Sequelize.Op.and] = [];
47-
}
50+
query.where[db.Sequelize.Op.and] = query.where[db.Sequelize.Op.and] || [];
4851
query.where[db.Sequelize.Op.and].push(
49-
db.sequelize.where(
50-
db.sequelize.literal(`NOT EXISTS (
51-
SELECT 1 FROM person_setting email_unconfirmed
52-
WHERE email_unconfirmed.person = person.id
53-
AND email_unconfirmed.tag = 'email_unconfirmed'
54-
AND email_unconfirmed.value = '1'
55-
)`),
56-
db.Sequelize.Op.eq,
57-
db.sequelize.literal('1')
58-
)
52+
db.Sequelize.literal(`NOT EXISTS (SELECT 1 FROM person_setting ps WHERE ps.person = person.id AND ps.tag = 'email_unconfirmed')`)
5953
);
6054
}
61-
62-
// Require a person to have a valid paradigm setting, and have been updated since the last paradigm review cutoff
63-
if (opts.hasValidParadigm) {
64-
const now = opts.now || new Date();
65-
const reviewSettings = await db.tabroomSetting.findAll({
66-
where: {
67-
tag: {
68-
[db.Sequelize.Op.in]: ['paradigm_review_cutoff', 'paradigm_review_start'],
69-
},
70-
},
71-
});
72-
const reviewCutoff = reviewSettings.find(setting => setting.tag === 'paradigm_review_cutoff');
73-
const reviewStart = reviewSettings.find(setting => setting.tag === 'paradigm_review_start');
74-
const reviewClause = (reviewCutoff?.value_date
75-
&& reviewStart?.value_date
76-
&& reviewCutoff.value_date < now)
77-
? ` AND paradigm.timestamp > ${db.sequelize.escape(reviewStart.value_date)}`
78-
: '';
79-
if (!query.where[db.Sequelize.Op.and]) {
80-
query.where[db.Sequelize.Op.and] = [];
81-
}
55+
if(opts.hasValidParadigm) {
56+
query.where[db.Sequelize.Op.and] = query.where[db.Sequelize.Op.and] || [];
57+
query.where[db.Sequelize.Op.and].push(
58+
db.Sequelize.literal(`EXISTS (SELECT 1 FROM person_setting ps WHERE ps.person = person.id AND ps.tag = 'paradigm')`)
59+
);
60+
}
61+
if(opts.hasJudged) {
62+
query.where[db.Sequelize.Op.and] = query.where[db.Sequelize.Op.and] || [];
8263
query.where[db.Sequelize.Op.and].push(
83-
db.sequelize.where(
84-
db.sequelize.literal(`EXISTS (
85-
SELECT 1 FROM person_setting paradigm
86-
WHERE paradigm.person = person.id
87-
AND paradigm.tag = 'paradigm'
88-
${reviewClause}
89-
)`),
90-
db.Sequelize.Op.eq,
91-
db.sequelize.literal('1')
92-
)
64+
db.Sequelize.literal(`EXISTS (SELECT 1 FROM judge j WHERE j.person = person.id)`)
9365
);
9466
}
9567

96-
query.include.push(
97-
...withSettingsInclude({
98-
model: db.personSetting,
99-
as: 'person_settings',
100-
settings: opts.settings,
101-
})
102-
);
68+
if (settingsInclude) {
69+
query.include.push(settingsInclude);
70+
}
10371

10472
// Add chapter join when requested
10573
if (opts.include?.chapterJudges) {
10674
query.include.push(chapterJudgeInclude(opts.include.chapterJudges));
10775
}
76+
if (opts.include?.judges){
77+
query.include.push({
78+
...judgeInclude(opts.include.judges),
79+
as: 'judges',
80+
required: false,
81+
});
82+
}
10883

10984
if (Number.isInteger(opts.limit)) {
11085
query.limit = opts.limit;
@@ -152,32 +127,28 @@ async function personSearch(searchTerm = '', opts = {}) {
152127
const cleanTerm = sanitize(searchTerm);
153128
const words = cleanTerm.split(/\s+/).filter(w => w.length > 0);
154129

130+
if (words.length === 0) {
131+
return [];
132+
}
133+
134+
// Use buildPersonQuery for all filters and includes
155135
const query = await buildPersonQuery(opts);
156-
query.order = [['last', 'ASC'], ['first', 'ASC']];
157-
query.attributes = [
158-
'id',
159-
'first',
160-
'last',
161-
];
162-
163-
// Build name search conditions that handle multi-word search
164-
// "john smith", "smith john", "john", "smith" should all match person with firstName=john, lastName=smith
165-
if (words.length > 0) {
166-
const nameConditions = words.map(word => ({
136+
137+
// Add name search to where clause
138+
query.where = query.where || {};
139+
query.where[db.Sequelize.Op.and] = query.where[db.Sequelize.Op.and] || [];
140+
for (const word of words) {
141+
query.where[db.Sequelize.Op.and].push({
167142
[db.Sequelize.Op.or]: [
168143
{ first: { [db.Sequelize.Op.like]: `${word}%` } },
169144
{ last: { [db.Sequelize.Op.like]: `${word}%` } },
170145
],
171-
}));
172-
173-
query.where = {
174-
...query.where,
175-
[db.Sequelize.Op.and]: nameConditions,
176-
};
146+
});
177147
}
178148

179-
query.limit = opts.limit || 75;
180-
query.subQuery = false;
149+
query.order = [['last', 'ASC'], ['first', 'ASC']];
150+
//query.subQuery = false;
151+
//query.distinct = true;
181152

182153
const results = await db.person.findAll(query);
183154
return results.map(toDomain);

0 commit comments

Comments
 (0)