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

CenterForOpenScience / ember-osf-web / 13504987529

24 Feb 2025 05:51PM UTC coverage: 66.825% (-1.2%) from 68.015%
13504987529

push

github

adlius
Merge branch 'release/25.04.0'

3107 of 5068 branches covered (61.31%)

Branch coverage included in aggregate %.

452 of 809 new or added lines in 38 files covered. (55.87%)

9 existing lines in 3 files now uncovered.

7897 of 11399 relevant lines covered (69.28%)

189.72 hits per line

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

62.32
/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/template';
7
import { tracked } from '@glimmer/tracking';
8
import { buildValidations, validator } from 'ember-cp-validations';
9
import Intl from 'ember-intl/services/intl';
10

11
import config from 'ember-osf-web/config/environment';
12
import { task } from 'ember-concurrency';
13
import { waitFor } from '@ember/test-waiters';
14

15
import getRelatedHref from 'ember-osf-web/utils/get-related-href';
16
import captureException from 'ember-osf-web/utils/capture-exception';
17

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

34
const {
35
    OSF: {
36
        apiUrl,
37
        apiNamespace,
38
    },
39
} = config;
1✔
40

41
const Validations = buildValidations({
1✔
42
    title: [
43
        validator('presence', true),
44
    ],
45
});
46

47
const CollectableValidations = buildValidations({
1✔
48
    description: [
49
        validator('presence', {
50
            presence: true,
51
        }),
52
    ],
53
    license: [
54
        validator('presence', {
55
            presence: true,
56
        }),
57
    ],
58
    nodeLicense: [
59
        validator('presence', {
60
            presence: true,
61
        }),
62
        validator('node-license', {
63
            on: 'license',
64
        }),
65
    ],
66
    tags: [
67
        validator('presence', {
68
            presence: true,
69
            disabled: true,
70
        }),
71
    ],
72
}, {
73
    disabled: not('model.collectable'),
74
});
75

76
export enum NodeType {
77
    Fork = 'fork',
78
    Generic = 'generic',
79
    Registration = 'registration',
80
}
81

82
export enum NodeCategory {
83
    Data = 'data',
84
    Other = 'other',
85
    Project = 'project',
86
    Software = 'software',
87
    Analysis = 'analysis',
88
    Procedure = 'procedure',
89
    Hypothesis = 'hypothesis',
90
    Uncategorized = 'uncategorized',
91
    Communication = 'communication',
92
    Instrumentation = 'instrumentation',
93
    MethodsAndMeasures = 'methods and measures',
94
}
95

96
export interface NodeLicense {
97
    readonly copyrightHolders?: string;
98
    readonly year?: string;
99
}
100

