ShowManager.vue 32.2 KB
Newer Older
1
<template>
2
  <b-container>
3
4
    <!-- This first row is so far only used to provide a dropdown for
    choosing one of the loaded shows (which the user has access to) -->
5
    <b-row>
6
7
8
      <b-col>
        <h3>Sendungen verwalten</h3>
      </b-col>
9
      <b-col align="right">
10
11
12
        <b-button
          v-if="$parent.user.steeringUser.is_superuser"
          v-b-popover.hover.top="'Add a new show'"
13
          variant="info"
14
15
16
17
18
          @click="$refs.appModalSuperuser.showModalAddShow()"
        >
          +
        </b-button>
        &nbsp;
19
20
21
        <b-dropdown
          id="ddshows"
          text="Sendereihe auswählen"
22
          variant="outline-info"
23
24
        >
          <b-dropdown-item
25
            v-for="(show, index) in shows"
26
27
28
29
30
            :key="show.id"
            @click="switchShow(index)"
          >
            {{ show.name }}
          </b-dropdown-item>
31
32
33
        </b-dropdown>
      </b-col>
    </b-row>
34
    <hr>
35

36
37
    <!-- The jumbotron is used to display the name and description of the
    currently selected show -->
38
    <show-jumbotron />
39

40
    <!-- If the shows are not fully loaded yet, we just put the loading sign -->
41
    <div v-if="!loaded.shows">
42
43
      <b-row>
        <b-col align="center">
44
45
46
47
          <img
            src="../assets/radio.gif"
            alt="loading data"
          >
48
49
        </b-col>
      </b-row>
50
    </div>
51
52

    <!-- When all show data is loaded, here we display all the rest -->
53
    <div v-else>
54
      <!-- include the modals to edit show and timeslot entries from the modal compontents -->
55
56
57
58
59
60
61
62
63
      <app-modalShow
        ref="appModalShow"
        :show="shows[currentShow]"
      />
      <app-modalNotes
        ref="appModalNotes"
        :show="shows[currentShow]"
        :show-aggregate="current"
      />
64
65
66
      <app-modalSuperuser
        ref="appModalSuperuser"
      />
67
68
69
      <app-modalPlaylist
        ref="appModalPlaylist"
      />
70

71
      <!-- here are the filter settings for our timeslots table -->
72
      <b-card>
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
        <b-row>
          <b-col>
            <b-btn v-b-toggle.timeslotFilterCollapse>
              Toggle timeslot filters
            </b-btn>
          </b-col>
          <b-col align="right">
            <b-button
              v-if="$parent.user.steeringUser.is_superuser"
              variant="info"
              @click="$router.push({path: 'emissions', query: { show: currentShow }})"
            >
              Switch to Emission Manager
            </b-button>
          </b-col>
        </b-row>

90
        <b-collapse id="timeslotFilterCollapse">
91
          <br>
92
          <!-- How many slots to show per table page -->
93
          <b-row>
94
95
96
97
98
99
100
101
102
103
            <b-col sm="3">
              <label for="inputNumSlots">Number of slots to show:</label>
            </b-col>
            <b-col sm="9">
              <b-form-input
                id="inputNumSlots"
                v-model="numSlots"
                type="number"
              />
            </b-col>
104
          </b-row>
105
          <!-- The start date to display timeslots from (defaults to today) -->
106
          <b-row>
107
108
109
110
111
112
113
114
115
116
            <b-col sm="3">
              <label for="inputDateStart">From:</label>
            </b-col>
            <b-col sm="9">
              <b-form-input
                id="inputDateStart"
                v-model="dateStart"
                type="date"
              />
            </b-col>
117
          </b-row>
118
          <!-- The end date until to wich to display timeslots -->
119
          <b-row>
120
121
122
123
124
125
126
127
128
129
            <b-col sm="3">
              <label for="inputNumSlots">Until (exclusive):</label>
            </b-col>
            <b-col sm="9">
              <b-form-input
                id="inputDateEnd"
                v-model="dateEnd"
                type="date"
              />
            </b-col>
130
          </b-row>
131
          <br>
132
          <!-- And finally two buttons, one to reset and one to apply the filter -->
