Skip to content

Commit

Permalink
[IMP] survey: customize preview link design
Browse files Browse the repository at this point in the history
Purpose:
--------
When sharing URLs on social medias, a link preview is usually shown.
When sharing a survey (or a survey's results), this preview now shows info
related to the survey (survey's title, description, and website), as well as
an image generated on the fly using the survey's information (title, user
and date, background image, some statistics, and "powered by odoo" logo).

The preview is not shown for surveys requiring users to log in, in order
to not disclose info about the survey.

Task-2842682
  • Loading branch information
schoffeniels committed Aug 22, 2023
1 parent 590fdc5 commit 7a9bcb1
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 1 deletion.
16 changes: 16 additions & 0 deletions addons/survey/controllers/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,22 @@ def survey_get_question_image(self, survey_token, answer_token, question_id, sug
suggested_answer, 'value_image'
).get_response()

def get_preview_picture(self, survey_token, for_results=False):
survey_sudo, dummy = self._fetch_from_access_token(survey_token, False)
if not survey_sudo:
return

return request.make_response(survey_sudo._create_preview_image(for_results), [('Content-Type', ' image/png')])

@http.route('/survey/<string:survey_token>/preview_picture', type='http', auth='public')
def survey_get_preview_picture(self, survey_token):
""" Route used by meta tags for the link preview picture. """
return self.get_preview_picture(survey_token)

@http.route('/survey/<string:survey_token>/results/preview_picture', type='http', auth='public')
def survey_get_results_preview_picture(self, survey_token):
return self.get_preview_picture(survey_token, True)

# ----------------------------------------------------------------
# JSON ROUTES to begin / continue survey (ajax navigation) + Tools
# ----------------------------------------------------------------
Expand Down
105 changes: 104 additions & 1 deletion addons/survey/models/survey_survey.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@

import werkzeug

from base64 import b64decode
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont

from odoo import api, exceptions, fields, models, _
from odoo.exceptions import AccessError, UserError
from odoo.modules.module import get_module_resource
from odoo.osv import expression
from odoo.tools import is_html_empty
from odoo.tools import format_decimalized_number, is_html_empty


class Survey(models.Model):
Expand Down Expand Up @@ -1200,3 +1205,101 @@ def _handle_certification_badges(self, vals):
# delete all challenges and goals because not needed anymore (challenge lines are deleted in cascade)
challenges_to_delete.unlink()
goals_to_delete.unlink()

# ------------------------------------------------------------
# PREVIEW
# ------------------------------------------------------------
def _create_preview_image(self, for_results):
"""Create the image used inside link previews.
The image is created on the fly so that we don't need to store it.
Note: it is faster to create it from scratch than loading a template
and adding survey's data.
"""

# Preview dimensions: (w)idth, (h)eight, (m)argin
w, h, m = 1200, 630, 10
# Editon tools
img = Image.new('RGBA', (w, h), color="white")
d = ImageDraw.Draw(img)
font_size = 42
fonst_size_small = 32
font = ImageFont.truetype(get_module_resource("web", "static/fonts/google/Roboto", "Roboto-Regular.ttf"), font_size)
font_medium = ImageFont.truetype(get_module_resource("web", "static/fonts/google/Roboto", "Roboto-Medium.ttf"), font_size)

# Background picture (top right)
if self.background_image:
bg_img_w, bg_img_h = 300, 200
survey_img = Image.open(BytesIO(b64decode(self.background_image)))
survey_img.thumbnail((bg_img_w, bg_img_h))
img.paste(survey_img, (w - survey_img.size[0] - m, m))

# Title (top left)
title_line_h = 60
title_font = font.font_variant(size=title_line_h)
title_w = w - 2 * m
if (self.background_image):
# Reduce width to not overlap on the bg picture
title_w -= bg_img_w
# Break title in multiple lines no not overflow
title_lines = ['']
for word in self.title.split():
line = f"{title_lines[-1]} {word}".strip()
if title_font.getlength(line) <= title_w:
title_lines[-1] = line
elif len(title_lines) == 3:
title_lines[-1] += " ..."
break
else:
title_lines.append(word)
d.text((m, m), "\n".join(title_lines), font=title_font, fill="darkgrey")

# User picture and create date
x, y = m, title_line_h * len(title_lines) + font_size
if self.user_id.image_128:
avatar = b64decode(self.user_id.image_128)
else:
avatar = self.env['avatar.mixin']._avatar_get_placeholder()
user_img = Image.open(BytesIO(avatar)).resize((title_line_h, title_line_h))
back_color = Image.new(user_img.mode, user_img.size, "white")
mask = Image.new("L", user_img.size, 0)
d2 = ImageDraw.Draw(mask)
d2.ellipse((0, 0, *user_img.size), fill=255)
rounded_user = Image.composite(user_img, back_color, mask)
img.paste(rounded_user, (x, y))
d.text((x + user_img.size[0] + m, y), f"- {self.create_date.strftime('%b %Y')}", font=font_medium, fill="black")

# Certification trophy (bottom left)
if self.certification:
trophy_img = Image.open(get_module_resource("survey", "static/src/img", "trophy-light.png")).resize((int(h/2), int(h/2)))
img.paste(trophy_img, (int(-h/4), int(h/2)))
# "Powered by Odoo" (bottom left)
logo_font = font_medium.font_variant(size=fonst_size_small)
x, y = 3 * m, h - m - fonst_size_small
string = _("Powered by")
string_w = int(logo_font.getlength(string))
d.text((x, y), string, font=logo_font, fill="dimgrey")
logo = Image.open(get_module_resource("web", "static/img", "logo.png"))
logo.thumbnail((w, font_size))
img.paste(logo, (x + string_w + m, y), logo.convert('RGBA'))

# Number of registered users (center left)
statistics_font = font.font_variant(size=90)
statistics_description_font = font.font_variant(size=fonst_size_small)
d.text((170, 400), _("Registered"), font=statistics_description_font, fill="dimgrey")
d.text((170, 300), format_decimalized_number(self.answer_count), font=statistics_font, fill="deepskyblue")
# Number of attempts (center middle)
d.text((485, 400), _("Completed"), font=statistics_description_font, fill="dimgrey")
d.text((485, 300), format_decimalized_number(self.answer_done_count), font=statistics_font, fill="deepskyblue")
# Success rate (center right)
d.text((800, 400), _("Success Rate"), font=statistics_description_font, fill="dimgrey")
d.text((800, 300), f"{self.success_ratio}%", font=statistics_font, fill="deepskyblue")
# Bottom right "button"
string = _("See Results") if for_results else _("Take Certification") if self.certification else _("Take Survey")
string_w = font.getlength(string)
x, y = w - string_w - 5 * m, h - 4 * m - font_size
d.rounded_rectangle([(x - 2 * m, y - m), (w - 2 * m, h - 2 * m)], 10, fill="deepskyblue")
d.text((x, y), string, font=font_medium, fill="white")

output = BytesIO()
img.save(output, 'png')
return output.getvalue()
Binary file added addons/survey/static/src/img/trophy-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions addons/survey/tests/test_survey_flow.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

import re

from odoo.addons.survey.tests import common
from odoo.tests import tagged
from odoo.tests.common import HttpCase
Expand Down Expand Up @@ -80,6 +82,15 @@ def test_flow_public(self):
self.assertTrue(answer_token)
self.assertAnswer(answers, 'new', self.env['survey.question'])

# -> check if the meta data for link previews have been correctly set
for method in ['og', 'twitter']: # Opengraph and twitter cards
title_re = re.compile(f'(?<="{method}:title" content=")(.*)(?=")')
self.assertEqual(title_re.search(r.text).group(), survey.title)
image_re = re.compile(f'(?<="{method}:image" content=")(.*)(?=")')
# check that preview image url returns an image
r_image = self.url_open(image_re.search(r.text).group())
self.assertEqual(r_image.headers['Content-Type'], "image/png")

# Customer begins survey with first page
r = self._access_page(survey, answer_token)
self.assertResponse(r, 200)
Expand Down
17 changes: 17 additions & 0 deletions addons/survey/views/survey_templates.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@
<data>
<!-- Main survey layout -->
<template id="survey.layout" name="Survey Layout" inherit_id="web.frontend_layout" primary="True">
<xpath expr="//head/meta[last()]" position="after">
<t t-if="survey and not survey.users_login_required">
<!-- Remove seo_object to not define og and twitter tags twice when wesbite is installed -->
<t t-set="seo_object" t-value="False"/>
<t t-set="title" t-value="survey.title"/>
<meta property="og:title" t-att-content="survey.title"/>
<meta property="og:description" t-att-content="survey.description.striptags() if survey.description else ''"/>
<meta property="og:site_name" t-att-content="request.env.company.name"/>
<meta property="og:image" t-attf-content="#{request.httprequest.host_url}survey/#{survey.access_token}#{'/results' if statistics_page else ''}/preview_picture"/>
<meta property="og:image:width" content="1200"/>
<meta property="og:image:height" content="630"/>
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:title" t-att-content="survey.title"/>
<meta name="twitter:description" t-att-content="survey.description.striptags() if survey.description else ''"/>
<meta name="twitter:image" t-attf-content="#{request.httprequest.host_url}survey/#{survey.access_token}#{'/results' if statistics_page else ''}/preview_picture"/>
</t>
</xpath>
<xpath expr="//div[@id='wrapwrap']" position="before">
<!--TODO DBE Fix me : If one day, there is a survey_livechat bridge module, put this in that module-->
<t t-set="no_livechat" t-value="True"/>
Expand Down
1 change: 1 addition & 0 deletions addons/survey/views/survey_templates_statistics.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<data>
<template id="survey_page_statistics" name="Survey: result statistics page">
<t t-call="survey.layout">
<t t-set="statistics_page" t-value="True"/>
<t t-call="survey.survey_button_form_view" />
<t t-set="page_record_limit" t-value="10"/><!-- Change this record_limit to change number of record per page-->
<div class="container o_survey_result">
Expand Down

0 comments on commit 7a9bcb1

Please sign in to comment.