diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index 85aeab3b224888026c67c2ecd1c0f578f737b232..d33867331ab3277eea6262998032147cfad37d44 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -795,6 +795,10 @@ class WebsiteSlides(WebsiteProfile): if not can_upload: return {'error': _('You cannot upload on this channel.')} + if post.get('duration'): + # minutes to hours conversion + values['completion_time'] = int(post['duration']) / 60 + # handle creation of new categories on the fly if post.get('category_id'): if post['category_id'][0] == 0: diff --git a/addons/website_slides/data/slide_slide_demo.xml b/addons/website_slides/data/slide_slide_demo.xml index b35f2defc5f7dd68a91c1f8076fa5f9e7e4cee08..0a88c4a7498d035a7f5ee356a4eaa63f8a8ae680 100644 --- a/addons/website_slides/data/slide_slide_demo.xml +++ b/addons/website_slides/data/slide_slide_demo.xml @@ -445,7 +445,7 @@ <field name="is_published" eval="True"/> <field name="is_preview" eval="False"/> <field name="public_views">0</field> - <field name="completion_time">1</field> + <field name="completion_time">3</field> <field name="description">Learn of to identify quality wood in order to create solid furnitures.</field> </record> <record id="slide_slide_demo_5_2" model="slide.slide"> diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py index 2536817eedf0552e30a549f908b0d110161a5f81..a1b99fe90f710433db4948abfa3e140411ebb901 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -6,6 +6,7 @@ import datetime import io import re import requests +import PyPDF2 from PIL import Image from werkzeug import urls @@ -104,7 +105,7 @@ class Slide(models.Model): channel_id = fields.Many2one('slide.channel', string="Channel", required=True) tag_ids = fields.Many2many('slide.tag', 'rel_slide_tag', 'slide_id', 'tag_id', string='Tags') is_preview = fields.Boolean('Is Preview', default=False, help="The course is accessible by anyone : the users don't need to join the channel to access the content of the course.") - completion_time = fields.Float('Duration', default=1, digits=(10, 2)) + completion_time = fields.Float('Completion Time', digits=(10, 4), help="The estimated completion time for this slide") # Categories is_category = fields.Boolean('Is a category', default=False) category_id = fields.Many2one('slide.slide', string="Category", compute="_compute_category_id", store=True) @@ -305,6 +306,15 @@ class Slide(models.Model): for key, value in values.items(): self[key] = value + @api.onchange('datas') + def _on_change_datas(self): + """ For PDFs, we assume that it takes 5 minutes to read a page. """ + if self.datas: + data = base64.b64decode(self.datas) + if data.startswith(b'%PDF-'): + pdf = PyPDF2.PdfFileReader(io.BytesIO(data), overwriteWarnings=False) + self.completion_time = (5 * len(pdf.pages)) / 60 + @api.depends('name', 'channel_id.website_id.domain') def _compute_website_url(self): # TDE FIXME: clena this link.tracker strange stuff @@ -655,8 +665,11 @@ class Slide(models.Model): return {'error': _('Unknown document')} def _parse_youtube_document(self, document_id, only_preview_fields): + """ If we receive a duration (YT video), we use it to determine the slide duration. + The received duration is under a special format (e.g: PT1M21S15, meaning 1h 21m 15s). """ + key = self.env['website'].get_current_website().website_slide_google_app_key - fetch_res = self._fetch_data('https://www.googleapis.com/youtube/v3/videos', {'id': document_id, 'key': key, 'part': 'snippet', 'fields': 'items(id,snippet)'}, 'json') + fetch_res = self._fetch_data('https://www.googleapis.com/youtube/v3/videos', {'id': document_id, 'key': key, 'part': 'snippet,contentDetails', 'fields': 'items(id,snippet,contentDetails)'}, 'json') if fetch_res.get('error'): return fetch_res @@ -665,6 +678,14 @@ class Slide(models.Model): if not items: return {'error': _('Please enter valid Youtube or Google Doc URL')} youtube_values = items[0] + + youtube_duration = youtube_values.get('contentDetails', {}).get('duration') + if youtube_duration: + parsed_duration = re.search(r'^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$', youtube_duration) + values['completion_time'] = (int(parsed_duration.group(1) or 0)) + \ + (int(parsed_duration.group(2) or 0) / 60) + \ + (int(parsed_duration.group(3) or 0) / 3600) + if youtube_values.get('snippet'): snippet = youtube_values['snippet'] if only_preview_fields: @@ -673,7 +694,9 @@ class Slide(models.Model): 'title': snippet['title'], 'description': snippet['description'] }) + return values + values.update({ 'name': snippet['title'], 'image_1920': self._fetch_data(snippet['thumbnails']['high']['url'], {}, 'image')['values'], diff --git a/addons/website_slides/static/src/js/slides_upload.js b/addons/website_slides/static/src/js/slides_upload.js index 6b704c5d1c07ed5bdd21f8d119c5e59a525743e3..23e6411f51286eae2d3cb113e51a18f993c2c744 100644 --- a/addons/website_slides/static/src/js/slides_upload.js +++ b/addons/website_slides/static/src/js/slides_upload.js @@ -130,6 +130,7 @@ var SlideUploadDialog = Dialog.extend({ 'url': this._formGetFieldValue('url'), 'description': this._formGetFieldValue('description'), 'index_content': this._formGetFieldValue('index_content'), + 'duration': this._formGetFieldValue('duration'), 'is_published': forcePublished, }, this._getSelect2DropdownValues()); // add tags and category @@ -441,6 +442,7 @@ var SlideUploadDialog = Dialog.extend({ */ Util = PDFJS.Util; PDFJS.getDocument(new Uint8Array(buffer), null, passwordNeeded).then(function getPdf(pdf) { + self._formSetFieldValue('duration', (pdf.pdfInfo.numPages || 0) * 5); pdf.getPage(1).then(function getFirstPage(page) { var scale = 1; var viewport = page.getViewport(scale); @@ -508,6 +510,10 @@ var SlideUploadDialog = Dialog.extend({ if (data.error) { self._alertDisplay(data.error); } else { + if (data.completion_time) { + // hours to minutes conversion + self._formSetFieldValue('duration', Math.round(data.completion_time * 60)); + } self.$('#slide-image').attr('src', data.url_src); self._formSetFieldValue('name', data.title); self._formSetFieldValue('description', data.description); diff --git a/addons/website_slides/static/src/xml/website_slides_upload.xml b/addons/website_slides/static/src/xml/website_slides_upload.xml index 64668bfc15398ccc9d0c980f047047cd83dce1eb..e5d5e0d839f705b65e73dcfe3fcecbcbe79b6409 100644 --- a/addons/website_slides/static/src/xml/website_slides_upload.xml +++ b/addons/website_slides/static/src/xml/website_slides_upload.xml @@ -60,6 +60,12 @@ <input id="tag_ids" name="tag_ids" type="hidden"/> </div> </div> + <div class="form-group row"> + <label for="duration" class="col-form-label col-md-3">Completion Time (minutes)</label> + <div class="col-md-9"> + <input type="number" id="duration" name="duration" placeholder="Estimated slide completion time" class="form-control"/> + </div> + </div> </t> <!-- diff --git a/addons/website_slides/views/slide_channel_views.xml b/addons/website_slides/views/slide_channel_views.xml index e877311de22f134281d23d77bfed1f6a062694f1..3a7dd71c9643dbbe393d5b2610d6012a27bb2095 100644 --- a/addons/website_slides/views/slide_channel_views.xml +++ b/addons/website_slides/views/slide_channel_views.xml @@ -257,7 +257,7 @@ <field name="total_views"/> </div> <div class="d-flex" name="info_total_time"> - <span class="mr-auto"><label for="total_time" class="mb0">Watch Time</label></span> + <span class="mr-auto"><label for="total_time" class="mb0">Completion Time</label></span> <field name="total_time" widget="float_time"/> </div> </div> diff --git a/addons/website_slides/views/slide_slide_views.xml b/addons/website_slides/views/slide_slide_views.xml index d145869f52f8d117695371aa7b9fc1116a5672d8..050a40f62018e5b9fc3d7f2a9169ba93e6470076 100644 --- a/addons/website_slides/views/slide_slide_views.xml +++ b/addons/website_slides/views/slide_slide_views.xml @@ -78,6 +78,11 @@ <field name="website_id" groups="website.group_multi_website"/> <field name="website_url"/> <field name="is_preview"/> + <label for="completion_time" string="Completion Time"/> + <div> + <field name="completion_time" widget="float_time" class="oe_inline"/> + <span> hours</span> + </div> <field name="date_published" string="Published Date"/> </group> </group> diff --git a/addons/website_slides/views/website_slides_templates_course.xml b/addons/website_slides/views/website_slides_templates_course.xml index 554b8486239b1a39a7d8e34189d9e689418a2a0a..15b41e90c71c0db1d2ab9e4d881b4c1035178ca3 100644 --- a/addons/website_slides/views/website_slides_templates_course.xml +++ b/addons/website_slides/views/website_slides_templates_course.xml @@ -278,6 +278,10 @@ <th class="border-top-0">Last Update</th> <td class="border-top-0"><t t-esc="channel.slide_last_update" t-options="{'widget': 'date'}"/></td> </tr> + <tr t-if="channel.total_time"> + <th class="border-top-0">Completion Time</th> + <td class="border-top-0"><t class="font-weight-bold" t-esc="channel.total_time" t-options="{'widget': 'duration', 'unit': 'hour', 'round': 'minute'}"/></td> + </tr> <tr> <th>Members</th> <td><t t-esc="channel.members_count"/></td> diff --git a/addons/website_slides/views/website_slides_templates_homepage.xml b/addons/website_slides/views/website_slides_templates_homepage.xml index 561ab5f2823901cd146010310f083168d5e89b04..d326044dd42d71914923ac26bef003e3b4ada452 100644 --- a/addons/website_slides/views/website_slides_templates_homepage.xml +++ b/addons/website_slides/views/website_slides_templates_homepage.xml @@ -318,7 +318,7 @@ </div> <div class="card-footer bg-white text-600 px-3"> <div class="d-flex justify-content-between align-items-center"> - <small t-if="channel.total_time" class="font-weight-bold"><t t-esc="channel.total_time * 60" t-options="{"widget": "integer"}"/> min</small> + <small t-if="channel.total_time" class="font-weight-bold" t-esc="channel.total_time" t-options="{'widget': 'duration', 'unit': 'hour', 'round': 'minute'}"/> <div class="d-flex flex-grow-1 justify-content-end"> <t t-if="channel.is_member and channel.completed"> <span class="badge badge-pill badge-success pull-right py-1 px-2"><i class="fa fa-check"/> Completed</span>