ShowManager.vue 32.4 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
          @click="$refs.addShowModal.openModal()"
15
16
17
18
        >
          +
        </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
      <addShowModal
        ref="addShowModal"
66
      />
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
      <show-metaOwners />
238
239
240

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

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

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

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

export default {
309
310
  // 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
311
  components: {
312
    'app-modalNotes': modalNotes,
313
    'app-modalShow': modalShow,
314
    'app-modalPlaylist': modalPlaylist,
315
    'addShowModal': modalAddShow,
316
    'show-jumbotron': showJumbotron,
317
    'show-metaArrays': showMetaArrays,
318
    'show-metaSimpleTypes': showMetaSimpleTypes,
319
    'show-metaOwners': showMetaOwners,
320
  },
321
322

  // generic functions that we want to use from our mixins folder
323
  mixins: [ timeslotSort, prettyDate ],
324
325
326

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

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

      // 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
353
354
355
356
357
358
      current: {
        categories: [],
        hosts: [],
        languages: [],
        topics: [],
        musicfocus: [],
359
        fundingcategory: [],
360
        type: [],
361
        playlists: [],
362
363
364
365
        timeslots: [],
        timeslotmeta: {    // meta info when pagination is used
          count: 0,
          next: null,
366
367
368
          previous: null,
          page: 1,   // page indexes start at 1 for <b-pagination> components
          perpage: 10
369
370
        },
        note: {},
371
372
        notes: [],
        image: '',
373
374
        logo: '',
        owners: [],
375
      },
376
377

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

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

392
393
    currentShow () { return this.$store.state.shows.selected.index },
    currentShowID () { return this.$store.state.shows.selected.id },
394
    isSuperuser () { return this.$store.state.auth.user.steeringUser.is_superuser },
395

396
397
398
399
    // 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.
400
401
402
    sanitizedShowDescription: function () {
      return DOMPurify.sanitize(this.shows[this.currentShow].description)
    },
403
404
405

    // 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
406
    notesTableArray: function () {
407
      let rows = []
408
      for (var i in this.current.timeslots) {
409
        let note = this.getNoteByTimeslotID(this.current.timeslots[i].id)
410
        if (note !== null) { note = note.title }
411
412
413
414

        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)
415
416
417
418
419
420
          if (playlist) {
            if (playlist.description.length > 0) {
              playlistTitle = playlist.description
            } else {
              playlistTitle = playlist.id
            }
421
422
423
424
          }
        }

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

  // Right after this component is set up, we want to fetch all available shows
  // from the AuRa steering module.
442
  created () {
443
444
445
    // 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)
446
  },
447
448
449

  // 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)
450
  methods: {
451
452

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

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

    // Load a different page of timeslots for the timeslots table
468
469
470
471
472
473
    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)
      }
    },
474
475
476
477
478
479
480
481

    // 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.
482
    switchShow: function (index) {
483
484
485
      // 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
486
487
488
489
490
491
492
493
      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
494
      this.loaded.fundingcategory = false
495
      this.loaded.owners = false
496
      // set the current show and its ID to whatever we want to switch to now
497
      this.$store.commit('shows/switchShow', index)
498
499
      // and check if images are available and set image strings, because we
      // cannot use them directly inside the b-img if they are null
500
501
502
503
      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 }
504
505
506
507
508
509
      // before we load timeslots and notes, we want to fetch the general settings first
      this.getCategories()
      this.getHosts()
      this.getLanguages()
      this.getTopics()
      this.getMusicfocus()
510
      this.getFundingCategory()
511
      this.getType()
512
513
514
      if (this.$parent.user.steeringUser.is_superuser) {
        this.getOwners()
      }
515
      this.getTimeslots(this.dateStart, this.dateEnd, this.numSlots)
516
      this.loadPlaylists()
517
    },
518

519
520
521
    // (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) {
522
      this.$store.dispatch('shows/fetchShows')
523
524
525
526
527
528
529
      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'})
530
      if (this.isSuperuser) { this.$store.dispatch('auth/fetchUsers') }
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
      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 }
        }
557
        this.$store.commit('shows/switchShow', index)
558
559
560
561
562
563
564
565
566
        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.')
      })
    },

567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
    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.')
      })
    },

586
    // Fetch timeslots for the current show and use filter variables if provided
587
    getTimeslots: function (start, end, limit, offset) {
588
      var dateRegex = new RegExp('^\\d{4}-\\d{2}-\\d{2}$')
589
      var uri = process.env.VUE_APP_API_STEERING_SHOWS + this.currentShowID + '/timeslots/?'
590
591
592
593
      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) }
594
595
      this.loaded.timeslots = false
      this.loaded.notes = false
