Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
c8da4fa
Use BooleanInstruction approach for getting fasttrack concepts
axlewin May 26, 2026
edfe46c
Use BooleanInstruction approach for getting fasttrack questions
axlewin May 27, 2026
94dd09d
Use BooleanInstruction approach for finding available quizzes
axlewin May 27, 2026
73c5b09
Use BooleanInstruction approach for finding glossary terms
axlewin May 27, 2026
d211f00
Use BooleanInstruction approach for getting notifications
axlewin May 27, 2026
00dbc6c
Remove deprecated gameboard endpoint and methods
jsharkey13 May 27, 2026
b785872
Use BooleanInstruction approach for getting wildcards
axlewin May 27, 2026
490872a
Use BooleanInstruction approach for getting subject pods
axlewin May 29, 2026
2d72a00
Use BooleanInstruction approach for finding events to delete PII for
axlewin May 29, 2026
ea12d1a
Use BooleanInstruction approach for finding events for notification e…
axlewin May 29, 2026
7deb79d
Use BooleanInstruction approach for listing content objects
axlewin May 29, 2026
6311da2
Remove ContentService
axlewin May 29, 2026
085ff84
Use BooleanInstruction approach for finding invalid q ids in gameboards
axlewin Jun 1, 2026
83745fe
Use BooleanInstruction approach for populating related content
axlewin Jun 1, 2026
c5f480c
Remove deprecated find by field name methods
axlewin Jun 1, 2026
53e41d0
Use BooleanInstruction approach for school lookup
axlewin Jun 1, 2026
a6dd00b
Remove deprecated fuzzySearch
axlewin Jun 1, 2026
49a2942
Remove findByExactMatch
axlewin Jun 1, 2026
32351da
Remove term search
axlewin Jun 1, 2026
345c52d
Remove duplicated base filters logic
axlewin Jun 1, 2026
1ecfdfb
Remove deprecated Elasticsearch query builders & helpers
axlewin Jun 1, 2026
e00e259
Remove AbstractFilterInstruction & inheritors
axlewin Jun 1, 2026
2125d57
Remove BooleanSearchClause
axlewin Jun 1, 2026
905ff45
Show nofilter content by default in content manager
axlewin Jun 1, 2026
87ca6e0
Fix nested boolean query structure
axlewin Jun 1, 2026
6fc7a82
Remove ElasticSearchProviderTest
axlewin Jun 3, 2026
9a6708c
Don't unnecessarily nest match instructions inside boolean instructions
axlewin Jun 3, 2026
681ab6d
Don't unnecessarily exclude unpublished/nofilter (etc) content
axlewin Jun 3, 2026
c0bf8df
Remove unnecessary "minimum should match" argument
axlewin Jun 5, 2026
42df78a
Add documentation link to BooleanInstruction class
axlewin Jun 11, 2026
b7284de
Sort concepts listing alphabetically
axlewin Jun 11, 2026
c118c77
Revert change to include nofilter content by default
axlewin Jun 16, 2026
08c44d8
Update Javadoc
axlewin Jun 16, 2026
c39bf1f
Remove duplicated nofilter filter
axlewin Jun 16, 2026
b8d99b5
Exclude unpublished & regression test content where necessary
axlewin Jun 16, 2026
2fe8151
In search instruction builder, don't filter by content type by default
axlewin Jun 17, 2026
0bbf0ca
Fix Checkstyle warnings
axlewin Jun 17, 2026
8faa217
Allow for events without end dates when finding past events
axlewin Jun 17, 2026
88c5d6e
Remove duplicated logic
axlewin Jun 17, 2026
078d16d
Merge branch 'main' into hotfix/remove-deprecated-es
axlewin Jun 19, 2026
5516d1b
Revert accidental change
axlewin Jun 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main/java/uk/ac/cam/cl/dtg/isaac/api/EventsFacade.java
Original file line number Diff line number Diff line change
Expand Up @@ -1288,6 +1288,7 @@ public final Response getEventOverviews(@Context final HttpServletRequest reques

/**
* REST end point to provide a summary of events suitable for mapping.
* Excludes nofilter events.
*
* @param request - this allows us to check to see if a user is currently logged in.
* @param startIndex - the initial index for the first result.
Expand Down
90 changes: 0 additions & 90 deletions src/main/java/uk/ac/cam/cl/dtg/isaac/api/GameboardsFacade.java
Original file line number Diff line number Diff line change
Expand Up @@ -124,96 +124,6 @@ public GameboardsFacade(final AbstractConfigLoader properties, final ILogManager
this.fastTrackManger = fastTrackManger;
}

/**
* REST end point to provide a Temporary Gameboard stored in volatile storage.
*
* @param request
* - this allows us to check to see if a user is currently loggedin.
* @param title
* - the title of the generated board
* @param subjects
* - a comma separated list of subjects
* @param fields
* - a comma separated list of fields
* @param topics
* - a comma separated list of topics
* @param levels
* - a comma separated list of levels
* @param concepts
* - a comma separated list of conceptIds
* @param questionCategories
* - a comma separated list of question categories
* @return a Response containing a gameboard object or containing a SegueErrorResponse.
*/
@Deprecated
@GET
@Path("gameboards")
@Produces(MediaType.APPLICATION_JSON)
@GZIP
@Operation(summary = "Get a temporary board of questions matching provided constraints.")
public final Response generateTemporaryGameboard(@Context final HttpServletRequest request,
@QueryParam("title") final String title, @QueryParam("subjects") final String subjects,
@QueryParam("fields") final String fields, @QueryParam("topics") final String topics,
@QueryParam("stages") final String stages, @QueryParam("difficulties") final String difficulties,
@QueryParam("examBoards") final String examBoards, @QueryParam("levels") final String levels,
@QueryParam("concepts") final String concepts, @QueryParam("questionCategories") final String questionCategories) {
List<String> subjectsList = splitCsvStringQueryParam(subjects);
List<String> fieldsList = splitCsvStringQueryParam(fields);
List<String> topicsList = splitCsvStringQueryParam(topics);
List<Integer> levelsList = null;
List<String> stagesList = splitCsvStringQueryParam(stages);
List<String> difficultiesList = splitCsvStringQueryParam(difficulties);
List<String> examBoardsList = splitCsvStringQueryParam(examBoards);
List<String> conceptsList = splitCsvStringQueryParam(concepts);
List<String> questionCategoriesList = splitCsvStringQueryParam(questionCategories);

if (null != levels && !levels.isEmpty()) {
String[] levelsAsString = levels.split(",");

levelsList = Lists.newArrayList();
for (String s : levelsAsString) {
try {
levelsList.add(Integer.parseInt(s));
} catch (NumberFormatException e) {
return new SegueErrorResponse(Status.BAD_REQUEST, "Levels must be numbers if specified.", e)
.toResponse();
}
}
}

try {
log.warn("Method generateTemporaryGameboard was called by an API request!");
// FIXME: remove endpoint after 2026-05. brownout below, in a revertible way, until removal.
if (!getProperties().getProperty(SEGUE_APP_ENVIRONMENT).equals(EnvironmentType.DEV.name())) {
return SegueErrorResponse.getGoneResponse();
}

AbstractSegueUserDTO boardOwner = this.userManager.getCurrentUser(request);
GameboardDTO gameboard;

gameboard = gameManager.generateRandomGameboard(title, subjectsList, fieldsList, topicsList, levelsList,
conceptsList, questionCategoriesList, stagesList, difficultiesList, examBoardsList,boardOwner);

if (null == gameboard) {
return new SegueErrorResponse(Status.NO_CONTENT,
"We cannot find any questions based on your filter criteria.").toResponse();
}

return Response.ok(gameboard).cacheControl(getCacheControl(NEVER_CACHE_WITHOUT_ETAG_CHECK, false)).build();
} catch (IllegalArgumentException e) {
return new SegueErrorResponse(Status.BAD_REQUEST, "Your gameboard filter request is invalid.").toResponse();
} catch (SegueDatabaseException e) {
String message = "SegueDatabaseException whilst generating a gameboard";
log.error(message, e);
return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, message).toResponse();
} catch (ContentManagerException e1) {
SegueErrorResponse error = new SegueErrorResponse(Status.NOT_FOUND, "Error locating the version requested",
e1);
log.error(error.getErrorMessage(), e1);
return error.toResponse();
}
}

/**
* REST end point to retrieve a specific gameboard by Id.
*
Expand Down
107 changes: 44 additions & 63 deletions src/main/java/uk/ac/cam/cl/dtg/isaac/api/PagesFacade.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,13 @@
import uk.ac.cam.cl.dtg.isaac.dto.users.RegisteredUserDTO;
import uk.ac.cam.cl.dtg.segue.api.managers.QuestionManager;
import uk.ac.cam.cl.dtg.segue.api.managers.UserAccountManager;
import uk.ac.cam.cl.dtg.segue.api.services.ContentService;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.NoUserLoggedInException;
import uk.ac.cam.cl.dtg.segue.dao.ILogManager;
import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException;
import uk.ac.cam.cl.dtg.segue.dao.content.ContentManagerException;
import uk.ac.cam.cl.dtg.segue.dao.content.GitContentManager;
import uk.ac.cam.cl.dtg.segue.search.BooleanInstruction;
import uk.ac.cam.cl.dtg.segue.search.MatchInstruction;
import uk.ac.cam.cl.dtg.util.AbstractConfigLoader;
import uk.ac.cam.cl.dtg.util.mappers.MainMapper;

Expand Down Expand Up @@ -103,7 +104,6 @@
public class PagesFacade extends AbstractIsaacFacade {
private static final Logger log = LoggerFactory.getLogger(PagesFacade.class);

private final ContentService api;
private final MainMapper mapper;
private final UserAccountManager userManager;
private final URIManager uriManager;
Expand All @@ -116,8 +116,6 @@ public class PagesFacade extends AbstractIsaacFacade {
/**
* Creates an instance of the pages controller which provides the REST endpoints for accessing page content.
*
* @param api
* - Instance of ContentService
* @param propertiesLoader
* - Instance of properties Loader
* @param logManager
Expand All @@ -138,13 +136,12 @@ public class PagesFacade extends AbstractIsaacFacade {
* - For looking up bookmark information.
*/
@Inject
public PagesFacade(final ContentService api, final AbstractConfigLoader propertiesLoader,
public PagesFacade(final AbstractConfigLoader propertiesLoader,
final ILogManager logManager, final MainMapper mapper, final GitContentManager contentManager,
final UserAccountManager userManager, final URIManager uriManager,
final QuestionManager questionManager, final GameManager gameManager,
final UserAttemptManager userAttemptManager, final BookmarksManager bookmarksManager) {
super(propertiesLoader, logManager);
this.api = api;
this.mapper = mapper;
this.contentManager = contentManager;
this.userManager = userManager;
Expand Down Expand Up @@ -181,31 +178,25 @@ public final Response getConceptList(@Context final Request request, @QueryParam
@QueryParam("tags") final String tags,
@DefaultValue(DEFAULT_START_INDEX_AS_STRING) @QueryParam("start_index") final Integer startIndex,
@DefaultValue(DEFAULT_RESULTS_LIMIT_AS_STRING) @QueryParam("limit") final Integer limit) {
Map<String, List<String>> fieldsToMatch = Maps.newHashMap();
fieldsToMatch.put(TYPE_FIELDNAME, List.of(CONCEPT_TYPE));

StringBuilder etagCodeBuilder = new StringBuilder();

Integer newLimit = null;
Integer newLimit = limit;

if (limit != null) {
newLimit = limit;
etagCodeBuilder.append(limit);
}

// options
List<String> idsList = null;
if (ids != null) {
List<String> idsList = Arrays.asList(ids.split(","));
fieldsToMatch.put(ID_FIELDNAME, idsList);
idsList = Arrays.asList(ids.split(","));
newLimit = idsList.size();
etagCodeBuilder.append(ids);
}

List<String> tagList = null;
if (tags != null) {
fieldsToMatch.put(TAGS_FIELDNAME, Arrays.asList(tags.split(",")));
tagList = Arrays.asList(tags.split(","));
etagCodeBuilder.append(tags);
}
Map<String, BooleanOperator> booleanOperatorOverrideMap = ImmutableMap.of(TAGS_FIELDNAME, BooleanOperator.OR);

// Calculate the ETag on last modified date of tags list
// NOTE: Assumes that the latest version of the content is being used.
Expand All @@ -219,10 +210,39 @@ public final Response getConceptList(@Context final Request request, @QueryParam
}

try {
return listContentObjects(fieldsToMatch, booleanOperatorOverrideMap, startIndex, newLimit).tag(etag)
BooleanInstruction searchInstruction = new BooleanInstruction();
searchInstruction.must(new MatchInstruction(TYPE_FIELDNAME, CONCEPT_TYPE));

if (idsList != null && !idsList.isEmpty()) {
BooleanInstruction idsInstruction = new BooleanInstruction();
for (String id : idsList) {
idsInstruction.should(new MatchInstruction(ID_FIELDNAME, id));
}
searchInstruction.must(idsInstruction);
}

if (tagList != null && !tagList.isEmpty()) {
BooleanInstruction tagsInstruction = new BooleanInstruction();
for (String tag : tagList) {
tagsInstruction.should(new MatchInstruction(TAGS_FIELDNAME, tag));
}
searchInstruction.must(tagsInstruction);
}

Map<String, SortOrder> sortInstructions = Maps.newHashMap();
sortInstructions.put(TITLE_FIELDNAME + "." + UNPROCESSED_SEARCH_FIELD_SUFFIX, SortOrder.ASC);

ResultsWrapper<ContentDTO> c = this.contentManager.nestedMatchSearch(
searchInstruction, startIndex, newLimit, null, sortInstructions);

ResultsWrapper<ContentSummaryDTO> summarizedContent = new ResultsWrapper<>(
this.extractContentSummaryFromList(c.getResults()),
c.getTotalResults());

return Response.ok(summarizedContent).tag(etag)
.cacheControl(getCacheControl(NUMBER_SECONDS_IN_ONE_HOUR, true))
.build();
} catch (ContentManagerException e1) {
} catch (final ContentManagerException e1) {
SegueErrorResponse error = new SegueErrorResponse(Status.NOT_FOUND,
"Error locating the content requested", e1);
log.error(error.getErrorMessage(), e1);
Expand Down Expand Up @@ -1172,17 +1192,16 @@ public final Response getPodList(@Context final Request request,
}

try {
Map<String, List<String>> fieldsToMatch = Maps.newHashMap();
fieldsToMatch.put(TYPE_FIELDNAME, List.of(POD_FRAGMENT_TYPE));
fieldsToMatch.put(TAGS_FIELDNAME, List.of(subject));
BooleanInstruction searchInstruction = new BooleanInstruction();
searchInstruction.must(new MatchInstruction(TYPE_FIELDNAME, POD_FRAGMENT_TYPE));
searchInstruction.must(new MatchInstruction(TAGS_FIELDNAME, subject));

Map<String, SortOrder> sortInstructions = new HashMap<>();
sortInstructions.put("id.raw", SortOrder.DESC); // Sort by ID (i.e. most recent; all pod ids start yyyymmdd)
// We would ideally also sort by presence of 'featured' tag, tricky with current implementation

ResultsWrapper<ContentDTO> pods = api.findMatchingContent(
ContentService.generateDefaultFieldToMatch(fieldsToMatch), startIndex, MAX_PODS_TO_RETURN,
sortInstructions);
ResultsWrapper<ContentDTO> pods = this.contentManager.nestedMatchSearch(
searchInstruction, startIndex, MAX_PODS_TO_RETURN, null, sortInstructions);

return Response.ok(pods).cacheControl(getCacheControl(NUMBER_SECONDS_IN_TEN_MINUTES, true))
.tag(etag)
Expand Down Expand Up @@ -1292,44 +1311,6 @@ private List<ContentSummaryDTO> extractContentSummaryFromList(final List<Content
return listOfContentInfo;
}

/**
* Helper method to query segue for a list of content objects.
*
* This method will only use the latest version of the content.
*
* @param fieldsToMatch
* - expects a map of the form fieldname -> list of queries to match
* @param booleanOperatorOverrideMap
* - an optional map of the form fieldname -> one of 'AND', 'OR' or 'NOT', to specify the
* type of matching needed for that field. Overrides any other default matching behaviour
* for the given fields
* @param startIndex
* - the initial index for the first result.
* @param limit
* - the maximums number of results to return
* @return Response builder containing a list of content summary objects or containing a SegueErrorResponse
*/
private Response.ResponseBuilder listContentObjects(final Map<String,
List<String>> fieldsToMatch,
@Nullable final Map<String, BooleanOperator> booleanOperatorOverrideMap,
final Integer startIndex,
final Integer limit)
throws ContentManagerException {
ResultsWrapper<ContentDTO> c;

c = api.findMatchingContent(
ContentService.generateDefaultFieldToMatch(fieldsToMatch, booleanOperatorOverrideMap),
startIndex,
limit
);

ResultsWrapper<ContentSummaryDTO> summarizedContent = new ResultsWrapper<>(
this.extractContentSummaryFromList(c.getResults()),
c.getTotalResults());

return Response.ok(summarizedContent);
}

/**
* Convert an optional comma-separated URL parameter into a value suitable for logging.
* @param urlParam - the URL parameter value, potentially null or empty.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,19 @@
import uk.ac.cam.cl.dtg.isaac.dto.users.UserSummaryDTO;
import uk.ac.cam.cl.dtg.segue.api.Constants;
import uk.ac.cam.cl.dtg.segue.api.managers.UserAccountManager;
import uk.ac.cam.cl.dtg.segue.api.services.ContentService;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.NoUserException;
import uk.ac.cam.cl.dtg.segue.comm.EmailManager;
import uk.ac.cam.cl.dtg.segue.comm.EmailType;
import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException;
import uk.ac.cam.cl.dtg.segue.dao.content.ContentManagerException;
import uk.ac.cam.cl.dtg.segue.dao.content.GitContentManager;
import uk.ac.cam.cl.dtg.segue.search.AbstractFilterInstruction;
import uk.ac.cam.cl.dtg.segue.search.DateRangeFilterInstruction;
import uk.ac.cam.cl.dtg.segue.search.BooleanInstruction;
import uk.ac.cam.cl.dtg.segue.search.MatchInstruction;
import uk.ac.cam.cl.dtg.segue.search.RangeInstruction;
import uk.ac.cam.cl.dtg.util.AbstractConfigLoader;

import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -97,22 +96,21 @@ public void sendReminderEmails() {
// Magic number
Integer limit = 10000;
Integer startIndex = 0;
Map<String, List<String>> fieldsToMatch = Maps.newHashMap();
Map<String, Constants.SortOrder> sortInstructions = Maps.newHashMap();
Map<String, AbstractFilterInstruction> filterInstructions = Maps.newHashMap();
fieldsToMatch.put(TYPE_FIELDNAME, Collections.singletonList(EVENT_TYPE));
sortInstructions.put(DATE_FIELDNAME, Constants.SortOrder.DESC);
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime threeDaysAhead = now.plusDays(3);
DateRangeFilterInstruction
eventsWithinThreeDays = new DateRangeFilterInstruction(new Date(), Date.from(threeDaysAhead.toInstant()));
filterInstructions.put(DATE_FIELDNAME, eventsWithinThreeDays);

BooleanInstruction searchInstruction = new BooleanInstruction();
searchInstruction.must(new MatchInstruction(TYPE_FIELDNAME, EVENT_TYPE));
searchInstruction.must(new RangeInstruction<Long>(DATE_FIELDNAME)
.greaterThanOrEqual(new Date().getTime())
.lessThanOrEqual(Date.from(threeDaysAhead.toInstant()).getTime()));

try {
ResultsWrapper<ContentDTO> findByFieldNames = this.contentManager.findByFieldNames(
ContentService.generateDefaultFieldToMatch(fieldsToMatch), startIndex, limit, sortInstructions,
filterInstructions);
for (ContentDTO contentResult : findByFieldNames.getResults()) {
ResultsWrapper<ContentDTO> results = this.contentManager.nestedMatchSearch(
searchInstruction, startIndex, limit, null, sortInstructions);
for (ContentDTO contentResult : results.getResults()) {
if (contentResult instanceof IsaacEventPageDTO event) {
// Skip sending emails for cancelled events
if (EventStatus.CANCELLED.equals(event.getEventStatus())) {
Expand All @@ -138,23 +136,21 @@ public void sendFeedbackEmails() {
// Magic number
Integer limit = 10000;
Integer startIndex = 0;
Map<String, List<String>> fieldsToMatch = Maps.newHashMap();
Map<String, Constants.SortOrder> sortInstructions = Maps.newHashMap();
Map<String, AbstractFilterInstruction> filterInstructions = Maps.newHashMap();
fieldsToMatch.put(TYPE_FIELDNAME, Collections.singletonList(EVENT_TYPE));
sortInstructions.put(DATE_FIELDNAME, Constants.SortOrder.DESC);
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime sixtyDaysAgo = now.plusDays(-60);

DateRangeFilterInstruction eventsInLastSixtyDays = new DateRangeFilterInstruction(
Date.from(sixtyDaysAgo.toInstant()), new Date());
filterInstructions.put(DATE_FIELDNAME, eventsInLastSixtyDays);
BooleanInstruction searchInstruction = new BooleanInstruction();
searchInstruction.must(new MatchInstruction(TYPE_FIELDNAME, EVENT_TYPE));
searchInstruction.must(new RangeInstruction<Long>(DATE_FIELDNAME)
.greaterThanOrEqual(Date.from(sixtyDaysAgo.toInstant()).getTime())
.lessThanOrEqual(new Date().getTime()));

try {
ResultsWrapper<ContentDTO> findByFieldNames = this.contentManager.findByFieldNames(
ContentService.generateDefaultFieldToMatch(fieldsToMatch), startIndex, limit, sortInstructions,
filterInstructions);
for (ContentDTO contentResult : findByFieldNames.getResults()) {
ResultsWrapper<ContentDTO> results = this.contentManager.nestedMatchSearch(
searchInstruction, startIndex, limit, null, sortInstructions);
for (ContentDTO contentResult : results.getResults()) {
if (contentResult instanceof IsaacEventPageDTO event) {
// Skip sending emails for cancelled events
if (EventStatus.CANCELLED.equals(event.getEventStatus())) {
Expand Down
Loading
Loading