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

Revision 1621, 18.1 KB (checked in by robert@…, 3 years ago)

Fixed some bugs in simple wizard and study view page

  • Property svn:keywords set to Rev Author Date
Line 
1package dbnp.studycapturing
2import org.dbnp.gdt.*
3
4import dbnp.authentication.SecUser
5
6/**
7 * Domain class describing the basic entity in the study capture part: the Study class.
8 *
9 * Revision information:
10 * $Rev$
11 * $Author$
12 * $Date$
13 */
14class Study extends TemplateEntity {
15        static searchable = true
16
17        def moduleNotificationService
18
19        SecUser owner           // The owner of the study. A new study is automatically owned by its creator.
20        String title            // The title of the study
21        String description      // A brief synopsis of what the study is about
22        String code                     // currently used as the external study ID, e.g. to reference a study in a SAM module
23        Date dateCreated
24        Date lastUpdated
25        Date startDate
26        List subjects
27        List events
28        List samplingEvents
29        List eventGroups
30        List samples
31        List assays
32        boolean published = false // Determines whether a study is private (only accessable by the owner and writers) or published (also visible to readers)
33        boolean publicstudy = false  // Determines whether anonymous users are allowed to see this study. This has only effect when published = true
34
35        /**
36         * UUID of this study
37         */
38        String studyUUID
39
40
41        static hasMany = [
42                subjects: Subject,
43                samplingEvents: SamplingEvent,
44                events: Event,
45                eventGroups: EventGroup,
46                samples: Sample,
47                assays: Assay,
48                persons: StudyPerson,
49                publications: Publication,
50                readers: SecUser,
51                writers: SecUser
52        ]
53
54        static constraints = {
55                title(nullable:false, blank: false, maxSize: 255)
56                owner(nullable: true, blank: true)
57                code(nullable: true, blank: true, unique: true, maxSize: 255)
58                studyUUID(nullable:true, unique:true, maxSize: 255)
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 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 the study
273                this.removeFromEvents(event)
274
275                // remove event from eventGroups
276                this.eventGroups.each() { eventGroup ->
277                        eventGroup.removeFromEvents(event)
278                }
279        }
280
281        /**
282         * Delete a sample from the study, including all its relations
283         * @param Event
284         * @void
285         */
286        void deleteSample(Sample sample) {
287                // remove the sample from the study
288                this.removeFromSamples(sample)
289
290                // remove the sample from any sampling events it belongs to
291                this.samplingEvents.findAll { it.samples.any { it == sample }}.each {
292                        it.removeFromSamples(sample)
293                }
294
295                // remove the sample from any assays it belongs to
296                this.assays.findAll { it.samples.any { it == sample }}.each {
297                        it.removeFromSamples(sample)
298                }
299
300                // Also here, contrary to documentation, an extra delete() is needed
301                // otherwise date is not properly deleted!
302                sample.delete()
303        }
304
305        /**
306         * Delete a samplingEvent from the study, including all its relations
307         * @param SamplingEvent
308         * @void
309         */
310        void deleteSamplingEvent(SamplingEvent samplingEvent) {
311                // remove event from eventGroups
312                this.eventGroups.each() { eventGroup ->
313                        eventGroup.removeFromSamplingEvents(samplingEvent)
314                }
315
316                // Delete the samples that have this sampling event as parent
317                this.samples.findAll { it.parentEvent.equals(samplingEvent) }.each {
318                        // This should remove the sample itself too, because of the cascading belongsTo relation
319                        this.deleteSample(it)
320                }
321
322                // Remove event from the study
323                // This should remove the event group itself too, because of the cascading belongsTo relation
324                this.removeFromSamplingEvents(samplingEvent)
325
326                // But apparently it needs an explicit delete() too
327                // (Which can be verified by outcommenting this line, then SampleTests.testDeleteViaParentSamplingEvent fails
328                samplingEvent.delete()
329        }
330
331        /**
332         * Delete an eventGroup from the study, including all its relations
333         * @param EventGroup
334         * @void
335         */
336        void deleteEventGroup(EventGroup eventGroup) {
337                // If the event group contains sampling events
338                if (eventGroup.samplingEvents) {
339                        // remove all samples that originate from this eventGroup
340                        if (eventGroup.samplingEvents.size()) {
341                                // find all samples related to this eventGroup
342                                // - subject comparison is relatively straightforward and
343                                //   behaves as expected
344                                // - event comparison behaves strange, so now we compare
345                                //              1. database id's or,
346                                //              2. object identifiers or,
347                                //              3. objects itself
348                                //   this seems now to work as expected
349                                this.samples.findAll { sample ->
350                                        (
351                                                        (eventGroup.subjects.findAll {
352                                                                it.equals(sample.parentSubject)
353                                                        })
354                                                        &&
355                                                        (eventGroup.samplingEvents.findAll {
356                                                                (
357                                                                                (it.id && sample.parentEvent.id && it.id == sample.parentEvent.id)
358                                                                                ||
359                                                                                (it.getIdentifier() == sample.parentEvent.getIdentifier())
360                                                                                ||
361                                                                                it.equals(sample.parentEvent)
362                                                                                )
363                                                        })
364                                                        )
365                                }.each() { sample ->
366                                        // remove sample from study
367                                        this.deleteSample(sample)
368                                }
369                        }
370
371                        // remove all samplingEvents from this eventGroup
372                        eventGroup.samplingEvents.findAll {}.each() {
373                                eventGroup.removeFromSamplingEvents(it)
374                        }
375                }
376
377                // If the event group contains subjects
378                if (eventGroup.subjects) {
379                        // remove all subject from this eventGroup
380                        eventGroup.subjects.findAll {}.each() {
381                                eventGroup.removeFromSubjects(it)
382                        }
383                }
384
385                // remove the eventGroup from the study
386                this.removeFromEventGroups(eventGroup)
387
388                // Also here, contrary to documentation, an extra delete() is needed
389                // otherwise cascaded deletes are not properly performed
390                eventGroup.delete()
391        }
392
393        /**
394         * Returns true if the given user is allowed to read this study
395         */
396        public boolean canRead(SecUser loggedInUser) {
397                // Anonymous readers are only given access when published and public
398                if (loggedInUser == null) {
399                        return this.publicstudy && this.published;
400                }
401
402                // Administrators are allowed to read every study
403                if (loggedInUser.hasAdminRights()) {
404                        return true;
405                }
406
407                // Owners and writers are allowed to read this study
408                if (this.owner == loggedInUser || this.writers.contains(loggedInUser)) {
409                        return true
410                }
411
412                // Readers are allowed to read this study when it is published
413                if (this.readers.contains(loggedInUser) && this.published) {
414                        return true
415                }
416
417                return false
418        }
419
420        /**
421         * Returns true if the given user is allowed to write this study
422         */
423        public boolean canWrite(SecUser loggedInUser) {
424                if (loggedInUser == null) {
425                        return false;
426                }
427
428                // Administrators are allowed to write every study
429                if (loggedInUser.hasAdminRights()) {
430                        return true;
431                }
432
433                return this.owner == loggedInUser || this.writers.contains(loggedInUser)
434        }
435
436        /**
437         * Returns true if the given user is the owner of this study
438         */
439        public boolean isOwner(SecUser loggedInUser) {
440                if (loggedInUser == null) {
441                        return false;
442                }
443                return this.owner == loggedInUser
444        }
445
446        /**
447         * Returns a list of studies that are writable for the given user
448         */
449        public static giveWritableStudies(SecUser user, int max) {
450                // User that are not logged in, are not allowed to write to a study
451                if (user == null)
452                        return [];
453
454                def c = Study.createCriteria()
455
456                // Administrators are allowed to read everything
457                if (user.hasAdminRights()) {
458                        return c.list {
459                                maxResults(max)
460                                order("title", "asc")
461                               
462                        }.unique()
463                }
464
465                return c.list {
466                        maxResults(max)
467                        order("title", "asc")
468                        or {
469                                eq("owner", user)
470                                writers {
471                                        eq("id", user.id)
472                                }
473                        }
474                }.unique()
475        }
476
477        /**
478         * Returns a list of studies that are readable by the given user
479         */
480        public static giveReadableStudies(SecUser user, int max, int offset = 0) {
481                def c = Study.createCriteria()
482
483                // Administrators are allowed to read everything
484                if (user == null) {
485                        return c.list {
486                                maxResults(max)
487                                firstResult(offset)
488                                order("title", "asc")
489                                and {
490                                        eq("published", true)
491                                        eq("publicstudy", true)
492                                }
493                        }.unique()
494                } else if (user.hasAdminRights()) {
495                        return c.list {
496                                maxResults(max)
497                                firstResult(offset)
498                                order("title", "asc")
499                        }.unique()
500                } else {
501                        return c.list {
502                                maxResults(max)
503                                firstResult(offset)
504                                order("title", "asc")
505                                or {
506                                        eq("owner", user)
507                                        writers {
508                                                eq("id", user.id)
509                                        }
510                                        and {
511                                                readers {
512                                                        eq("id", user.id)
513                                                }
514                                                eq("published", true)
515                                        }
516                                }
517                        }.unique()
518                }
519        }
520
521        /**
522         * perform a text search on studies
523         * @param query
524         * @return
525         */
526        public static textSearchReadableStudies(SecUser user, String query) {
527                def c = Study.createCriteria()
528
529                if (user == null) {
530                        // regular user
531                        return c.list {
532                                or {
533                                        ilike("title", "%${query}%")
534                                        ilike("description", "%${query}%")
535                                }
536                                and {
537                                        eq("published", true)
538                                        eq("publicstudy", true)
539                                }
540                        }
541                } else if (user.hasAdminRights()) {
542                        // admin can search everything
543                        return c.list {
544                                or {
545                                        ilike("title", "%${query}%")
546                                        ilike("description", "%${query}%")
547                                }
548                        }
549                } else {
550                        return c.list {
551                                or {
552                                        ilike("title", "%${query}%")
553                                        ilike("description", "%${query}%")
554                                }
555                                and {
556                                        or {
557                                                eq("owner", user)
558                                                writers {
559                                                        eq("id", user.id)
560                                                }
561                                                and {
562                                                        readers {
563                                                                eq("id", user.id)
564                                                        }
565                                                        eq("published", true)
566                                                }
567                                        }
568                                }
569                        }
570
571                }
572        }
573
574        /**
575         * Returns the number of public studies
576         * @return int
577         */
578        public static countPublicStudies() { return countPublicStudies(true) }
579        public static countPublicStudies(boolean published) {
580                def c = Study.createCriteria()
581                return c.count {
582                        and {
583                                eq("published", published)
584                                eq("publicstudy", true)
585                        }
586                }
587        }
588
589        /**
590         * Returns the number of private studies
591         * @return int
592         */
593        public static countPrivateStudies() { return countPrivateStudies(false) }
594        public static countPrivateStudies(boolean published) {
595                def c = Study.createCriteria()
596                return c.count {
597                        and {
598                                eq("publicstudy", false)
599                        }
600                        or {
601                                eq("published", published)
602                                eq("publicstudy", true)
603                        }
604                }
605        }
606
607        /**
608         * Returns the number of studies that are readable by the given user
609         */
610        public static countReadableStudies(SecUser user) {
611                def c = Study.createCriteria()
612
613                // got a user?
614                if (user == null) {
615                        return c.count {
616                                and {
617                                        eq("published", true)
618                                        eq("publicstudy", true)
619                                }
620                        }
621                } else if (user.hasAdminRights()) {
622                        // Administrators are allowed to read everything
623                        return Study.count()
624                } else {
625                        return c.count {
626                                or {
627                                        eq("owner", user)
628                                        writers {
629                                                eq("id", user.id)
630                                        }
631                                        and {
632                                                readers {
633                                                        eq("id", user.id)
634                                                }
635                                                eq("published", true)
636                                        }
637                                }
638                        }
639                }
640        }
641
642        /**
643         * Returns the number of studies that are readable & writable by the given user
644         */
645        public static countReadableAndWritableStudies(SecUser user) {
646                def c = Study.createCriteria()
647
648                // got a user?
649                if (user == null) {
650                        return 0
651                } else if (user.hasAdminRights()) {
652                        return Study.count()
653                } else {
654                        return c.count {
655                                or {
656                                        eq("owner", user)
657                                        writers {
658                                                eq("id", user.id)
659                                        }
660                                }
661                        }
662                }
663        }
664
665        /**
666         * Returns the UUID of this study and generates one if needed
667         */
668        public String giveUUID() {
669                if( !this.studyUUID ) {
670                        this.studyUUID = UUID.randomUUID().toString();
671                        if( !this.save(flush:true) ) {
672                                log.error "Couldn't save study UUID: " + this.getErrors();
673                        }
674                }
675
676                return this.studyUUID;
677        }
678
679        /**
680         * Basic equals method to check whether objects are equals, by comparing the ids
681         * @param o             Object to compare with
682         * @return              True iff the id of the given Study is equal to the id of this Study
683         */
684        public boolean equals( Object o ) {
685                if( o == null )
686                        return false;
687
688                if( !( o instanceof Study ) )
689                        return false
690
691                Study s = (Study) o;
692
693                return this.id == s.id
694        }
695
696    // This closure is used in the before{Insert,Update,Delete} closures below.
697    // It is necessary to prevent flushing in the same session as a top level
698    // database action such as 'save' or 'addTo...'. This confuses hibernate and
699    // produces hard to trace errors.
700    // The same holds for flushing during validation (but that's not the case
701    // here).
702    // http://grails.1312388.n4.nabble.com/Grails-hibernate-flush-causes-IndexOutOfBoundsException-td3031979.html
703    static manualFlush(closure) {
704        withSession {session ->
705            def save
706            try {
707                save = session.flushMode
708                session.flushMode = org.hibernate.FlushMode.MANUAL
709                closure()
710            } finally {
711                if (save) {
712                    session.flushMode = save
713                }
714         }
715        }
716    }
717
718        // Send messages to modules about changes in this study
719        def beforeInsert = {
720        manualFlush{
721            moduleNotificationService.invalidateStudy( this )
722        }
723        }
724        def beforeUpdate = {
725        manualFlush{
726            moduleNotificationService.invalidateStudy( this )
727        }
728        }
729        def beforeDelete = {
730                manualFlush{
731            moduleNotificationService.invalidateStudy( this )
732        }
733        }
734}
Note: See TracBrowser for help on using the browser.