596
597
598
599
      axios.get(uri, {
        withCredentials: true,
        headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
      }).then(response => {
600
601
        // if we use the limit argument results are paginated and look different
        // than without the limit argument
602
        if (!isNaN(parseInt(limit))) {
603
604
605
606
          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
607
          this.current.timeslotmeta.perpage = parseInt(limit)
608
        } else {
609
610
611
612
          this.current.timeslots = response.data
          this.current.timeslotmeta.count = response.data.length
          this.current.timeslotmeta.next = null
          this.current.timeslotmeta.previous = null
613
          this.current.timeslotmeta.perpage = response.data.length
614
        }
615
        this.loaded.timeslots = true
616
        // now that we have the timeslots we can fetch notes for all those timeslots
617
        uri = process.env.VUE_APP_API_STEERING_SHOWS + this.currentShowID + '/notes/?ids='
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
        // 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 = []
        }
645
      }).catch(error => {
646
647
648
        this.$log.error(error.response.status + ' ' + error.response.statusText)
        this.$log.error(error.response)
        alert('Error: could not fetch timeslots. See console for details.')
649
      })
650
651
      // done fetching timeslots
    },
652
653

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

665
    editTimeslotPlaylist: function (show, schedule, timeslot) {
666
      this.$refs.appModalPlaylist.open(show, schedule, timeslot, this.current.playlists)
667
668
    },

669
    // For a given timeslot ID return the corresponding note, if there is one
670
671
672
673
    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]
674
675
676
677
        }
      }
      return null
    },
678
679

    // For a given timeslot ID, check if there is a note and return it
680
    prettyTimeslotNote: function (timeslotID) {
681
      var note = this.getTimeslotNoteTitle(timeslotID)
682
683
684
685
686
687
      if (note !== null) {
        return this.prettyTitle(note)
      } else {
        return ''
      }
    },
688
689

    // Limiting display of strings up to 25 characters plus "..."
690
    prettyTitle: function (title) {
691
692
693
      if (title === '') { return '...' }
      else if (title.length > 25) { return title.slice(0, 25) + '...' }
      else { return title }
694
    },
695
696
697
698

    // 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?
699
700
701
702
703
704
705
    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) {
706
          axios.get(process.env.VUE_APP_API_STEERING + 'categories/' + this.shows[this.currentShow].category[i] + '/', {
707
708
709
            withCredentials: true,
            headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
          }).then(response => {
710
711
712
713
714
715
            this.current.categories.push(response.data)
          }).catch(error => {
            loadingError = true
            alert('There was an error fetching categories from the server: ' + error)
          })
        }
716
        if (!loadingError) { this.loaded.categories = true }
717
718
      }
    },
719

720
721
722
723
724
725
726
    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) {
727
          axios.get(process.env.VUE_APP_API_STEERING + 'hosts/' + this.shows[this.currentShow].hosts[i] + '/', {
728
729
730
            withCredentials: true,
            headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
          }).then(response => {
731
732
733
734
735
736
            this.current.hosts.push(response.data)
          }).catch(error => {
            loadingError = true
            alert('There was an error fetching hosts from the server: ' + error)
          })
        }
737
        if (!loadingError) { this.loaded.hosts = true }
738
739
      }
    },
740

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

762
763
764
765
766
767
768
    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) {
769
          axios.get(process.env.VUE_APP_API_STEERING + 'topics/' + this.shows[this.currentShow].topic[i] + '/', {
770
771
772
            withCredentials: true,
            headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
          }).then(response => {
773
774
775
776
777
778
            this.current.topics.push(response.data)
          }).catch(error => {
            loadingError = true
            alert('There was an error fetching topics from the server: ' + error)
          })
        }
779
        if (!loadingError) { this.loaded.topics = true }
780
781
      }
    },
782

783
784
785
786
787
788
789
    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) {
790
          axios.get(process.env.VUE_APP_API_STEERING + 'musicfocus/' + this.shows[this.currentShow].musicfocus[i] + '/', {
791
792
793
            withCredentials: true,
            headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
          }).then(response => {
794
795
796
797
798
799
800
            this.current.musicfocus.push(response.data)
          }).catch(error => {
            loadingError = true
            alert('There was an error fetching musicfocus from the server: ' + error)
          })
        }
      }
801
      if (!loadingError) { this.loaded.musicfocus = true }
802
    },
803

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

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

842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
    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 }
      }
    },

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

874
<style>
875
876
877
878
879
880
881
882
883
span.timeslotEditLink {
  color: #8d5f82 !important;
  font-weight: bold;
  font-size: 0.8em;
  padding: 0.25em;
}
span.timeslotEditLink:hover {
  color: #ad7fa8 !important;
}
884
</style>