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

Revision 1728, 18.3 KB (checked in by s.h.sikkema@…, 3 years ago)

Changed max parameter from type int to Integer

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