• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CenterForOpenScience / ember-osf-web / 4105442023

pending completion
4105442023

push

github

Yuhuai Liu
Merge branch 'release/23.03.0'

2491 of 3708 branches covered (67.18%)

Branch coverage included in aggregate %.

13 of 13 new or added lines in 6 files covered. (100.0%)

5705 of 7763 relevant lines covered (73.49%)

232.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

54.39
/app/models/node.ts
1
import { attr, belongsTo, hasMany, AsyncBelongsTo, AsyncHasMany } from '@ember-data/model';
2

3
import { computed } from '@ember/object';
4
import { alias, bool, equal, not } from '@ember/object/computed';
5
import { inject as service } from '@ember/service';
6
import { htmlSafe } from '@ember/string';
7
import { buildValidations, validator } from 'ember-cp-validations';
8
import Intl from 'ember-intl/services/intl';
9

10
import getRelatedHref from 'ember-osf-web/utils/get-related-href';
11

12
import AbstractNodeModel from 'ember-osf-web/models/abstract-node';
13
import CitationModel from './citation';
14
import CommentModel from './comment';
15
import ContributorModel from './contributor';
16
import IdentifierModel from './identifier';
17
import InstitutionModel from './institution';
18
import LicenseModel from './license';
19
import LogModel from './log';
20
import NodeStorageModel from './node-storage';
21
import { Permission } from './osf-model';
22
import PreprintModel from './preprint';
23
import RegionModel from './region';
24
import RegistrationModel from './registration';
25
import SubjectModel from './subject';
26
import WikiModel from './wiki';
27

28
const Validations = buildValidations({
1✔
29
    title: [
30
        validator('presence', true),
31
    ],
32
});
33

34
const CollectableValidations = buildValidations({
1✔
35
    description: [
36
        validator('presence', {
37
            presence: true,
38
        }),
39
    ],
40
    license: [
41
        validator('presence', {
42
            presence: true,
43
        }),
44
    ],
45
    nodeLicense: [
46
        validator('presence', {
47
            presence: true,
48
        }),
49
        validator('node-license', {
50
            on: 'license',
51
        }),
52
    ],
53
    tags: [
54
        validator('presence', {
55
            presence: true,
56
            disabled: true,
57
        }),
58
    ],
59
}, {
60
    disabled: not('model.collectable'),
61
});
62

63
export enum NodeType {
64
    Fork = 'fork',
65
    Generic = 'generic',
66
    Registration = 'registration',
67
}
68

69
export enum NodeCategory {
70
    Data = 'data',
71
    Other = 'other',
72
    Project = 'project',
73
    Software = 'software',
74
    Analysis = 'analysis',
75
    Procedure = 'procedure',
76
    Hypothesis = 'hypothesis',
77
    Uncategorized = 'uncategorized',
78
    Communication = 'communication',
79
    Instrumentation = 'instrumentation',
80
    MethodsAndMeasures = 'methods and measures',
81
}
82

83
export interface NodeLicense {
84
    readonly copyrightHolders?: string;
85
    readonly year?: string;
86
}
87

