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

Revision 1222, 14.7 KB (checked in by robert@…, 3 years ago)

Implemented improved authorisation to give administrators all permissions (ticket #207)

  • Property svn:keywords set to Author Date Rev
Line 
1package dbnp.studycapturing
2
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        SecUser owner           // The owner of the study. A new study is automatically owned by its creator.
17        String title        // The title of the study
18        String code             // currently used as the external study ID, e.g. to reference a study in a SAM module
19        Date dateCreated
20        Date lastUpdated
21        Date startDate
22        List subjects
23        List events
24        List samplingEvents
25        List eventGroups
26        List samples
27        List assays
28        boolean published = false // Determines whether a study is private (only accessable by the owner and writers) or published (also visible to readers)
29    boolean publicstudy = false  // Determines whether anonymous users are allowed to see this study. This has only effect when published = true
30       
31        static hasMany = [             
32                subjects: Subject,
33                samplingEvents: SamplingEvent,
34                events: Event,
35                eventGroups: EventGroup,
36                samples: Sample,
37                assays: Assay,
38                persons: StudyPerson,
39                publications: Publication,
40                readers: SecUser,
41                writers: SecUser
42        ]
43
44        static constraints = {
45                owner(nullable: true, blank: true)
46                code(nullable:false, blank:true,unique:true)
47
48                // TODO: add custom validator for 'published' to assess whether the study meets all quality criteria for publication
49                // tested by SampleTests.testStudyPublish
50        }
51
52        static mapping = {
53                autoTimestamp true
54                sort "title"
55
56                // Workaround for bug http://jira.codehaus.org/browse/GRAILS-6754
57                templateTextFields type: 'text'
58        }
59
60        // The external identifier (studyToken) is currently the code of the study.
61        // It is used from within dbNP submodules to refer to particular study in this GSCF instance.
62        def getToken() { code }
63
64        /**
65         * return the domain fields for this domain class
66         * @return List
67         */
68        static List<TemplateField> giveDomainFields() { return Study.domainFields }
69
70        static final List<TemplateField> domainFields = [
71                new TemplateField(
72                        name: 'title',
73                        type: TemplateFieldType.STRING,
74                        required: true),
75                new TemplateField(
76                        name: 'code',
77                        type: TemplateFieldType.STRING,
78                        preferredIdentifier:true,
79                        comment: 'Fill out the code by which many people will recognize your study',
80                        required: true),
81                new TemplateField(
82                        name: 'startDate',
83                        type: TemplateFieldType.DATE,
84                        comment: 'Fill out the official start date or date of first action',
85                        required: true),
86                new TemplateField(
87                        name: 'published',
88                        type: TemplateFieldType.BOOLEAN,
89                        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.',
90                        required: false)
91        ]
92
93        /**
94         * return the title of this study
95         */
96        def String toString() {
97                return title
98        }
99
100        /**
101         * returns all events and sampling events that do not belong to a group
102         */
103        def List<Event> getOrphanEvents() {
104                def orphans =   events.findAll { event -> !event.belongsToGroup(eventGroups) } +
105                                                samplingEvents.findAll { event -> !event.belongsToGroup(eventGroups) }
106
107                return orphans
108        }
109
110        /**
111         * Return the unique Subject templates that are used in this study
112         */
113        def List<Template> giveSubjectTemplates() {
114                TemplateEntity.giveTemplates(subjects)
115        }
116
117        /**
118         * Return all subjects for a specific template
119         * @param Template
120         * @return ArrayList
121         */
122        def ArrayList<Subject> giveSubjectsForTemplate(Template template) {
123                subjects.findAll { it.template.equals(template) }
124        }
125
126        /**
127         * Return all unique assay templates
128         * @return Set
129         */
130        List<Template> giveAllAssayTemplates() {
131                TemplateEntity.giveTemplates(( (assays) ? assays : [] ))
132        }
133
134        /**
135         * Return all assays for a particular template
136         * @return ArrayList
137         */
138        def ArrayList giveAssaysForTemplate(Template template) {
139                assays.findAll { it.template.equals(template) }
140        }
141
142        /**
143         * Return the unique Event and SamplingEvent templates that are used in this study
144         */
145        List<Template> giveAllEventTemplates() {
146                // For some reason, giveAllEventTemplates() + giveAllSamplingEventTemplates()
147                // gives trouble when asking .size() to the result
148                // So we also use giveTemplates here
149                TemplateEntity.giveTemplates( ((events) ? events : []) + ((samplingEvents) ? samplingEvents : []) )
150        }
151
152
153        /**
154         * Return all events and samplingEvenets for a specific template
155         * @param Template
156         * @return ArrayList
157         */
158        def ArrayList giveEventsForTemplate(Template template) {
159                def events = events.findAll { it.template.equals(template) }
160                def samplingEvents = samplingEvents.findAll { it.template.equals(template) }
161
162                return (events) ? events : samplingEvents
163        }
164
165        /**
166         * Return the unique Event templates that are used in this study
167         */
168        List<Template> giveEventTemplates() {
169                TemplateEntity.giveTemplates(events)
170        }
171
172        /**
173         * Return the unique SamplingEvent templates that are used in this study
174         */
175        List<Template> giveSamplingEventTemplates() {
176                TemplateEntity.giveTemplates(samplingEvents)
177        }
178
179        /**
180         * Returns the unique Sample templates that are used in the study
181         */
182        List<Template> giveSampleTemplates() {
183                TemplateEntity.giveTemplates(samples)
184        }
185
186        /**
187         * Return all samples for a specific template
188         * @param Template
189         * @return ArrayList
190         */
191        def ArrayList<Subject> giveSamplesForTemplate(Template template) {
192                samples.findAll { it.template.equals(template) }
193        }
194
195        /**
196         * Returns the template of the study
197         */
198        Template giveStudyTemplate() {
199                return this.template
200        }
201
202
203        /**
204         * Delete a specific subject from this study, including all its relations
205         * @param subject The subject to be deleted
206         * @return A String which contains a (user-readable) message describing the changes to the database
207         */
208        String deleteSubject(Subject subject) {
209                String msg = "Subject ${subject.name} was deleted"
210
211                // Delete the subject from the event groups it was referenced in
212                this.eventGroups.each {
213                        if (it.subjects.contains(subject)) {
214                                it.removeFromSubjects(subject)
215                                msg += ", deleted from event group '${it.name}'"
216                        }
217                }
218
219                // Delete the samples that have this subject as parent
220                this.samples.findAll { it.parentSubject.equals(subject) }.each {
221                        // This should remove the sample itself too, because of the cascading belongsTo relation
222                        this.removeFromSamples(it)
223                        // But apparently it needs an explicit delete() too
224                        it.delete()
225                        msg += ", sample '${it.name}' was deleted"
226                }
227
228                // This should remove the subject itself too, because of the cascading belongsTo relation
229                this.removeFromSubjects(subject)
230                // But apparently it needs an explicit delete() too
231                subject.delete()
232
233                return msg
234        }
235
236        /**
237         * Delete an assay from the study
238         * @param Assay
239         * @void
240         */
241        def deleteAssay(Assay assay) {
242                if (assay && assay instanceof Assay) {
243                        // iterate through linked samples
244                        assay.samples.findAll { true }.each() { sample ->
245                                assay.removeFromSamples(sample)
246                        }
247
248                        // remove this assay from the study
249                        this.removeFromAssays(assay)
250
251                        // and delete it explicitly
252                        assay.delete()
253                }
254        }
255
256        /**
257         * Delete an event from the study, including all its relations
258         * @param Event
259         * @return String
260         */
261        String deleteEvent(Event event) {
262                String msg = "Event ${event} was deleted"
263
264                // remove event from the study
265                this.removeFromEvents(event)
266
267                // remove event from eventGroups
268                this.eventGroups.each() { eventGroup ->
269                        eventGroup.removeFromEvents(event)
270                }
271
272                return msg
273        }
274
275        /**
276         * Delete a samplingEvent from the study, including all its relations
277         * @param SamplingEvent
278         * @return String
279         */
280        String deleteSamplingEvent(SamplingEvent samplingEvent) {
281                String msg = "SamplingEvent ${samplingEvent} was deleted"
282
283                // remove event from eventGroups
284                this.eventGroups.each() { eventGroup ->
285                        eventGroup.removeFromSamplingEvents(samplingEvent)
286                }
287
288                // Delete the samples that have this sampling event as parent
289                this.samples.findAll { it.parentEvent.equals(samplingEvent) }.each {
290                        // This should remove the sample itself too, because of the cascading belongsTo relation
291                        this.removeFromSamples(it)
292                        // But apparently it needs an explicit delete() too
293                        it.delete()
294                        msg += ", sample '${it.name}' was deleted"
295                }
296
297                // Remove event from the study
298                // This should remove the event group itself too, because of the cascading belongsTo relation
299                this.removeFromSamplingEvents(samplingEvent)
300
301                // But apparently it needs an explicit delete() too
302                // (Which can be verified by outcommenting this line, then SampleTests.testDeleteViaParentSamplingEvent fails
303                samplingEvent.delete()
304
305                return msg
306        }
307       
308        /**
309         * Delete an eventGroup from the study, including all its relations
310         * @param EventGroup
311         * @return String
312         */
313        String deleteEventGroup(EventGroup eventGroup) {
314                String msg = "EventGroup ${eventGroup} was deleted"
315
316                // If the event group contains sampling events
317                if (eventGroup.samplingEvents) {
318                        // remove all samples that originate from this eventGroup
319                        if (eventGroup.samplingEvents.size()) {
320                                // find all samples related to this eventGroup
321                                // - subject comparison is relatively straightforward and
322                                //   behaves as expected
323                                // - event comparison behaves strange, so now we compare
324                                //              1. database id's or,
325                                //              2. object identifiers or,
326                                //              3. objects itself
327                                //   this seems now to work as expected
328                                this.samples.findAll { sample ->
329                                        (
330                                                (eventGroup.subjects.findAll {
331                                                        it.equals(sample.parentSubject)
332                                                })
333                                                &&
334                                                (eventGroup.samplingEvents.findAll {
335                                                        (
336                                                                (it.id && sample.parentEvent.id && it.id==sample.parentEvent.id)
337                                                                ||
338                                                                (it.getIdentifier() == sample.parentEvent.getIdentifier())
339                                                                ||
340                                                                it.equals(sample.parentEvent)
341                                                        )
342                                                })
343                                        )
344                                }.each() { sample ->
345                                        // remove sample from study
346
347                                        // -------
348                                        // NOTE, the right samples are found, but the don't
349                                        // get deleted from the database!
350                                        // -------
351
352                                        println ".removing sample '${sample.name}' from study '${this.title}'"
353                                        msg += ", sample '${sample.name}' was deleted"
354                                        this.removeFromSamples( sample )
355
356                                        // remove the sample from any sampling events it belongs to
357                                        this.samplingEvents.findAll { it.samples.any { it == sample }} .each {
358                                                println ".removed sample ${sample.name} from sampling event ${it} at ${it.getStartTimeString()}"
359                                                it.removeFromSamples(sample)
360                                        }
361
362                                        // remove the sample from any assays it belongs to
363                                        this.assays.findAll { it.samples.any { it == sample }} .each {
364                                                println ".removed sample ${sample.name} from assay ${it.name}"
365                                                it.removeFromSamples(sample)
366                                        }
367
368                                        // Also here, contrary to documentation, an extra delete() is needed
369                                        // otherwise date is not properly deleted!
370                                        sample.delete()
371                                }
372                        }
373
374                        // remove all samplingEvents from this eventGroup
375                        eventGroup.samplingEvents.findAll{}.each() {
376                                eventGroup.removeFromSamplingEvents(it)
377                                println ".removed samplingEvent '${it.name}' from eventGroup '${eventGroup.name}'"
378                                msg += ", samplingEvent '${it.name}' was removed from eventGroup '${eventGroup.name}'"
379                        }
380                }
381
382                // If the event group contains subjects
383                if (eventGroup.subjects) {
384                        // remove all subject from this eventGroup
385                        eventGroup.subjects.findAll{}.each() {
386                                eventGroup.removeFromSubjects(it)
387                                println ".removed subject '${it.name}' from eventGroup '${eventGroup.name}'"
388                                msg += ", subject '${it.name}' was removed from eventGroup '${eventGroup.name}'"
389                        }
390                }
391
392                // remove the eventGroup from the study
393                println ".remove eventGroup '${eventGroup.name}' from study '${this.title}'"
394                this.removeFromEventGroups(eventGroup)
395
396                // Also here, contrary to documentation, an extra delete() is needed
397                // otherwise cascaded deletes are not properly performed
398                eventGroup.delete()
399
400                return msg
401        }
402
403    /**
404     * Returns true if the given user is allowed to read this study
405     */
406    public boolean canRead(SecUser loggedInUser) {
407        // Anonymous readers are only given access when published and public
408        if( loggedInUser == null ) {
409            return this.publicstudy && this.published;
410        }
411
412                // Administrators are allowed to read every study
413                if( loggedInUser.hasAdminRights() ) {
414                        return true;
415                }
416
417        // Owners and writers are allowed to read this study
418        if( this.owner == loggedInUser || this.writers.contains(loggedInUser) ) {
419            return true
420        }
421           
422        // Readers are allowed to read this study when it is published
423        if( this.readers.contains(loggedInUser) && this.published ) {
424            return true
425        }
426       
427        return false
428    }
429
430    /**
431     * Returns true if the given user is allowed to write this study
432     */
433    public boolean canWrite(SecUser loggedInUser) {
434        if( loggedInUser == null ) {
435            return false;
436        }
437
438                // Administrators are allowed to write every study
439                if( loggedInUser.hasAdminRights() ) {
440                        return true;
441                }
442
443        return this.owner == loggedInUser || this.writers.contains(loggedInUser)
444    }
445
446    /**
447     * Returns true if the given user is the owner of this study
448     */
449    public boolean isOwner(SecUser loggedInUser) {
450        if( loggedInUser == null ) {
451            return false;
452        }
453        return this.owner == loggedInUser
454    }
455
456        /**
457         * Returns a list of studies that are writable for the given user
458         */
459        public static giveWritableStudies(SecUser user, int max) {
460                // User that are not logged in, are not allowed to write to a study
461                if( user == null )
462                        return [];
463                       
464                def c = Study.createCriteria()
465
466                // Administrators are allowed to read everything
467                if( user.hasAdminRights() ) {
468                        return c.list {
469                                maxResults(max)
470                        }
471                }
472
473                return c.list {
474                        maxResults(max)
475                        or {
476                                eq( "owner", user )
477                                writers {
478                                        eq( "id", user.id )
479                                }
480                        }
481                }
482        }
483
484        /**
485         * Returns a list of studies that are readable by the given user
486         */
487        public static giveReadableStudies(SecUser user, int max) {
488                def c = Study.createCriteria()
489
490        // Administrators are allowed to read everything
491                if( user == null ) {
492            return c.list {
493                                maxResults(max)
494                and {
495                    eq( "published", true )
496                    eq( "publicstudy", true )
497                }
498            }
499        } else if( user.hasAdminRights() ) {
500            return c.list {
501                                maxResults(max)
502                        }
503                } else  {
504            return c.list {
505                                maxResults(max)
506                or {
507                    eq( "owner", user )
508                    writers {
509                        eq( "id", user.id )
510                    }
511                    and {
512                        readers {
513                            eq( "id", user.id )
514                        }
515                        eq( "published", true )
516                    }
517                }
518            }
519        }
520        }
521}
Note: See TracBrowser for help on using the browser.