xolo-server 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,648 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+ #
7
+
8
+ # frozen_string_literal: true
9
+
10
+ # main module
11
+ module Xolo
12
+
13
+ module Core
14
+
15
+ module BaseClasses
16
+
17
+ # The base class for dealing with Titles in the
18
+ # Xolo Server, Admin, and Client modules.
19
+ #
20
+ # This class holds the common aspects of Xolo Titles as used
21
+ # on the Xolo server, in the Xolo Admin CLI app 'xadm', and the
22
+ # client app 'xolo' - most importately it defines which data they
23
+ # exchange.
24
+ #
25
+ ############################
26
+ class Title < Xolo::Core::BaseClasses::ServerObject
27
+
28
+ # Mixins
29
+ #############################
30
+ #############################
31
+
32
+ # Constants
33
+ #############################
34
+ #############################
35
+
36
+ MIN_TITLE_DESC_LENGTH = 25
37
+
38
+ # Attributes
39
+ ######################
40
+ ######################
41
+
42
+ # Attributes of Titles.
43
+ #
44
+ # This hash defines the common attributes of all Xolo titles - which should
45
+ # be available in all subclasses, both on the server, in xadm, and via the
46
+ # client.
47
+ #
48
+ # Each of those subclasses might define (and store or calculate) other
49
+ # attributes as needed.
50
+ #
51
+ # Importantly, the attributes defined here are those that are transfered
52
+ # between xadm and the server as JSON data, and then used to instantiate the
53
+ # title in the local context. They are also stored in the JSON file deployed
54
+ # to clients containing all currently known titles and versions.
55
+ #
56
+ # In this hash, each key is the name of an attribute, and the values are
57
+ # hashes defining the details of that attribute.
58
+ #
59
+ # Since they are used by all subclasses, the details may include info used
60
+ # by one subclass but not another. For example, these attributes are
61
+ # used to create/define CLI & walkthru options for xadm, the settings for
62
+ # which are ignored on the server.
63
+ #
64
+ # The info below also applies to {Xolo:Core::BaseClasses::Version::ATTRIBUTES}, q.v.
65
+ #
66
+ # Title attributes have these details:
67
+ #
68
+ # - label: [String] to be displayed in admin interaction, walkthru, help msgs
69
+ # or error messages. e.g. 'Display Name' for the attribute display_name
70
+ #
71
+ # - required: [Boolean] Must be provided when making a new title, cannot be
72
+ # deleted from existing titles. (but can be changed if not immutable)
73
+ #
74
+ # - immutable: [Boolean] This value can only be set when creatimg a new title.
75
+ # When editing an existing title, it cannot be changed.
76
+ #
77
+ # - cli: [Symbol, false] What is the 'short' option flag for this option?
78
+ # The long option flag is the key, preceded with '--' and with underscores
79
+ # changed to '-', so display_name is set using the cli option '--display-name'.
80
+ # If this is set to :n then the short version is '-n'
81
+ #
82
+ # If this attribute can't be set via walkthru or a CLI option, set this to false.
83
+ #
84
+ # - multi: [Boolean] If true, this option takes multiple values and is stored as
85
+ # an Array. On the commandline, it can be given multiple times and all the values
86
+ # will be in an array in the options hash.
87
+ # E.g. if foo: { multi: true } and these options are given on the commandline
88
+ # --foo val1 --foo val2 --foo 'val 3'
89
+ # then the options hash will contain: :foo => ['val1', 'val2', 'val 3']
90
+ # It can also be given as a single comma-separated string, e.g.
91
+ # --foo 'val1, val2, val 3'
92
+ #
93
+ # In --walkthru the user will be asked to keep entering values, and to
94
+ # end input with an 'x' by itself.
95
+ #
96
+ # - default: [String, Numeric, Boolean, Proc] the default value if nothing is
97
+ # provided or inherited. Note that titles never inherit values, only versions do.
98
+ #
99
+ # - validate: [Boolean, Symbol] how to validate & convert values for this attribute.
100
+ #
101
+ # - If true (not just truthy) call method named 'validate_<key>' in {Xolo::Admin::Validate}
102
+ # passing in the value to validate.
103
+ # e.g. Xolo::Admin::Validate.validate_display_name(new_display_name)
104
+ #
105
+ # - If a Symbol, it's an arbitrary method to call on the {Xolo::Admin::Validate} module
106
+ # e.g. :non_empty_array will validate the value using
107
+ # Xolo::Admin::Validate.non_empty_array(some_array_value)
108
+ #
109
+ # - Anything else: no validation, and the value will be a String
110
+ #
111
+ # - type: [Symbol] the data type of the value. One of: :boolean, :string, :integer,
112
+ # :time
113
+ #
114
+ # NOTE: We are not using Optimist's validation & auto-conversion of these types, they
115
+ # all come from the CLI as strings, and the matching method in {Xolo::Admin::Validate}
116
+ # is used to validate and convert the values.
117
+ # The YARD docs for each attribute indicate the Class of the value in the
118
+ # Title object after CLI processing.
119
+ #
120
+ # - invalid_msg: [String] Custom message to display when the value fails validation.
121
+ #
122
+ # - desc: [String] Helpful text explaining what the attribute is, and what its CLI option means.
123
+ # Displayed during walkthru, in help messages, and in some err messages.
124
+ #
125
+ # - walkthru_na: [Symbol] The name of a method to call (usually defined in
126
+ # {Xolo::Admin::Interactive}) when building the walk thru menu item for this option.
127
+ # If it returns a string, it is an explanation of wny this option
128
+ # is not available at the moment, and the item is not selectable. If it returns nil, the
129
+ # item is displayed and handled as normal.
130
+ #
131
+ # - multiline [Boolean] If true, the value for this option can be many lines lione, and in
132
+ # walkthru, will be presented to the user in an editor like vim. See also multi: above
133
+ #
134
+ # - ted_attribute: [Symbol] If this attribute has a matching one on the related
135
+ # Title Editor Title, this is the name of that attribute in the Windoo::SoftwareTitle
136
+ #
137
+ # - readline: [Symbol] If set, use readline to get the value from the user during walkthru.
138
+ # If the symbol is :get_files, a custom method is called that uses readline with shell-style
139
+ # auto-complete to get one or more file paths.
140
+ # Any other symbol is the name of a method to call, which will return an array of possible
141
+ # values for the option, which will be used for readline-based auto-completion.
142
+ #
143
+ # - readline_prompt: When using readline to gather mulit: values, this prompt is shown at the
144
+ # start of each line of input for the next item.
145
+ #
146
+ # - read_only: [Boolean] defaults to false. When true, the server maintains this value, and
147
+ # its only readable via xadm.
148
+ #
149
+ # - hide_from_info: [Boolean] when true, do not show this attribute in the 'info' xadm output
150
+ # NOTE: it will still be available when --json is given with the info command.
151
+ #
152
+ # - changelog: [Boolean] When true, changes to this attribute is included in the changelog for the title.
153
+ #
154
+ ATTRIBUTES = {
155
+
156
+ # @!attribute title
157
+ # @return [String] The unique title-string for this title.
158
+ title: {
159
+ label: 'Title',
160
+ ted_attribute: :id,
161
+ required: true,
162
+ immutable: true,
163
+ cli: false,
164
+ type: :string,
165
+ hide_from_info: true,
166
+ validate: true,
167
+ invalid_msg: 'Not a valid title: must be lowercase alphanumeric and dashes only',
168
+ desc: <<~ENDDESC
169
+ A unique string identifying this Title, e.g. 'folio' or 'google-chrome'.
170
+ The same as a 'basename' in d3.
171
+ Must contain only lowercase letters, numbers, and dashes.
172
+ ENDDESC
173
+ },
174
+
175
+ # @!attribute display_name
176
+ # @return [String] The display-name for this title
177
+ display_name: {
178
+ label: 'Display Name',
179
+ ted_attribute: :name,
180
+ required: true,
181
+ cli: :n,
182
+ type: :string,
183
+ validate: :validate_title_display_name,
184
+ invalid_msg: 'Not a valid display name, must be at least three characters long.',
185
+ changelog: true,
186
+ desc: <<~ENDDESC
187
+ A human-friendly name for the Software Title, e.g. 'Google Chrome', or 'NFS Menubar'.
188
+ Must be at least three characters long.
189
+ ENDDESC
190
+ },
191
+
192
+ # @!attribute description
193
+ # @return [String] A description of what this title installs
194
+ description: {
195
+ label: 'Description',
196
+ required: true,
197
+ cli: :d,
198
+ type: :string,
199
+ validate: :validate_title_desc,
200
+ changelog: true,
201
+ multiline: true,
202
+ invalid_msg: <<~ENDINV,
203
+ Not a valid description, must be at least #{MIN_TITLE_DESC_LENGTH} characters.
204
+
205
+ Provide a useful dscription of what the software does, URLs, developer names, etc.
206
+
207
+ DO NOT USE, e.g. 'Installs Some App', because we know that already and it isn't helpful.
208
+ ENDINV
209
+ desc: <<~ENDDESC
210
+ A useful dscription of what the software installed by this title does. You can also include URLs, developer names, support info, etc.
211
+
212
+ DO NOT use, e.g. 'Installs Some App', because we know that already and it isn't helpful.
213
+
214
+ IMPORTANT: If this title appears in Self Service, the description will be visible to users.
215
+
216
+ Must be at least #{MIN_TITLE_DESC_LENGTH} Characters.
217
+ ENDDESC
218
+ },
219
+
220
+ # @!attribute publisher
221
+ # @return [String] The entity that publishes this title
222
+ publisher: {
223
+ label: 'Publisher',
224
+ ted_attribute: :publisher,
225
+ required: true,
226
+ cli: :P,
227
+ type: :string,
228
+ validate: true,
229
+ changelog: true,
230
+ invalid_msg: '"Not a valid Publisher, must be at least three characters.',
231
+ desc: <<~ENDDESC
232
+ The company or entity that publishes this title, e.g. 'Apple, Inc.' or 'Pixar Animation Studios'.
233
+ ENDDESC
234
+ },
235
+
236
+ # @!attribute app_name
237
+ # @return [String] The name of the .app installed by this title. Nil if no .app is installed
238
+ app_name: {
239
+ label: 'App Name',
240
+ ted_attribute: :appName,
241
+ cli: :a,
242
+ validate: true,
243
+ type: :string,
244
+ changelog: true,
245
+ walkthru_na: :app_name_bundleid_na,
246
+ invalid_msg: "Not a valid App name, must end with '.app'",
247
+ desc: <<~ENDDESC
248
+ If this title installs a .app bundle, the app's name must be provided. This the name of the bundle itself on disk, e.g. 'Google Chrome.app'.
249
+
250
+ Jamf Patch Management uses this, plus the app's bundle id, to determine if the title is installed on a computer, and if so, which version.
251
+
252
+ If the title does not install a .app bundle, leave this blank, and provide a --version-script.
253
+
254
+ REQUIRED if --app-bundle-id is used.
255
+ ENDDESC
256
+ },
257
+
258
+ # @!attribute app_bundle_id
259
+ # @return [String] The bundle ID of the .app installed by this title. Nil if no .app is installed
260
+ app_bundle_id: {
261
+ label: 'App Bundle ID',
262
+ ted_attribute: :bundleId,
263
+ cli: :b,
264
+ validate: true,
265
+ type: :string,
266
+ changelog: true,
267
+ walkthru_na: :app_name_bundleid_na,
268
+ invalid_msg: '"Not a valid bundle-id, must include at least one dot.',
269
+ desc: <<~ENDDESC
270
+ If this title installs a .app bundle, the app's bundle-id must be provided. This is found in the CFBundleIdentifier key of the app's Info.plist, e.g. 'com.google.chrome'
271
+
272
+ Jamf Patch Management uses this, plus the app's name, to determine if the title is installed on a computer, and if so, which version.
273
+
274
+ If the title does not install a .app bundle, or if the .app doesn't provide its version via the bundle id (e.g. Firefox) leave this blank, and provide a --version-script.
275
+
276
+ REQUIRED if --app-name is used.
277
+ ENDDESC
278
+ },
279
+
280
+ # @!attribute version_script
281
+ # @return [String] A script that will return the currently installed version of this
282
+ # title on a client mac
283
+ version_script: {
284
+ label: 'Version Script',
285
+ cli: :v,
286
+ # while the script is stored in the Title Editor as the extension attribute
287
+ # its handled differently, so we don't specify a ted_attribute here.
288
+ validate: true,
289
+ type: :string,
290
+ readline: :get_files,
291
+ changelog: true,
292
+ walkthru_na: :version_script_na,
293
+ invalid_msg: "Invalid Script Path. Local File must exist and start with '#!'.",
294
+ desc: <<~ENDDESC
295
+ If this title does NOT install a .app bundle, enter the local path to a script which will run on managed computers and output a <result> tag with the currently installed version of this title.
296
+
297
+ E.g. if version 1.2.3 is installed, the script should output the text:
298
+ <result>1.2.3</result>
299
+ and if no version of the title is installed, it should output:
300
+ <result></result>
301
+
302
+ NOTE: This cannot be used if --app-name & --app-bundle-id are used, but must be used if they are not
303
+ ENDDESC
304
+ },
305
+
306
+ # Whenever one of the groups listed is Xolo::TARGET_ALL ('all') then all other groups are
307
+ # ignored or deleted and the array contains only 'all'
308
+ #
309
+ # @!attribute release_groups
310
+ # @return [Array<String>] Jamf groups that will automatically get this title installed or
311
+ # updated when released
312
+ release_groups: {
313
+ label: 'Release Computer Groups',
314
+ cli: :r,
315
+ validate: true,
316
+ type: :string,
317
+ multi: true,
318
+ readline_prompt: 'Group Name',
319
+ changelog: true,
320
+ readline: :jamf_computer_group_names,
321
+ invalid_msg: 'Invalid release group(s). Must exist in Jamf and not be excluded.',
322
+ desc: <<~ENDDESC
323
+ One or more Jamf Computer Groups whose members will automatically have this title installed when new versions are released.
324
+
325
+ If your Xolo administrators allow it, you can use '#{Xolo::TARGET_ALL}' to auto-install on all computers that aren't excluded. If not, you'll be told how to request setting release groups to '#{Xolo::TARGET_ALL}'.
326
+
327
+ NOTE: Titles can always be installed manually (via command line or Self Service) on non-excluded computers. It's OK to have no release groups.
328
+
329
+ When using the --release-groups CLI option, you can specify more than one group by using the option more than once, or by providing a single option value with the groups separated by commas.
330
+
331
+ To remove all existing, use '#{Xolo::NONE}'.
332
+
333
+ NOTE: When a version is in 'pilot', before it is released, these groups are ignored, but instead a set of 'pilot' groups is defined for each version - and those groups will have that version auto-installed.
334
+ ENDDESC
335
+ },
336
+
337
+ # @!attribute excluded_groups
338
+ # @return [Array<String>] Jamf groups that are not allowed to install this title
339
+ excluded_groups: {
340
+ label: 'Excluded Computer Groups',
341
+ cli: :x,
342
+ validate: true,
343
+ type: :string,
344
+ multi: true,
345
+ changelog: true,
346
+ readline_prompt: 'Group Name',
347
+ readline: :jamf_computer_group_names,
348
+ invalid_msg: 'Invalid excluded computer group(s). Must exist in Jamf.',
349
+ desc: <<~ENDDESC
350
+ One or more Jamf Computer Groups whose members are not allowed to install this title.
351
+
352
+ When a computer is in one of these groups, the title is not available even if the computer is in a pilot or release group.
353
+
354
+ When using the --excluded-groups CLI option, you can specify more than one group by using the option more than once, or by providing a single option value with the groups separated by commas.
355
+
356
+ To remove all existing, use '#{Xolo::NONE}'.
357
+
358
+ NOTE: Regardless of the excluded groups set here, if the server has defined a 'forced_exclusion' in its config, that group is always excluded from all xolo titles. Also, computers that are 'frozen' for a title are excluded.
359
+ ENDDESC
360
+ },
361
+
362
+ # @!attribute uninstall_script
363
+ # @return [String] The path to a script starting with '#!' that will uninstall any version of
364
+ # this title.
365
+ uninstall_script: {
366
+ label: 'Uninstall Script',
367
+ cli: :U,
368
+ validate: true,
369
+ type: :string,
370
+ readline: :get_files,
371
+ walkthru_na: :uninstall_script_na,
372
+ changelog: true,
373
+ invalid_msg: "Invalid Script Path. Local File must exist and start with '#!'.",
374
+ desc: <<~ENDDESC
375
+ By default, Xolo cannot un-install a title. To make it do so you must provide either --uninstall-script or --uninstall-ids.
376
+
377
+ When using --uninstall-script, provide a path to a local file containing a script (starting with '#!') that will uninstall any version of this title.
378
+
379
+ This is useful when the publisher provides a custom uninstaller, or to uninstall things that were not installed via .pkg files (e.g. drag-installs).
380
+
381
+ Either --uninstall-script or --uninstall-ids must be provided if you want to set --expiration.
382
+
383
+ Use '#{Xolo::NONE}' to unset this,
384
+ ENDDESC
385
+ },
386
+
387
+ # @!attribute uninstall_ids
388
+ # @return [String] One or more package identifiers recognized by pkgutil, that will uninstall the
389
+ # currently installed version of if this title.
390
+ uninstall_ids: {
391
+ label: 'Uninstall IDs',
392
+ cli: :u,
393
+ validate: true,
394
+ type: :string,
395
+ multi: true,
396
+ multi_prompt: 'Pkg ID',
397
+ walkthru_na: :uninstall_ids_na,
398
+ changelog: true,
399
+ invalid_msg: 'Invalid uninstall ids',
400
+ desc: <<~ENDDESC
401
+ By default, Xolo cannot un-install a title. To make it do so you must provide either --uninstall-ids or --uninstall-script.
402
+
403
+ When using --uninstall-ids, provide one or more package identifiers, as listed by `pkgutil --pkgs`.
404
+ Xolo will use them to create a Jamf Script that will delete all the files that were installed by the matching .pkg installers.
405
+
406
+ NOTE: Package IDs will not work if the item was installed without using an installer.pkg
407
+ (e.g. drag-installing)
408
+
409
+ Either --uninstall-script or --uninstall-ids must be provided if you want to set --expiration.
410
+
411
+ When using the --uninstall-ids CLI option, you can specify more than one ID by using the option more than once, or by providing a single option value with the IDs separated by commas.
412
+
413
+ Use '#{Xolo::NONE}' to unset this,
414
+ ENDDESC
415
+ },
416
+
417
+ # @!attribute expiration
418
+ # @return [Integer] Number of days of disuse before the title is uninstalled.
419
+ expiration: {
420
+ label: 'Expire Days',
421
+ cli: :e,
422
+ validate: true,
423
+ type: :integer,
424
+ walkthru_na: :expiration_na,
425
+ changelog: true,
426
+ invalid_msg: "Invalid expiration. Must be a non-negative integer, or zero or '#{Xolo::NONE}' for no expiration.",
427
+ desc: <<~ENDDESC
428
+ Automatically uninstall this title if none of the items listed as '--expire-paths' have been opened in this number of days.
429
+ This can be useful for reclaiming unused licenses, especially if users can re-install as needed via Self Service.
430
+
431
+ IMPORTANT:
432
+ - This title must have an '--uninstall-method' set, or it can't be uninstalled by xolo
433
+ - You must define one or more --expire-paths
434
+
435
+ Setting this to '#{Xolo::NONE}' or zero, means 'do not expire'.
436
+ ENDDESC
437
+ },
438
+
439
+ # @!attribute expire_paths
440
+ # @return [Array<String>] App names that are considered to be 'used' if they spend any time
441
+ # in the foreground.
442
+ expire_paths: {
443
+ label: 'Expiration Paths',
444
+ cli: :E,
445
+ validate: true,
446
+ type: :string,
447
+ multi: true,
448
+ walkthru_na: :expiration_paths_na,
449
+ changelog: true,
450
+ readline: :get_files,
451
+ readline_prompt: 'Path',
452
+ invalid_msg: 'Invalid expiration paths. Must be absolute paths starting with /',
453
+ desc: <<~ENDDESC
454
+ The full paths to one or items (e.g. '/Applications/Google Chrome.app') that must be opened within the --expiration period to prevent automatic uninstall of the title. The paths need not be .apps, but can be anything. If the item at the path has never been opened, its date-added is used. If it doesn't exist, it is considered to never have been opened.
455
+
456
+ If multiple paths are specified, any one of them being opened will count. This is useful for multi-app titles, such as Microsoft Office, or when different versions have different app names.
457
+
458
+ When using the --expiration-paths CLI option, you can specify more than one path by using the option more than once, or by providing a single option value with the paths separated by commas.
459
+ ENDDESC
460
+ },
461
+
462
+ # @!attribute self_service
463
+ # @return [Boolean] Does this title appear in Self Service?
464
+ self_service: {
465
+ label: 'In Self Service?',
466
+ cli: :s,
467
+ type: :boolean,
468
+ validate: :validate_boolean,
469
+ default: false,
470
+ changelog: true,
471
+ walkthru_na: :ssvc_na,
472
+ desc: <<~ENDDESC
473
+ Make this title available in Self Service. Only the currently released version will be available.
474
+
475
+ While in pilot, a version is installed via its auto-install policy,
476
+ or 'sudo xolo install <title> <version>' or updated via its Patch Policy.
477
+
478
+ It will never be available to excluded computers.
479
+
480
+ Self Service is not available for titles with the release_group 'all'.
481
+ ENDDESC
482
+ },
483
+
484
+ # @!attribute self_service_category
485
+ # @return [String] The main Self Service category in which to show this title
486
+ self_service_category: {
487
+ label: 'Self Service Category',
488
+ cli: :c,
489
+ validate: true,
490
+ type: :string,
491
+ walkthru_na: :ssvc_na,
492
+ changelog: true,
493
+ readline: :jamf_category_names,
494
+ invalid_msg: 'Invalid category. Must exist in Jamf Pro.',
495
+ desc: <<~ENDDESC
496
+ The Category in which to display this title in Self Service.
497
+ REQUIRED if self_service is true, ignored otherwise
498
+ ENDDESC
499
+ },
500
+
501
+ # NOTE: This isn't stored anywhere after its used. If it is provided via
502
+ # xadm add-title or edit-title, it will be uploaded and applied at that
503
+ # time, but then if you fetch the title again, this value will be nil.
504
+ #
505
+ # @!attribute self_service_icon
506
+ # @return [String] Path to a local image file to use as the Self Service icon for this title.
507
+ self_service_icon: {
508
+ label: 'Self Service Icon',
509
+ cli: :i,
510
+ validate: true,
511
+ type: :string,
512
+ readline: :get_files,
513
+ changelog: true,
514
+ walkthru_na: :ssvc_na,
515
+ invalid_msg: 'Invalid Icon. Must exist locally and be a PNG, JPG, or GIF file.',
516
+ desc: <<~ENDDESC
517
+ Path to a local image file to use as the icon for this title in Self Service.
518
+ The file must be a PNG, JPG, or GIF file. The recommended size is 512x512 pixels.
519
+ If one has already been set, this will replace it.
520
+ ENDDESC
521
+ },
522
+
523
+ # @!attribute contact_email
524
+ # @return [String] Email address for the person or team responsible for this title
525
+ contact_email: {
526
+ label: 'Contact Email Address',
527
+ cli: :m,
528
+ required: true,
529
+ validate: true,
530
+ type: :string,
531
+ changelog: true,
532
+ invalid_msg: 'Invalid Email address.',
533
+ desc: <<~ENDDESC
534
+ The email address of the team or person responsible for this title. Used for notifications, questions, etc.
535
+
536
+ A mailing list for a team is preferable to an individual's email address, since individuals may leave the team.
537
+ ENDDESC
538
+ },
539
+
540
+ # @!attribute created_by
541
+ # @return [String] The login of the admin who created this title
542
+ created_by: {
543
+ label: 'Created By',
544
+ type: :string,
545
+ cli: false,
546
+ read_only: true, # maintained by the server, not editable by xadm TODO: same as cli: false??
547
+ desc: <<~ENDDESC
548
+ The login of the admin who created this title.
549
+ ENDDESC
550
+ },
551
+
552
+ # @!attribute creation_date
553
+ # @return [Time] The date this title was created.
554
+ creation_date: {
555
+ label: 'Creation Date',
556
+ type: :time,
557
+ cli: false,
558
+ read_only: true, # maintained by the server, not editable by xadm
559
+ desc: <<~ENDDESC
560
+ The date this title was created.
561
+ ENDDESC
562
+ },
563
+
564
+ # @!attribute modified_by
565
+ # @return [String] The login of the admin who last modified this title
566
+ modified_by: {
567
+ label: 'Modified By',
568
+ type: :string,
569
+ cli: false,
570
+ read_only: true, # maintained by the server, not editable by xadm
571
+ desc: <<~ENDDESC
572
+ The login of the admin who last modified this title.
573
+ ENDDESC
574
+ },
575
+
576
+ # @!attribute modification_date
577
+ # @return [Time] The date this title was last modified.
578
+ modification_date: {
579
+ label: 'Modification Date',
580
+ type: :time,
581
+ cli: false,
582
+ read_only: true, # maintained by the server, not editable by xadm
583
+ desc: <<~ENDDESC
584
+ The date this title was last modified.
585
+ ENDDESC
586
+ },
587
+
588
+ # @!attribute version_order
589
+ # @return [Array<String>] The known versions, newest to oldest
590
+ version_order: {
591
+ label: 'Versions',
592
+ type: :string,
593
+ multi: true,
594
+ cli: false,
595
+ read_only: true, # maintained by the server, not editable by xadm
596
+ desc: <<~ENDDESC
597
+ The known versions of the title, newest to oldest
598
+ ENDDESC
599
+ },
600
+
601
+ # @!attribute released_version
602
+ # @return [String] The currently released version, if any
603
+ released_version: {
604
+ label: 'Released Version',
605
+ type: :string,
606
+ cli: false,
607
+ changelog: true,
608
+ read_only: true, # maintained by the server, not editable by xadm
609
+ desc: <<~ENDDESC
610
+ The currently released version
611
+ ENDDESC
612
+ }
613
+
614
+ }.freeze
615
+
616
+ ATTRIBUTES.each_key do |attr|
617
+ attr_accessor attr
618
+ end
619
+
620
+ # Constructor
621
+ ######################
622
+ ######################
623
+
624
+ def initialize(data_hash)
625
+ super
626
+ # zero means no expiration
627
+ @expiration = nil if @expiration.to_i.zero?
628
+ end
629
+
630
+ # Instance Methods
631
+ ######################
632
+ ######################
633
+
634
+ # the latest version of this title in Xolo
635
+ # @param cnx [Faraday::Connection] The connection to use, must be logged in already
636
+ # @return [String]
637
+ ####################
638
+ def latest_version
639
+ version_order&.first
640
+ end
641
+
642
+ end # class Title
643
+
644
+ end # module BaseClasses
645
+
646
+ end # module Core
647
+
648
+ end # module Xolo