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

Revision 1787, 18.4 KB (checked in by t.w.abma@…, 3 years ago)

- added null-check to Study domain when using toString()-method

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