diff --git a/amelie/api/narrowcasting.py b/amelie/api/narrowcasting.py index e740bce..c6fcaf1 100644 --- a/amelie/api/narrowcasting.py +++ b/amelie/api/narrowcasting.py @@ -9,7 +9,7 @@ from amelie.api.activitystream_utils import add_images_property, add_thumbnails_property from amelie.api.decorators import authentication_optional from amelie.api.utils import parse_datetime_parameter -from amelie.companies.models import TelevisionBanner +from amelie.companies.models import TelevisionBanner, TelevisionVideo from amelie.news.models import NewsItem from amelie.narrowcasting.models import TelevisionPromotion from amelie.room_duty.models import RoomDuty @@ -20,13 +20,23 @@ def get_narrowcasting_banners(request): result = [] banners = TelevisionBanner.objects.filter(start_date__lte=timezone.now(), end_date__gte=timezone.now(), active=True) + videos = TelevisionVideo.objects.filter(start_date__lte=timezone.now(), end_date__gte=timezone.now(), active=True) for banner in banners: result.append({ "name": banner.name, "image": "%s%s" % (settings.MEDIA_URL, str(banner.picture)), + "type": "image", "id": banner.id }) + + for video in videos: + result.append({ + "name": video.name, + "videoId": video.video_id, + "type": video.video_type, + "id": video.id + }) return result @@ -54,13 +64,13 @@ def get_television_promotions(request): result = [] promotions = TelevisionPromotion.objects.filter(start__lte=timezone.now(), end__gte=timezone.now()) - for promotion in promotions: + for promotion in promotions: res_dict = { "image": "%s%s" % (settings.MEDIA_URL, str(promotion.attachment)) } if promotion.activity: - res_dict["title"] = promotion.activity.description + res_dict["title"] = promotion.activity.summary if promotion.activity.enrollment: res_dict["signup"] = promotion.activity.enrollment res_dict["signupStart"] = promotion.activity.enrollment_begin.isoformat() diff --git a/amelie/companies/admin.py b/amelie/companies/admin.py index 701da7e..5555bf5 100644 --- a/amelie/companies/admin.py +++ b/amelie/companies/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from amelie.companies.models import Company, TelevisionBanner, WebsiteBanner +from amelie.companies.models import Company, TelevisionBanner, WebsiteBanner, TelevisionVideo class CompanyAdmin(admin.ModelAdmin): @@ -14,8 +14,12 @@ class WebsiteBannerAdmin(admin.ModelAdmin): class TelevisionBannerAdmin(admin.ModelAdmin): list_display = ['name', 'start_date', 'end_date', 'active'] + +class TelevisionVideoAdmin(admin.ModelAdmin): + list_display = ['name', 'start_date', 'end_date', 'active'] admin.site.register(Company, CompanyAdmin) admin.site.register(WebsiteBanner, WebsiteBannerAdmin) admin.site.register(TelevisionBanner, TelevisionBannerAdmin) +admin.site.register(TelevisionVideo, TelevisionVideoAdmin) diff --git a/amelie/companies/migrations/0006_televisionvideo.py b/amelie/companies/migrations/0006_televisionvideo.py new file mode 100644 index 0000000..5907f5b --- /dev/null +++ b/amelie/companies/migrations/0006_televisionvideo.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.16 on 2023-02-06 21:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('companies', '0005_add_default_career_label'), + ] + + operations = [ + migrations.CreateModel( + name='TelevisionVideo', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('start_date', models.DateField()), + ('end_date', models.DateField()), + ('active', models.BooleanField(default=True)), + ('video_id', models.CharField(max_length=12)), + ], + options={ + 'verbose_name': 'Television Promotion Video', + 'verbose_name_plural': 'Television Promotion Videos', + 'ordering': ['-start_date'], + }, + ), + ] diff --git a/amelie/companies/migrations/0007_televisionvideo_video_type.py b/amelie/companies/migrations/0007_televisionvideo_video_type.py new file mode 100644 index 0000000..7bcb7a0 --- /dev/null +++ b/amelie/companies/migrations/0007_televisionvideo_video_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2023-02-06 22:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('companies', '0006_televisionvideo'), + ] + + operations = [ + migrations.AddField( + model_name='televisionvideo', + name='video_type', + field=models.CharField(choices=[('youtube', 'YouTube'), ('streamingia', 'StreamingIA')], default='streamingia', max_length=11), + ), + ] diff --git a/amelie/companies/models.py b/amelie/companies/models.py index 4723251..76c798f 100644 --- a/amelie/companies/models.py +++ b/amelie/companies/models.py @@ -9,6 +9,7 @@ from amelie.companies.managers import CompanyManager from amelie.calendar.managers import EventManager from amelie.calendar.models import Event +from amelie.activities.models import ActivityLabel from amelie.tools.discord import send_discord class Company(models.Model): @@ -175,9 +176,31 @@ class Meta: ordering = ['-end_date'] verbose_name = _('Television banner') verbose_name_plural = _('Television banners') + + +class TelevisionVideo(models.Model): + class VideoTypes(models.TextChoices): + YOUTUBE = 'youtube', 'YouTube' + STREAMINGIA = 'streamingia', 'StreamingIA' + + name = models.CharField(max_length=100) + start_date = models.DateField() + end_date = models.DateField() + active = models.BooleanField(default=True) + video_id = models.CharField(max_length=12) + video_type = models.CharField( + max_length=11, + choices=VideoTypes.choices, + default=VideoTypes.STREAMINGIA) + + class Meta: + ordering = ['-start_date'] + verbose_name = _('Television Promotion Video') + verbose_name_plural = _('Television Promotion Videos') class CompanyEvent(Event): + objects = EventManager() company = models.ForeignKey('Company', blank=True, null=True, on_delete=models.SET_NULL) company_text = models.CharField(max_length=100, blank=True) @@ -186,6 +209,10 @@ class CompanyEvent(Event): visible_from = models.DateTimeField() visible_till = models.DateTimeField() + @property + def activity_label(self): + return ActivityLabel.objects.filter(name_en="Career").first() + @property def activity_type(self): return "external" diff --git a/amelie/narrowcasting/static/css/narrowcasting.css b/amelie/narrowcasting/static/css/narrowcasting.css index f0cadf6..920b28c 100644 --- a/amelie/narrowcasting/static/css/narrowcasting.css +++ b/amelie/narrowcasting/static/css/narrowcasting.css @@ -132,8 +132,7 @@ body { } .footer { - height: 137px; - padding: 15px 793px 15px 30px; + padding: 7.5px 793px 7.5px 20px; box-shadow: -25px 25px 215px 25px #222222; } @@ -143,7 +142,6 @@ body { .news-header { font-size:24px; - margin-bottom:5px; } .news-header span { diff --git a/amelie/narrowcasting/static/js/promotion_widget.js b/amelie/narrowcasting/static/js/promotion_widget.js index daecebb..9421e6f 100644 --- a/amelie/narrowcasting/static/js/promotion_widget.js +++ b/amelie/narrowcasting/static/js/promotion_widget.js @@ -3,6 +3,7 @@ function PromotionWidget() { this.television_promotions = []; this.current_promotion = null; + this.current_idx = 0; this.ticks = 0; this.duration = 0; } @@ -36,12 +37,15 @@ PromotionWidget.prototype.tick = function () { PromotionWidget.prototype.change_photo = function () { if (this.television_promotions.length > 0){ - var res = this.television_promotions.indexOf(this.current_promotion); - if (res >= 0) { - this.current_promotion = this.television_promotions[(res + 1) % (this.television_promotions.length)]; + if (this.current_idx >= 0) { + var idx = (this.current_idx + 1) % this.television_promotions.length; + + this.current_promotion = this.television_promotions[idx]; + this.current_idx = idx; } else if (this.television_promotions.length > 0) { this.current_promotion = this.television_promotions[0]; + this.current_idx = 0; } var photo_url = this.current_promotion.image; @@ -54,12 +58,16 @@ PromotionWidget.prototype.change_photo = function () { }).load(function(){ $(".photo-wrapper #photo").remove(); $(".photo-wrapper").append($(this)); - if(self.current_promotion.title != undefined){ - $("#photo-activity").html(self.current_promotion.title); - } else { - // This is a hack, hiding and showing does not yet work correctly - $("#photo-activity").html("Promotion"); - } + + // if(self.current_promotion.title != undefined){ + // $("#photo-activity").html(self.current_promotion.title); + // } else { + // // This is a hack, hiding and showing does not yet work correctly + // $("#photo-activity").html("Promotion"); + // } + + $("#photo-activity").hide(); + if($(this).height() > $(this).width()){ $("#photo").addClass("portrait"); } @@ -71,4 +79,5 @@ PromotionWidget.prototype.change_photo = function () { } }; -PromotionWidget.prototype.get_duration = function () { return this.duration; }; +// PromotionWidget.prototype.get_duration = function () { return this.duration; }; +PromotionWidget.prototype.get_duration = function () { return 5; }; diff --git a/amelie/narrowcasting/static/js/tv_narrowcasting.js b/amelie/narrowcasting/static/js/tv_narrowcasting.js new file mode 100644 index 0000000..1063287 --- /dev/null +++ b/amelie/narrowcasting/static/js/tv_narrowcasting.js @@ -0,0 +1,245 @@ +// Constants +const SECOND = 1000 +const MINUTE = 60 * SECOND +const SWAP_TIME = 20 * SECOND + +// Elements +let date +let time +let activityTable +let companyAd +let news +let photo + +// State +let companyBanners = [] +let selectedCompanyBanner = undefined +let activityPhotos = [] +let selectedActivity = undefined + +window.addEventListener('load', async () => { + // Initializes the screen + date = document.querySelector('#date') + time = document.querySelector('#time') + activityTable = document.querySelector('#activity-table') + adContainer = document.querySelector('.ads') + news = document.querySelector('.footer') + photo = document.querySelector('#photo') + photoName = document.querySelector('#photo-activity') + + // Update state + await update(updateBannerState, 5*MINUTE) + await update(updateActivityState, 5*MINUTE) + await update(updateNewsState, 5*MINUTE) + await update(updatePhotoState, 5*MINUTE) + + // Update UI + update(updateDateTimeUI, SECOND) + update(updateBannerUI, 20*SECOND) + update(updatePhotoUI, 20*SECOND) +}) + +/* + Helper functions +*/ + +const update = async (callback, interval) => { + await callback() + setInterval(async () => await callback(), interval) +} + +const getYouTubeElement = (videoId) => { + return `` +} + +const getStreamingElement = (videoId) => { + return `` +} + +const getImgElement = (imageId) => { + return `` +} + +/** + * Do an JSON-RPC request to the backend + * @param {string} endpoint + * @param {[any]} params + */ +const req = async (endpoint, params) => { + const body = { + jsonrpc: '2.0', + id: 1, + method: endpoint, + params + } + + return fetch('/api/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }) + .then(res => res.json()) + .then(res => { + if (res.error) { + throw new Error(res.error.message) + } + + return res.result + }) +} + +/* + Update state +*/ +const updateBannerState = async () => { + await req('getBanners', []) + .then(banners => { + console.log(banners) + companyBanners = banners + }) + .catch(console.error) +} + +const updateActivityState = async () => { + await req('getUpcomingActivities', [4, true]) + .then(updateActivityUI) + .catch(console.error) +} + +const updateNewsState = async () => { + await req('getNews', [2, true]) + .then(updateNewsUI) + .catch(console.error) +} + +const updatePhotoState = async () => { + await req('getLatestActivitiesWithPictures', [10]) + .then(activities => activityPhotos = activities) + .catch(console.error) +} + +/* + Update UI elements +*/ +const updateDateTimeUI = () => { + const currentDate = new Date() + + date.textContent = currentDate.toLocaleDateString('en-GB', { + weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' + }) + time.textContent = currentDate.toLocaleTimeString('en-GB', { + hour: '2-digit', minute: '2-digit', second: '2-digit' + }) +} + + +const updateActivityUI = (activities) => { + if (activities.length > 0) { + // First remove all old activities + activityTable.textContent = '' + + const options = { + weekday: "short", month: "short", + day: "numeric", hour: "2-digit", minute: "2-digit" + } + + const rows = [] + + // Now generate the new table + activities.forEach(activity => { + const date = new Date(activity['beginDate']).toLocaleString('en-GB', options) + + const elem = ` +