133
134
135
136
137
138
139
140
141
142
143
          <b-container
            fluid
            class="text-right"
          >
            <b-btn
              variant="outline-danger"
              @click="resetFilter()"
            >
              Reset filter
            </b-btn> &nbsp;
            <b-btn
144
              variant="outline-success"
145
146
147
148
              @click="applyFilter()"
            >
              Apply filter
            </b-btn>
149
150
151
152
          </b-container>
        </b-collapse>
      </b-card>

153
      <br>
154

155
156
      <!-- here we show our table of timeslots, if the timeslots are already
      loaded (otherwise we just show the loading symbol) -->
157
      <div v-if="loaded.timeslots">
158
159
160
161
162
163
164
        <b-table
          striped
          hover
          outlined
          :fields="notesTableArrayFields"
          :items="notesTableArray"
        >
165
          <!-- Title of the timeslot (if already set) -->
166
          <template v-slot:cell(title)="data">
167
168
169
            <span v-if="data.value">{{ data.value }}</span>
            <span v-else><small><i>(none set)</i></small></span>
          </template>
170
          <!-- Date and time when this timeslot starts -->
171
          <template v-slot:cell(starts)="data">
172
173
            {{ data.value }}
          </template>
174
          <!-- The duration of this timeslot -->
175
          <template v-slot:cell(duration)="data">
176
177
            {{ data.value }}
          </template>
178
179
          <!-- And here all the buttons for editing and doing other things
          with the displayed timeslot -->
180
          <template v-slot:cell(options)="data">
181
182
183
184
185
186
187
188
189
190
            <span
              class="timeslotEditLink"
              @click="editTimeslotNote(data.item.options.id, data.item.options.schedule)"
            ><img
              src="../assets/16x16/emblem-system.png"
              alt="Edit description"
              title="Edit description"
            ></span>
            <span
              class="timeslotEditLink"
191
              @click="editTimeslotPlaylist(shows[currentShow], data.item.options.schedule, data.item.options.id)"
192
193
            ><img
              src="../assets/16x16/media-eject.png"
194
195
              alt="Edit playlist"
              title="Edit playlist"
196
197
            ></span>
            <span
198
              v-if="data.item.options.play"
199
200
201
202
              class="timeslotEditLink"
              @click="notYetImplemented()"
            ><img
              src="../assets/16x16/media-playback-start.png"
203
204
              alt="Open player"
              title="Open player"
205
            ></span>
206
207
208
209
210
          </template>

          <template v-slot:cell(playlist)="data">
            <span v-if="data.value">{{ data.value }}</span>
            <span v-else><small><i>(none set)</i></small></span>
211
212
          </template>
        </b-table>
213
214
215
216
217
218
219
220
        <b-pagination
          v-model="current.timeslotmeta.page"
          align="center"
          :total-rows="current.timeslotmeta.count"
          :per-page="current.timeslotmeta.perpage"
          @change="timeslotsPage"
        />
      </div>
221
222
223
224
225
226
227
228
229
      <!-- If the timeslot data is not loaded, we just show the spinner instead
      of the table itself -->
      <div v-else>
        <div style="text-align: center;">
          <img
            src="../assets/radio.gif"
            alt="loading data"
          ><br>
        </div>
230
231
      </div>

232
      <hr>
233

234
      <h2>Allgemeine Einstellungen zur Sendereihe:</h2>
235
      <show-metaSimpleTypes />
236
      <show-metaArrays />
237
238
239

      <b-row>
        <b-col lg="2">
240
241
242
243
244
245
246
          <b-badge style="width:80%;">
            Logo:
          </b-badge> <img
            src="../assets/16x16/emblem-system.png"
            alt="edit"
            @click="$refs.appModalShow.showLogo()"
          >
247
248
        </b-col>
        <b-col lg="4">
249
          <div v-if="current.logo.length === 0">
250
251
252
            <small><i>(none set)</i></small>
          </div>
          <div v-else>
253
254
255
256
257
258
259
            <br>
            <b-img
              thumbnail
              :src="current.logo"
              fluid
              @click="$refs.appModalShow.showLogo()"
            />
