[MERGE] survey, website_slides: fix various issues
PURPOSE
Fix various issues in eLearning application after new round of testing
and bug hunt. Some surveys fixes are included as they are used a lot
in eLearning.
SPECIFICATIONS
[FIX] mail: remove channel and partner default context keys when creating
followers
[FIX] survey: fix another crashing computed field
[FIX] survey: set email_from on certification mail template
[FIX] website_profile: fix another broken image thingy
[FIX] website_slides: make content stat button redirection match counters
[FIX] website_slides: remove unnecessary action
[FIX] website_{slides, profile}: fix display of not square user image
[FIX] website_slides: avoid admin being twice member of courses in demo data
[FIX] website_slides: still another fix of computed fields
[FIX] website_sale_slides: synchronize publish status of a channel and its
product
[FIX] website_slides_survey: create certificates with scoring by default
See sub commits for more details.
LINKS
Task 2065425 (avatar ratio badly displayed)
Task 2067324 (counters / action on content mismatch)
Task 2067872 (eLearning internal testing)
Task 2058598 (survey internal testing)
closes odoo/odoo#36756
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
@@ -241,7 +241,7 @@ GROUP BY fol.id%s""" % (
|
||||
:param check_existing: see ``_add_followers``;
|
||||
:param existing_policy: see ``_add_followers``;
|
||||
"""
|
||||
sudo_self = self.sudo()
|
||||
sudo_self = self.sudo().with_context(default_partner_id=False, default_channel_id=False)
|
||||
if not partner_subtypes and not channel_subtypes: # no subtypes -> default computation, no force, skip existing
|
||||
new, upd = self._add_default_followers(res_model, res_ids, partner_ids, channel_ids, customer_ids=customer_ids)
|
||||
else:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="mail_template_user_input_invite" model="mail.template">
|
||||
<field name="name">Survey: Send by email</field>
|
||||
<field name="name">Survey: Invite</field>
|
||||
<field name="model_id" ref="model_survey_user_input" />
|
||||
<field name="subject">Participate to ${object.survey_id.title} survey</field>
|
||||
<field name="email_to">${(object.partner_id.email_formatted or object.email) |safe}</field>
|
||||
@@ -39,9 +39,10 @@
|
||||
|
||||
<!-- Certification Email template -->
|
||||
<record id="mail_template_certification" model="mail.template">
|
||||
<field name="name">Certification: Send by email</field>
|
||||
<field name="name">Survey: Send certification by email</field>
|
||||
<field name="model_id" ref="survey.model_survey_user_input"/>
|
||||
<field name="subject">Certification: ${object.survey_id.display_name}</field>
|
||||
<field name="email_from">${(object.survey_id.create_uid.email_formatted or user.email_formatted or user.company_id.catchall) |safe}</field>
|
||||
<field name="email_to">${(object.partner_id.email_formatted or object.email) |safe}</field>
|
||||
<field name="body_html" type="xml">
|
||||
<div style="background:#F0F0F0;color:#515166;padding:10px 0px;font-family:Arial,Helvetica,sans-serif;font-size:14px;">
|
||||
|
||||
@@ -123,7 +123,11 @@ class Survey(models.Model):
|
||||
|
||||
@api.depends('user_input_ids.state', 'user_input_ids.test_entry', 'user_input_ids.quizz_score', 'user_input_ids.quizz_passed')
|
||||
def _compute_survey_statistic(self):
|
||||
stat = dict((cid, dict(answer_count=0, answer_done_count=0, success_count=0, answer_score_avg_total=0.0)) for cid in self.ids)
|
||||
default_vals = {
|
||||
'answer_count': 0, 'answer_done_count': 0, 'success_count': 0,
|
||||
'answer_score_avg': 0.0, 'success_ratio': 0.0
|
||||
}
|
||||
stat = dict((cid, dict(default_vals, answer_score_avg_total=0.0)) for cid in self.ids)
|
||||
UserInput = self.env['survey.user_input']
|
||||
base_domain = ['&', ('survey_id', 'in', self.ids), ('test_entry', '!=', True)]
|
||||
|
||||
@@ -136,11 +140,13 @@ class Survey(models.Model):
|
||||
if item['quizz_passed']:
|
||||
stat[item['survey_id'][0]]['success_count'] += item['__count']
|
||||
|
||||
for survey_id, values in stat.items():
|
||||
avg_total = stat[survey_id].pop('answer_score_avg_total')
|
||||
stat[survey_id]['answer_score_avg'] = avg_total / (stat[survey_id]['answer_done_count'] or 1)
|
||||
stat[survey_id]['success_ratio'] = (stat[survey_id]['success_count'] / (stat[survey_id]['answer_done_count'] or 1.0))*100
|
||||
|
||||
for survey in self:
|
||||
avg_total = stat[survey.id].pop('answer_score_avg_total')
|
||||
stat[survey.id]['answer_score_avg'] = avg_total / (stat[survey.id]['answer_done_count'] or 1)
|
||||
stat[survey.id]['success_ratio'] = (stat[survey.id]['success_count'] / (stat[survey.id]['answer_done_count'] or 1.0))*100
|
||||
survey.update(stat[survey.id])
|
||||
survey.update(stat.get(survey._origin.id, default_vals))
|
||||
|
||||
def _compute_survey_url(self):
|
||||
""" Computes a public URL for the survey """
|
||||
|
||||
@@ -14,7 +14,7 @@ class TestUi(tests.HttpCase):
|
||||
user_demo.write({
|
||||
'groups_id': [(5, 0), (4, self.env.ref('base.group_user').id)]
|
||||
})
|
||||
self.env.ref('website_slides.slide_channel_demo_6_furn3')._remove_membership(self.env.ref('base.partner_demo'))
|
||||
self.env.ref('website_slides.slide_channel_demo_6_furn3')._remove_membership(self.env.ref('base.partner_demo').ids)
|
||||
|
||||
self.phantom_js(
|
||||
'/slides',
|
||||
|
||||
@@ -137,10 +137,10 @@ class WebsiteProfile(http.Controller):
|
||||
}
|
||||
|
||||
if 'clear_image' in kwargs:
|
||||
values['image'] = False
|
||||
values['image_1920'] = False
|
||||
elif kwargs.get('ufile'):
|
||||
image = kwargs.get('ufile').read()
|
||||
values['image'] = base64.b64encode(image)
|
||||
values['image_1920'] = base64.b64encode(image)
|
||||
|
||||
if request.uid == user.id: # the controller allows to edit only its own privacy settings; use partner management for other cases
|
||||
values['website_published'] = kwargs.get('website_published') == 'True'
|
||||
|
||||
@@ -514,7 +514,9 @@
|
||||
<div class="card text-center mb-2 border-bottom-0 o_wprofile_pointer">
|
||||
<div class="card-body">
|
||||
<div class="d-inline-block position-relative">
|
||||
<img class="rounded-circle img-fluid" t-att-src="'/profile/avatar/%s?field=image_128%s' % (user['id'], '&res_model=%s&res_id=%s' % (record._name, record.id) if record else '')"/>
|
||||
<img class="rounded-circle img-fluid"
|
||||
style="width: 128px; height: 128px; object-fit: cover;"
|
||||
t-att-src="'/profile/avatar/%s?field=image_256%s' % (user['id'], '&res_model=%s&res_id=%s' % (record._name, record.id) if record else '')"/>
|
||||
<img class="position-absolute" t-attf-src="/website_profile/static/src/img/rank_#{user_index + 1}.svg" alt="User rank" style="bottom: 0; right: -10px"/>
|
||||
</div>
|
||||
<h3 class="mt-2 mb-0" t-esc="user['name']"></h3>
|
||||
|
||||
@@ -31,6 +31,23 @@ class Channel(models.Model):
|
||||
for channel in self:
|
||||
channel.product_sale_revenues = rg_data.get(channel.product_id.id, 0)
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
channel = super(Channel, self).create(vals)
|
||||
if channel.enroll == 'payment':
|
||||
channel._synchronize_product_publish()
|
||||
return channel
|
||||
|
||||
def write(self, vals):
|
||||
res = super(Channel, self).write(vals)
|
||||
if 'is_published' in vals:
|
||||
self.filtered(lambda channel: channel.enroll == 'payment')._synchronize_product_publish()
|
||||
return res
|
||||
|
||||
def _synchronize_product_publish(self):
|
||||
self.filtered(lambda channel: channel.is_published and not channel.product_id.is_published).sudo().product_id.write({'is_published': True})
|
||||
self.filtered(lambda channel: not channel.is_published and channel.product_id.is_published).sudo().product_id.write({'is_published': False})
|
||||
|
||||
def action_view_sales(self):
|
||||
action = self.env.ref('website_sale_slides.sale_report_action_slides').read()[0]
|
||||
action['domain'] = [('product_id', 'in', self.product_id.ids)]
|
||||
|
||||
@@ -13,6 +13,7 @@ class TestCoursePurchaseFlow(common.SlidesCase):
|
||||
'list_price': 150,
|
||||
'type': 'service',
|
||||
'invoice_policy': 'order',
|
||||
'is_published': True,
|
||||
})
|
||||
|
||||
self.channel.write({
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="1">
|
||||
|
||||
<!-- First of all: remove admin of all memberships to avoid noisy demo data -->
|
||||
<function model="slide.channel"
|
||||
name="_remove_membership"
|
||||
eval="[
|
||||
[ref('website_slides.slide_channel_demo_0_gard_0'),
|
||||
ref('website_slides.slide_channel_demo_1_gard1'),
|
||||
ref('website_slides.slide_channel_demo_2_gard2'),
|
||||
ref('website_slides.slide_channel_demo_3_furn0'),
|
||||
ref('website_slides.slide_channel_demo_4_furn1'),
|
||||
ref('website_slides.slide_channel_demo_5_furn2'),
|
||||
ref('website_slides.slide_channel_demo_6_furn3')],
|
||||
[ref('base.partner_admin')]
|
||||
]"/>
|
||||
|
||||
<!-- CHANNEL 0: Basics of Gardening -->
|
||||
<!-- ================================================== -->
|
||||
<record id="slide_channel_0_partner_admin" model="slide.channel.partner">
|
||||
|
||||
@@ -474,25 +474,19 @@ class Channel(models.Model):
|
||||
for channel in self:
|
||||
removed_channel_partner_domain = expression.OR([
|
||||
removed_channel_partner_domain,
|
||||
[('partner_id', 'in', partner_ids.ids),
|
||||
[('partner_id', 'in', partner_ids),
|
||||
('channel_id', '=', channel.id)]
|
||||
])
|
||||
|
||||
if removed_channel_partner_domain:
|
||||
self.env['slide.channel.partner'].sudo().search(removed_channel_partner_domain).unlink()
|
||||
|
||||
def action_view(self):
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'slide.channel',
|
||||
'res_id': self.id,
|
||||
}
|
||||
|
||||
def action_view_slides(self):
|
||||
action = self.env.ref('website_slides.slide_slide_action').read()[0]
|
||||
action['context'] = {'default_channel_id': self.id}
|
||||
action['context'] = {
|
||||
'search_default_published': 1,
|
||||
'default_channel_id': self.id
|
||||
}
|
||||
action['domain'] = [('channel_id', "=", self.id), ('is_category', '=', False)]
|
||||
return action
|
||||
|
||||
|
||||
@@ -265,7 +265,8 @@ class Slide(models.Model):
|
||||
def _compute_slides_statistics(self):
|
||||
# Do not use dict.fromkeys(self.ids, dict()) otherwise it will use the same dictionnary for all keys.
|
||||
# Therefore, when updating the dict of one key, it updates the dict of all keys.
|
||||
result = {_id: {} for _id in self.ids}
|
||||
keys = ['nbr_%s' % slide_type for slide_type in self.env['slide.slide']._fields['slide_type'].get_values(self.env)]
|
||||
default_vals = dict((key, 0) for key in keys)
|
||||
|
||||
res = self.env['slide.slide'].read_group(
|
||||
[('is_published', '=', True), ('category_id', 'in', self.ids), ('is_category', '=', False)],
|
||||
@@ -273,11 +274,9 @@ class Slide(models.Model):
|
||||
lazy=False)
|
||||
|
||||
type_stats = self._compute_slides_statistics_type(res)
|
||||
for cid, cdata in type_stats.items():
|
||||
result[cid].update(cdata)
|
||||
|
||||
for record in self:
|
||||
record.update(result[record.id])
|
||||
record.update(type_stats.get(record._origin.id, default_vals))
|
||||
|
||||
def _compute_slides_statistics_type(self, read_group_res):
|
||||
""" Compute statistics based on all existing slide types """
|
||||
|
||||
@@ -13,7 +13,7 @@ class TestUi(tests.HttpCase):
|
||||
user_demo.write({
|
||||
'groups_id': [(5, 0), (4, self.env.ref('base.group_user').id)]
|
||||
})
|
||||
self.env.ref('website_slides.slide_channel_demo_0_gard_0')._remove_membership(self.env.ref('base.partner_demo'))
|
||||
self.env.ref('website_slides.slide_channel_demo_0_gard_0')._remove_membership(self.env.ref('base.partner_demo').ids)
|
||||
|
||||
self.phantom_js(
|
||||
'/slides',
|
||||
@@ -29,7 +29,7 @@ class TestUi(tests.HttpCase):
|
||||
user_demo.write({
|
||||
'groups_id': [(5, 0), (4, self.env.ref('base.group_user').id), (4, self.env.ref('website.group_website_publisher').id)]
|
||||
})
|
||||
self.env.ref('website_slides.slide_channel_demo_0_gard_0')._remove_membership(self.env.ref('base.partner_demo'))
|
||||
self.env.ref('website_slides.slide_channel_demo_0_gard_0')._remove_membership(self.env.ref('base.partner_demo').ids)
|
||||
|
||||
self.phantom_js(
|
||||
'/slides',
|
||||
@@ -42,7 +42,7 @@ class TestUi(tests.HttpCase):
|
||||
# group_website_designer
|
||||
user_portal = self.env.ref('base.demo_user0')
|
||||
user_portal.flush()
|
||||
self.env.ref('website_slides.slide_channel_demo_0_gard_0')._remove_membership(self.env.ref('base.partner_demo_portal'))
|
||||
self.env.ref('website_slides.slide_channel_demo_0_gard_0')._remove_membership(self.env.ref('base.partner_demo_portal').ids)
|
||||
|
||||
self.phantom_js(
|
||||
'/slides',
|
||||
@@ -61,7 +61,7 @@ class TestUiYoutube(tests.HttpCase):
|
||||
user_demo.write({
|
||||
'groups_id': [(5, 0), (4, self.env.ref('base.group_user').id)]
|
||||
})
|
||||
self.env.ref('website_slides.slide_channel_demo_0_gard_0')._remove_membership(self.env.ref('base.partner_demo'))
|
||||
self.env.ref('website_slides.slide_channel_demo_3_furn0')._remove_membership(self.env.ref('base.partner_demo').ids)
|
||||
|
||||
self.phantom_js(
|
||||
'/slides',
|
||||
@@ -77,7 +77,6 @@ class TestUiYoutube(tests.HttpCase):
|
||||
user_demo.write({
|
||||
'groups_id': [(5, 0), (4, self.env.ref('base.group_user').id), (4, self.env.ref('website.group_website_designer').id)]
|
||||
})
|
||||
self.env.ref('website_slides.slide_channel_demo_0_gard_0')._remove_membership(self.env.ref('base.partner_demo'))
|
||||
|
||||
self.phantom_js(
|
||||
'/slides',
|
||||
|
||||
@@ -236,7 +236,7 @@
|
||||
<div class="o_kanban_card_header">
|
||||
<div class="o_kanban_card_header_title mb16">
|
||||
<div class="o_primary">
|
||||
<a name="action_view" type="object" class="mr-auto">
|
||||
<a type="edit" class="mr-auto">
|
||||
<span><field name="name" class="o_primary"/></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -300,7 +300,7 @@
|
||||
t-att-data-enroll="channel.enroll">
|
||||
<t t-call="website_slides.slides_misc_user_image">
|
||||
<t t-set="img_class" t-value="'rounded-circle mr-1'"/>
|
||||
<t t-set="img_style" t-value="'height: 1.4em'"/>
|
||||
<t t-set="img_style" t-value="'width: 1.4em; height: 1.4em; object-fit: cover;'"/>
|
||||
</t>
|
||||
<h6 class="d-flex flex-grow-1 my-0">You're enrolled</h6>
|
||||
<i class="fa fa-check"/>
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
<div class="ml-4 ml-md-2 mt-3">
|
||||
<t t-foreach="top3_users" t-as="user">
|
||||
<img class="rounded-circle img-fluid shadow ml-n4"
|
||||
style="border: 6px solid white; max-width: 40%"
|
||||
t-attf-src="/web/image/res.users/#{user['id']}/image_128"/>
|
||||
style="width: 140px; height: 140px; max-width: 40%; max-height: 40%; object-fit: cover; border: 6px solid white;"
|
||||
t-attf-src="/web/image/res.users/#{user['id']}/image_256"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,7 +99,7 @@
|
||||
<div class="d-flex align-items-center">
|
||||
<t t-call="website_slides.slides_misc_user_image">
|
||||
<t t-set="img_class" t-value="'rounded-circle mr-1'"/>
|
||||
<t t-set="img_style" t-value="'height: 22px'"/>
|
||||
<t t-set="img_style" t-value="'width: 22px; height: 22px; object-fit: cover;'"/>
|
||||
</t>
|
||||
<h5 t-esc="user.name" class="d-flex flex-grow-1 mb-0"/>
|
||||
<a class="d-none d-lg-block" t-att-href="'/profile/user/%s' % user.id">View</a>
|
||||
@@ -451,7 +451,7 @@
|
||||
|
||||
<template id='slides_misc_user_image' name="User Avatar">
|
||||
<img t-att-class="img_class if img_class else 'rounded-circle float-left'"
|
||||
t-att-style="img_style if img_style else 'max-height: 32px;'"
|
||||
t-att-style="img_style if img_style else 'width: 32px; height: 32px; object-fit: cover;'"
|
||||
t-att-src="'/profile/avatar/%s?field=image_128' % user.id"
|
||||
t-att-alt="user.name"/>
|
||||
</template>
|
||||
|
||||
@@ -61,4 +61,4 @@ class SurveyUserInput(models.Model):
|
||||
removed_memberships_per_partner[user_input.partner_id] = removed_memberships
|
||||
|
||||
for partner_id, removed_memberships in removed_memberships_per_partner.items():
|
||||
removed_memberships._remove_membership(partner_id)
|
||||
removed_memberships._remove_membership(partner_id.ids)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<field name="res_model">survey.survey</field>
|
||||
<field name="view_mode">kanban,tree,form</field>
|
||||
<field name="domain">[('certificate', '=', True)]</field>
|
||||
<field name="context">{'default_certificate': True}</field>
|
||||
<field name="context">{'default_certificate': True, 'default_scoring_type': 'scoring_with_answers'}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Add a new certification
|
||||
|
||||
Reference in New Issue
Block a user