source: trunk/grails-app/domain/dbnp/studycapturing/Study.groovy @ 1233

Last change on this file since 1233 was 1233, checked in by work@…, 11 years ago
  • introduced deleteSample method to be sure a sample -and it's relations- are correctly deleted.
  • hopefully this resolves #223
  • Property svn:keywords set to Author Date Rev
File size: 12.7 KB
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: 1233 $
10 * $Author: work@osx.eu $
11 * $Date: 2010-12-02 17:03:36 +0000 (do, 02 dec 2010) $
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
63        def getToken() { code }
64
65        /**
66         * return the domain fields for this domain class
67         * @return List
68         */
69        static List<TemplateField> giveDomainFields() { return Study.domainFields }
70
71        static final List<TemplateField> domainFields = [
72                new TemplateField(
73                        name: 'title',
74                        type: TemplateFieldType.STRING,
75                        required: true),
76                new TemplateField(
77                        name: 'code',
78                        type: TemplateFieldType.STRING,
79                        preferredIdentifier: true,
80                        comment: 'Fill out the code by which many people will recognize your study',
81                        required: true),
82                new TemplateField(
83                        name: 'startDate',
84                        type: TemplateFieldType.DATE,
85                        comment: 'Fill out the official start date or date of first action',
86                        required: true),
87                new TemplateField(
88                        name: 'published',
89                        type: TemplateFieldType.BOOLEAN,
90                        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.',
91                        required: false)
92        ]
93
94        /**
95         * return the title of this study
96         */
97        def String toString() {
98                return title
99        }
100
101        /**
102         * returns all events and sampling events that do not belong to a group
103         */
104        def List<Event> getOrphanEvents() {
105                def orphans = events.findAll { event -> !event.belongsToGroup(eventGroups) } +
106                        samplingEvents.findAll { event -> !event.belongsToGroup(eventGroups) }
107
108                return orphans
109        }
110
111        /**
112         * Return the unique Subject templates that are used in this study
113         */
114        def List<Template> giveSubjectTemplates() {
115                TemplateEntity.giveTemplates(subjects)
116        }
117
118        /**
119         * Return all subjects for a specific template
120         * @param Template
121         * @return ArrayList
122         */
123        def ArrayList<Subject> giveSubjectsForTemplate(Template template) {
124                subjects.findAll { it.template.equals(template) }
125        }
126
127        /**
128         * Return all unique assay templates
129         * @return Set
130         */
131        List<Template> giveAllAssayTemplates() {
132                TemplateEntity.giveTemplates(((assays) ? assays : []))
133        }
134
135        /**
136         * Return all assays for a particular template
137         * @return ArrayList
138         */
139        def ArrayList giveAssaysForTemplate(Template template) {
140                assays.findAll { it.template.equals(template) }
141        }
142
143        /**
144         * Return the unique Event and SamplingEvent templates that are used in this study
145         */
146        List<Template> giveAllEventTemplates() {
147                // For some reason, giveAllEventTemplates() + giveAllSamplingEventTemplates()
148                // gives trouble when asking .size() to the result
149                // So we also use giveTemplates here
150                TemplateEntity.giveTemplates(((events) ? events : []) + ((samplingEvents) ? samplingEvents : []))
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         * Delete a specific subject from this study, including all its relations
204         * @param subject The subject to be deleted
205         * @void
206         */
207        void deleteSubject(Subject subject) {
208                // Delete the subject from the event groups it was referenced in
209                this.eventGroups.each {
210                        if (it.subjects.contains(subject)) {
211                                it.removeFromSubjects(subject)
212                        }
213                }
214
215                // Delete the samples that have this subject as parent
216                this.samples.findAll { it.parentSubject.equals(subject) }.each {
217                        this.deleteSample(it)
218                }
219
220                // This should remove the subject itself too, because of the cascading belongsTo relation
221                this.removeFromSubjects(subject)
222
223                // But apparently it needs an explicit delete() too
224                subject.delete()
225        }
226
227        /**
228         * Delete an assay from the study
229         * @param Assay
230         * @void
231         */
232        def deleteAssay(Assay assay) {
233                if (assay && assay instanceof Assay) {
234                        // iterate through linked samples
235                        assay.samples.findAll { true }.each() { sample ->
236                                assay.removeFromSamples(sample)
237                        }
238
239                        // remove this assay from the study
240                        this.removeFromAssays(assay)
241
242                        // and delete it explicitly
243                        assay.delete()
244                }
245        }
246
247        /**
248         * Delete an event from the study, including all its relations
249         * @param Event
250         * @void
251         */
252        void deleteEvent(Event event) {
253                // remove event from the study
254                this.removeFromEvents(event)
255
256                // remove event from eventGroups
257                this.eventGroups.each() { eventGroup ->
258                        eventGroup.removeFromEvents(event)
259                }
260        }
261
262        /**
263         * Delete a sample from the study, including all its relations
264         * @param Event
265         * @void
266         */
267        void deleteSample(Sample sample) {
268                // remove the sample from the study
269                this.removeFromSamples(sample)
270
271                // remove the sample from any sampling events it belongs to
272                this.samplingEvents.findAll { it.samples.any { it == sample }}.each {
273                        it.removeFromSamples(sample)
274                }
275
276                // remove the sample from any assays it belongs to
277                this.assays.findAll { it.samples.any { it == sample }}.each {
278                        it.removeFromSamples(sample)
279                }
280
281                // Also here, contrary to documentation, an extra delete() is needed
282                // otherwise date is not properly deleted!
283                sample.delete()
284        }
285
286        /**
287         * Delete a samplingEvent from the study, including all its relations
288         * @param SamplingEvent
289         * @void
290         */
291        void deleteSamplingEvent(SamplingEvent samplingEvent) {
292                // remove event from eventGroups
293                this.eventGroups.each() { eventGroup ->
294                        eventGroup.removeFromSamplingEvents(samplingEvent)
295                }
296
297                // Delete the samples that have this sampling event as parent
298                this.samples.findAll { it.parentEvent.equals(samplingEvent) }.each {
299                        // This should remove the sample itself too, because of the cascading belongsTo relation
300                        this.deleteSample(it)
301                }
302
303                // Remove event from the study
304                // This should remove the event group itself too, because of the cascading belongsTo relation
305                this.removeFromSamplingEvents(samplingEvent)
306
307                // But apparently it needs an explicit delete() too
308                // (Which can be verified by outcommenting this line, then SampleTests.testDeleteViaParentSamplingEvent fails
309                samplingEvent.delete()
310        }
311
312        /**
313         * Delete an eventGroup from the study, including all its relations
314         * @param EventGroup
315         * @void
316         */
317        void deleteEventGroup(EventGroup eventGroup) {
318                // If the event group contains sampling events
319                if (eventGroup.samplingEvents) {
320                        // remove all samples that originate from this eventGroup
321                        if (eventGroup.samplingEvents.size()) {
322                                // find all samples related to this eventGroup
323                                // - subject comparison is relatively straightforward and
324                                //   behaves as expected
325                                // - event comparison behaves strange, so now we compare
326                                //              1. database id's or,
327                                //              2. object identifiers or,
328                                //              3. objects itself
329                                //   this seems now to work as expected
330                                this.samples.findAll { sample ->
331                                        (
332                                        (eventGroup.subjects.findAll {
333                                                it.equals(sample.parentSubject)
334                                        })
335                                                &&
336                                                (eventGroup.samplingEvents.findAll {
337                                                        (
338                                                        (it.id && sample.parentEvent.id && it.id == sample.parentEvent.id)
339                                                                ||
340                                                                (it.getIdentifier() == sample.parentEvent.getIdentifier())
341                                                                ||
342                                                                it.equals(sample.parentEvent)
343                                                        )
344                                                })
345                                        )
346                                }.each() { sample ->
347                                        // remove sample from study
348                                        this.deleteSample(sample)
349                                }
350                        }
351
352                        // remove all samplingEvents from this eventGroup
353                        eventGroup.samplingEvents.findAll {}.each() {
354                                eventGroup.removeFromSamplingEvents(it)
355                        }
356                }
357
358                // If the event group contains subjects
359                if (eventGroup.subjects) {
360                        // remove all subject from this eventGroup
361                        eventGroup.subjects.findAll {}.each() {
362                                eventGroup.removeFromSubjects(it)
363                        }
364                }
365
366                // remove the eventGroup from the study
367                this.removeFromEventGroups(eventGroup)
368
369                // Also here, contrary to documentation, an extra delete() is needed
370                // otherwise cascaded deletes are not properly performed
371                eventGroup.delete()
372        }
373
374        /**
375         * Returns true if the given user is allowed to read this study
376         */
377        public boolean canRead(SecUser loggedInUser) {
378                // Anonymous readers are only given access when published and public
379                if (loggedInUser == null) {
380                        return this.publicstudy && this.published;
381                }
382
383                // Administrators are allowed to read every study
384                if (loggedInUser.hasAdminRights()) {
385                        return true;
386                }
387
388                // Owners and writers are allowed to read this study
389                if (this.owner == loggedInUser || this.writers.contains(loggedInUser)) {
390                        return true
391                }
392
393                // Readers are allowed to read this study when it is published
394                if (this.readers.contains(loggedInUser) && this.published) {
395                        return true
396                }
397
398                return false
399        }
400
401        /**
402         * Returns true if the given user is allowed to write this study
403         */
404        public boolean canWrite(SecUser loggedInUser) {
405                if (loggedInUser == null) {
406                        return false;
407                }
408
409                // Administrators are allowed to write every study
410                if (loggedInUser.hasAdminRights()) {
411                        return true;
412                }
413
414                return this.owner == loggedInUser || this.writers.contains(loggedInUser)
415        }
416
417        /**
418         * Returns true if the given user is the owner of this study
419         */
420        public boolean isOwner(SecUser loggedInUser) {
421                if (loggedInUser == null) {
422                        return false;
423                }
424                return this.owner == loggedInUser
425        }
426
427        /**
428         * Returns a list of studies that are writable for the given user
429         */
430        public static giveWritableStudies(SecUser user, int max) {
431                // User that are not logged in, are not allowed to write to a study
432                if (user == null)
433                return [];
434
435                def c = Study.createCriteria()
436
437                // Administrators are allowed to read everything
438                if (user.hasAdminRights()) {
439                        return c.list {
440                                maxResults(max)
441                        }
442                }
443
444                return c.list {
445                        maxResults(max)
446                        or {
447                                eq("owner", user)
448                                writers {
449                                        eq("id", user.id)
450                                }
451                        }
452                }
453        }
454
455        /**
456         * Returns a list of studies that are readable by the given user
457         */
458        public static giveReadableStudies(SecUser user, int max) {
459                def c = Study.createCriteria()
460
461                // Administrators are allowed to read everything
462                if (user == null) {
463                        return c.list {
464                                maxResults(max)
465                                and {
466                                        eq("published", true)
467                                        eq("publicstudy", true)
468                                }
469                        }
470                } else if (user.hasAdminRights()) {
471                        return c.list {
472                                maxResults(max)
473                        }
474                } else {
475                        return c.list {
476                                maxResults(max)
477                                or {
478                                        eq("owner", user)
479                                        writers {
480                                                eq("id", user.id)
481                                        }
482                                        and {
483                                                readers {
484                                                        eq("id", user.id)
485                                                }
486                                                eq("published", true)
487                                        }
488                                }
489                        }
490                }
491        }
492}
Note: See TracBrowser for help on using the repository browser.