260
261
262
263
          </div>
        </b-col>

        <b-col lg="2">
264
265
266
267
268
269
270
          <b-badge style="width:80%;">
            Image:
          </b-badge> <img
            src="../assets/16x16/emblem-system.png"
            alt="edit"
            @click="$refs.appModalShow.showImage()"
          >
271
272
        </b-col>
        <b-col lg="4">
273
          <div v-if="current.image.length === 0">
274
275
276
            <small><i>(none set)</i></small>
          </div>
          <div v-else>
277
278
279
280
281
282
283
            <br>
            <b-img
              thumbnail
              :src="current.image"
              fluid
              @click="$refs.appModalShow.showImage()"
            />
284
285
286
287
          </div>
        </b-col>
      </b-row>

288
      <hr>
289
    </div>
290
  </b-container>
291
292
293
</template>

<script>
294
import showJumbotron from './ShowJumbotron.vue'
295
296
import showMetaSimpleTypes from './ShowMetaArrays.vue'
import showMetaArrays from './ShowMetaSimpleTypes.vue'
297
import modalNotes from './ShowManagerModalNotes.vue'
298
import modalShow from './ShowManagerModalShow.vue'
299
import modalSuperuser from './ShowManagerModalSuperuser.vue'
300
import modalPlaylist from './ShowManagerModalPlaylist.vue'
301
import timeslotSort from '../mixins/timeslotSort'
302
import prettyDate from '../mixins/prettyDate'
303
import axios from 'axios'
304
import DOMPurify from 'dompurify'
305
306