101
export default class NodeModel extends AbstractNodeModel.extend(Validations, CollectableValidations) {
102
    @service intl!: Intl;
103

104
    @attr('fixstring') title!: string;
105
    @attr('fixstring') description!: string;
106
    @attr('node-category') category!: NodeCategory;
107
    @attr('boolean') currentUserIsContributor!: boolean;
108
    @attr('boolean') fork!: boolean;
109
    @alias('fork') isFork!: boolean;
110
    @attr('boolean') collection!: boolean;
111
    @attr('boolean') registration!: boolean;
112
    @attr('boolean') public!: boolean;
113
    @attr('date') dateCreated!: Date;
114
    @attr('date') dateModified!: Date;
115
    @attr('date') forkedDate!: Date;
116
    @attr('node-license') nodeLicense!: NodeLicense | null;
117
    @attr('fixstringarray') tags!: string[];
118
    @attr('fixstring') templateFrom!: string;
119
    @attr('string') analyticsKey?: string;
120
    @attr('boolean') preprint!: boolean;
121
    @attr('boolean') currentUserCanComment!: boolean;
122
    @attr('boolean') wikiEnabled!: boolean;
123

124
    @hasMany('subject', { inverse: null, async: false }) subjectsAcceptable?: SubjectModel[];
125

126
    // FE-only property to check enabled addons.
127
    // null until getEnabledAddons has been called
128
    @tracked addonsEnabled?: string[];
129

130
    @hasMany('contributor', { inverse: 'node' })
131
    contributors!: AsyncHasMany<ContributorModel> & ContributorModel[];
132

133
    @hasMany('contributor', { inverse: null })
134
    bibliographicContributors!: AsyncHasMany<ContributorModel>;
135

136
    @belongsTo('node', { inverse: 'children' })
137
    parent!: AsyncBelongsTo<NodeModel> & NodeModel;
138

139
    @belongsTo('region')
140
    region!: RegionModel;
141

142
    @hasMany('node', { inverse: 'parent' })
143
    children!: AsyncHasMany<NodeModel>;
144

145
    @hasMany('preprint', { inverse: 'node' })
146
    preprints!: AsyncHasMany<PreprintModel>;
147

148
    @hasMany('institution', { inverse: 'nodes' })
149
    affiliatedInstitutions!: AsyncHasMany<InstitutionModel> | InstitutionModel[];
150

151
    @hasMany('comment', { inverse: 'node' })
152
    comments!: AsyncHasMany<CommentModel>;
153

154
    @belongsTo('citation')
155
    citation!: AsyncBelongsTo<CitationModel> & CitationModel;
156

157
    @belongsTo('license', { inverse: null })
158
    license!: AsyncBelongsTo<LicenseModel> & LicenseModel;
159

160
    @hasMany('node', { inverse: null })
161
    linkedNodes!: AsyncHasMany<NodeModel> & NodeModel[];
162

163
    @hasMany('registration', { inverse: null })
164
    linkedRegistrations!: AsyncHasMany<RegistrationModel>;
165

166
    @hasMany('registration', { inverse: 'registeredFrom' })
167
    registrations!: AsyncHasMany<RegistrationModel>;
168

169
    @hasMany('node', { inverse: 'forkedFrom', polymorphic: true })
170
    forks!: AsyncHasMany<NodeModel>;
171

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

175
    @belongsTo('node', { inverse: null })
176
    root!: AsyncBelongsTo<NodeModel> & NodeModel;
177

178
    @belongsTo('node-storage', { inverse: null })
179
    storage!: AsyncBelongsTo<NodeStorageModel> & NodeStorageModel;
180

181
    @hasMany('node', { inverse: null })
182
    linkedByNodes!: AsyncHasMany<NodeModel>;
183

184
    @hasMany('node', { inverse: null })
185
    linkedByRegistrations!: AsyncHasMany<RegistrationModel>;
186

187
    @hasMany('wiki', { inverse: 'node' })
188
    wikis!: AsyncHasMany<WikiModel>;
189

190
    @hasMany('log', { inverse: 'originalNode' })
191
    logs!: AsyncHasMany<LogModel>;
192

193
    @hasMany('identifier', { inverse: 'referent' })
194
    identifiers!: AsyncHasMany<IdentifierModel>;
195

196
    @hasMany('subject', { inverse: null, async: false })
197
    subjects!: SubjectModel[];
198

199
    // These are only computeds because maintaining separate flag values on
200
    // different classes would be a headache TODO: Improve.
201

202
    /**
203
     * Is this a project? Flag can be used to provide template-specific behavior
204
     * for different resource types.
205
     */
206
    @equal('constructor.modelName', 'node') isProject!: boolean;
207

208
    /**
209
     * Is this a registration? Flag can be used to provide template-specific
210
     * behavior for different resource types.
211
     */
212
    @equal('constructor.modelName', 'registration') isRegistration!: boolean;
213

214
    /**
215
     * Is this node being viewed through an anonymized, view-only link?
216
     */
217
    @bool('apiMeta.anonymous') isAnonymous!: boolean;
218

219
    /**
220
     * Does the current user have write permission on this node?
221
     */
222
    @computed('currentUserPermissions')
223
    get userHasWritePermission() {
224
        return Array.isArray(this.currentUserPermissions) && this.currentUserPermissions.includes(Permission.Write);
232✔
225
    }
226

227
    /**
228
     * Is the current user an admin on this node?
229
     */
230
    @computed('currentUserPermissions')
231
    get userHasAdminPermission() {
232
        return Array.isArray(this.currentUserPermissions) && this.currentUserPermissions.includes(Permission.Admin);
176✔
233
    }
234

235
    /**
236
     * Does the current user have read permission on this node?
237
     */
238
    @computed('currentUserPermissions')
239
    get userHasReadPermission() {
240
        return Array.isArray(this.currentUserPermissions) && this.currentUserPermissions.includes(Permission.Read);
157✔
241
    }
242

243
    @computed('currentUserPermissions.length')
244
    get currentUserIsReadOnly() {
245
        return Array.isArray(this.currentUserPermissions) && this.currentUserPermissions.includes(Permission.Read)
16✔
246
            && this.currentUserPermissions.length === 1;
247
    }
248

249
    /**
250
     * The type of this node.
251
     */
252
    @computed('isFork', 'isRegistration')
253
    get nodeType(): NodeType {
254
        if (this.isRegistration) {
67✔
255
            return NodeType.Registration;
38✔
256
        }
257
        if (this.isFork) {
29✔
258
            return NodeType.Fork;
28✔
259
        }
260
        return NodeType.Generic;
1✔
261
    }
262

263
    /**
264
     * The type of this node, as a string.
265
     */
266
    get nodeTypeTranslation(): string {
267
        let translationNode = this.isRoot ? 'project' : 'component';
×
268
        if (this.isRegistration) {
×
269
            translationNode = 'registration';
×
270
        }
271
        return this.intl.t(`general.${translationNode}`);
×
272
    }
273

274
    // This is for the title helper, which does its own encoding of unsafe characters
275
    @computed('title')
276
    get unsafeTitle() {
277
        return htmlSafe(this.title);
33✔
278
    }
279

280
    @computed('id', 'root')
281
    get isRoot() {
282
        const rootId = (this as NodeModel).belongsTo('root').id();
110✔
283
        return !rootId || rootId === this.id;
110✔
284
    }
285

286
    // BaseFileItem override
287
    isNode = true;
581✔
288
    collectable = false;
581✔
289

290
    makeFork(): Promise<object> {
291
        const url = getRelatedHref(this.links.relationships!.forks);
3✔
292
        return this.currentUser.authenticatedAJAX({
3✔
293
            url,
294
            type: 'POST',
295
            headers: {
296
                'Content-Type': 'application/json',
297
            },
298
            data: JSON.stringify({
299
                data: { type: 'nodes' },
300
            }),
301
        });
302
    }
303

304
    /**
305
     * Sets the nodeLicense field defaults based on required fields from a License
306
     */
307
    setNodeLicenseDefaults(requiredFields: Array<keyof NodeLicense>): void {
308
        if (!requiredFields.length && this.nodeLicense) {
×
309
            // If the nodeLicense exists, notify property change so that validation is triggered
310
            this.notifyPropertyChange('nodeLicense');
×
311

312
            return;
×
313
        }
314

315
        const {
316
            copyrightHolders = '',
×
317
            year = new Date().getUTCFullYear().toString(),
×
318
        } = (this.nodeLicense || {});
×
319

320
        const nodeLicenseDefaults: NodeLicense = {
×
321
            copyrightHolders,
322
            year,
323
        };
324

325
        // Only set the required fields on nodeLicense
326
        const props = requiredFields.reduce(
×
327
            (acc, val) => ({ ...acc, [val]: nodeLicenseDefaults[val] }),
×
328
            {},
329
        );
330

331
        this.set('nodeLicense', props);
×
332
    }
333

334
    @task
335
    @waitFor
336
    async getEnabledAddons() {
337
        try {
4✔
338
            const endpoint = `${apiUrl}/${apiNamespace}/nodes/${this.id}/addons/`;
4✔
339
            const response = await this.currentUser.authenticatedAJAX({
4✔
340
                url: endpoint,
341
                type: 'GET',
342
                headers: {
343
                    'Content-Type': 'application/json',
344
                },
345
                xhrFields: { withCredentials: true },
346
            });
347
            if (response.data) {
4!
348
                const addonList = response.data
4✔
349
                    .filter((addon: any) => addon.attributes.node_has_auth)
2✔
350
                    .map((addon: any) => addon.id);
2✔
351
                this.set('addonsEnabled', addonList);
4✔
352
            }
353
        } catch (e) {
NEW
354
            captureException(e);
×
355
        }
356
    }
357
}
358

359
declare module 'ember-data/types/registries/model' {
360
    export default interface ModelRegistry {
361
        node: NodeModel;
362
    } // eslint-disable-line semi
363
}
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