diff --git a/app/client/stylesheets/style.less b/app/client/stylesheets/style.less index dde1840..15b3b8e 100644 --- a/app/client/stylesheets/style.less +++ b/app/client/stylesheets/style.less @@ -1,549 +1,552 @@ @import "bootstrap/custom.bootstrap.output.import.less"; @import "theme/flatly.import.less"; @import "x_editable.import.less"; @import "sweetalert.import.less"; @grid-float-breakpoint: 830px; body { padding-top: 60px; } @media print { body { padding-top: 0px; } } td, th { line-height: 1.2; padding-bottom: 0.6em; } table.tight { td, th { padding-bottom: 0; } } table.collapsed { width: inherit; } .reactive-table { th { color: @text-color !important; } td.buttons { text-align: right; } .btn { width: 61px; } .btn-stack { padding: 5.5px 10px; } } .reactive-table-navigation .form-inline input { width: 60px; } .form-control { padding: 3px 5px; } .daterangepicker { color: black; } .page-header, .page-header-small { border-bottom: none; } .nav-tabs { li > form { padding-top: 6px; input:first-child { padding-bottom: 6px; } } } .errors { margin-top: 15px; } //////////////////////////////// // generic .valid { color: @brand-success; } .invalid { color: @brand-warning; alpha: 0.5; } //////////////////////////////// // patients .patients { margin-top: 10px; .fa-chevron-right { vertical-align: -29%; } #patientsTable { tr { cursor: pointer; } } } //////////////////////////////// // patient .patient { .profile { .table { width: initial; //tr:first-child { // font-weight: bold; //} th { font-weight: lighter; } td { padding-left: 15px; vertical-align: top; } img { vertical-align: top; margin-bottom: 15px; } } } #visits { table { .due{color: @brand-success;} .over-due {color: @brand-warning;} .future {font-style: italic;} .no-date span { font-style: italic; color: @text-color !important; } .showQuestionnaire { display: inline-block; padding: 0px 15px; line-height: 1.5em; } td:last-child { vertical-align: middle; } } } #visit { .questionnaires { table { width: initial; font-size: 20px; td:not(:first-child) { padding-left: 15px; } td.buttons { min-width: 250px; } } } } } //////////////////////////////// // editQuestionnaire .editQuestionnaire { margin-top: 10px; .markdownControls { display: none; } .question { cursor: pointer; padding: 10px 10px; &:nth-child(even) { background-color: #f5f5f5; } .af-radio-group { margin-left: 7px; .radio-inline, .fix-indent { margin-right: 40px; } } &.selectedQuestion { border: 1px solid @brand-info; border-radius: 2px; //padding: 10px 5px; } } #questionEditor { margin-bottom: 20px; .panel > .list-group .list-group-item { padding: 5px 5px 0px 5px; border: 0; } .panel .autoform-add-item { margin-left: 10px; margin-bottom: 5px; } } .buttons-container { height: 60px; padding-right: 15px; } } //////////////////////////////// // questions .question { .control-label { font-weight: bold; } .question-table { td:not(:first-child) { text-align: center; } tr:nth-child(even) { background-color: #ecf0f1; } tr:nth-child(odd).missing-answer { background-color: rgba(231, 76, 60, 0.3) !important; } tr:nth-child(even).missing-answer { background-color: rgba(231, 76, 60, 0.45) !important; } } .question-table-polar { td:not(:first-child):not(:last-child) { text-align: center; } td:last-child { text-align: right; } } .questionAutoform { &.missing-answer { padding: 15px; background-color: rgba(231, 76, 60, 0.3) !important; } } .code { color: @brand-danger; margin-right: 15px; display: inline-block; } } //////////////////////////////// // editStudy #studyPageOverlay { z-index: 1000; //navbar is 1030 cursor: not-allowed; //&:hover { // background-color: rgba(222, 219, 219, 0.5); //} } .editStudyPatients { table tr{ cursor: pointer; } } .editStudyDesigns { @media print { font-size: 0.8em; .collapse { display: block !important; height: auto !important; visibility: visible !important; } .panel { page-break-after: always; page-break-inside: avoid; } .btn-group { display: none; } } .panel-group { margin-top: 1em; .panel { &:first-child { border-top: none; } &:nth-child(odd) { .panel-heading { background-color: @table-bg-accent !important; } } margin-top: 0px; border-radius: 0 !important; border: none; box-shadow: none; -webkit-box-shadow: none; .panel-heading { height: 61px; padding: 14px 15px; background-color: transparent; .panel-title-inline { display: inline; font-size: 28px; } .btn-group { margin-top: -5px; .accordion-toggle { &:after { font-family: 'FontAwesome'; content: "\f078"; //font-size: 1.2em; } &.collapsed:after { content: "\f054"; } } } } .panel-body { border: none; } } } .studyDesignTable { .visitTd { border-bottom: 1px solid #dddddd; } .btn-group { min-width: 150px; text-align: right; } td.title { font-weight: bold; white-space: nowrap; span { margin-right: 10px; } } } .bootstrap-tagsinput { width: 90%; input { box-shadow: none; } } // th { // white-space: nowrap; // &:first-child { // width: 250px; // } // &:not(:first-child) { // text-align: center; // } // &:nth-of-type(even):not(:first-child):not(:last-child) { // border-bottom: 2px solid @brand-primary; // } // &:nth-of-type(odd):not(:first-child):not(:last-child) { // border-bottom: 2px dashed @brand-primary; // } // .text-muted { // font-weight: normal; // } // } // td { // &:not(:first-child) { // text-align: center; // } // &:last-child { // width: 200px; // } // } // } } //////////////////////////////// // questionnaireWizzard .questionnaireWizzard { .modal-dialog, .modal-content { margin: 0px; width: 100%; height: 100%; overflow-y: auto; border: none; box-shadow: none; border-radius: 0; .modal-header { background-color: white; position: fixed; width: 100%; z-index: 9999; h2 { display: inline-block; } .regulations { float: right; td { line-height: 0.9; } } } .modal-body { margin-top: 75px; } } .questionOverview { color: white; i { color: grey; } .active { .activeCircle { color: @brand-warning; font-size: 2.5em; top: -4px; left: -1.5px; } } .answered { i { color: @brand-success; } } .answeredPartly { i { color: @brand-info; } } } input[type=radio], input[type="checkbox"] { font-size: 30px; } .nav-buttons { display: flex; flex-direction: row-reverse; justify-content: space-between; } } //////////////////////////////// // export #export { margin-top .treeCol { overflow-x: scroll; } .contentCol { margin-top: 10px; #table { th { white-space: nowrap; - border-right: 1px solid black; + //border-right: 1px solid black; padding: 0px 20px 0px 5px; } td { white-space: nowrap; padding: 0px 20px 0px 5px; } } } + .btn-danger{ + margin-left: 20px; + } } //////////////////////////////// // segmentation .row-fill { overflow-y: scroll; -webkit-overflow-scrolling: touch; } .row-max-half { overflow-y: scroll; -webkit-overflow-scrolling: touch; } //////////////////////////////// // misc #spinner { height: 300px; } .label { font-size: 85%; display: inline-block; } .label-warning { background-color: rgba(243, 156, 18, 0.3); } .smallSelect { width: auto; height: auto; padding: 2px 5px; display: inline-block; border: none; margin: 3px 0px; } .smallInput { //height: auto; padding: 2px 5px; display: inline-block; border: none; margin: 3px 0px; } .btn-img { padding: 0; border: none; background: none; &:hover, &:focus, &:active { color: inherit !important; box-shadow: none; } -webkit-box-shadow: none; box-shadow: none; } .cursorPointer { cursor: pointer; } .page-header-small { &:extend(.page-header); margin: 0px; } .btn-hdr { margin-left: 15px; //vertical-align: bottom; } .btn-stack { padding: 5.5px 7px; } .hoverOpaque { opacity: 0.6; &:hover { opacity: 1.0; } } .hoverOpaqueExtreme { opacity: 0.4; &:hover { opacity: 1.0; } } .hoverOpaqueInvisible { opacity: 0.1; &:hover { opacity: 1.0; } } .brand-primary { color: @brand-primary; } .modal-content { background-color: white; } .italic { font-style: italic; } diff --git a/app/client/views/export/export.coffee b/app/client/views/export/export.coffee index e5105b4..2642a4b 100644 --- a/app/client/views/export/export.coffee +++ b/app/client/views/export/export.coffee @@ -1,380 +1,406 @@ _studyIcon = 'fa fa-book' _designIcon = 'fa fa-list-alt' _visitIcon = 'fa fa-calendar-check-o' _patientIcon = 'fa fa-user' _questionnaireIcon = 'fa fa-file-text-o' _questionIcon = 'fa fa-cube' _caseManagerIcon = 'fa fa-user-md' _answerIcon = 'fa fa-edit' Template.export.rendered = -> # nodes are leafes in the export tree # node id nomenclatura: # - ids beginning with 1 underscore are structure only (folders) and ignored # - ids beginning with 2 underscore are system variables selectors (regexed) # - ids not beginning with an underscore are content (visit, patient, etc.) nodes = [] nodes.push id: '_systemVariables' parent: '#' text: 'System Variables' icon: 'fa fa-gear' state: { opened: true } nodes.push id: '_patient' parent: '_systemVariables' text: 'Patient' icon: _patientIcon state: { opened: true } nodes.push id: '__patient.id' parent: '_patient' text: 'patient ID' icon: _patientIcon state: { selected: true, disabled: true } nodes.push id: '__patient.hrid' parent: '_patient' text: 'patient HRID' icon: _patientIcon nodes.push id: '__patient._id' parent: '_patient' text: 'internal ID' icon: _patientIcon nodes.push id: '__patient.caseManagerName' parent: '_patient' text: 'case manager' icon: _patientIcon #presentation ### nodes.push id: '_questionnaire' parent: '_systemVariables' text: 'Questionnaire' icon: _questionnaireIcon state: { opened: false } nodes.push id: '_question' parent: '_systemVariables' text: 'Question' icon: _questionIcon state: { opened: false } nodes.push id: '_visitTemplate' parent: '_systemVariables' text: 'VisitTemplate' icon: _visitIcon state: { opened: true } nodes.push id: '_caseManager' parent: '_systemVariables' text: 'Case Manager' icon: _caseManagerIcon state: { opened: true } nodes.push id: '_answer' parent: '_systemVariables' text: 'Answer' icon: _answerIcon state: { opened: true } ### # ## nodes.push id: '_study' parent: '_systemVariables' text: 'Study' icon: _studyIcon state: { opened: true } nodes.push id: '__study.title' parent: '_study' text: 'title' icon: _studyIcon nodes.push id: '_studyDesign' parent: '_systemVariables' text: 'Study Design' icon: _designIcon state: { opened: true } nodes.push id: '__studyDesign.title' parent: '_studyDesign' text: 'title' icon: _designIcon nodes.push id: '_visit' parent: '_systemVariables' text: 'Visit' icon: _visitIcon state: { opened: true } nodes.push id: '__visit.title' parent: '_visit' text: 'title' icon: _visitIcon state: { selected: true, disabled: true } nodes.push id: '__visit.date' parent: '_visit' text: 'date' icon: _visitIcon state: { selected: false } nodes.push id: '__visit.dateScheduled' parent: '_visit' text: 'scheduled date' icon: _visitIcon state: { selected: false } nodes.push id: '_studies' parent: '#' text: 'Studies' icon: _studyIcon state: { opened: true } Studies.find().forEach (study) -> studyNode = id: 'study_'+study._id parent: '_studies' text: study.title icon: _studyIcon state: { opened: true } nodes.push studyNode StudyDesigns.find( studyId: study._id ).forEach (design) -> nodes.push id: 'design_'+design._id parent: 'study_'+study._id text: design.title icon: _designIcon state: opened: false nodes.push id: '_patients_'+design._id parent: 'design_'+design._id text: 'Patients' icon: _patientIcon state: opened: true nodes.push id: '_visits_'+design._id parent: 'design_'+design._id text: 'Visits' icon: _visitIcon state: opened: true nodes.push id: '_questionnaires_'+design._id parent: 'design_'+design._id text: 'Questionnaires' icon: _questionnaireIcon state: opened: true Patients.find( studyDesignId: design._id ).forEach (patient) -> title = patient.id if patient.hrid title += " - "+patient.hrid nodes.push id: 'patient_'+patient._id parent: '_patients_'+design._id text: title icon: _patientIcon state: opened: true design.visits.forEach (visit) -> nodes.push id: 'visit_'+visit._id parent: '_visits_'+design._id text: visit.title icon: _visitIcon state: opened: true questionnaireIds = design.questionnaireIds || [] Questionnaires.find( _id: {$in: questionnaireIds} ).forEach (questionnaire) -> nodes.push id: 'questionnaire_'+questionnaire._id+'_'+design._id parent: '_questionnaires_'+design._id text: questionnaire.title icon: _questionnaireIcon state: opened: false Questions.find( questionnaireId: questionnaire._id , sort: {index: 1} ).forEach (question) -> title = "#{question.index} - #{question.label}" nodes.push id: 'question_'+question._id+'_'+design._id parent: 'questionnaire_'+questionnaire._id+'_'+design._id text: title icon: _questionIcon #console.log nodes $('#tree').jstree( plugins: [ "checkbox" ] checkbox: "keep_selected_style": false core: data: nodes themes: name: 'proton' responsive: true ).on 'changed.jstree', (evt, data) -> selectedIds = data.instance.get_selected() #dicts for easier searching systemVariables = {} patientsAndVisitsByDesignsDict = {} questionnairesAndQuestionsDict = {} selectedIds.forEach (sId) -> #system variables if sId.lastIndexOf("__", 0) is 0 regex = /__(.+?)\.(.+)/ match = regex.exec sId entity = match[1] variable = match[2] e = systemVariables[entity] || [] #e[variable] = true e.push variable systemVariables[entity] = e #content else if sId.lastIndexOf("patient_", 0) is 0 or sId.lastIndexOf("visit_", 0) is 0 or sId.lastIndexOf("question_", 0) is 0 path = data.instance.get_path(sId, false, true) #remove "folders" path = path.filter (pId) -> pId.substring(0, 1) isnt '_' #console.log path if sId.lastIndexOf("patient_", 0) is 0 designId = null patientId = null path.forEach (step) -> if step.lastIndexOf("design_", 0) is 0 designId = step.replace "design_", "" if step.lastIndexOf("patient_", 0) is 0 patientId = step.replace "patient_", "" debugger if !designId? or !patientId? design = patientsAndVisitsByDesignsDict[designId] if !design? design = _id: designId patientIds: [patientId] visitIds: [] else design.patientIds.push patientId design.patientIds = _.unique design.patientIds patientsAndVisitsByDesignsDict[designId] = design else if sId.lastIndexOf("visit_", 0) is 0 designId = null visitId = null path.forEach (step) -> if step.lastIndexOf("design_", 0) is 0 designId = step.replace "design_", "" if step.lastIndexOf("visit_", 0) is 0 visitId = step.replace "visit_", "" debugger if !designId? or !visitId? design = patientsAndVisitsByDesignsDict[designId] if !design? design = _id: designId patientIds: [] visitIds: [visitId] else design.visitIds.push visitId design.visitIds = _.unique design.visitIds patientsAndVisitsByDesignsDict[designId] = design else if sId.lastIndexOf("question_", 0) is 0 questionnaireId = null questionId = null path.forEach (step) -> if step.lastIndexOf("questionnaire_", 0) is 0 questionnaireId = step.replace "questionnaire_", "" questionnaireId = questionnaireId.replace(/_.*/, '') if step.lastIndexOf("question_", 0) is 0 questionId = step.replace "question_", "" debugger if !questionnaireId? or !questionId? questionnaire = questionnairesAndQuestionsDict[questionnaireId] if !questionnaire? questionnaire = _id: questionnaireId questionIds: [questionId] else questionnaire.questionIds.push questionId questionnaire.questionIds = _.unique questionnaire.questionIds questionnairesAndQuestionsDict[questionnaireId] = questionnaire #convert dicts to arrays designs = [] for key in Object.keys(patientsAndVisitsByDesignsDict) designs.push patientsAndVisitsByDesignsDict[key] questionnaires = [] for key in Object.keys(questionnairesAndQuestionsDict) questionnaires.push questionnairesAndQuestionsDict[key] selection = system: systemVariables designs: designs questionnaires: questionnaires console.log selection _selection.set selection return _selection = new ReactiveVar [] _waitingForDownload = new ReactiveVar false +_waitingForExportTable = new ReactiveVar false Template.export.helpers columnHeaders: -> selection = _selection.get() Export.columnHeaders(selection) #rows: -> # selection = _selection.get() # return if not selection.designs? # Export.rows(selection) #columns: -> # row = @ # Tracker.nonreactive -> # selection = _selection.get() # Export.columns(selection, row) waitingForDownload: -> _waitingForDownload.get() + waitingForExportTable: -> + _waitingForExportTable.get() + + hasExportTables: -> + ExportTables.find().count() > 0 + + exportTables: -> + ExportTables.find({}, {sort: name: -1}) + Template.export.events 'click #downloadCSV': (evt) -> _waitingForDownload.set true selection = _selection.get() loginToken = Accounts._storedLoginToken() Meteor.call 'createCSV', selection, loginToken, (error, url) -> _waitingForDownload.set false throwError error if error? window.open url#, '_blank' + return false + + 'click #createExportTable': (evt) -> + _waitingForExportTable.set true + selection = _selection.get() + Meteor.call 'createExportTable', selection, (error) -> + _waitingForExportTable.set false + throwError error if error? + return false + + 'click .removeExportTable': (evt) -> + _waitingForExportTable.set true + Meteor.call 'removeExportTable', @name, (error) -> + _waitingForExportTable.set false + throwError error if error? + return false diff --git a/app/client/views/export/export.html b/app/client/views/export/export.html index d473079..e7d1d15 100644 --- a/app/client/views/export/export.html +++ b/app/client/views/export/export.html @@ -1,44 +1,74 @@ diff --git a/app/lib/collections/export_tables.coffee b/app/lib/collections/export_tables.coffee new file mode 100644 index 0000000..323809f --- /dev/null +++ b/app/lib/collections/export_tables.coffee @@ -0,0 +1 @@ +@ExportTables = new Meteor.Collection "export_tables" diff --git a/app/lib/router.coffee b/app/lib/router.coffee index 899f351..0ee724f 100644 --- a/app/lib/router.coffee +++ b/app/lib/router.coffee @@ -1,114 +1,115 @@ Router.configure layoutTemplate: "layout" loadingTemplate: "loading" notFoundTemplate: "not_found" # automatically render notFoundTemplate if data is null #Router.onBeforeAction('dataNotFound') Router.onBeforeAction( -> AccountsEntry.signInRequired(this) , {except: ["entrySignIn", "entrySignUp", "entryForgotPassword", "entrySignOut", "entryResetPassword", "appDumpHTTP"] }) #Router.plugin('ensureSignedIn', # except: ["entrySignIn", "entrySignUp", "entryForgotPassword", "entrySignOut", "entryResetPassword"] #) previousPage = null Router.map -> @route "root", path: "/" onBeforeAction: (pause)-> @redirect "/patients" @route "dashboard", path: "dashboard" @route "patients", path: "patients" waitOn: -> [ Meteor.subscribe("patients") ] @route "questionnaires", path: "questionnaires" waitOn: -> [ Meteor.subscribe("questionnaires") Meteor.subscribe("questions") Meteor.subscribe("userProfiles") ] @route "editQuestionnaire", path: "questionnaires/edit/:_id" waitOn: -> [ Meteor.subscribe("questionnaires") Meteor.subscribe("questionsForQuestionnaire", @params._id) ] data: -> Questionnaires.findOne {_id: @params._id} @route "studies", path: "studies" waitOn: -> [ Meteor.subscribe("studies") Meteor.subscribe("userProfiles") ] @route "editStudy", path: "studies/edit/:_id/:page?" waitOn: -> [ Meteor.subscribe("study", @params._id ) Meteor.subscribe("patientsForStudy", @params._id ) Meteor.subscribe("caseManagers") Meteor.subscribe("studyDesignsForStudy", @params._id ) Meteor.subscribe("questionnaires") ] data: -> Studies.findOne {_id: @params._id} @route "export", path: "export" waitOn: -> [ Meteor.subscribe("questionnaires") Meteor.subscribe("questions") Meteor.subscribe("studies") Meteor.subscribe("studyDesigns") Meteor.subscribe("patients") Meteor.subscribe("visits") Meteor.subscribe("caseManagers") + Meteor.subscribe("exportTables") ] @route "users", path: "users" waitOn: -> [ Meteor.subscribe("users") ] @route "activities", path: "activities" waitOn: -> [ Meteor.subscribe("users") Meteor.subscribe("activities") ] @route "backup", path: "backup" if Meteor.isClient Accounts.ui.config passwordSignupFields: 'USERNAME_AND_EMAIL' Meteor.startup -> AccountsEntry.config homeRoute: '/' #redirect to this path after sign-out dashboardRoute: '/patients' #redirect to this path after sign-in passwordSignupFields: 'USERNAME_AND_EMAIL' diff --git a/app/server/collections/exports.coffee b/app/server/collections/exports.coffee new file mode 100644 index 0000000..5bba7ba --- /dev/null +++ b/app/server/collections/exports.coffee @@ -0,0 +1,33 @@ +@ExportTable = new Meteor.Collection "export_table" + +Meteor.methods + 'createExportTable': (selection) -> + checkIfAdmin() + check selection, Object + + #ExportTable.remove() + + tableName = "export_#{moment().toISOString()}" + ExportTables.insert + name: tableName + + headers = Export.columnHeaders(selection) + Export.rows(selection).forEach (row) -> + cols = Export.columns(selection, row) + tableRow = + tableName: tableName + i = 0 + while i < headers.length + header = headers[i].title.replace(".", "_") + tableRow[header] = cols[i] + i++ + ExportTable.insert tableRow + return + + 'removeExportTable': (tableName) -> + checkIfAdmin() + check tableName, String + ExportTable.remove + tableName: tableName + ExportTables.remove + name: tableName diff --git a/app/server/publications.coffee b/app/server/publications.coffee index 33c7e21..b4a555f 100644 --- a/app/server/publications.coffee +++ b/app/server/publications.coffee @@ -1,185 +1,191 @@ onlyIfAdmin = -> if Roles.userIsInRole(@userId, ['admin']) return true else @ready() return onlyIfAdminOrCaseManager = -> if Roles.userIsInRole(@userId, ['admin', 'caseManager']) return true else @ready() return onlyIfUser = -> if @userId return true else @ready() return ################################################# Meteor.publish "caseManagers", -> return unless onlyIfAdmin.call(@) Meteor.users.find( roles: "caseManager" , fields: _id: 1 username: 1 emails: 1 profile: 1 roles: 1 status: 1 createdAt: 1 ) Meteor.publish "users", -> return unless onlyIfAdmin.call(@) Meteor.users.find( {}, fields: _id: 1 username: 1 emails: 1 profile: 1 roles: 1 status: 1 createdAt: 1 ) Meteor.publish "userProfiles", -> return unless onlyIfAdmin.call(@) Meteor.users.find( {}, fields: _id: 1 username: 1 emails: 1 profile: 1 ) Meteor.publish "studies", -> return unless onlyIfAdminOrCaseManager.call(@) Studies.find() Meteor.publish "study", (_id) -> return unless onlyIfAdminOrCaseManager.call(@) Studies.find(_id: _id) Meteor.publish "studyDesigns", -> return unless onlyIfAdminOrCaseManager.call(@) StudyDesigns.find() Meteor.publish "studyDesignsForStudy", (studyIds) -> return unless onlyIfAdminOrCaseManager.call(@) if typeof studyIds is 'string' studyIds = [studyIds] StudyDesigns.find studyId: {$in: studyIds} Meteor.publish "patients", -> if Roles.userIsInRole(@userId, ['admin']) return Patients.find() else if Roles.userIsInRole(@userId, ['caseManager']) return Patients.find caseManagerId: @userId else @ready() return Meteor.publish "patientsForStudy", (studyID) -> return unless onlyIfAdmin.call(@) Patients.find studyId: studyID Meteor.publish "studyForPatient", (_id) -> if Roles.userIsInRole(@userId, ['admin']) patient = Patients.findOne _id: _id if patient? return Studies.find _id: patient.studyId else if Roles.userIsInRole(@userId, ['caseManager']) patient = Patients.findOne _id: _id caseManagerId: @userId if patient? return Studies.find _id: patient.studyId @ready() Meteor.publish "studyDesignForPatient", (_id) -> patient = Patients.findOne _id: _id if patient? studyDesign = StudyDesigns.find studyId: patient.studyId if Roles.userIsInRole(@userId, ['admin']) or (Roles.userIsInRole(@userId, 'caseManager') and patient.caseManagerId is @userId) return studyDesign @ready() Meteor.publishComposite 'studyCompositesForPatient', (patientId) -> find: -> patient = Patients.findOne _id: patientId if patient? if Roles.userIsInRole(@userId, ['admin']) or (Roles.userIsInRole(@userId, 'caseManager') and patient.caseManagerId is @userId) return Patients.find _id: patientId return null children: [ find: (patient) -> Studies.find _id: patient.studyId , find: (patient) -> StudyDesigns.find _id: patient.studyDesignId children: [ find: (studyDesign) -> Questionnaires.find _id: {$in: studyDesign.questionnaireIds } children: [ find: (questionnaire) -> Questions.find questionnaireId: questionnaire._id ] ] ] Meteor.publish "visits", -> return unless onlyIfAdmin.call(@) Visits.find() Meteor.publishComposite 'visitsCompositeForPatient', (patientId) -> find: -> patient = Patients.findOne patientId if patient? if Roles.userIsInRole(@userId, ['admin']) or (Roles.userIsInRole(@userId, 'caseManager') and patient.caseManagerId is @userId) return Patients.find _id: patientId return null children: [ find: (patient) -> Visits.find patientId: patient._id children: [ find: (visit) -> Answers.find visitId: visit._id ] ] ##################################### Meteor.publish "questionnaires", -> return unless onlyIfUser.call(@) Questionnaires.find() Meteor.publish "questions", -> return unless onlyIfUser.call(@) Questions.find() Meteor.publish "questionsForQuestionnaire", (questionnaireId)-> return unless onlyIfUser.call(@) Questions.find questionnaireId: questionnaireId ##################################### Meteor.publish "activities", -> return unless onlyIfAdmin.call(@) Activities.find() + +##################################### + +Meteor.publish "exportTables", -> + return unless onlyIfAdmin.call(@) + ExportTables.find()