export default {
307
308
  // all modals to edit a show and its timeslots/notes, are importet as separate
  // components, to make it a tiny lickle bit less messy here
309
  components: {
310
    'app-modalNotes': modalNotes,
311
312
    'app-modalShow': modalShow,
    'app-modalSuperuser': modalSuperuser,
313
    'app-modalPlaylist': modalPlaylist,
314
    'show-jumbotron': showJumbotron,
315
    'show-metaArrays': showMetaArrays,
316
    'show-metaSimpleTypes': showMetaSimpleTypes,
317
  },
318
319

  // generic functions that we want to use from our mixins folder
320
  mixins: [ timeslotSort, prettyDate ],
321
322
323

  // this component will be handling a lot of data - probably the component can
  // be refactored to get rid of some redundancy here
324
325
  data () {
    return {
326
      shows: [],      // an array of objects describing our shows (empty at load, will be populated on created())
327
      numSlots: process.env.VUE_APP_TIMESLOT_FILTER_DEFAULT_NUMSLOTS, // all form input values are provided as strings
328
      dateStart: this.apiDate(new Date()),
329
      dateEnd: this.apiDate(new Date(new Date().getTime() + process.env.VUE_APP_TIMESLOT_FILTER_DEFAULT_DAYS * 86400000)),
330
331
332

      // the loaded object holds flags for the different things we will fetch
      // from the AuRa steering module
333
334
335
336
337
338
339
340
341
      loaded: {
        shows: false,
        timeslots: false,
        notes: false,
        categories: false,
        hosts: false,
        languages: false,
        topics: false,
        musicfocus: false,
342
        fundingcategory: false,
343
344
        type: false,
        owners: false,
345
        playlists: false,
346
      },
347
348
349

      // the current object is used to hold all the necessary data to describe
      // the show which is currently selected by the user in the frontend
350
351
352
353
354
355
      current: {
        categories: [],
        hosts: [],
        languages: [],
        topics: [],
        musicfocus: [],
356
        fundingcategory: [],
357
        type: [],
358
        playlists: [],
359
360
361
362
        timeslots: [],
        timeslotmeta: {    // meta info when pagination is used
          count: 0,
          next: null,
363
364
365
          previous: null,
          page: 1,   // page indexes start at 1 for <b-pagination> components
          perpage: 10
366
367
        },
        note: {},
368
369
        notes: [],
        image: '',
370
371
        logo: '',
        owners: [],
372
      },
373
374

      // this is used to configure the table with all the filtered timeslots
375
376
377
378
      notesTableArrayFields: [
        { key: 'title', label: 'Title of emission' },
        { key: 'starts', label: 'Emission start' },
        { key: 'duration', label: 'Duration' },
379
380
        { key: 'options', label: 'Edit' },
        { key: 'playlist', label: 'Playlist' },
381
      ]
382
383
    }
  },
384

jackie / Andrea Ida Malkah Klaura's avatar
jackie / Andrea Ida Malkah Klaura committed
385
  // Some of the info we need in the template are not easily and directly
386
  // retrievable, so we are computing them on the fly, when they are needed
387
  computed: {
388

389
390
391
    currentShow () { return this.$store.state.shows.selected.index },
    currentShowID () { return this.$store.state.shows.selected.id },

392
393
394
395
    // As the show description should allow to be html-formatted, we have to
    // make sure no malicous code can be inserted into the DOM. For that the
    // DOMPurify library (https://github.com/cure53/DOMPurify) does us a much
    // better service than trying to sanitize it with some RegExp.
396
397
398
    sanitizedShowDescription: function () {
      return DOMPurify.sanitize(this.shows[this.currentShow].description)
    },
399
400
401

    // As we do not have a single object which holds all info we need to display
    // in the table with our timeslots, we use this computed array to do that
402
    notesTableArray: function () {
403
      let rows = []
404
      for (var i in this.current.timeslots) {
405
        let note = this.getNoteByTimeslotID(this.current.timeslots[i].id)
406
        if (note !== null) { note = note.title }
407
408
409
410

        let playlistTitle = ''
        if (this.current.timeslots[i].playlist_id !== null) {
          let playlist = this.current.playlists.find(list => list.id === this.current.timeslots[i].playlist_id)
411
412
413
414
415
416
          if (playlist) {
            if (playlist.description.length > 0) {
              playlistTitle = playlist.description
            } else {
              playlistTitle = playlist.id
            }
417
418
419
420
          }
        }

        rows.push({
421
422
          title: note,
          starts: this.prettyDateTime(this.current.timeslots[i].start),
423
          duration: this.prettyDuration(this.current.timeslots[i].start, this.current.timeslots[i].end),
424
425
          options: {
            id: this.current.timeslots[i].id,
426
427
428
429
            schedule: this.current.timeslots[i].schedule,
            play: this.current.timeslots[i].playlist_id !== null
          },
          playlist: playlistTitle
430
431
        })
      }
432
      return rows
433
434
    }
  },
435
436
437

  // Right after this component is set up, we want to fetch all available shows
  // from the AuRa steering module.
438
  created () {
439
440
441
    // As we don't know any shows yet, we use null as id parameter, so the
    // first show in the returned show array will be displayed
    this.loadAndSwitch(null)
442
  },
443
444
445

  // Now for our hotchpotch of methods, mostly used for fetching data from the
  // AuRa steering API (updateing will be done in the imported modal components)
446
  methods: {
447
448

    // Apply the newly set filter parameters for our timeslot table
449
450
451
452
    applyFilter: function () {
      this.current.timeslotmeta.page = 1
      this.getTimeslots(this.dateStart, this.dateEnd, this.numSlots)
    },
453
454

    // Reset the filter parameters for our timeslot table to config defaults
455
    resetFilter: function () {
456
      this.numSlots = process.env.VUE_APP_TIMESLOT_FILTER_DEFAULT_NUMSLOTS
457
      this.dateStart = this.apiDate(new Date())
458
      this.dateEnd = this.apiDate(new Date(new Date().getTime() + process.env.VUE_APP_TIMESLOT_FILTER_DEFAULT_DAYS * 86400000))
459
460
461
      this.current.timeslotmeta.page = 1
      this.getTimeslots(this.dateStart, this.dateEnd, this.numSlots)
    },
462
463

    // Load a different page of timeslots for the timeslots table
464
465
466
467
468
469
    timeslotsPage: function (page) {
      if (this.current.timeslotmeta.page !== page) {
        this.current.timeslotmeta.page = page
        this.getTimeslots(this.dateStart, this.dateEnd, this.numSlots, (page - 1) * this.numSlots)
      }
    },
470
471
472
473
474
475
476
477

    // Every time the user switches to another show, we will load all related
    // data (as e.g. categories, hosts, etc.) from the AuRa steering API, in case
    // something changed.
    // TODO/discuss: maybe this is too inefficient and we could only load this
    // data just in the beginning after shows are loaded and assume they do not
    // change throughout a session or put it into the user's responsibility to
    // reload the show manager page after relevant changes.
478
    switchShow: function (index) {
479
480
481
      // if we already had some show loaded with timeslots and notes, set these to
      // not loaded, so we don't display old timeslots and notes while already
      // the new show is displayed and new timeslots and notes are still loading
482
483
484
485
486
487
488
489
      this.loaded.timeslots = false
      this.loaded.notes = false
      // also for those settings of the show which are only ids or arrays of ids
      // we have to fetch the corresponding names first
      this.loaded.categories = false
      this.loaded.hosts = false
      this.loaded.languages = false
      this.loaded.musicfocus = false
490
      this.loaded.fundingcategory = false
491
      this.loaded.owners = false
492
      // set the current show and its ID to whatever we want to switch to now
493
      this.$store.commit('shows/switchShow', index)
494
495
      // and check if images are available and set image strings, because we
      // cannot use them directly inside the b-img if they are null
496
497
498
499
      if (this.shows[this.currentShow].logo === null) { this.current.logo = '' }
      else { this.current.logo = this.shows[this.currentShow].logo }
      if (this.shows[this.currentShow].image === null) { this.current.image = '' }
      else { this.current.image = this.shows[this.currentShow].image }
500
501
502
503
504
505
      // before we load timeslots and notes, we want to fetch the general settings first
      this.getCategories()
      this.getHosts()
      this.getLanguages()
      this.getTopics()
      this.getMusicfocus()
506
      this.getFundingCategory()
507
      this.getType()
508
509
510
      if (this.$parent.user.steeringUser.is_superuser) {
        this.getOwners()
      }
511
      this.getTimeslots(this.dateStart, this.dateEnd, this.numSlots)
512
      this.loadPlaylists()
513
    },
514

515
516
517
    // (Re)Load all shows from server and switch to a show with a specific ID.
    // If the id argument is null, the first show in the show array will be used
    loadAndSwitch: function (id) {
518
      this.$store.dispatch('shows/fetchShows')
519
520
521
522
523
524
525
      this.$store.dispatch('shows/fetchMetaArray', {property: 'types', onlyActive: true})
      this.$store.dispatch('shows/fetchMetaArray', {property: 'fundingcategories', onlyActive: true})
      this.$store.dispatch('shows/fetchMetaArray', {property: 'categories'})
      this.$store.dispatch('shows/fetchMetaArray', {property: 'topics'})
      this.$store.dispatch('shows/fetchMetaArray', {property: 'musicfocus'})
      this.$store.dispatch('shows/fetchMetaArray', {property: 'languages'})
      this.$store.dispatch('shows/fetchMetaArray', {property: 'hosts'})
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
      this.loaded.shows = false
      var uri = process.env.VUE_APP_API_STEERING_SHOWS
      // normal users should only see their own shows, only superusers see all shows
      if (!this.$parent.user.steeringUser.is_superuser) {
        uri += '?owner=' + this.$parent.user.steeringUser.id
      }
      axios.get(uri, {
        withCredentials: true,
        headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
      }).then(response => {
        // if now shows are found, we'll just print a short info message and leave
        if (response.data.length === 0) {
          this.$log.info('The returned show set has 0 length:')
          this.$log.info(response)
          alert('Info: There are no shows connected to your account. See console for details.')
          this.loaded.shows = true
          return
        }
        // now set the new show array and find the index of the show ID
        this.shows = response.data
        let index = 0
        if (id !== null) {
          index = this.shows.findIndex(show => show.id === id)
          // if no show with the given ID was found, we use the first item in the show array
          if (index === -1) { index = 0 }
        }
552
        this.$store.commit('shows/switchShow', index)
553
554
555
556
557
558
559
560
561
        this.loaded.shows = true
        this.switchShow(this.currentShow)
      }).catch(error => {
        this.$log.error(error.response.status + ' ' + error.response.statusText)
        this.$log.error(error.response)
        alert('Error: could not fetch show data. See console for details.')
      })
    },

562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
    loadPlaylists () {
      this.loaded.playlists = false
      let uri = process.env.VUE_APP_API_TANK + 'shows/' + this.shows[this.currentShow].slug + '/playlists'
      axios.get(uri, {
        withCredentials: true,
        headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
      }).then(response => {
        // we don't have to check separately, if there are playlists, because tank
        // always provides an empty array if there are no playlists (or even if there is no corresponding show)
        this.current.playlists = response.data.results
        this.loaded.playlists = true
      }).catch(error => {
        //this.$log.error(error.response.status + ' ' + error.response.statusText)
        //this.$log.error(error.response)
        this.$log.error(error)
        alert('Error: could not fetch playlists from tank. See console for details.')
      })
    },

581
    // Fetch timeslots for the current show and use filter variables if provided
582
    getTimeslots: function (start, end, limit, offset) {
583
      var dateRegex = new RegExp('^\\d{4}-\\d{2}-\\d{2}$')
584
      var uri = process.env.VUE_APP_API_STEERING_SHOWS + this.currentShowID + '/timeslots/?'
585
586
587
588
      if (dateRegex.test(start)) { uri += 'start=' + start + '&' }
      if (dateRegex.test(end)) { uri += 'end=' + end + '&' }
      if (!isNaN(parseInt(limit))) { uri += 'limit=' + parseInt(limit) + '&' }
      if (!isNaN(parseInt(offset))) { uri += 'offset=' + parseInt(offset) }
589
590
      this.loaded.timeslots = false
      this.loaded.notes = false
591
592
593
594
      axios.get(uri, {
        withCredentials: true,
        headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
      }).then(response => {
595
596
        // if we use the limit argument results are paginated and look different
        // than without the limit argument
597
        if (!isNaN(parseInt(limit))) {
598
599
600
601
          this.current.timeslots = response.data.results
          this.current.timeslotmeta.count = response.data.count
          this.current.timeslotmeta.next = response.data.next
          this.current.timeslotmeta.previous = response.data.previous
602
          this.current.timeslotmeta.perpage = parseInt(limit)
603
        } else {
604
605
606
607
          this.current.timeslots = response.data
          this.current.timeslotmeta.count = response.data.length
          this.current.timeslotmeta.next = null
          this.current.timeslotmeta.previous = null
608
          this.current.timeslotmeta.perpage = response.data.length
609
        }
610
        this.loaded.timeslots = true
611
        // now that we have the timeslots we can fetch notes for all those timeslots
612
        uri = process.env.VUE_APP_API_STEERING_SHOWS + this.currentShowID + '/notes/?ids='
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
        // add all note IDs from the timeslots that have existing notes
        var thereIsANote = false
        for (var i in this.current.timeslots) {
          if (typeof this.current.timeslots[i].note_id === 'number') {
            uri += this.current.timeslots[i].note_id + ','
            thereIsANote = true
          }
        }
        // now remove trailing ',' if at least one note already exists
        // and make the api call to fetch them
        if (thereIsANote) {
          uri = uri.slice(0, -1)
          axios.get(uri, {
            withCredentials: true,
            headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
          }).then(response => {
            this.current.notes = response.data
            this.loaded.notes = true
          }).catch(error => {
            alert('There was an error fetching notes from the server' + error)
          })
          // done fetching notes
        } else {
          // if no notes exist that correspond to our selected timeslots, empty
          // the corresponding array
          this.current.notes = []
        }
640
      }).catch(error => {
641
642
643
        this.$log.error(error.response.status + ' ' + error.response.statusText)
        this.$log.error(error.response)
        alert('Error: could not fetch timeslots. See console for details.')
644
      })
645
646
      // done fetching timeslots
    },
647
648

    // Open the modal to edit a timeslot's note, given its ID and schedule ID
649
650
    editTimeslotNote: function (timeslotID, scheduleID) {
      this.current.note = null
651
652
653
      for (var i in this.current.notes) {
        if (this.current.notes[i].timeslot === timeslotID) {
          this.current.note = this.current.notes[i]
654
655
656
          break
        }
      }
657
      this.$refs.appModalNotes.showModal(this.current.note, timeslotID, scheduleID)
658
    },
659

660
    editTimeslotPlaylist: function (show, schedule, timeslot) {
661
      this.$refs.appModalPlaylist.open(show, schedule, timeslot, this.current.playlists)
662
663
    },

664
    // For a given timeslot ID return the corresponding note, if there is one
665
666
667
668
    getNoteByTimeslotID: function (timeslotID) {
      for (var i in this.current.notes) {
        if (this.current.notes[i].timeslot === timeslotID && this.current.notes[i].title !== undefined) {
          return this.current.notes[i]
669
670
671
672
        }
      }
      return null
    },
673
674

    // For a given timeslot ID, check if there is a note and return it
675
    prettyTimeslotNote: function (timeslotID) {
676
      var note = this.getTimeslotNoteTitle(timeslotID)
677
678
679
680
681
682
      if (note !== null) {
        return this.prettyTitle(note)
      } else {
        return ''
      }
    },
683
684

    // Limiting display of strings up to 25 characters plus "..."
685
    prettyTitle: function (title) {
686
687
688
      if (title === '') { return '...' }
      else if (title.length > 25) { return title.slice(0, 25) + '...' }
      else { return title }
689
    },
690
691
692
693

    // TODO: all those getSomething functions could be probably merged into one
    // generic getItem function. Maybe this.current should be implemented
    // in a dictionary kind of way then?
694
695
696
697
698
699
700
    getCategories: function () {
      this.current.categories = []
      var loadingError = false
      if (this.shows[this.currentShow].category.length === 0) {
        this.loaded.categories = true
      } else {
        for (var i in this.shows[this.currentShow].category) {
701
          axios.get(process.env.VUE_APP_API_STEERING + 'categories/' + this.shows[this.currentShow].category[i] + '/', {
702
703
704
            withCredentials: true,
            headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
          }).then(response => {
705
706
707
708
709
710
            this.current.categories.push(response.data)
          }).catch(error => {
            loadingError = true
            alert('There was an error fetching categories from the server: ' + error)
          })
        }
711
        if (!loadingError) { this.loaded.categories = true }
712
713
      }
    },
714

715
716
717
718
719
720
721
    getHosts: function () {
      this.current.hosts = []
      var loadingError = false
      if (this.shows[this.currentShow].hosts.length === 0) {
        this.loaded.hosts = true
      } else {
        for (var i in this.shows[this.currentShow].hosts) {
722
          axios.get(process.env.VUE_APP_API_STEERING + 'hosts/' + this.shows[this.currentShow].hosts[i] + '/', {
723
724
725
            withCredentials: true,
            headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
          }).then(response => {
726
727
728
729
730
731
            this.current.hosts.push(response.data)
          }).catch(error => {
            loadingError = true
            alert('There was an error fetching hosts from the server: ' + error)
          })
        }
732
        if (!loadingError) { this.loaded.hosts = true }
733
734
      }
    },
735

736
737
738
739
    getLanguages: function () {
      this.current.languages = []
      var loadingError = false
      if (this.shows[this.currentShow].language.length === 0) {
740
        this.loaded.languages = true
741
742
      } else {
        for (var i in this.shows[this.currentShow].language) {
743
          axios.get(process.env.VUE_APP_API_STEERING + 'languages/' + this.shows[this.currentShow].language[i] + '/', {
744
745
746
            withCredentials: true,
            headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
          }).then(response => {
747
748
749
750
751
752
            this.current.languages.push(response.data)
          }).catch(error => {
            loadingError = true
            alert('There was an error fetching languages from the server: ' + error)
          })
        }
753
        if (!loadingError) { this.loaded.languages = true }
754
755
      }
    },
756

757
758
759
760
761
762
763
    getTopics: function () {
      this.current.topics = []
      var loadingError = false
      if (this.shows[this.currentShow].topic.length === 0) {
        this.loaded.topics = true
      } else {
        for (var i in this.shows[this.currentShow].topic) {
764
          axios.get(process.env.VUE_APP_API_STEERING + 'topics/' + this.shows[this.currentShow].topic[i] + '/', {
765
766
767
            withCredentials: true,
            headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
          }).then(response => {
768
769
770
771
772
773
            this.current.topics.push(response.data)
          }).catch(error => {
            loadingError = true
            alert('There was an error fetching topics from the server: ' + error)
          })
        }
774
        if (!loadingError) { this.loaded.topics = true }
775
776
      }
    },
777

778
779
780
781
782
783
784
    getMusicfocus: function () {
      this.current.musicfocus = []
      var loadingError = false
      if (this.shows[this.currentShow].musicfocus.length === 0) {
        this.loaded.musicfocus = true
      } else {
        for (var i in this.shows[this.currentShow].musicfocus) {
785
          axios.get(process.env.VUE_APP_API_STEERING + 'musicfocus/' + this.shows[this.currentShow].musicfocus[i] + '/', {
786
787
788
            withCredentials: true,
            headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
          }).then(response => {
789
790
791
792
793
794
795
            this.current.musicfocus.push(response.data)
          }).catch(error => {
            loadingError = true
            alert('There was an error fetching musicfocus from the server: ' + error)
          })
        }
      }
796
      if (!loadingError) { this.loaded.musicfocus = true }
797
    },
798

799
    getFundingCategory: function () {
800
      this.current.fundingcategory = []
801
      var loadingError = false
802
803
      if (typeof this.shows[this.currentShow].fundingcategory !== 'number') {
        this.loaded.fundingcategory = true
804
      } else {
805
        axios.get(process.env.VUE_APP_API_STEERING + 'fundingcategories/' + this.shows[this.currentShow].fundingcategory + '/', {
806
807
808
          withCredentials: true,
          headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
        }).then(response => {
809
          this.current.fundingcategory.push(response.data)
810
811
        }).catch(error => {
          loadingError = true
812
          alert('There was an error fetching funding category from the server: ' + error)
813
814
        })
      }
815
      if (!loadingError) { this.loaded.fundingcategory = true }
816
    },
817

818
819
820
821
822
823
    getType: function () {
      this.current.type = []
      var loadingError = false
      if (typeof this.shows[this.currentShow].type !== 'number') {
        this.loaded.type = true
      } else {
824
        axios.get(process.env.VUE_APP_API_STEERING + 'types/' + this.shows[this.currentShow].type + '/', {
825
826
827
          withCredentials: true,
          headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
        }).then(response => {
828
829
830
831
832
          this.current.type.push(response.data)
        }).catch(error => {
          loadingError = true
          alert('There was an error fetching show type from the server: ' + error)
        })
833
      }
834
      if (!loadingError) { this.loaded.type = true }
835
    },
836

837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
    getOwners: function () {
      this.current.owners = []
      let loadingError = false
      if (this.shows[this.currentShow].owners.length === 0) {
        this.loaded.owners = true
      } else {
        for (let i in this.shows[this.currentShow].owners) {
          axios.get(process.env.VUE_APP_API_STEERING + 'users/' + this.shows[this.currentShow].owners[i] + '/', {
            withCredentials: true,
            headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
          }).then(response => {
            this.current.owners.push(response.data)
          }).catch(error => {
            loadingError = true
            this.$log.error(error.response.status + ' ' + error.response.statusText)
            this.$log.error(error.response)
            alert('Error: could not load owner data. See console for details.')
          })
        }
        if (!loadingError) { this.loaded.owners = true }
      }
    },

860
861
    // Just a placeholder function we can use in the UI, to signal if something
    // is not yet implemented
862
    notYetImplemented: function () {
863
      alert('By the mighty witchcraftry of the mother of time!\n\nThis feature is not implemented yet.')
864
865
866
867
868
    }
  }
}
</script>

869
<style>
870
871
872
873
874
875
876
877
878
span.timeslotEditLink {
  color: #8d5f82 !important;
  font-weight: bold;
  font-size: 0.8em;
  padding: 0.25em;
}
span.timeslotEditLink:hover {
  color: #ad7fa8 !important;
}
879
</style>