[IMP] website_slides: add possibility to request access to private course

PURPOSE

Before this commit, a user could not contact a course responsible to ask him
to join. With this commit he is be able to do it.

SPECIFICATIONS

Change the message 'Contact website administrator' to 'Contact Responsible'.

Add the name of the course responsible in the course description panel. When
clicking on its name it opens a modal to request access to the course. It
creates an activity linked to the customer and assigned to the responsible.

When hovering the 'You're enrolled' button the fa-check icon changes to a
fa-times icon to show that you can unsubscribe when clicking.

LINKS

Task ID : 2127364
PR : #40708
This commit is contained in:
Patrick Hoste
2020-06-08 10:43:53 +00:00
committed by Thibault Delavallée
parent ad8c53ac2c
commit 786374399e
15 changed files with 335 additions and 7 deletions
@@ -2,7 +2,7 @@
<templates xml:space="preserve">
<t t-extend="slides.course.unsubscribe.modal.leave">
<t t-jquery="p" t-operation="after">
<t t-jquery="p:last" t-operation="after">
<t t-if="widget.enroll === 'payment'">
<p class="alert alert-warning">
<i class="fa fa-exclamation-triangle fa-3x float-left mr-3"></i>
+4
View File
@@ -48,6 +48,7 @@ Featuring
'wizard/slide_channel_invite_views.xml',
'data/gamification_data.xml',
'data/mail_data.xml',
'data/mail_activity_data.xml',
'data/slide_data.xml',
'data/website_data.xml',
],
@@ -58,6 +59,9 @@ Featuring
'data/slide_slide_demo.xml',
'data/slide_user_demo.xml',
],
'qweb': [
'static/src/xml/activity.xml',
],
'installable': True,
'application': True,
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="mail_activity_data_access_request" model="mail.activity.type">
<field name="name">Access Request</field>
<field name="icon">fa-check-circle</field>
<field name="sequence">50</field>
<field name="res_model_id" ref="website_slides.model_slide_channel"/>
</record>
</data>
</odoo>
+64 -1
View File
@@ -78,6 +78,7 @@ class Channel(models.Model):
_description = 'Course'
_inherit = [
'mail.thread', 'rating.mixin',
'mail.activity.mixin',
'image.mixin',
'website.seo.metadata', 'website.published.multi.mixin']
_order = 'sequence, id'
@@ -159,6 +160,7 @@ class Channel(models.Model):
string='Members', help="All members of the channel.", context={'active_test': False}, copy=False, depends=['channel_partner_ids'])
members_count = fields.Integer('Attendees count', compute='_compute_members_count')
members_done_count = fields.Integer('Attendees Done Count', compute='_compute_members_done_count')
has_requested_access = fields.Boolean(string='Access Requested', compute='_compute_has_requested_access', compute_sudo=False)
is_member = fields.Boolean(string='Is Member', compute='_compute_is_member', compute_sudo=False)
channel_partner_ids = fields.One2many('slide.channel.partner', 'channel_id', string='Members Information', groups='website_slides.group_website_slides_officer', depends=['partner_ids'])
upload_group_ids = fields.Many2many(
@@ -200,6 +202,17 @@ class Channel(models.Model):
for channel in self:
channel.members_done_count = data.get(channel.id, 0)
@api.depends('activity_ids.request_partner_id')
@api.depends_context('uid')
@api.model
def _compute_has_requested_access(self):
requested_cids = self.sudo().activity_search(
['website_slides.mail_activity_data_access_request'],
additional_domain=[('request_partner_id', '=', self.env.user.partner_id.id)]
).mapped('res_id')
for channel in self:
channel.has_requested_access = channel.id in requested_cids
@api.depends('channel_partner_ids.partner_id')
@api.depends_context('uid')
@api.model
@@ -394,6 +407,7 @@ class Channel(models.Model):
if vals.get('user_id'):
self._action_add_members(self.env['res.users'].sudo().browse(vals['user_id']).partner_id)
self.activity_reschedule(['website_slides.mail_activity_data_access_request'], new_user_id=vals.get('user_id'))
if 'enroll_group_ids' in vals:
self._add_groups_members()
@@ -590,8 +604,41 @@ class Channel(models.Model):
action['domain'] = [('res_id', 'in', self.ids)]
return action
def action_request_access(self):
""" Request access to the channel. Returns a dict with keys being either 'error'
(specific error raised) or 'done' (request done or not). """
if self.env.user.has_group('base.group_public'):
return {'error': _('You have to sign in before')}
if not self.is_published:
return {'error': _('Course not published yet')}
if self.is_member:
return {'error': _('Already member')}
if self.enroll == 'invite':
activities = self.sudo()._action_request_access(self.env.user.partner_id)
if activities:
return {'done': True}
return {'error': _('Already Requested')}
return {'done': False}
def action_grant_access(self, partner_id):
partner = self.env['res.partner'].browse(partner_id).exists()
if partner:
if self._action_add_members(partner):
self.activity_search(
['website_slides.mail_activity_data_access_request'],
user_id=self.user_id.id, additional_domain=[('request_partner_id', '=', partner.id)]
).action_feedback(feedback=_('Access Granted'))
def action_refuse_access(self, partner_id):
partner = self.env['res.partner'].browse(partner_id).exists()
if partner:
self.activity_search(
['website_slides.mail_activity_data_access_request'],
user_id=self.user_id.id, additional_domain=[('request_partner_id', '=', partner.id)]
).action_feedback(feedback=_('Access Refused'))
# ---------------------------------------------------------
# Rating Mixin API
# Mailing Mixin API
# ---------------------------------------------------------
def _rating_domain(self):
@@ -599,6 +646,22 @@ class Channel(models.Model):
domain = super(Channel, self)._rating_domain()
return expression.AND([domain, [('is_internal', '=', False)]])
def _action_request_access(self, partner):
activities = self.env['mail.activity']
requested_cids = self.sudo().activity_search(
['website_slides.mail_activity_data_access_request'],
additional_domain=[('request_partner_id', '=', partner.id)]
).mapped('res_id')
for channel in self:
if channel.id not in requested_cids:
activities += channel.activity_schedule(
'website_slides.mail_activity_data_access_request',
note=_('<b>%s</b> is requesting access to this course.') % partner.name,
user_id=channel.user_id.id,
request_partner_id=partner.id
)
return activities
# ---------------------------------------------------------
# Data / Misc
# ---------------------------------------------------------
@@ -0,0 +1,47 @@
odoo.define('website_slides.Activity', function (require) {
"use strict";
var field_registry = require('web.field_registry');
require('mail.Activity');
var KanbanActivity = field_registry.get('kanban_activity');
var MailActivity = field_registry.get('mail_activity');
function applyInclude(Activity) {
Activity.include({
events: _.extend({}, Activity.prototype.events, {
'click .o_activity_action_grant_access': '_onGrantAccess',
'click .o_activity_action_refuse_access': '_onRefuseAccess',
}),
_onGrantAccess: function (event) {
var self = this;
var partnerId = $(event.currentTarget).data('partner-id');
this._rpc({
model: 'slide.channel',
method: 'action_grant_access',
args: [this.res_id, partnerId],
}).then(function (result) {
self.trigger_up('reload');
});
},
_onRefuseAccess: function (event) {
var self = this;
var partnerId = $(event.currentTarget).data('partner-id');
this._rpc({
model: 'slide.channel',
method: 'action_refuse_access',
args: [this.res_id, partnerId],
}).then(function () {
self.trigger_up('reload');
});
},
});
}
applyInclude(KanbanActivity);
applyInclude(MailActivity);
});
@@ -0,0 +1,83 @@
odoo.define('website_slides.course.enroll', function (require) {
'use strict';
var core = require('web.core');
var Dialog = require('web.Dialog');
var publicWidget = require('web.public.widget');
var _t = core._t;
var SlideEnrollDialog = Dialog.extend({
template: 'slide.course.join.request',
init: function (parent, options, modalOptions) {
modalOptions = _.defaults(modalOptions || {}, {
title: _t('Request Access.'),
size: 'medium',
buttons: [{
text: _t('Yes'),
classes: 'btn-primary',
click: this._onSendRequest.bind(this)
}, {
text: _t('Cancel'),
close: true
}]
});
this.$element = options.$element;
this.channelId = options.channelId;
this._super(parent, modalOptions);
},
_onSendRequest: function () {
var self = this;
this._rpc({
model: 'slide.channel',
method: 'action_request_access',
args: [self.channelId]
}).then(function (result) {
if (result.error) {
self.$element.replaceWith('<div class="alert alert-danger" role="alert"><strong>' + result.error + '</strong></div>');
} else if (result.done) {
self.$element.replaceWith('<div class="alert alert-success" role="alert"><strong>' + _t('Request sent !') + '</strong></div>');
} else {
self.$element.replaceWith('<div class="alert alert-danger" role="alert"><strong>' + _t('Unknown error, try again.') + '</strong></div>');
}
self.close();
});
}
});
publicWidget.registry.websiteSlidesEnroll = publicWidget.Widget.extend({
selector: '.o_wslides_js_channel_enroll',
xmlDependencies: ['/website_slides/static/src/xml/slide_course_join.xml'],
events: {
'click': '_onSendRequestClick',
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
_openDialog: function (channelId) {
new SlideEnrollDialog(this, {
channelId: channelId,
$element: this.$el
}).open();
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
_onSendRequestClick: function (ev) {
ev.preventDefault();
this._openDialog($(ev.currentTarget).data('channelId'));
}
});
return {
slideEnrollDialog: SlideEnrollDialog,
websiteSlidesEnroll: publicWidget.registry.websiteSlidesEnroll
};
});
@@ -10,6 +10,8 @@ odoo.define('website_slides.quiz', function (require) {
var QuestionFormWidget = require('website_slides.quiz.question.form');
var SlideQuizFinishModal = require('website_slides.quiz.finish');
var SlideEnrollDialog = require('website_slides.course.enroll').slideEnrollDialog;
var QWeb = core.qweb;
var _t = core._t;
@@ -26,7 +28,10 @@ odoo.define('website_slides.quiz', function (require) {
*/
var Quiz = publicWidget.Widget.extend({
template: 'slide.slide.quiz',
xmlDependencies: ['/website_slides/static/src/xml/slide_quiz.xml'],
xmlDependencies: [
'/website_slides/static/src/xml/slide_quiz.xml',
'/website_slides/static/src/xml/slide_course_join.xml'
],
events: {
"click .o_wslides_quiz_answer": '_onAnswerClick',
"click .o_wslides_js_lesson_quiz_submit": '_submitQuiz',
@@ -36,6 +41,7 @@ odoo.define('website_slides.quiz', function (require) {
'click .o_wslides_js_quiz_add': '_onCreateQuizClick',
'click .o_wslides_js_quiz_edit_question': '_onEditQuestionClick',
'click .o_wslides_js_quiz_delete_question': '_onDeleteQuestionClick',
'click .o_wslides_js_channel_enroll': '_onSendRequestToResponsibleClick',
},
custom_events: {
@@ -531,6 +537,20 @@ odoo.define('website_slides.quiz', function (require) {
}).open();
},
/**
* Handler for the contact responsible link below a Quiz
* @param ev
* @private
*/
_onSendRequestToResponsibleClick: function(ev) {
ev.preventDefault();
var channelId = $(ev.currentTarget).data('channelId');
new SlideEnrollDialog(this, {
channelId: channelId,
$element: $(ev.currentTarget).closest('.alert.alert-info')
}).open();
},
/**
* Displays the created Question at the correct place (after the last question or
* at the first place if there is no questions yet) It also displays the 'Add Question'
@@ -718,6 +738,7 @@ odoo.define('website_slides.quiz', function (require) {
return {
channelId: slideData.channelId,
channelEnroll: slideData.channelEnroll,
channelRequestedAccess: slideData.channelRequestedAccess || false,
signupAllowed: slideData.signupAllowed
};
},
@@ -237,6 +237,37 @@ $o-wslides-fs-side-width: 300px;
@include media-breakpoint-up(md) {
border-top-width: 0;
}
.o_wslides_js_channel_unsubscribe {
> .fa-times {
display: none;
}
&:hover {
> .fa-check {
display: none;
}
> .fa-times {
display: inline-block;
}
}
}
.o_wslides_js_channel_enroll {
cursor: pointer;
&:hover, &:hover .o_wslides_enroll_msg small {
font-weight: bold;
}
}
.o_wslides_enroll_msg {
p {
display: inline-block;
margin-bottom: 0px;
}
}
}
@mixin o-wslides-tabs($tab-active-color: $o-wslides-color-bg) {
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-extend="mail.activity_items">
<t t-jquery=".o_thread_message .o_thread_message_core .o_thread_message_tools" t-operation="replace">
<t t-if="activity.activity_type_id[1] == 'Access Request'">
<t t-if="activity.user_id[0] === uid">
<a role="button" class="btn btn-link btn-success text-muted text-success o_activity_link o_activity_action_grant_access" t-att-data-partner-id="activity.request_partner_id[0]">
<i class="fa fa-check"/> Grant Access
</a>
<a role="button" class="btn btn-link btn-danger text-muted text-danger o_activity_link o_activity_action_refuse_access" t-att-data-partner-id="activity.request_partner_id[0]">
<i class="fa fa-times"/> Refuse Access
</a>
</t>
</t>
<t t-else="">
<div class="o_thread_message_tools btn-group">
<t t-call="mail.activity_thread_message_tools"/>
</div>
</t>
</t>
</t>
</templates>
@@ -15,4 +15,10 @@
</a>
</div>
</t>
<t t-name="slide.course.join.request">
<div>
<p>Do you want to request access to this course ?</p>
</div>
</t>
</templates>
@@ -47,8 +47,23 @@
<div t-if="!widget.isMember">
<div class="o_wslides_join_course alert alert-info d-flex align-items-center justify-content-between">
<div t-if="widget.channel.channelEnroll == 'invite'">
<b class="h5 mb-0">
<b>This course is private. <a href="/contactus" class="font-weight-bold">Contact the website administrator</a> to enroll.</b>
<b>This course is private.
<span t-if="widget.publicUser">
Please
<a t-att-href="'/web/login?redirect=' + widget.redirectURL" class="font-weight-bold">
sign in
</a>
to enroll.
</span>
<a t-else="" href="#" class="font-weight-bold o_wslides_js_channel_enroll"
t-att-data-channel-id="widget.channel.channelId">
<span t-if="widget.channel.channelRequestedAccess" class="text-success">
Responsible already contacted.
</span>
<span t-else="">
Contact the responsible to enroll.
</span>
</a>
</b>
<span class="my-0 h4">
<span title="Succeed and gain karma" aria-label="Succeed and gain karma" class="badge badge-pill badge-warning text-white font-weight-bold ml-3 px-2 py-1">
+2
View File
@@ -7,6 +7,7 @@
<link rel="stylesheet" type="text/scss" href="/website_slides/static/src/scss/slide_views.scss"/>
<script type="text/javascript" src="/website_slides/static/src/js/slide_category_one2many.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/rating_field_backend.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/activity.js"/>
</xpath>
</template>
@@ -27,6 +28,7 @@
<script type="text/javascript" src="/website_slides/static/src/js/slides_course_slides_list.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/slides_course_fullscreen_player.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/slides_course_join.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/slides_course_enroll_email.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/slides_course_quiz.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/slides_course_quiz_question_form.js"/>
<script type="text/javascript" src="/website_slides/static/src/js/slides_course_quiz_finish.js"/>
@@ -148,6 +148,7 @@
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="activity_ids" widget="mail_activity"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
@@ -294,9 +294,25 @@
<t t-else="">Join Course</t>
</span>
</a>
<div t-if="not channel.is_member and channel.enroll == 'invite'" class="text-center">
<div class="alert my-0 bg-100 p-2">Private Course</div>
<div class="o_wslides_enroll_msg mt-2" t-field="channel.enroll_msg"/>
<div t-attf-class="alert my-0 bg-100 p-2 #{'o_wslides_js_channel_enroll' if not is_public_user else ''}"
t-att-data-channel-id="channel.id">
Private Course
<div t-if="is_public_user">
<small>
Please <a t-att-href="'/web/login?redirect=/slides/%s' % (slug(channel))">sign in</a> to contact responsible.
</small>
</div>
<div t-elif="channel.has_requested_access">
<small class="text-success">
Request already sent
</small>
</div>
<div t-else="" class="o_wslides_enroll_msg">
<small t-field="channel.enroll_msg"/>
</div>
</div>
</div>
<t t-if="channel.is_member">
<button class="d-flex align-items-center alert my-0 px-2 px-xl-3 bg-100 w-100 o_wslides_js_channel_unsubscribe"
@@ -309,6 +325,7 @@
</t>
<h6 class="d-flex flex-grow-1 my-0">You're enrolled</h6>
<i class="fa fa-check"/>
<i class="fa fa-times"/>
</button>
<div class="d-flex align-items-center pt-3">
<t t-if="channel.completed">
@@ -330,6 +347,10 @@
<div id="o_wslides_sidebar_collapse" class="collapse d-md-block">
<table class="table table-sm mt-3">
<tr>
<th class="border-top-0">Responsible</th>
<td class="border-top-0"><span t-field="channel.user_id"/></td>
</tr>
<tr>
<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>
@@ -452,6 +452,7 @@
t-att-data-next-slide-url="'/slides/slide/%s' % (slug(next_slide)) if next_slide else None"
t-att-data-channel-id="slide.channel_id.id"
t-att-data-channel-enroll="slide.channel_id.enroll"
t-att-data-channel-requested-access="slide.channel_id.has_requested_access"
t-att-data-channel-can-upload="slide.channel_id.can_upload"
t-att-data-signup-allowed="signup_allowed"
t-att-data-session-answers="session_answers">