root/trunk/grails-app/domain/dbnp/studycapturing/Study.groovy @ 1794

Revision 1794, 18.5 KB (checked in by work@…, 3 years ago)

- added support for fuzzymatching (resolves #418)
- removed debug lines
- gdt up to 0.0.38

  • Property svn:keywords set to Rev Author Date
Line 
1package dbnp.studycapturing
2import org.dbnp.gdt.*
3import dbnp.authentication.SecUser
4
5/**
6 * Domain class describing the basic entity in the study capture part: the Study class.
7 *
8 * Revision information:
9 * $Rev$
10 * $Author$
11 * $Date$
12 */
13class Study extends TemplateEntity {
14        static searchable = true
15
16        def moduleNotificationService
17
18        SecUser owner           // The owner of the study. A new study is automatically owned by its creator.
19        String title            // The title of the study
20        String description      // A brief synopsis of what the study is about
21        String code                     // currently used as the external study ID, e.g. to reference a study in a SAM module
22        Date dateCreated
23        Date lastUpdated
24        Date startDate
25        List subjects
26        List events
27        List samplingEvents
28        List eventGroups
29        List samples
30        List assays
31        boolean published = false // Determines whether a study is private (only accessable by the owner and writers) or published (also visible to readers)
32        boolean publicstudy = false  // Determines whether anonymous users are allowed to see this study. This has only effect when published = true
33
34        /**
35         * UUID of this study
36         */
37        String studyUUID
38
39
40        static hasMany = [
41                subjects: Subject,
42                samplingEvents: SamplingEvent,
43                events: Event,
44                eventGroups: EventGroup,
45                samples: Sample,
46                assays: Assay,
47                persons: StudyPerson,
48                publications: Publication,
49                readers: SecUser,
50                writers: SecUser
51        ]
52
53        static constraints = {
54                title(nullable:false, blank: false, unique:true, maxSize: 255)
55                owner(nullable: true, blank: true)
56                code(nullable: true, blank: true, unique: true, maxSize: 255)
57                studyUUID(nullable:true, unique:true, maxSize: 255)
58                persons(size:1..1000)
59                // TODO: add custom validator for 'published' to assess whether the study meets all quality criteria for publication
60                // tested by SampleTests.testStudyPublish
61        }
62
63        // see org.dbnp.gdt.FuzzyStringMatchController and Service
64        static fuzzyStringMatchable = [
65            "title",
66                "code"
67        ]
68
69        static mapping = {
70                autoTimestamp true
71                sort "title"
72
73                // Make sure the TEXT field description is persisted with a TEXT field in the database
74                description type: 'text'
75                // Workaround for bug http://jira.codehaus.org/browse/GRAILS-6754
76                templateTextFields type: 'text'
77
78        }
79
80        // The external identifier (studyToken) is currently the code of the study.
81        // It is used from within dbNP submodules to refer to particular study in this GSCF instance.
82
83        def getToken() { return giveUUID() }
84
85        /**
86         * return the domain fields for this domain class
87         * @return List
88         */
89        static List<TemplateField> giveDomainFields() { return Study.domainFields }
90
91        static final List<TemplateField> domainFields = [
92                new TemplateField(
93                name: 'title',
94                type: TemplateFieldType.STRING,
95                required: true),
96                new TemplateField(
97                name: 'description',
98                type: TemplateFieldType.TEXT,
99                comment:'Give a brief synopsis of what your study is about',
100                required: true),
101                new TemplateField(
102                name: 'code',
103                type: TemplateFieldType.STRING,
104                preferredIdentifier: true,
105                comment: 'Fill out the code by which many people will recognize your study',
106                required: false),
107                new TemplateField(
108                name: 'startDate',
109                type: TemplateFieldType.DATE,
110                comment: 'Fill out the official start date or date of first action',
111                required: true),
112                new TemplateField(
113                name: 'published',
114                type: TemplateFieldType.BOOLEAN,
115                comment: 'Determines whether this study is published (accessible for the study readers and, if the study is public, for anonymous users). A study can only be published if it meets certain quality criteria, which will be checked upon save.',
116                required: false)
117        ]
118
119        /**
120         * return the title of this study
121         */
122        def String toString() {
123                return ( (code) ? code : "[no code]") + " - "+ title
124        }
125
126        /**
127         * returns all events and sampling events that do not belong to a group
128         */
129        def List<Event> getOrphanEvents() {
130                def orphans = events.findAll { event -> !event.belongsToGroup(eventGroups) } +
131                samplingEvents.findAll { event -> !event.belongsToGroup(eventGroups) }
132
133                return orphans
134        }
135
136        /**
137         * Return the unique Subject templates that are used in this study
138         */
139        def List<Template> giveSubjectTemplates() {
140                TemplateEntity.giveTemplates(subjects)
141        }
142
143        /**
144         * Return all subjects for a specific template
145         * @param Template
146         * @return ArrayList
147         */
148        def ArrayList<Subject> giveSubjectsForTemplate(Template template) {
149                subjects.findAll { it.template.equals(template) }
150        }
151
152        /**
153         * Return all unique assay templates
154         * @return Set
155         */
156        List<Template> giveAllAssayTemplates() {
157                TemplateEntity.giveTemplates(((assays) ? assays : []))
158        }
159
160        /**
161         * Return all assays for a particular template
162         * @return ArrayList
163         */
164        def ArrayList giveAssaysForTemplate(Template template) {
165                assays.findAll { it && it.template.equals(template) }
166        }
167
168        /**
169         * Return the unique Event and SamplingEvent templates that are used in this study
170         */
171        List<Template> giveAllEventTemplates() {
172                // For some reason, giveAllEventTemplates() + giveAllSamplingEventTemplates()
173                // gives trouble when asking .size() to the result
174                // So we also use giveTemplates here
175                TemplateEntity.giveTemplates(((events) ? events : []) + ((samplingEvents) ? samplingEvents : []))
176        }
177
178        /**
179         * Return all events and samplingEvenets for a specific template
180         * @param Template
181         * @return ArrayList
182         */
183        def ArrayList giveEventsForTemplate(Template template) {
184                def events = events.findAll { it.template.equals(template) }
185                def samplingEvents = samplingEvents.findAll { it.template.equals(template) }
186
187                return (events) ? events : samplingEvents
188        }
189
190        /**
191         * Return the unique Event templates that are used in this study
192         */
193        List<Template> giveEventTemplates() {
194                TemplateEntity.giveTemplates(events)
195        }
196
197        /**
198         * Return the unique SamplingEvent templates that are used in this study
199         */
200        List<Template> giveSamplingEventTemplates() {
201                TemplateEntity.giveTemplates(samplingEvents)
202        }
203
204        /**
205         * Returns the unique Sample templates that are used in the study
206         */
207        List<Template> giveSampleTemplates() {
208                TemplateEntity.giveTemplates(samples)
209        }
210
211        /**
212         * Return all samples for a specific template
213         * @param Template
214         * @return ArrayList
215         */
216        def ArrayList<Subject> giveSamplesForTemplate(Template template) {
217                samples.findAll { it.template.equals(template) }
218        }
219
220        /**
221         * Returns the template of the study
222         */
223        Template giveStudyTemplate() {
224                return this.template
225        }
226
227        /**
228         * Delete a specific subject from this study, including all its relations
229         * @param subject The subject to be deleted
230         * @void
231         */
232        void deleteSubject(Subject subject) {
233                // Delete the subject from the event groups it was referenced in
234                this.eventGroups.each {
235                        if (it.subjects?.contains(subject)) {
236                                it.removeFromSubjects(subject)
237                        }
238                }
239
240                // Delete the samples that have this subject as parent
241                this.samples.findAll { it.parentSubject.equals(subject) }.each {
242                        this.deleteSample(it)
243                }
244
245                // This should remove the subject itself too, because of the cascading belongsTo relation
246                this.removeFromSubjects(subject)
247
248                // But apparently it needs an explicit delete() too
249                subject.delete()
250        }
251
252        /**
253         * Delete an assay from the study
254         * @param Assay
255         * @void
256         */
257        def deleteAssay(Assay assay) {
258                if (assay && assay instanceof Assay) {
259                        // iterate through linked samples
260                        assay.samples.findAll { true }.each() { sample ->
261                                assay.removeFromSamples(sample)
262                        }
263
264                        // remove this assay from the study
265                        this.removeFromAssays(assay)
266
267                        // and delete it explicitly
268                        assay.delete()
269                }
270        }
271
272        /**
273         * Delete an event from the study, including all its relations
274         * @param Event
275         * @void
276         */
277        void deleteEvent(Event event) {
278                // remove event from eventGroups
279                this.eventGroups.each() { eventGroup ->
280                        eventGroup.removeFromEvents(event)
281                }
282
283                // remove event from the study
284                this.removeFromEvents(event)
285
286                // and perform a hard delete
287                event.delete()
288        }
289
290        /**
291         * Delete a sample from the study, including all its relations
292         * @param Event
293         * @void
294         */
295        void deleteSample(Sample sample) {
296                // remove the sample from the study
297                this.removeFromSamples(sample)
298
299                // remove the sample from any sampling events it belongs to
300                this.samplingEvents.findAll { it.samples.any { it == sample }}.each {
301                        it.removeFromSamples(sample)
302                }
303
304                // remove the sample from any assays it belongs to
305                this.assays.findAll { it.samples.any { it == sample }}.each {
306                        it.removeFromSamples(sample)
307                }
308
309                // Also here, contrary to documentation, an extra delete() is needed
310                // otherwise date is not properly deleted!
311                sample.delete()
312        }
313
314        /**
315         * Delete a samplingEvent from the study, including all its relations
316         * @param SamplingEvent
317         * @void
318         */
319        void deleteSamplingEvent(SamplingEvent samplingEvent) {
320                // remove event from eventGroups
321                this.eventGroups.each() { eventGroup ->
322                        eventGroup.removeFromSamplingEvents(samplingEvent)
323                }
324
325                // Delete the samples that have this sampling event as parent
326                this.samples.findAll { it.parentEvent.equals(samplingEvent) }.each {
327                        // This should remove the sample itself too, because of the cascading belongsTo relation
328                        this.deleteSample(it)
329                }
330
331                // Remove event from the study
332                // This should remove the event group itself too, because of the cascading belongsTo relation
333                this.removeFromSamplingEvents(samplingEvent)
334
335                // But apparently it needs an explicit delete() too
336                // (Which can be verified by outcommenting this line, then SampleTests.testDeleteViaParentSamplingEvent fails
337                samplingEvent.delete()
338        }
339
340        /**
341         * Delete an eventGroup from the study, including all its relations
342         * @param EventGroup
343         * @void
344         */
345        void deleteEventGroup(EventGroup eventGroup) {
346                // If the event group contains sampling events
347                if (eventGroup.samplingEvents) {
348                        // remove all samples that originate from this eventGroup
349                        if (eventGroup.samplingEvents.size()) {
350                                // find all samples related to this eventGroup
351                                // - subject comparison is relatively straightforward and
352                                //   behaves as expected
353                                // - event comparison behaves strange, so now we compare
354                                //              1. database id's or,
355                                //              2. object identifiers or,
356                                //              3. objects itself
357                                //   this seems now to work as expected
358                                this.samples.findAll { sample ->
359                                        (
360                                                        (eventGroup.subjects.findAll {
361                                                                it.equals(sample.parentSubject)
362                                                        })
363                                                        &&
364                                                        (eventGroup.samplingEvents.findAll {
365                                                                (
366                                                                                (it.id && sample.parentEvent.id && it.id == sample.parentEvent.id)
367                                                                                ||
368                                                                                (it.getIdentifier() == sample.parentEvent.getIdentifier())
369                                                                                ||
370                                                                                it.equals(sample.parentEvent)
371                                                                                )
372                                                        })
373                                                        )
374                                }.each() { sample ->
375                                        // remove sample from study
376                                        this.deleteSample(sample)
377                                }
378                        }
379
380                        // remove all samplingEvents from this eventGroup
381                        eventGroup.samplingEvents.findAll {}.each() {
382                                eventGroup.removeFromSamplingEvents(it)
383                        }
384                }
385
386                // If the event group contains subjects
387                if (eventGroup.subjects) {
388                        // remove all subject from this eventGroup
389                        eventGroup.subjects.findAll {}.each() {
390                                eventGroup.removeFromSubjects(it)
391                        }
392                }
393
394                // remove the eventGroup from the study
395                this.removeFromEventGroups(eventGroup)
396
397                // Also here, contrary to documentation, an extra delete() is needed
398                // otherwise cascaded deletes are not properly performed
399                eventGroup.delete()
400        }
401
402        /**
403         * Returns true if the given user is allowed to read this study
404         */
405        public boolean canRead(SecUser loggedInUser) {
406                // Anonymous readers are only given access when published and public
407                if (loggedInUser == null) {
408                        return this.publicstudy && this.published;
409                }
410
411                // Administrators are allowed to read every study
412                if (loggedInUser.hasAdminRights()) {
413                        return true;
414                }
415
416                // Owners and writers are allowed to read this study
417                if (this.owner == loggedInUser || this.writers.contains(loggedInUser)) {
418                        return true
419                }
420
421                // Readers are allowed to read this study when it is published
422                if (this.readers.contains(loggedInUser) && this.published) {
423                        return true
424                }
425
426                return false
427        }
428
429        /**
430         * Returns true if the given user is allowed to write this study
431         */
432        public boolean canWrite(SecUser loggedInUser) {
433                if (loggedInUser == null) {
434                        return false;
435                }
436
437                // Administrators are allowed to write every study
438                if (loggedInUser.hasAdminRights()) {
439                        return true;
440                }
441
442                return this.owner == loggedInUser || this.writers.contains(loggedInUser)
443        }
444
445        /**
446         * Returns true if the given user is the owner of this study
447         */
448        public boolean isOwner(SecUser loggedInUser) {
449                if (loggedInUser == null) {
450                        return false;
451                }
452                return this.owner == loggedInUser
453        }
454
455        /**
456         * Returns a list of studies that are writable for the given user
457         */
458        public static giveWritableStudies(SecUser user, Integer max = null) {
459                // User that are not logged in, are not allowed to write to a study
460                if (user == null)
461                        return [];
462
463                def c = Study.createCriteria()
464
465                // Administrators are allowed to read everything
466                if (user.hasAdminRights()) {
467                        return c.listDistinct {
468                                if (max != null) maxResults(max)
469                                order("title", "asc")
470                               
471                        }
472                }
473
474                return c.listDistinct {
475                        if (max != null) maxResults(max)
476                        order("title", "asc")
477                        or {
478                                eq("owner", user)
479                                writers {
480                                        eq("id", user.id)
481                                }
482                        }
483                }
484        }
485
486        /**
487         * Returns a list of studies that are readable by the given user
488         */
489        public static giveReadableStudies(SecUser user, Integer max = null, int offset = 0) {
490                def c = Study.createCriteria()
491
492                // Administrators are allowed to read everything
493                if (user == null) {
494                        return c.listDistinct {
495                                if (max != null) maxResults(max)
496                                firstResult(offset)
497                                order("title", "asc")
498                                and {
499                                        eq("published", true)
500                                        eq("publicstudy", true)
501                                }
502                        }
503                } else if (user.hasAdminRights()) {
504                        return c.listDistinct {
505                                if (max != null) maxResults(max)
506                                firstResult(offset)
507                                order("title", "asc")
508                        }
509                } else {
510                        return c.listDistinct {
511                                if (max != null) maxResults(max)
512                                firstResult(offset)
513                                order("title", "asc")
514                                or {
515                                        eq("owner", user)
516                                        writers {
517                                                eq("id", user.id)
518                                        }
519                                        and {
520                                                readers {
521                                                        eq("id", user.id)
522                                                }
523                                                eq("published", true)
524                                        }
525                                }
526                        }
527                }
528        }
529
530        /**
531         * perform a text search on studies
532         * @param query
533         * @return
534         */
535        public static textSearchReadableStudies(SecUser user, String query) {
536                def c = Study.createCriteria()
537
538                if (user == null) {
539                        // regular user
540                        return c.listDistinct {
541                                or {
542                                        ilike("title", "%${query}%")
543                                        ilike("description", "%${query}%")
544                                }
545                                and {
546                                        eq("published", true)
547                                        eq("publicstudy", true)
548                                }
549                        }
550                } else if (user.hasAdminRights()) {
551                        // admin can search everything
552                        return c.listDistinct {
553                                or {
554                                        ilike("title", "%${query}%")
555                                        ilike("description", "%${query}%")
556                                }
557                        }
558                } else {
559                        return c.listDistinct {
560                                or {
561                                        ilike("title", "%${query}%")
562                                        ilike("description", "%${query}%")
563                                }
564                                and {
565                                        or {
566                                                eq("owner", user)
567                                                writers {
568                                                        eq("id", user.id)
569                                                }
570                                                and {
571                                                        readers {
572                                                                eq("id", user.id)
573                                                        }
574                                                        eq("published", true)
575                                                }
576                                        }
577                                }
578                        }
579
580                }
581        }
582
583        /**
584         * Returns the number of public studies
585         * @return int
586         */
587        public static countPublicStudies() { return countPublicStudies(true) }
588        public static countPublicStudies(boolean published) {
589                def c = Study.createCriteria()
590                return (c.listDistinct {
591                        and {
592                                eq("published", published)
593                                eq("publicstudy", true)
594                        }
595                }).size()
596        }
597
598        /**
599         * Returns the number of private studies
600         * @return int
601         */
602        public static countPrivateStudies() { return countPrivateStudies(false) }
603        public static countPrivateStudies(boolean published) {
604                def c = Study.createCriteria()
605                return (c.listDistinct {
606                        and {
607                                eq("publicstudy", false)
608                        }
609                        or {
610                                eq("published", published)
611                                eq("publicstudy", true)
612                        }
613                }).size()
614        }
615
616        /**
617         * Returns the number of studies that are readable by the given user
618         */
619        public static countReadableStudies(SecUser user) {
620                def c = Study.createCriteria()
621
622                // got a user?
623                if (user == null) {
624                        return c.count {
625                                and {
626                                        eq("published", true)
627                                        eq("publicstudy", true)
628                                }
629                        }
630                } else if (user.hasAdminRights()) {
631                        // Administrators are allowed to read everything
632                        return Study.count()
633                } else {
634                        return (c.listDistinct {
635                                or {
636                                        eq("owner", user)
637                                        writers {
638                                                eq("id", user.id)
639                                        }
640                                        and {
641                                                readers {
642                                                        eq("id", user.id)
643                                                }
644                                                eq("published", true)
645                                        }
646                                }
647                        }).size()
648                }
649        }
650
651        /**
652         * Returns the number of studies that are readable & writable by the given user
653         */
654        public static countReadableAndWritableStudies(SecUser user) {
655                def c = Study.createCriteria()
656
657                // got a user?
658                if (user == null) {
659                        return 0
660                } else if (user.hasAdminRights()) {
661                        return Study.count()
662                } else {
663                        return (c.listDistinct {
664                                or {
665                                        eq("owner", user)
666                                        writers {
667                                                eq("id", user.id)
668                                        }
669                                }
670                        }).size()
671                }
672        }
673
674        /**
675         * Returns the UUID of this study and generates one if needed
676         */
677        public String giveUUID() {
678                if( !this.studyUUID ) {
679                        this.studyUUID = UUID.randomUUID().toString();
680                        if( !this.save(flush:true) ) {
681                                log.error "Couldn't save study UUID: " + this.getErrors();
682                        }
683                }
684
685                return this.studyUUID;
686        }
687
688        /**
689         * Basic equals method to check whether objects are equals, by comparing the ids
690         * @param o             Object to compare with
691         * @return              True iff the id of the given Study is equal to the id of this Study
692         */
693        public boolean equals( Object o ) {
694                if( o == null )
695                        return false;
696
697                if( !( o instanceof Study ) )
698                        return false
699
700                Study s = (Study) o;
701
702                return this.id == s.id
703        }
704
705    // This closure is used in the before{Insert,Update,Delete} closures below.
706    // It is necessary to prevent flushing in the same session as a top level
707    // database action such as 'save' or 'addTo...'. This confuses hibernate and
708    // produces hard to trace errors.
709    // The same holds for flushing during validation (but that's not the case
710    // here).
711    // http://grails.1312388.n4.nabble.com/Grails-hibernate-flush-causes-IndexOutOfBoundsException-td3031979.html
712    static manualFlush(closure) {
713        withSession {session ->
714            def save
715            try {
716                save = session.flushMode
717                session.flushMode = org.hibernate.FlushMode.MANUAL
718                closure()
719            } finally {
720                if (save) {
721                    session.flushMode = save
722                }
723         }
724        }
725    }
726
727        // Send messages to modules about changes in this study
728        def beforeInsert = {
729        manualFlush{
730            moduleNotificationService.invalidateStudy( this )
731        }
732        }
733        def beforeUpdate = {
734        manualFlush{
735            moduleNotificationService.invalidateStudy( this )
736        }
737        }
738        def beforeDelete = {
739                manualFlush{
740            moduleNotificationService.invalidateStudy( this )
741        }
742        }
743    }
Note: See TracBrowser for help on using the browser.