88
export default class NodeModel extends AbstractNodeModel.extend(Validations, CollectableValidations) {
89
    @service intl!: Intl;
90

91
    @attr('fixstring') title!: string;
92
    @attr('fixstring') description!: string;
93
    @attr('node-category') category!: NodeCategory;
94
    @attr('boolean') currentUserIsContributor!: boolean;
95
    @attr('boolean') fork!: boolean;
96
    @alias('fork') isFork!: boolean;
97
    @attr('boolean') collection!: boolean;
98
    @attr('boolean') registration!: boolean;
99
    @attr('boolean') public!: boolean;
100
    @attr('date') dateCreated!: Date;
101
    @attr('date') dateModified!: Date;
102
    @attr('date') forkedDate!: Date;
103
    @attr('node-license') nodeLicense!: NodeLicense | null;
104
    @attr('fixstringarray') tags!: string[];
105
    @attr('fixstring') templateFrom!: string;
106
    @attr('string') analyticsKey?: string;
107
    @attr('boolean') preprint!: boolean;
108
    @attr('boolean') currentUserCanComment!: boolean;
109
    @attr('boolean') wikiEnabled!: boolean;
110

111
    @hasMany('contributor', { inverse: 'node' })
112
    contributors!: AsyncHasMany<ContributorModel> & ContributorModel[];
113

114
    @hasMany('contributor', { inverse: null })
115
    bibliographicContributors!: AsyncHasMany<ContributorModel>;
116

117
    @belongsTo('node', { inverse: 'children' })
118
    parent!: AsyncBelongsTo<NodeModel> & NodeModel;
119

120
    @belongsTo('region')
121
    region!: RegionModel;
122

123
    @hasMany('node', { inverse: 'parent' })
124
    children!: AsyncHasMany<NodeModel>;
125

126
    @hasMany('preprint', { inverse: 'node' })
127
    preprints!: AsyncHasMany<PreprintModel>;
128

129
    @hasMany('institution', { inverse: 'nodes' })
130
    affiliatedInstitutions!: AsyncHasMany<InstitutionModel> | InstitutionModel[];
131

132
    @hasMany('comment', { inverse: 'node' })
133
    comments!: AsyncHasMany<CommentModel>;
134

135
    @belongsTo('citation')
136
    citation!: AsyncBelongsTo<CitationModel> & CitationModel;
137

138
    @belongsTo('license', { inverse: null })
139
    license!: AsyncBelongsTo<LicenseModel> & LicenseModel;
140

141
    @hasMany('node', { inverse: null })
142
    linkedNodes!: AsyncHasMany<NodeModel> & NodeModel[];
143

144
    @hasMany('registration', { inverse: null })
145
    linkedRegistrations!: AsyncHasMany<RegistrationModel>;
146

147
    @hasMany('registration', { inverse: 'registeredFrom' })
148
    registrations!: AsyncHasMany<RegistrationModel>;
149

150
    @hasMany('node', { inverse: 'forkedFrom' })
151
    forks!: AsyncHasMany<NodeModel>;
152

153
    @belongsTo('node', { inverse: 'forks', polymorphic: true })
154
    forkedFrom!: (AsyncBelongsTo<NodeModel> & NodeModel) | (AsyncBelongsTo<RegistrationModel> & RegistrationModel);
155

156
    @belongsTo('node', { inverse: null })
157
    root!: AsyncBelongsTo<NodeModel> & NodeModel;
158

159
    @belongsTo('node-storage', { inverse: null })
160
    storage!: AsyncBelongsTo<NodeStorageModel> & NodeStorageModel;
161

162
    @hasMany('node', { inverse: null })
163
    linkedByNodes!: AsyncHasMany<NodeModel>;
164

165
    @hasMany('node', { inverse: null })
166
    linkedByRegistrations!: AsyncHasMany<RegistrationModel>;
167

168
    @hasMany('wiki', { inverse: 'node' })
169
    wikis!: AsyncHasMany<WikiModel>;
170

171
    @hasMany('log', { inverse: 'originalNode' })
172
    logs!: AsyncHasMany<LogModel>;
173

174
    @hasMany('identifier', { inverse: 'referent' })
175
    identifiers!: AsyncHasMany<IdentifierModel>;
176

177
    @hasMany('subject', { inverse: null, async: false })
178
    subjects!: SubjectModel[];
179

180
    // These are only computeds because maintaining separate flag values on
181
    // different classes would be a headache TODO: Improve.
182

183
    /**
184
     * Is this a project? Flag can be used to provide template-specific behavior
185
     * for different resource types.
186
     */
187
    @equal('constructor.modelName', 'node') isProject!: boolean;
188

189
    /**
190
     * Is this a registration? Flag can be used to provide template-specific
191
     * behavior for different resource types.
192
     */
193
    @equal('constructor.modelName', 'registration') isRegistration!: boolean;
194

195
    /**
196
     * Is this node being viewed through an anonymized, view-only link?
197
     */
198
    @bool('apiMeta.anonymous') isAnonymous!: boolean;
199

200
    /**
201
     * Does the current user have write permission on this node?
202
     */
203
    @computed('currentUserPermissions')
204
    get userHasWritePermission() {
205
        return Array.isArray(this.currentUserPermissions) && this.currentUserPermissions.includes(Permission.Write);
220✔
206
    }
207

208
    /**
209
     * Is the current user an admin on this node?
210
     */
211
    @computed('currentUserPermissions')
212
    get userHasAdminPermission() {
213
        return Array.isArray(this.currentUserPermissions) && this.currentUserPermissions.includes(Permission.Admin);
176✔
214
    }
215

216
    /**
217
     * Does the current user have read permission on this node?
218
     */
219
    @computed('currentUserPermissions')
220
    get userHasReadPermission() {
221
        return Array.isArray(this.currentUserPermissions) && this.currentUserPermissions.includes(Permission.Read);
162✔
222
    }
223

224
    @computed('currentUserPermissions.length')
225
    get currentUserIsReadOnly() {
226
        return Array.isArray(this.currentUserPermissions) && this.currentUserPermissions.includes(Permission.Read)
16✔
227
            && this.currentUserPermissions.length === 1;
228
    }
229

230
    /**
231
     * The type of this node.
232
     */
233
    @computed('isFork', 'isRegistration')
234
    get nodeType(): NodeType {
235
        if (this.isRegistration) {
63✔
236
            return NodeType.Registration;
35✔
237
        }
238
        if (this.isFork) {
28!
239
            return NodeType.Fork;
28✔
240
        }
241
        return NodeType.Generic;
×
242
    }
243

244
    /**
245
     * The type of this node, as a string.
246
     */
247
    get nodeTypeTranslation(): string {
248
        let translationNode = this.isRoot ? 'project' : 'component';
×
249
        if (this.isRegistration) {
×
250
            translationNode = 'registration';
×
251
        }
252
        return this.intl.t(`general.${translationNode}`);
×
253
    }
254

255
    // This is for the title helper, which does its own encoding of unsafe characters
256
    @computed('title')
257
    get unsafeTitle() {
258
        return htmlSafe(this.title);
33✔
259
    }
260

261
    @computed('id', 'root')
262
    get isRoot() {
263
        const rootId = (this as NodeModel).belongsTo('root').id();
110✔
264
        return !rootId || rootId === this.id;
110✔
265
    }
266

267
    // BaseFileItem override
268
    isNode = true;
563✔
269
    collectable = false;
563✔
270

271
    makeFork(): Promise<object> {
272
        const url = getRelatedHref(this.links.relationships!.forks);
3✔
273
        return this.currentUser.authenticatedAJAX({
3✔
274
            url,
275
            type: 'POST',
276
            headers: {
277
                'Content-Type': 'application/json',
278
            },
279
            data: JSON.stringify({
280
                data: { type: 'nodes' },
281
            }),
282
        });
283
    }
284

285
    /**
286
     * Sets the nodeLicense field defaults based on required fields from a License
287
     */
288
    setNodeLicenseDefaults(requiredFields: Array<keyof NodeLicense>): void {
289
        if (!requiredFields.length && this.nodeLicense) {
×
290
            // If the nodeLicense exists, notify property change so that validation is triggered
291
            this.notifyPropertyChange('nodeLicense');
×
292

293
            return;
×
294
        }
295

296
        const {
297
            copyrightHolders = '',
×
298
            year = new Date().getUTCFullYear().toString(),
×
299
        } = (this.nodeLicense || {});
×
300

301
        const nodeLicenseDefaults: NodeLicense = {
×
302
            copyrightHolders,
303
            year,
304
        };
305

306
        // Only set the required fields on nodeLicense
307
        const props = requiredFields.reduce(
×
308
            (acc, val) => ({ ...acc, [val]: nodeLicenseDefaults[val] }),
×
309
            {},
310
        );
311

312
        this.set('nodeLicense', props);
×
313
    }
314
}
315

316
declare module 'ember-data/types/registries/model' {
317
    export default interface ModelRegistry {
318
        node: NodeModel;
319
    } // eslint-disable-line semi
320
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc