+
Search Results: {{totalRows}}
{{countCaption}}
-
+
@@ -201,6 +203,13 @@ export default {
stateName: {
type: String,
default: null
+ },
+ stickyHeader: {
+ default: false
+ },
+ showBottomControls: {
+ type: Boolean,
+ default: true
}
},
data () {
diff --git a/app/javascript/constants/strings.js b/app/javascript/constants/strings.js
index e28077154..ef70d641d 100644
--- a/app/javascript/constants/strings.js
+++ b/app/javascript/constants/strings.js
@@ -301,7 +301,7 @@ module.exports = {
rejected: "Rejected"
},
PERSON_ATTENDANCE_TYPE: {
- 'in person': "In Person",
+ in_person: "In Person",
hybrid: "In Person AND Online",
virtual: "Online",
},
diff --git a/app/javascript/integrations/clyde_settings.vue b/app/javascript/integrations/clyde_settings.vue
index 2faad60cb..5787ba8bc 100644
--- a/app/javascript/integrations/clyde_settings.vue
+++ b/app/javascript/integrations/clyde_settings.vue
@@ -2,11 +2,21 @@
+
Registration Sync Management
+
+ Run Registration Data Sync
+
+
+
+ - Last completed full sync: {{ lastSync }}
+ - Records matched: {{ stats.matched }}
+ - Records updated: {{ stats.updated }}
+ - Records not found: {{ stats.not_found }}
+
Configuration
-
+
- Registration Synchronize
@@ -24,8 +34,8 @@
- Synchonize Registration Info
- This will sync with the Registration system. This will bring the server down for a short time.
+ Run Registration Data Sync
+ This will sync with the Registration system's data. It will bring the server down for a short time.
Please double check that you wish to perform this action.
@@ -36,18 +46,26 @@ import { clydeMixin } from './clyde.mixin'
import PlanoModal from '@/components/plano_modal.vue';
import { toastMixin } from '@/mixins';
import { http } from '@/http';
+import { registrationSyncStatsMixin} from '@/store/registration_sync_stats.mixin';
+import RegSyncModal from './reg-sync-modal.vue';
export default {
name: "ClydeSettings",
- mixins: [clydeMixin, toastMixin],
+ mixins: [clydeMixin, toastMixin, registrationSyncStatsMixin],
components: {
- PlanoModal
+ PlanoModal,
+ RegSyncModal
},
methods: {
synchronizeSchedule() {
- this.toastPromise(http.get('/registration_sync_data/synchronize'), "Succesfully requested registration sync")
+ this.toastPromise(http.get('/registration_sync_data/synchronize'), "Succesfully requested registration sync").then(() => {
+ this.fetchStats();
+ })
},
},
+ mounted() {
+ this.fetchStats();
+ }
}
diff --git a/app/javascript/integrations/reg-sync-modal.vue b/app/javascript/integrations/reg-sync-modal.vue
new file mode 100644
index 000000000..fe5fd2369
--- /dev/null
+++ b/app/javascript/integrations/reg-sync-modal.vue
@@ -0,0 +1,174 @@
+
+
+
{{
+ possibleMatchCount
+ }}
+
+ Review potential matches
+
+
+
+
Last completed full sync: {{ lastSync }}
+
+
People with potential matches: {{ fullTotal }}
+
+ No more people to match!
+
+
+
+
{{ selected.published_name }}
+
+
+
+
Planorama Data
+
+
Email: {{ selected.primary_email.email }}
+
Published Name: {{ selected.published_name }}
+
Name: {{ selected.name }}
+
+
Potential Matches (from registration system)
+
+
There are no more potential matches.
+
+
+
+
+
+
+
+
diff --git a/app/javascript/integrations/reg-sync-person-search.vue b/app/javascript/integrations/reg-sync-person-search.vue
new file mode 100644
index 000000000..33a8c7ded
--- /dev/null
+++ b/app/javascript/integrations/reg-sync-person-search.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
diff --git a/app/javascript/registrations/person_sync_table.vue b/app/javascript/registrations/person_sync_table.vue
index 9a6d16e35..effdd8f36 100644
--- a/app/javascript/registrations/person_sync_table.vue
+++ b/app/javascript/registrations/person_sync_table.vue
@@ -6,7 +6,16 @@
ref="person-sync-table"
stateName="person-sync-table-search-state"
:showControls="false"
+ stickyHeader="450px"
+ :showBottomControls="false"
>
+
+
+
Last completed full sync: {{ lastSync }}
+
People with potential matches: {{ total }}
+
+
+
{{item.primary_email.email}}
@@ -19,18 +28,20 @@
-
+
+ -
- Match
- Dismiss
+ Match
+ Dismiss
-
+
+
@@ -39,9 +50,14 @@
diff --git a/app/javascript/store/app.store.js b/app/javascript/store/app.store.js
index cd81f4a4f..6d3973f88 100644
--- a/app/javascript/store/app.store.js
+++ b/app/javascript/store/app.store.js
@@ -1,6 +1,7 @@
export const MAGICAL_RELOAD = 'MAGICAL RELOAD'
export const SET_PER_PAGE = 'SET PER PAGE';
export const SET_SPINNER = 'SET SPINNER';
+export const SET_RELOADED_AT = 'SET RELOADED AT'
export const appStore = {
state: {
@@ -19,6 +20,9 @@ export const appStore = {
},
[SET_SPINNER] (state, spinner) {
state.wholePageSpinner = spinner;
+ },
+ [SET_RELOADED_AT] (state) {
+ state.reloadedAt = new Date();
}
}
}
diff --git a/app/javascript/store/model.mixin.js b/app/javascript/store/model.mixin.js
index b93c6fc9d..846e56537 100644
--- a/app/javascript/store/model.mixin.js
+++ b/app/javascript/store/model.mixin.js
@@ -1,4 +1,4 @@
-import { SELECTED, SELECT, UNSELECT, FETCH, FETCH_BY_ID, CLEAR, SEARCH, PATCH_FIELDS, SAVE, DELETE } from "./model.store";
+import { SELECTED, SELECT, UNSELECT, FETCH, FETCH_BY_ID, FETCH_SELECTED, CLEAR, SEARCH, PATCH_FIELDS, SAVE, DELETE, FETCH_NEXT_PAGE, FETCH_PREV_PAGE, SELECT_NEXT, SELECT_PREV, SELECT_FIRST, FULL_TOTAL, SELECTED_INDEX } from "./model.store";
import { mapActions } from 'vuex';
import { toastMixin } from "@/mixins";
import { MODEL_SAVE_ERROR, MODEL_SAVE_SUCCESS, MODEL_DELETE_SUCCESS, MODEL_DELETE_ERROR, SPECIFIC_MODEL_SAVE_ERROR, SPECIFIC_MODEL_SAVE_SUCCESS } from "@/constants/strings";
@@ -13,6 +13,12 @@ export const modelMixinNoProp = {
},
collection() {
return Object.values(this.$store.getters['jv/get']({_jv: { type: this.model }}))
+ },
+ fullTotal() {
+ return this.$store.getters[FULL_TOTAL]({model: this.model})
+ },
+ selectedOrdinal() {
+ return this.$store.getters[SELECTED_INDEX]({model: this.model}) + 1;
}
},
methods: {
@@ -21,6 +27,15 @@ export const modelMixinNoProp = {
select(itemOrId) {
this.$store.commit(SELECT, {model: this.model, itemOrId});
},
+ selectNext() {
+ return this.$store.dispatch(SELECT_NEXT, {model: this.model});
+ },
+ selectPrev() {
+ return this.$store.dispatch(SELECT_PREV, {model: this.model});
+ },
+ selectFirst() {
+ this.$store.commit(SELECT_FIRST, {model: this.model});
+ },
unselect() {
this.$store.commit(UNSELECT, {model: this.model});
},
@@ -30,6 +45,12 @@ export const modelMixinNoProp = {
fetch(params, url = null) {
return this.$store.dispatch(FETCH, {model: this.model, url: url, params});
},
+ fetchNextPage() {
+ return this.$store.dispatch(FETCH_NEXT_PAGE, {model: this.model, url: url, params});
+ },
+ fetchPrevPage() {
+ return this.$store.dispatch(FETCH_PREV_PAGE, {model: this.model, url: url, params});
+ },
fetch_by_id(id) {
return this.$store.dispatch(FETCH_BY_ID, {model: this.model, id: id});
},
diff --git a/app/javascript/store/model.store.js b/app/javascript/store/model.store.js
index 853cec908..fe30ee44d 100644
--- a/app/javascript/store/model.store.js
+++ b/app/javascript/store/model.store.js
@@ -3,6 +3,7 @@ import Vuex from 'vuex'
import { jsonapiModule, utils } from 'jsonapi-vuex'
import { http } from '../http';
import { getId } from '../utils/jsonapi_utils';
+import { of } from 'rxjs';
export const SELECT = 'SELECT';
export const UNSELECT = 'UNSELECT';
@@ -22,6 +23,17 @@ export const UPDATE_ALL = 'UPDATE ALL';
export const PATCH_RELATED = 'PATCH RELATED';
export const PATCH_FIELDS = 'PATCH FIELDS';
+// for paged models
+export const SET_MODEL_PAGE_SIZE = 'SET MODEL PAGE SIZE';
+export const SET_MODEL_PAGE_META = 'SET MODEL PAGE META';
+export const SELECT_NEXT = 'SELECT NEXT';
+export const SELECT_PREV = 'SELECT PREV';
+export const SELECT_FIRST = 'SELECT FIRST';
+export const FETCH_NEXT_PAGE = 'FETCH NEXT PAGE';
+export const FETCH_PREV_PAGE = 'FETCH PREV PAGE';
+export const FULL_TOTAL = 'FULL TOTAL';
+export const SELECTED_INDEX = 'SELECTED INDEX';
+
// people add-ons
import { personStore, personEndpoints } from './person.store';
@@ -172,6 +184,9 @@ export const store = new Vuex.Store({
...publishedSessionStore.selected,
...publicationDatesStore.selected,
},
+ page: {
+ ...personSyncDatumStore.page,
+ },
...personSessionStore.state,
...settingsStore.state,
...surveyStore.state,
@@ -183,6 +198,8 @@ export const store = new Vuex.Store({
...appStore.state,
...scheduleWorkflowStore.state,
...integrationStore.state,
+ ...registrationSyncDatumStore.state,
+ ...personSyncDatumStore.state,
// ...mailingStore.state
},
getters: {
@@ -201,6 +218,25 @@ export const store = new Vuex.Store({
}
}
},
+ [FULL_TOTAL] (state) {
+ return ({model}) => {
+ return state.page[model].fullTotal;
+ }
+ },
+ [SELECTED_INDEX] (state) {
+ return ({model}) => {
+ const {perPage, currentPage, correctOrder } = state.page[model] ?? {};
+ const selectedId = state.selected[model];
+ if(perPage && currentPage && correctOrder && selectedId) {
+ // we can calculate which one this is
+ const previousPageCount = perPage * (currentPage - 1);
+ const currentIndex = correctOrder.findIndex(id => id === selectedId);
+ return previousPageCount + currentIndex;
+ }
+ // we cannot calculate which one this is, we're missing some data
+ return -1;
+ }
+ },
...personStore.getters,
...agreementStore.getters,
...roomStore.getters,
@@ -238,16 +274,35 @@ export const store = new Vuex.Store({
[UNSELECT] (state, {model}) {
state.selected[model] = undefined;
},
+ [SELECT_FIRST] (state, {model}) {
+ // this only works if the model is paged
+ if(state.page[model].usePaged) {
+ state.selected[model] = state.page[model].correctOrder[0];
+ }
+ },
[CLEAR] (state, {model}) {
this.commit('jv/clearRecords', { _jv: { type: model } })
},
+ [SET_MODEL_PAGE_META] (state, {model, meta}) {
+ state.page[model] = {
+ ...state.page[model],
+ ...meta
+ };
+ },
+ [SET_MODEL_PAGE_SIZE] (state, {model, perPage}) {
+ state.page[model] ||= {}
+ state.page[model].perPage = perPage;
+ state.page[model].currentPage = 1;
+ },
...personSessionStore.mutations,
...settingsStore.mutations,
...surveyStore.mutations,
...searchStateStore.mutations,
...roomStore.mutations,
...appStore.mutations,
- ...integrationStore.mutations
+ ...integrationStore.mutations,
+ ...registrationSyncDatumStore.mutations,
+ ...personSyncDatumStore.mutations,
},
actions: {
/**
@@ -360,11 +415,39 @@ export const store = new Vuex.Store({
return dispatch('jv/search', [endpoints[model], {params}])
},
// need a way to override the default URL
- [FETCH] ({dispatch}, {model, url = null, params}) {
+ [FETCH] ({dispatch, state, commit}, {model, url = null, params}) {
+ let isPaged = false;
+ if (state.page?.[model]?.usePaged) {
+ isPaged = true;
+ // modify params to fetch paged if they don't have page info already
+ let {current_page, perPage} = params ?? {}
+ if (!current_page) {
+ current_page = state.page[model]?.currentPage ?? 1;
+ }
+ if (!perPage) {
+ perPage = state.page[model]?.perPage ?? state.perPage ?? 20
+ commit(SET_MODEL_PAGE_SIZE, {model, perPage})
+ }
+ params = {...params, perPage, current_page}
+ }
if (url) {
return dispatch('jv/get', [url, {params}])
} else {
- return dispatch('jv/get', [endpoints[model], {params}])
+ // return dispatch('jv/get', [endpoints[model], {params}])
+ return new Promise((res, rej) => {
+ dispatch('jv/get', [endpoints[model], {params}]).then(data => {
+ if(isPaged) {
+ const meta = {correctOrder: data._jv.json.data.map(m => m.id)};
+ if (typeof data._jv.json.meta !== 'undefined') {
+ meta.currentPage = data._jv.json.meta.current_page;
+ meta.total = data._jv.json.meta.total;
+ meta.fullTotal = data._jv.json.meta.full_total;
+ }
+ commit(SET_MODEL_PAGE_META, {model, meta})
+ }
+ res(data);
+ }).catch(rej);
+ });
}
},
// [CLEAR] ({dispatch}, {model}) {
@@ -380,6 +463,109 @@ export const store = new Vuex.Store({
// We do need this - not all fetch by id will be selected models
return dispatch('jv/get', `${endpoints[model]}/${id}`)
},
+ [FETCH_NEXT_PAGE] ({state, dispatch}, {model, url = null, params}) {
+ //model must be paged
+ if(state.page[model]?.usePaged) {
+ let {currentPage, perPage, fullTotal} = state.page[model];
+ // model must have a next page
+ if(perPage * currentPage < fullTotal) {
+ // now we can fetch the next page
+ return dispatch(FETCH, {model, url, params: {...params, current_page: currentPage + 1}})
+ } else {
+ console.warn("Attempting to fetch next page when there aren't more pages: ", model)
+ }
+ } else {
+ console.warn("Attempting to fetch next page on an unpaged model: ", model);
+ }
+ },
+ [FETCH_PREV_PAGE] ({state, dispatch}, {model, url = null, params}) {
+ //model must be paged
+ if(state.page[model]?.usePaged) {
+ let {currentPage} = state.page[model];
+ console.log('fetching previous. current page', currentPage)
+ // model must have a next page
+ if(currentPage > 1) {
+ // now we can fetch the next page
+ return dispatch(FETCH, {model, url, params: {...params, current_page: currentPage - 1}})
+ } else {
+ console.warn("Attempting to fetch prev page when there aren't more pages: ", model)
+ }
+ } else {
+ console.warn("Attempting to fetch prev page on an unpaged model: ", model);
+ }
+ },
+ // this is an action rather than a mutation because we might need to fetch
+ [SELECT_NEXT] ({state, dispatch, commit}, {model}) {
+ // this currently only works with paged models
+ if(state.page[model]?.usePaged) {
+ if(state.selected[model]) {
+ const selected = state.selected[model];
+ let { correctOrder, currentPage, fullTotal, perPage } = state.page[model];
+ let currentIndex = correctOrder.findIndex((id) => id === selected);
+ if(currentIndex === correctOrder.length - 1) {
+ if(perPage * currentPage < fullTotal) {
+ // need to fetch next
+ return new Promise((res, rej) => {
+ // no params here, might need to add later
+ dispatch(FETCH_NEXT_PAGE, {model}).then((data) => {
+ const itemOrId = data._jv.json.data[0].id;
+ commit(SELECT, {model, itemOrId });
+ res(itemOrId);
+ }).catch(rej);
+ })
+ } else {
+ // don't select anything cause it's on the last one, just return the current id
+ return of(selected.id);
+ }
+ } else {
+ const itemOrId = correctOrder[currentIndex + 1]
+ commit(SELECT, {model, itemOrId});
+ return of(itemOrId);
+ }
+ } else {
+ console.log("Can't select next when there's nothing selected: ", model)
+ }
+ } else {
+ console.warn("Can't select next from unpaged model: ", model)
+ }
+ // todo what should i be returning here
+ },
+ // this is an action rather than a mutation because we might need to fetch
+ [SELECT_PREV] ({state, dispatch, commit}, {model}) {
+ // this currently only works with paged models
+ if(state.page[model]?.usePaged) {
+ if(state.selected[model]) {
+ const selected = state.selected[model];
+ let { correctOrder, currentPage } = state.page[model];
+ let currentIndex = correctOrder.findIndex((id) => id === selected);
+ if(currentIndex === 0) {
+ if (currentPage > 1) {
+ // need to fetch previous
+ return new Promise((res, rej) => {
+ // no params here, might need to add later
+ dispatch(FETCH_PREV_PAGE, {model}).then((data) => {
+ const itemOrId = data._jv.json.data[data._jv.json.data.length - 1].id;
+ commit(SELECT, {model, itemOrId });
+ res(itemOrId);
+ }).catch(rej);
+ })
+ } else {
+ // don't select anything cause it's on the first one, just return the current id
+ return of(selected.id);
+ }
+ } else {
+ const itemOrId = correctOrder[currentIndex - 1];
+ commit(SELECT, {model, itemOrId});
+ return of(itemOrId);
+ }
+ } else {
+ console.log("Can't select prev when there's nothing selected: ", model)
+ }
+ } else {
+ console.warn("Can't select prev from unpaged model: ", model)
+ }
+ // todo what should i be returning here
+ },
[PATCH_FIELDS] ({dispatch, commit}, {model, item, fields=[], selected = true}) {
// limited field selection
let smallItem = {
diff --git a/app/javascript/store/person_sync_datum.mixin.js b/app/javascript/store/person_sync_datum.mixin.js
index 95ae8f9dc..7164317a4 100644
--- a/app/javascript/store/person_sync_datum.mixin.js
+++ b/app/javascript/store/person_sync_datum.mixin.js
@@ -1,7 +1,7 @@
import { toastMixin } from "@/mixins";
import { SELECTED } from "./model.store"
import { personModel } from "./person.store"
-import { MATCH } from "./person_sync_datum.store";
+import { MATCH, DISMISS } from "./person_sync_datum.store";
import { registrationSyncDatumModel } from "./registration_sync_datum.store"
import { mapActions} from "vuex";
@@ -20,16 +20,30 @@ export const personSyncDatumMixin = {
methods: {
...mapActions({
matchPersonAndReg: MATCH,
+ dismiss: DISMISS,
}),
manualMatch(regId, personId) {
return this.toastPromise(this.matchPersonAndReg({
regId,
personId,
- regMatch: 'manual'
+ regMatch: 'manual',
+ reload: true
}), "Person successfully linked to Registration")
},
manualMatchSelected() {
return this.manualMatch(this.selectedRegDatum.reg_id, this.selectedPerson.id);
+ },
+ assistedMatch(regId, personId) {
+ return this.toastPromise(this.matchPersonAndReg({
+ regId,
+ personId,
+ regMatch: 'assisted'
+ }), "Person successfully linked to Registration")
+ },
+ dismissMatch(regId, personId) {
+ return this.toastPromise(this.dismiss({
+ regId, personId
+ }), "Potential match successfully dismissed")
}
}
}
diff --git a/app/javascript/store/person_sync_datum.store.js b/app/javascript/store/person_sync_datum.store.js
index 3e9a0d4ad..69619024d 100644
--- a/app/javascript/store/person_sync_datum.store.js
+++ b/app/javascript/store/person_sync_datum.store.js
@@ -5,6 +5,9 @@ import { personModel } from './person.store';
export const personSyncDatumModel = 'person_sync_datum';
export const MATCH = "PERSON SYNC MATCH"
+export const DISMISS = "PERSON SYNC DISMISS"
+export const FETCH_MATCH_COUNT = "PERSON SYNC POTENTIAL MATCHES COUNT"
+export const SET_MATCH_COUNT = "PERSON SYNC POTENTIAL MATCHES COUNT"
export const personSyncDatumEndpoints = {
[personSyncDatumModel]: 'person_sync_datum'
@@ -12,7 +15,7 @@ export const personSyncDatumEndpoints = {
export const personSyncDatumStore = {
actions: {
- [MATCH]({dispatch}, {regId, personId, regMatch}) {
+ [MATCH]({dispatch}, {regId, personId, regMatch, reload = false}) {
console.log('match action', regId, personId, regMatch)
return new Promise((res, rej) => {
http.post(`${personSyncDatumEndpoints[personSyncDatumModel]}/match`, {
@@ -22,17 +25,59 @@ export const personSyncDatumStore = {
}).then((data) => {
// if it was successful, also then fetch the person.
// todo i think we only wnat to do this sometimes?
- dispatch(FETCH_SELECTED, {model: personModel}).then(() => {
+ if (reload) {
+ dispatch(FETCH_SELECTED, {model: personModel}).then(() => {
+ res(data);
+ })
+ } else {
res(data);
- })
+ }
}).catch(rej);
});
+ },
+ [FETCH_MATCH_COUNT] ({commit}) {
+ return new Promise((res, rej) => {
+ http.get(`${personSyncDatumEndpoints[personSyncDatumModel]}/possible_match_count`).then(data => {
+ commit(SET_MATCH_COUNT, data.data.total);
+ console.log('match count data:', data)
+ res(data);
+ }).catch(rej);
+ })
+ },
+ [DISMISS] ({}, {regId, personId}) {
+ return new Promise((res, rej) => {
+ http.post(`${personSyncDatumEndpoints[personSyncDatumModel]}/dismiss_match`, {
+ reg_id: regId,
+ person_id: personId
+ }).then((data) => {
+ console.log(data);
+ res(data);
+ }).catch(rej)
+ });
}
},
selected: {
[personSyncDatumModel]: undefined
},
+ page: {
+ [personSyncDatumModel]: {
+ usePaged: true,
+ total: undefined,
+ fullTotal: undefined,
+ currentPage: undefined,
+ perPage: undefined,
+ correctOrder: [],
+ }
+ },
+ state: {
+ possibleMatchCount: undefined,
+ },
getters: {
+ },
+ mutations: {
+ [SET_MATCH_COUNT] (state, count) {
+ state.possibleMatchCount = count;
+ }
}
}
diff --git a/app/javascript/store/registration_sync_datum.store.js b/app/javascript/store/registration_sync_datum.store.js
index 76eac90a2..a65d123dd 100644
--- a/app/javascript/store/registration_sync_datum.store.js
+++ b/app/javascript/store/registration_sync_datum.store.js
@@ -1,36 +1,59 @@
+import { http } from "@/http";
import { FETCH, SELECT, UNSELECT } from "./model.store";
export const registrationSyncDatumModel = 'registration_sync_datum';
const model = registrationSyncDatumModel;
+export const registrationSyncStatisticsModel = 'registration_sync_statistics'
export const GET_REG_BY_ID = "GET REG BY ID";
+export const REG_SYNC_STATS = "REG SYNC STATS";
+export const SET_REG_SYNC_STATS = "SET REG SYNC STATS";
+export const FETCH_REG_SYNC_STATS = "FETCH REG SYNC STATS";
export const registrationSyncDatumEndpoints = {
- [registrationSyncDatumModel]: 'registration_sync_datum'
+ [registrationSyncDatumModel]: 'registration_sync_datum',
+ [registrationSyncStatisticsModel]: 'registration_sync_data/sync_statistics'
}
export const registrationSyncDatumStore = {
selected: {
[registrationSyncDatumModel]: undefined
},
+ state: {
+ registrationSyncStats: {}
+ },
getters: {
+ [REG_SYNC_STATS](state) {
+ return state.registrationSyncStats;
+ }
+ },
+ mutations: {
+ [SET_REG_SYNC_STATS](state, syncStats) {
+ state.registrationSyncStats = syncStats;
+ }
},
actions: {
- [GET_REG_BY_ID] ({commit, dispatch}, {id}) {
+ [GET_REG_BY_ID]({ commit, dispatch }, { id }) {
return new Promise((res, rej) => {
- dispatch(FETCH, {model, params: {
- // trying without the %23 here in the hope that fetch will serialize correctly
- filter: `{"op":"all","queries":[["registration_number","is","${id}"]]}`
- }}).then((data) => {
+ dispatch(FETCH, {
+ model, params: {
+ // trying without the %23 here in the hope that fetch will serialize correctly
+ filter: `{"op":"all","queries":[["registration_number","is","${id}"]]}`
+ }
+ }).then((data) => {
const keys = Object.keys(data).filter(key => key !== "_jv")
- if(keys.length) {
- commit(SELECT, {model, itemOrId: keys[0]})
+ if (keys.length) {
+ commit(SELECT, { model, itemOrId: keys[0] })
} else {
- commit(UNSELECT, {model});
+ commit(UNSELECT, { model });
}
res(data);
}).catch(rej);
})
+ },
+ [FETCH_REG_SYNC_STATS]({ commit }) {
+ return http.get(registrationSyncDatumEndpoints[registrationSyncStatisticsModel]).then(({ data }) =>
+ commit(SET_REG_SYNC_STATS, data))
}
},
}
diff --git a/app/javascript/store/registration_sync_stats.mixin.js b/app/javascript/store/registration_sync_stats.mixin.js
new file mode 100644
index 000000000..4b8c9a5e6
--- /dev/null
+++ b/app/javascript/store/registration_sync_stats.mixin.js
@@ -0,0 +1,19 @@
+import { mapGetters, mapActions } from "vuex";
+import { FETCH_REG_SYNC_STATS, REG_SYNC_STATS } from "./registration_sync_datum.store";
+import { DateTime } from "luxon";
+
+export const registrationSyncStatsMixin = {
+ computed: {
+ ...mapGetters({
+ stats: REG_SYNC_STATS
+ }),
+ lastSync() {
+ return DateTime.fromISO(this.stats?.updated_at).toFormat('D, t ZZZZ');
+ }
+ },
+ methods: {
+ ...mapActions({
+ fetchStats: FETCH_REG_SYNC_STATS
+ })
+ }
+}
diff --git a/app/lib/migration_helpers/plano_views.rb b/app/lib/migration_helpers/plano_views.rb
index 5b534fa25..0a59a8186 100644
--- a/app/lib/migration_helpers/plano_views.rb
+++ b/app/lib/migration_helpers/plano_views.rb
@@ -14,14 +14,26 @@ def self.create_views
# view for reg matching
self.create_registration_sync_matches
+ self.create_filtered_registration_sync_matches
self.create_registration_map_counts
self.create_registration_map_reg_counts
self.create_registration_map_people_counts
end
+ def self.create_filtered_registration_sync_matches
+ query = <<-SQL.squish
+ CREATE OR REPLACE VIEW filtered_registration_sync_matches AS
+ select * from registration_sync_matches rsm
+ where rsm.reg_id not in (select p2.reg_id from people p2 where p2.reg_id is not null)
+ SQL
+
+ ActiveRecord::Base.connection.execute(query)
+ end
+
def self.create_registration_sync_matches
query = <<-SQL.squish
- CREATE OR REPLACE VIEW registration_sync_matches AS
+ DROP materialized VIEW IF EXISTS registration_sync_matches;
+ CREATE MATERIALIZED VIEW registration_sync_matches AS
select p.name, null as email, p.id as pid, rsd.reg_id, rsd.id as rid, 'name' as mtype
from people p
join registration_sync_data rsd
@@ -34,16 +46,32 @@ def self.create_registration_sync_matches
or rsd."preferred_name" ilike p.pseudonym
or rsd."badge_name" ilike p.pseudonym
)
+ where
+ concat(p.id, '-', rsd.reg_id) not in
+ (select concat(drsm.person_id, '-' , drsm.reg_id) from dismissed_reg_sync_matches drsm)
union
- select null as name, e.email, e.person_id as pid, rsd.reg_id, rsd.id as rid, 'email' as mtype
+ select null as name, e.email, e.person_id as pid, rsd2.reg_id, rsd2.id as rid, 'email' as mtype
from email_addresses e
- join registration_sync_data rsd
+ join registration_sync_data rsd2
on
(
- rsd."email" ilike e.email or
- rsd."alternative_email" ilike e.email
+ rsd2."email" ilike e.email or
+ rsd2."alternative_email" ilike e.email
)
where e.isdefault = true
+ and
+ concat(e.person_id, '-', rsd2.reg_id) not in
+ (select concat(drsm.person_id, '-' , drsm.reg_id) from dismissed_reg_sync_matches drsm);
+ CREATE INDEX matches_reg_id ON registration_sync_matches (reg_id);
+ CREATE INDEX matches_pid ON registration_sync_matches (pid);
+ SQL
+
+ ActiveRecord::Base.connection.execute(query)
+ end
+
+ def self.refresh_registration_sync_matches
+ query = <<-SQL.squish
+ REFRESH MATERIALIZED VIEW registration_sync_matches;
SQL
ActiveRecord::Base.connection.execute(query)
@@ -587,6 +615,15 @@ def self.create_session_conflicts
ActiveRecord::Base.connection.execute(query)
end
+ def self.test_registration_sync_matches_type
+ query = <<-SQL.squish
+ select relkind from pg_catalog.pg_class where relname = 'registration_sync_matches';
+ SQL
+
+ res = ActiveRecord::Base.connection.execute(query)
+ res.first["relkind"]
+ end
+
def self.drop_views
ActiveRecord::Base.connection.execute <<-SQL
DROP VIEW IF EXISTS session_conflicts;
@@ -632,8 +669,18 @@ def self.drop_views
SQL
ActiveRecord::Base.connection.execute <<-SQL
- DROP VIEW IF EXISTS registration_sync_matches;
+ DROP VIEW IF EXISTS filtered_registration_sync_matches;
SQL
+
+ if self.test_registration_sync_matches_type == 'm'
+ ActiveRecord::Base.connection.execute <<-SQL
+ DROP materialized VIEW IF EXISTS registration_sync_matches;
+ SQL
+ else
+ ActiveRecord::Base.connection.execute <<-SQL
+ DROP VIEW IF EXISTS registration_sync_matches;
+ SQL
+ end
end
end
end
diff --git a/app/models/person.rb b/app/models/person.rb
index 04210e908..f91839dc2 100644
--- a/app/models/person.rb
+++ b/app/models/person.rb
@@ -115,8 +115,6 @@ class Person < ApplicationRecord
include PasswordArchivable
include DirtyAssociations
- # acts_as_taggable
- acts_as_taggable_on :tags
has_paper_trail versions: { class_name: 'Audit::PersonVersion' },
ignore: [:updated_at, :created_at, :lock_version, :integrations],
diff --git a/app/models/person_schedule.rb b/app/models/person_schedule.rb
index f31767a64..5b31ac41d 100644
--- a/app/models/person_schedule.rb
+++ b/app/models/person_schedule.rb
@@ -13,11 +13,13 @@
# participant_notes :text
# pronouns :string(400)
# published_name :string
+# recorded :boolean
# session_assignment_name :string(100)
# session_assignment_role_type :enum
# sort_order :integer
# start_time :datetime
# status :enum
+# streamed :boolean
# title :string(256)
# updated_at :datetime
# format_id :uuid
diff --git a/app/models/registration/registration_sync_match.rb b/app/models/registration/registration_sync_match.rb
index f06891439..d98adca9a 100644
--- a/app/models/registration/registration_sync_match.rb
+++ b/app/models/registration/registration_sync_match.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: registration_sync_matches
+# Table name: filtered_registration_sync_matches
#
# email :string
# mtype :text primary key
@@ -9,8 +9,13 @@
# rid :uuid primary key
# reg_id :string
#
+# Indexes
+#
+# matches_pid (pid)
+# matches_reg_id (reg_id)
+#
class Registration::RegistrationSyncMatch < ApplicationRecord
- self.table_name = :registration_sync_matches
+ self.table_name = :filtered_registration_sync_matches
self.primary_keys = :rid, :mtype
belongs_to :person, optional: true, foreign_key: 'pid'
diff --git a/app/models/survey/answer.rb b/app/models/survey/answer.rb
index 36ec8b346..399b671dd 100644
--- a/app/models/survey/answer.rb
+++ b/app/models/survey/answer.rb
@@ -57,7 +57,7 @@ def validate_answer
raise 'invalid answers for YewNoMaybe question type' unless ['yes', 'no', 'maybe'].include? value
end
if question.question_type == :attendance_type
- raise 'invalid answers for Attendance question type' unless ['in person', 'virtual', 'hybrid'].include? value
+ raise 'invalid answers for Attendance question type' unless ['in_person', 'virtual', 'hybrid'].include? value
end
end
diff --git a/app/policies/person_sync_datum_policy.rb b/app/policies/person_sync_datum_policy.rb
index 445d8fe3e..990df954b 100644
--- a/app/policies/person_sync_datum_policy.rb
+++ b/app/policies/person_sync_datum_policy.rb
@@ -1,5 +1,9 @@
class PersonSyncDatumPolicy < PlannerPolicy
+ def possible_match_count?
+ allowed?(action: :dismiss_match)
+ end
+
def dismiss_match?
allowed?(action: :dismiss_match)
end
diff --git a/app/serializers/conclar/participant_serializer.rb b/app/serializers/conclar/participant_serializer.rb
index a1eeaeb4f..2f59231d0 100644
--- a/app/serializers/conclar/participant_serializer.rb
+++ b/app/serializers/conclar/participant_serializer.rb
@@ -53,7 +53,7 @@ class Conclar::ParticipantSerializer < ActiveModel::Serializer
res = []
case object.attendance_type
- when 'in person'
+ when 'in_person'
t = {
value: "person_".concat(object.attendance_type),
category: "Attendance",
diff --git a/app/serializers/person_schedule_serializer.rb b/app/serializers/person_schedule_serializer.rb
index e1856becc..abadc3ba8 100644
--- a/app/serializers/person_schedule_serializer.rb
+++ b/app/serializers/person_schedule_serializer.rb
@@ -13,11 +13,13 @@
# participant_notes :text
# pronouns :string(400)
# published_name :string
+# recorded :boolean
# session_assignment_name :string(100)
# session_assignment_role_type :enum
# sort_order :integer
# start_time :datetime
# status :enum
+# streamed :boolean
# title :string(256)
# updated_at :datetime
# format_id :uuid
diff --git a/app/serializers/person_serializer.rb b/app/serializers/person_serializer.rb
index a61d03189..8ca539ecc 100644
--- a/app/serializers/person_serializer.rb
+++ b/app/serializers/person_serializer.rb
@@ -164,10 +164,6 @@ class PersonSerializer #< ActiveModel::Serializer
!person.encrypted_password.blank?
end
- attribute :tags do |person|
- person.base_tags.collect(&:name)
- end
-
attribute :session_count do |person|
if person.has_attribute?(:session_count)
person.session_count
diff --git a/app/serializers/person_sync_datum_serializer.rb b/app/serializers/person_sync_datum_serializer.rb
index ee4338805..076736f9f 100644
--- a/app/serializers/person_sync_datum_serializer.rb
+++ b/app/serializers/person_sync_datum_serializer.rb
@@ -112,17 +112,16 @@ class PersonSyncDatumSerializer
:primary_email, :contact_email,
:reg_match, :date_reg_synced
- has_many :email_addresses,
- if: Proc.new { |record, params| AccessControlService.shared_attribute_access?(instance: record, person: params[:current_person]) },
- lazy_load_data: true, serializer: EmailAddressSerializer,
- links: {
- self: -> (object, params) {
- "#{params[:domain]}/person/#{object.id}"
- },
- related: -> (object, params) {
- "#{params[:domain]}/person/#{object.id}/email_addresses"
- }
- }
+ # has_many :email_addresses,
+ # lazy_load_data: true, serializer: EmailAddressSerializer
+ # links: {
+ # self: -> (object, params) {
+ # "#{params[:domain]}/person/#{object.id}"
+ # },
+ # related: -> (object, params) {
+ # "#{params[:domain]}/person/#{object.id}/email_addresses"
+ # }
+ # }
# The reg data that this person could be matched to
has_many :registration_sync_data, serializer: RegistrationSyncDatumSerializer
diff --git a/app/services/access_control_service.rb b/app/services/access_control_service.rb
index aedbe446e..80c9621bd 100644
--- a/app/services/access_control_service.rb
+++ b/app/services/access_control_service.rb
@@ -24,7 +24,7 @@ def self.attribute_meta_data
registered: { sensitive: false, linkable: false, type: :boolean, hidable: false},
registration_type: { sensitive: true, linkable: false, type: :string, hidable: false},
registration_number: { sensitive: true, linkable: false, type: :string, hidable: false},
- attendance_type: { sensitive: false, linkable: true, type: :attendance_type, values: ['in person', 'virtual', 'hybrid'], hidable: false},
+ attendance_type: { sensitive: false, linkable: true, type: :attendance_type, values: ['in_person', 'virtual', 'hybrid'], hidable: false},
bio: { sensitive: false, linkable: true, type: :text, hidable: false},
# NOTE: we really do not want individual social media fields to be linked,
# this was done as the google form migration. Surveys in Plano should use
diff --git a/app/workers/registration_sync_worker.rb b/app/workers/registration_sync_worker.rb
index f3b98897a..da9f8f02c 100644
--- a/app/workers/registration_sync_worker.rb
+++ b/app/workers/registration_sync_worker.rb
@@ -23,6 +23,9 @@ def perform
status = RegistrationSyncStatus.order('created_at desc').first
status = RegistrationSyncStatus.new if status == nil
+ # Refresh the materialized view(s)
+ MigrationHelpers::PlanoViews.refresh_registration_sync_matches
+
status.result = {
updated: number_updated,
matched: number_matched,
diff --git a/config/routes.rb b/config/routes.rb
index 3b071c395..f3c23660a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -215,6 +215,7 @@
get 'people', to: 'registration_sync_data#people'
end
+ get 'person_sync_datum/possible_match_count', to: 'person_sync_data#possible_match_count'
post 'person_sync_datum/dismiss_match', to: 'person_sync_data#dismiss_match'
post 'person_sync_datum/match', to: 'person_sync_data#match'
resources :person_sync_data, path: 'person_sync_datum'
diff --git a/db/migrate/20240708121706_fix_in_person_values.rb b/db/migrate/20240708121706_fix_in_person_values.rb
new file mode 100644
index 000000000..692b2df9f
--- /dev/null
+++ b/db/migrate/20240708121706_fix_in_person_values.rb
@@ -0,0 +1,7 @@
+class FixInPersonValues < ActiveRecord::Migration[6.1]
+ def up
+ # One off change to make site "in person" is "in_person"
+ # use like just in case of case issues
+ Person.where("attendance_type ilike 'in person'").update_all(attendance_type: 'in_person')
+ end
+end
diff --git a/db/seeds/development/person.seeds.rb b/db/seeds/development/person.seeds.rb
index 3cf81a3ce..d1cdf923a 100644
--- a/db/seeds/development/person.seeds.rb
+++ b/db/seeds/development/person.seeds.rb
@@ -42,7 +42,7 @@
flickr: username,
reddit: username,
tiktok: username,
- attendance_type: ['in person', 'virtual', 'hybrid'].sample
+ attendance_type: ['in_person', 'virtual', 'hybrid'].sample
)
e = name.gsub(' ', '_') + i.to_s + '@test.com'
EmailAddress.create(
diff --git a/db/structure.sql b/db/structure.sql
index 6f1e0e2dc..48ad35aa5 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -1332,6 +1332,8 @@ CREATE VIEW public.person_schedules AS
sessions.description,
sessions.environment,
sessions.status,
+ sessions.streamed,
+ sessions.recorded,
CASE
WHEN (sa.updated_at > sessions.updated_at) THEN sa.updated_at
ELSE sessions.updated_at
@@ -1649,10 +1651,10 @@ CREATE TABLE public.registration_sync_data (
--
--- Name: registration_sync_matches; Type: VIEW; Schema: public; Owner: -
+-- Name: registration_sync_matches; Type: MATERIALIZED VIEW; Schema: public; Owner: -
--
-CREATE VIEW public.registration_sync_matches AS
+CREATE MATERIALIZED VIEW public.registration_sync_matches AS
SELECT p.name,
NULL::character varying AS email,
p.id AS pid,
@@ -1670,7 +1672,8 @@ UNION
'email'::text AS mtype
FROM (public.email_addresses e
JOIN public.registration_sync_data rsd ON ((((rsd.email)::text ~~* (e.email)::text) OR ((rsd.alternative_email)::text ~~* (e.email)::text))))
- WHERE (e.isdefault = true);
+ WHERE (e.isdefault = true)
+ WITH NO DATA;
--
@@ -2241,6 +2244,15 @@ CREATE TABLE public.tags (
);
+--
+-- Name: tt; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.tt (
+ relkind "char"
+);
+
+
--
-- Name: venues; Type: TABLE; Schema: public; Owner: -
--
@@ -3624,6 +3636,20 @@ CREATE UNIQUE INDEX index_tags_on_name ON public.tags USING btree (name);
CREATE INDEX index_versions_on_item_type_and_item_id ON public.versions USING btree (item_type, item_id);
+--
+-- Name: matches_pid; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX matches_pid ON public.registration_sync_matches USING btree (pid);
+
+
+--
+-- Name: matches_reg_id; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX matches_reg_id ON public.registration_sync_matches USING btree (reg_id);
+
+
--
-- Name: par_approle_person_idx; Type: INDEX; Schema: public; Owner: -
--
@@ -3953,6 +3979,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20240522190737'),
('20240602172220'),
('20240606115218'),
-('20240622165823');
+('20240622165823'),
+('20240708121706');
diff --git a/lib/tasks/rbac.rake b/lib/tasks/rbac.rake
index 81126b4fe..2cd2594ae 100644
--- a/lib/tasks/rbac.rake
+++ b/lib/tasks/rbac.rake
@@ -1019,7 +1019,8 @@ namespace :rbac do
"index": true,
"show": true,
"dismiss_match": true,
- "match": true
+ "match": true,
+ "possible_match_count": true
}
})
end
diff --git a/lib/tasks/submission.rake b/lib/tasks/submission.rake
index 9bee9e10f..88d6d6fd4 100644
--- a/lib/tasks/submission.rake
+++ b/lib/tasks/submission.rake
@@ -142,7 +142,7 @@ namespace :submission do
if value.include?('**In-person and virtual:**')
['hybrid']
elsif value.include?('**In-person only:**')
- ['in person']
+ ['in_person']
elsif value.include?('**Virtual only:**')
['virtual']
end