[MERGE][IMP] website{_profile, _slide,_forum,*} : improve slides integration in user profile and add karma generation

Purpose of the merge is to continue the work done from PR #30988.
This completes the website profile page,  gamifies the under development eLearning [1][2]
and continues integration between eLearning and the forum.

This merge contains notably:
- Complete profile page with course progression detail and eLearning certifications
- Apply Karma gains on various action on the eLearning
- Allow to set the communication type on eLearning course at creation (frontend)
- Redesign website users page

See sub commits for more details.

Merge linked to task ID 1922159 (elearning gamification / website_profile)
and task ID 1940516 (forum comments) PR #31097.

[1] see task ID 1902304 (main eLearning task) PR #29876.
[2] see task ID 1936153 (new homepage for slides) PR #30770.

closes odoo/odoo#31109
This commit is contained in:
Robot Odoo
2019-02-18 17:15:58 +01:00
committed by GitHub
31 changed files with 608 additions and 171 deletions
-25
View File
@@ -540,31 +540,6 @@ class WebsiteForum(WebsiteProfile):
# User
# --------------------------------------------------
@http.route(['''/forum/<model("forum.forum", "[('website_id', 'in', (False, current_website_id))]"):forum>/users''',
'''/forum/<model("forum.forum"):forum>/users/page/<int:page>'''],
type='http', auth="public", website=True)
def users(self, forum, page=1, **searches):
User = request.env['res.users']
step = 30
dom = [('karma', '>', 1), ('website_published', '=', True)]
tag_count = User.sudo().search_count(dom)
pager = request.website.pager(url="/forum/%s/users" % slug(forum), total=tag_count, page=page, step=step, scope=30)
if searches.get('user'):
dom += [('name', 'ilike', searches.get('user'))]
users = User.sudo().search(dom, limit=step, offset=pager['offset'], order='karma DESC')
searches['users'] = 'True'
values = self._prepare_user_values(forum=forum, searches=searches)
values .update({
'users': users,
'main_object': forum,
'pager': pager,
})
return request.render("website_forum.users", values)
@http.route(['/forum/<model("forum.forum"):forum>/partner/<int:partner_id>'], type='http', auth="public", website=True)
def open_partner(self, forum, partner_id=0, **post):
if partner_id:
+1 -46
View File
@@ -163,7 +163,7 @@
t-attf-class="nav-link #{sorting == 'create_date desc' and 'active' or ''}">Newest</a>
</li>
<li class="nav-item">
<a t-attf-href="/forum/#{ slug(forum) }/users"
<a t-attf-href="/profile/users"
t-attf-class="nav-link #{searches.get('users') and 'active' or ''}">People</a>
</li>
<li class="nav-item">
@@ -1160,51 +1160,6 @@
</t>
</template>
<template id="users">
<t t-call="website_forum.header">
<div class="row mb-3">
<div class="col-sm-12 col-md-5">
<form class="input-group" role="form" method="GET">
<input type="text" t-att-value="request.params.get('user')" name="user" class="search-query form-control oe_search_box" placeholder="Search..."/>
<div class="input-group-append">
<button type="submit" class="btn btn-primary oe_search_button" aria-label="Search" title="Search"><i class="fa fa-search"></i></button>
</div>
</form>
</div>
</div>
<div class="row">
<div t-foreach="users" t-as="u" class="col-xl-4 col-md-6 col-sm-12 mb-4">
<div class="card o_card_people h-100">
<div class="card-body">
<img class="o_wforum_people_photo_container float-left mr-3" t-attf-src="/web/image/res.users/#{u.id}/image_medium"/>
<b><i class="text-secondary mr-1 fa fa-diamond"/><t t-esc="u.karma"/></b>
<h4 class="font-weight-bold o_card_people_name"><a t-attf-href="/profile/user/#{u.id}?forum_id=#{forum.id}" t-field="u.name"/></h4>
<t t-if="u.country_id">
<span t-field="u.country_id.image" t-options='{"widget": "image", "class": "country_flag_people"}'/>
</t>
</div>
<div class="card-body text-muted">
<b> Badges:
<span class="float-right text-dark">
<i class="fa fa-trophy badge-gold" role="img" aria-label="Gold badge" title="Gold badge"/>
<span t-esc="u.gold_badge" class=""/>
<i class="fa fa-trophy badge-silver ml16" role="img" aria-label="Silver badge" title="Silver badge"/>
<span t-esc="u.silver_badge" class="" />
<i class="fa fa-trophy badge-bronze ml16" role="img" aria-label="Bronze badge" title="Bronze badge"/>
<span t-esc="u.bronze_badge" class="" />
</span>
</b>
<t t-raw="0"/>
</div>
</div>
</div>
</div>
<div class="float-left">
<t t-call="website.pager"/>
</div>
</t>
</template>
<!-- User Navbar -->
<template id="user_navbar_inherit_website_forum" inherit_id="website.user_navbar">
<xpath expr="//div[@id='o_new_content_menu_choices']//div[@name='module_website_forum']" position="attributes">
@@ -13,13 +13,13 @@
<xpath expr="//tr[@id='profile_abstract_info_company']" position="after">
<t t-if="forum and (up_votes or down_votes)">
<tr id="profile_abstract_info_company">
<th style="width: 30%"><b class="text-muted">Votes</b></th>
<th style="width: 30%"><span class="font-weight-bold text-muted">Votes</span></th>
<th style="width: 70%">
<span>
<i class="fa fa-thumbs-up text-success" role="img" aria-label="Positive votes" title="Positive votes"/>
<b t-esc="up_votes"/>
<span class="font-weight-bold" t-esc="up_votes"/>
<i class="fa fa-thumbs-down text-danger ml-3" role="img" aria-label="Negative votes" title="Negative votes"/>
<b t-esc="down_votes"/>
<span class="font-weight-bold" t-esc="down_votes"/>
</span>
</th>
</tr>
@@ -52,7 +52,7 @@
</div>
</t>
<t t-else="">
<b>No question posted yet.</b>
<span class="font-weight-bold">No question posted yet.</span>
</t>
</div>
<t t-if="favourite">
@@ -81,7 +81,7 @@
</div>
</t>
<t t-else="">
<b>No answer posted yet.</b>
<span class="font-weight-bold">No answer posted yet.</span>
</t>
</div>
</div>
@@ -122,7 +122,7 @@
</div>
</t>
<t t-else="">
<b>No activities yet!</b>
<span class="font-weight-bold">No activities yet!</span>
</t>
</template>
@@ -139,7 +139,7 @@
</t>
</div>
<div class="mb16" t-if="not vote_post">
<b>No vote given by you yet!</b>
<span class="font-weight-bold">No vote given by you yet!</span>
</div>
</template>
</data></odoo>
@@ -189,3 +189,45 @@ class WebsiteProfile(http.Controller):
'badges': badges,
})
return request.render("website_profile.badge_main", values)
# All Users Page
# ---------------------------------------------------
def _prepare_all_users_values(self, user, position):
return {
'position': position,
'id': user.id,
'name': user.name,
'rank': user.rank_id.name,
'karma': user.karma,
'badge_count': len(user.badge_ids),
}
@http.route(['/profile/users',
'/profile/users/page/<int:page>'], type='http', auth="public", website=True)
def view_all_users_page(self, page=1, **searches):
User = request.env['res.users']
dom = [('karma', '>', 1), ('website_published', '=', True)]
# Get the Top 3 users
top3_users = User.sudo().search(dom, limit=3, order='karma DESC')
top3_user_values = [self._prepare_all_users_values(user, position+1) for position, user in enumerate(top3_users)]
# Get the other users
if top3_users:
dom += [('id', 'not in', top3_users.ids)]
step = 30
user_count = User.sudo().search_count(dom)
pager = request.website.pager(url="/profile/users", total=user_count, page=page, step=step, scope=step)
if searches.get('user'):
dom += [('name', 'ilike', searches.get('user'))]
users = User.sudo().search(dom, limit=step, offset=pager['offset'], order='karma DESC')
user_values = [self._prepare_all_users_values(user, position + 4 + ((page-1) * step)) for position, user in enumerate(users)]
values = {
'top3_users': top3_user_values,
'users': user_values,
'pager': pager
}
return request.render("website_profile.users_page_main", values)
@@ -31,37 +31,38 @@ $bronze: #eea91e;
}
}
.o_wprofile_navtabs_container {
.o_wprofile_nav_tabs {
border-bottom: 0px;
}
.o_wprofile_navlink {
color: white;
border-radius: 0px;
height: 48px;
font-weight: bold;
padding-top: 12px;
border-left: 2px solid #875A7B;
text-align: center;
width: 120px;
&:hover {
border: 0px;
border-radius: 0px;
background-color: #875A7B;
}
}
.o_wprofile_navlink.active {
color: #212529;
background-color: #f2f2f2;
border: 0px;
}
}
.o_wprofile_content {
margin: auto;
background-color: #f2f2f2 !important;
.o_wprofile_content_container{
position: relative;
top: -80px;
.o_wprofile_navtabs_container {
.o_wprofile_nav_tabs {
border-bottom: 0px;
}
.o_wprofile_navlink {
color: white;
border-radius: 0px;
height: 48px;
font-weight: bold;
padding-top: 12px;
border-left: 2px solid #875A7B;
text-align: center;
width: 120px;
&:hover {
border: 0px;
border-radius: 0px;
background-color: #875A7B;
}
}
.o_wprofile_navlink.active {
color: #212529;
background-color: #f2f2f2;
border: 0px;
}
}
.o_wprofile_detail {
.o_wprofile_content_abstract {
top: 1px;
@@ -144,6 +145,88 @@ $bronze: #eea91e;
padding-right: 16px;
}
// All Users Page
.o_wprofile_all_users_header {
margin: auto;
background-color: #875A7B !important;
> div:first-child {
margin: auto;
width: 100%;
max-width: 1140px;
padding: 32px 32px 0px 32px;
color: white;
top: 48px;
padding-top: 0px;
height: 250px;
}
}
.o_wprofile_all_user_content {
position: relative;
top: -140px;
.o_wprofile_all_user_card_top3 {
cursor: pointer;
height: 325px;
text-align: center;
border-radius: 0px;
border: 0px;
.card-body {
padding-left: 0px;
padding-right: 0px;
> div:first-child {
margin-bottom: -40px;
div {
position: relative;
margin: auto;
right: -35px;
bottom: 55px;
width: 45px;
padding: 12px 0px 12px 0px;
border-radius: 50%;
}
.o_wprofile_top3_first {
background-color: $gold;
}
.o_wprofile_top3_second {
background-color: $silver;
}
.o_wprofile_top3_third {
background-color: $bronze;
}
}
:nth-child(4) {
position: absolute;
bottom: 0px;
width: 100%;
margin: auto;
border-top: 1px solid lightgrey;
div {
padding: 4% 0% 4% 0%;
border-radius: 0px;
}
}
}
}
.o_wprofile_all_users_card {
cursor: pointer;
border-radius: 0px;
border: 0px;
padding: 8px 15px 8px 15px;
> div > div:first-child > div > div:nth-child(3) {
padding-top: 12px;
padding-bottom: 12px;
}
> div > div:first-child > div > div:nth-child(2) {
padding: 0px;
width: 80px;
}
> div > div:first-child > div > div:first-child {
padding: 5% 0px 5% 16px;
width: 60px;
}
}
}
// Badges
.badge-gold {
color: $gold;
@@ -168,7 +251,7 @@ $bronze: #eea91e;
// Other stuffs
.o_wprofile_bb_grey {
border-bottom: 2px solid lightgrey;
border-bottom: 2px solid lightgrey !important;
}
.country_flag {
+112 -26
View File
@@ -6,6 +6,11 @@
</xpath>
</template>
<!--
Single User Profile Page
-->
<!-- Edit Profile Page -->
<template id="user_profile_edit_main" name="Edit Profile">
<t t-call="website.layout">
@@ -36,7 +41,7 @@
</a>
</div>
<div class="form-group mt-3 mb-0 pt-2 border-top">
<label class="text-primary" for="user_website_published" t-if="user.id == uid"><b>Public profile</b></label>
<label class="text-primary" for="user_website_published" t-if="user.id == uid"><span class="font-weight-bold">Public profile</span></label>
<div class=" mb-0 float-right" t-if="user.id == uid">
<input type="checkbox" class="mt8" name="website_published" id="user_website_published" value="True" t-if="not user.website_published"/>
<input type="checkbox" class="mt8" name="website_published" id="user_website_published" value="True" checked="checked" t-if="user.website_published"/>
@@ -51,14 +56,14 @@
<div class="row">
<input name="user_id" t-att-value="user.id" type="hidden"/>
<div class="form-group col-12">
<label class="text-primary mb-1 d-block" for="user_name"><b>Real name</b></label>
<label class="text-primary mb-1 d-block" for="user_name"><span class="font-weight-bold">Real name</span></label>
<div>
<input type="text" class="form-control" name="name" id="user_name" required="True" t-attf-value="#{user.name}"/>
</div>
</div>
<div class="form-group col-6">
<label class="mb-1 text-primary" for="user_website"><b>Website</b></label>
<label class="mb-1 text-primary" for="user_website"><span class="font-weight-bold">Website</span></label>
<div>
<input type="text" class="form-control" name="website" id="user_website" t-attf-value="#{user.partner_id.website or ''}"/>
</div>
@@ -68,19 +73,19 @@
<button type="button" class="close" data-dismiss="alert">x</button>
<p>Please enter a valid email address in order to receive notifications from answers or comments.</p>
</div>
<label class="mb-1 text-primary" for="user_email"><b>Email</b></label>
<label class="mb-1 text-primary" for="user_email"><span class="font-weight-bold">Email</span></label>
<div>
<input type="text" class="form-control" name="email" id="user_email" required="True" t-attf-value="#{user.partner_id.email}"/>
</div>
</div>
<div class="form-group col-6">
<label class="mb-1 text-primary" for="user_city"><b>City</b></label>
<label class="mb-1 text-primary" for="user_city"><span class="font-weight-bold">City</span></label>
<div>
<input type="text" class="form-control" name="city" id="user_city" t-attf-value="#{user.partner_id.city or ''}"/>
</div>
</div>
<div class="form-group col-6">
<label class="mb-1 text-primary"><b>Country</b></label>
<label class="mb-1 text-primary"><span class="font-weight-bold">Country</span></label>
<div>
<select class="form-control" name="country">
<option value="">Country...</option>
@@ -91,7 +96,7 @@
</div>
</div>
<div class="form-group col-12">
<label class="mb-1 text-primary" for="description"><b>Biography</b></label>
<label class="mb-1 text-primary" for="description"><span class="font-weight-bold">Biography</span></label>
<textarea name="description" id="description" style="min-height: 120px" required="True"
class="form-control load_editor"><t t-esc="user.partner_id.website_description"/></textarea>
</div>
@@ -124,9 +129,9 @@
<div class="col-8 o_wprofile_id">
<a id="edit_profile_button" class="btn btn-primary o_wprofile_edit"
t-if="request.env.user == user"
t-attf-href="/profile/edit?url_param=#{edit_button_url_param}"><b>EDIT PROFILE</b></a>
t-attf-href="/profile/edit?url_param=#{edit_button_url_param}"><span class="font-weight-bold">EDIT PROFILE</span></a>
<div class="row mb32">
<h3 class="o_card_people_name text-white"><b t-field="user.name"/></h3>
<h3 class="o_card_people_name text-white"><span class="font-weight-bold" t-field="user.name"/></h3>
</div>
<div class="row">
<div class="o_wprofile_id_info">
@@ -177,7 +182,7 @@
<div class="row">
<div class="col-lg-12 pt16 o_wprofile_rank">
<table><tbody><tr>
<th><b class="text-muted">Current <a href="/profile/ranks">Rank</a> : </b></th>
<th><span class="font-weight-bold text-muted">Current <a href="/profile/ranks">Rank</a> : </span></th>
<th><span class="float-right" t-field="user.rank_id"/></th>
</tr></tbody></table>
<t t-call="website_profile.profile_next_rank_card"></t>
@@ -190,11 +195,11 @@
<table>
<tbody>
<tr>
<th style="width: 30%"><b class="text-muted">Joined</b></th>
<th style="width: 30%"><span class="font-weight-bold text-muted">Joined</span></th>
<th style="width: 70%"><span t-field="user.create_date" t-options='{"format": "d MMM Y"}'/></th>
</tr>
<tr>
<th style="width: 30%"><b class="text-muted">Badges</b></th>
<th style="width: 30%"><span class="font-weight-bold text-muted">Badges</span></th>
<th style="width: 70%">
<t t-if="user.badge_ids">
<span t-esc="len(user.badge_ids)"/>
@@ -206,20 +211,20 @@
</tr>
<t t-if="user.website">
<tr>
<th style="width: 30%"><b class="text-muted">Website</b></th>
<th style="width: 30%"><span class="font-weight-bold text-muted">Website</span></th>
<th style="width: 70%"><span t-field="user.website"/></th>
</tr>
</t>
<t t-if="user.city or user.country_id">
<tr>
<th style="width: 30%"><b class="text-muted">Location</b></th>
<th style="width: 30%"><span class="font-weight-bold text-muted">Location</span></th>
<th style="width: 70%">
<span t-field="user.city"/><t t-if="user.country_id"> (</t><span t-field="user.country_id"/><t t-if="user.country_id">)</t>
</th>
</tr>
</t>
<tr id="profile_abstract_info_company">
<th style="width: 30%"><b class="text-muted">Company</b></th>
<th style="width: 30%"><span class="font-weight-bold text-muted">Company</span></th>
<th style="width: 70%"><span t-field="user.company_id"/></th>
</tr>
</tbody>
@@ -249,8 +254,8 @@
<template id="profile_next_rank_card" name="Profile Next Rank Card">
<div class="card-body o_wprofile_next_rank_card mt-2 pt-2">
<div><img class="o_wprofile_img_medium" t-attf-src="/web/image/gamification.karma.rank/#{user.next_rank_id.id}/image_medium"/></div>
<div><span class="text-center"><b>"<span t-field="user.next_rank_id.name"/>"</b></span></div>
<div><span class="text-center"><b class="text-primary" t-field="user.karma"/>/<b t-field="user.next_rank_id.karma_min"/> xp</span></div>
<div><span class="text-center font-weight-bold">"<span t-field="user.next_rank_id.name"/>"</span></div>
<div><span class="text-center"><span class="font-weight-bold text-primary" t-field="user.karma"/>/<span class="font-weight-bold" t-field="user.next_rank_id.karma_min"/> xp</span></div>
<div class="progress o_wprofile_next_rank_progress">
<t t-if="user.next_rank_id.karma_min - user.rank_id.karma_min > 0">
<div class="progress-bar o_wprofile_next_rank_progress_bar" style="background-color: #00A09D !important"
@@ -274,7 +279,7 @@
<img class="o_wprofile_img_thumb" t-attf-src="/web/image/gamification.badge/#{badge.badge_id.id}/image_small"/>
</th>
<th style="">
<b t-field="badge.badge_id.name"/>
<span class="font-weight-bold" t-field="badge.badge_id.name"/>
</th>
</tr></tbody></table>
</div>
@@ -282,7 +287,7 @@
</div>
</t>
<div class="mb16" t-if="not user.badge_ids">
<b>No badge yet!</b><br/>
<span class="font-weight-bold">No badge yet!</span><br/>
<t t-if="badge_category">
<a t-attf-href="/profile/badge?category=#{badge_category}" class="fa fa-arrow-right"> Check available badges</a>
</t>
@@ -303,10 +308,10 @@
Do not stop learning with <t t-esc="user.company_id.name"/>. Collects points on the forum or the eLearning platform. Those points will make you reach new ranks.
</p>
<p>
<b>How do I earn badges?</b> When you finish a course, reach milestones, you're awarded badges.
<span class="font-weight-bold">How do I earn badges?</span> When you finish a course, reach milestones, you're awarded badges.
</p>
<p>
<b>How do I score more points?</b> You can score more points by answering quizzes at the end of each course content. Points can also be earned on the forum. Follow this link to the guidelines of the forum.
<span class="font-weight-bold">How do I score more points?</span> You can score more points by answering quizzes at the end of each course content. Points can also be earned on the forum. Follow this link to the guidelines of the forum.
</p>
</div>
<t t-call="website_profile.rank_content">
@@ -321,7 +326,7 @@
<div class="row">
<div class="o_wprofile_rank_tile" t-foreach="ranks" t-as="rank">
<div><img class="mb-3" t-attf-src="/web/image/gamification.karma.rank/#{rank.id}/image"/></div>
<div><b t-field="rank.name"/></div>
<div><span class="font-weight-bold" t-field="rank.name"/></div>
<div><span style="color: #00A09D;"><span t-field="rank.karma_min"/> point<span t-if="rank.karma_min">s</span></span></div>
</div>
</div>
@@ -352,12 +357,12 @@
<tr t-foreach="badges" t-as="badge">
<td>
<span class="badge badge-pill float-left">
<span t-if="badge.level == 'gold'"><i class="fa fa-trophy badge-gold"/> <b t-field="badge.name" class="text-white bg-gold badge-pill"/></span>
<span t-if="badge.level == 'silver'"><i class="fa fa-trophy badge-silver"/> <b t-field="badge.name" class="text-white bg-silver badge-pill"/></span>
<span t-if="badge.level == 'bronze'"><i class="fa fa-trophy badge-bronze"/> <b t-field="badge.name" class="text-white bg-bronze badge-pill"/></span>
<span t-if="badge.level == 'gold'"><i class="fa fa-trophy badge-gold"/> <span t-field="badge.name" class="font-weight-bold text-white bg-gold badge-pill"/></span>
<span t-if="badge.level == 'silver'"><i class="fa fa-trophy badge-silver"/> <span t-field="badge.name" class="font-weight-bold text-white bg-silver badge-pill"/></span>
<span t-if="badge.level == 'bronze'"><i class="fa fa-trophy badge-bronze"/> <span t-field="badge.name" class="font-weight-bold text-white bg-bronze badge-pill"/></span>
</span>
</td><td>
<b t-esc="badge.stat_count_distinct"/>
<span class="font-weight-bold" t-esc="badge.stat_count_distinct"/>
<i class="text-muted"> awarded users</i>
</td><td>
<span t-field="badge.description"/>
@@ -377,4 +382,85 @@
</div>
</t>
</template>
<!--
All Users Page
-->
<template id="users_page_main" name="Users Page">
<t t-call="website.layout">
<t t-call="website_profile.users_page_header"></t>
<t t-call="website_profile.users_page_content"></t>
</t>
</template>
<template id="users_page_header" name="Users Page Header">
<div class="row o_wprofile_all_users_header">
<div class="col-lg-12">
<h2 class="row">Users</h2>
</div>
</div>
</template>
<template id="users_page_content">
<div class="row o_wprofile_all_user_content">
<div class="container mb32 mt48">
<div class="row">
<div class="col-lg-4 mb32" t-foreach="top3_users" t-as="user" t-attf-onclick="location.href='/profile/user/#{user['id']}';">
<t t-call="website_profile.top3_user_card"></t>
</div>
</div>
<div class="row">
<div class="col-lg-12" t-foreach="users" t-as="user" t-attf-onclick="location.href='/profile/user/#{user['id']}';">
<t t-call="website_profile.all_user_card"></t>
</div>
</div>
<div class="float-left">
<t t-call="website.pager"/>
</div>
</div>
</div>
</template>
<template id="top3_user_card" name="Top 3 User Card">
<div class="card o_wprofile_all_user_card_top3 o_wprofile_bb_grey">
<div class="card-body">
<div>
<img class="mb-3 rounded-circle o_wprofile_img_medium" t-att-src="'/profile/avatar/%s?field=image_medium%s' % (user['id'], '&amp;res_model=%s&amp;res_id=%s' % (record._name, record.id) if record else '')"/>
<div t-if="user['position'] == 1" class="o_wprofile_top3_first"><span>1st</span></div>
<div t-if="user['position'] == 2" class="o_wprofile_top3_second"><span>2nd</span></div>
<div t-if="user['position'] == 3" class="o_wprofile_top3_third"><span>3rd</span></div>
</div>
<div><h3 t-esc="user['name']"></h3></div>
<div><span t-esc="user['rank']"></span></div>
<div class="row">
<div class="col" style="border-right: 1px solid lightgrey"><span><span t-esc="user['karma']"/> XP</span></div>
<div id="top3_user_badge_count" class="col"><span><span t-esc="user['badge_count']"/> Badges</span></div>
</div>
</div>
</div>
</template>
<template id="all_user_card" name="All User Card">
<div class="card o_wprofile_all_users_card o_wprofile_bb_grey">
<div class="row">
<div class="col-lg-4">
<div class="row">
<div><span><span t-esc="user['position']"/>th</span></div>
<div><img class="rounded-circle o_wprofile_img_small" t-att-src="'/profile/avatar/%s?field=image_medium%s' % (user['id'], '&amp;res_model=%s&amp;res_id=%s' % (record._name, record.id) if record else '')"/></div>
<div>
<span class="font-weight-bold" t-esc="user['name']"/><br/>
<span t-esc="user['rank']"></span>
</div>
</div>
</div>
<div class="card-body col-lg-8">
<div class="row">
<div class="col"><span><span t-esc="user['karma']"/> XP</span></div>
<div id="all_user_badge_count" class="col"><span><span t-esc="user['badge_count']"/> Badges</span></div>
</div>
</div>
</div>
</div>
</template>
</data></odoo>
@@ -2,3 +2,4 @@
from . import mail
from . import main
from . import portal
+8 -3
View File
@@ -96,6 +96,9 @@ class WebsiteSlides(WebsiteProfile):
domain = expression.AND([domain, [('nbr_%ss' % slide_type, '>', 0)]])
return domain
def _prepare_channel_values(self, **kwargs):
return dict(**kwargs)
# --------------------------------------------------
# MAIN / SEARCH
# --------------------------------------------------
@@ -571,13 +574,15 @@ class WebsiteSlides(WebsiteProfile):
return channels
def _prepare_user_slides_profile(self, user):
courses = request.env['slide.channel.partner'].sudo().search([('partner_id', '=', user.partner_id.id)]).mapped('channel_id')
courses = request.env['slide.channel.partner'].sudo().search([('partner_id', '=', user.partner_id.id)])
courses_completed = courses.filtered(lambda c: c.completed)
courses_ongoing = courses - courses_completed
values = {
'uid': request.env.user.id,
'user': user,
'main_object': user,
'courses': courses,
'count_courses': len(courses),
'courses_completed': courses_completed,
'courses_ongoing': courses_ongoing,
'is_profile_page': True,
'badge_category': 'slides',
}
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http
from odoo.http import request
from odoo.addons.portal.controllers.mail import PortalChatter
class SlidesPortalChatter(PortalChatter):
@http.route(['/mail/chatter_post'], type='http', methods=['POST'], auth='public', website=True)
def portal_chatter_post(self, res_model, res_id, message, **kw):
result = super(SlidesPortalChatter, self).portal_chatter_post(res_model, res_id, message, **kw)
if res_model == 'slide.channel':
rating_value = kw.get('rating_value', False)
slide_channel = request.env[res_model].sudo().browse(int(res_id))
if rating_value and slide_channel and request.env.user.partner_id.id == int(kw.get('pid')):
# apply karma gain rule only once
request.env.user.add_karma(slide_channel.karma_gen_channel_rank)
return result
+18 -2
View File
@@ -45,8 +45,18 @@ class ChannelUsersRelation(models.Model):
slide_done = mapped_data.get(record.channel_id.id, dict()).get(record.partner_id.id, 0)
slide_total = channel_data.get(record.channel_id.id) or 1
record.completion = math.ceil(100.0 * slide_done / slide_total)
if record.completion >= 100:
record.update({'completed': True})
def _write(self, values):
if 'completion' in values and values['completion'] >= 100:
values['completed'] = True
result = super(ChannelUsersRelation, self)._write(values)
partner_has_completed = {channel_partner.partner_id.id: channel_partner.channel_id for channel_partner in self}
users = self.env['res.users'].sudo().search([('partner_id', 'in', list(partner_has_completed.keys()))])
for user in users:
users.add_karma(partner_has_completed[user.partner_id.id].karma_gen_channel_finish)
else:
result = super(ChannelUsersRelation, self)._write(values)
return result
class Channel(models.Model):
@@ -128,6 +138,12 @@ class Channel(models.Model):
completion = fields.Integer('Completion', compute='_compute_user_statistics')
can_upload = fields.Boolean('Can Upload', compute='_compute_access')
can_publish = fields.Boolean('Can Publish', compute='_compute_access')
# karma generation
karma_gen_slide_vote = fields.Integer(string='Lesson voted', default=1)
karma_gen_channel_share = fields.Integer(string='Course shared', default=2)
karma_gen_channel_rank = fields.Integer(string='Course ranked', default=5)
karma_gen_channel_finish = fields.Integer(string='Course finished', default=10)
# TODO DBE : Add karma based action rules (like in forum)
@api.depends('slide_ids.is_published')
def _compute_slide_last_update(self):
@@ -412,6 +412,7 @@ class Slide(models.Model):
new_slide.write({
'slide_partner_ids': [(0, 0, {'vote': new_vote, 'partner_id': self.env.user.partner_id.id})]
})
self.env.user.add_karma(new_slide.channel_id.karma_gen_slide_vote)
def action_set_viewed(self):
if not all(slide.channel_id.is_member for slide in self):
@@ -1,6 +1,7 @@
.o_wprofile_slides_card {
padding: 8px;
max-height: 68px;
min-width: 350px;
}
.o_wprofile_slides_title {
top: -15px;
@@ -10,7 +11,7 @@
// Quest - Course Card
.o_wprofile_slides_course_card_body {
cursor: pointer;
max-height: 64px;
height: 66px;
.o_wprofile_slides_course_img {
position: absolute;
padding: 0px;
@@ -24,4 +25,19 @@
top: 13px;
right: 16px;
}
.o_wprofile_slide_course_progress {
position: absolute;
bottom: 4px;
width: 100%;
padding-left: 88px;
.o_wprofile_slide_course_progress_bar {
background-color: lightgray;
top: 2px;
padding: 0px;
}
.o_wprofile_slide_course_progress_value {
min-width: 70px;
text-align: right;
}
}
}
@@ -37,7 +37,7 @@
<textarea name="description" id="description" style="height:8em" class="form-control" placeholder="Write here a short description of your first course"/>
</div>
</div>
<div class="form-group row">
<div class="form-group row o_wslide_channel_communication_type">
<div class="col-sm-2">Review</div>
<div class="col-sm-10">
<div class="form-check">
@@ -90,7 +90,6 @@
<group>
<group>
<field name="channel_type" widget="radio"/>
<field name="allow_comment"/>
<field name="website_id" options="{'no_create': True}" groups="website.group_multi_website"/>
<field name="promote_strategy" widget="radio"/>
</group>
@@ -105,6 +104,11 @@
<field name="enroll_msg" colspan="4" nolabel="1"/>
</group>
</page>
<page string="Communication">
<group>
<field name="allow_comment"/>
</group>
</page>
<page string="Statistics" groups="base.group_no_one">
<group>
<group>
@@ -13,38 +13,58 @@
<xpath expr="//div[@id='profile_about_badge']" position="before">
<t t-if="channel">
<div class="mb32">
<h4><i class="fa fa-graduation-cap"/> Courses</h4>
<t t-call="website_slides.display_course"/>
<h4><i class="fa fa-graduation-cap"/> Completed Courses</h4>
<t t-if="courses_completed">
<t t-call="website_slides.display_course">
<t t-set="courses" t-value="courses_completed"></t>
</t>
</t>
<t t-else="">
<span class="font-weight-bold">No completed course yet!</span>
</t>
</div>
<div class="mb32">
<h4><i class="fa fa-book"/> Followed Courses</h4>
<t t-if="courses_ongoing">
<t t-call="website_slides.display_course">
<t t-set="courses" t-value="courses_ongoing"></t>
</t>
</t>
<t t-else="">
<span class="font-weight-bold">No followed course!</span>
</t>
</div>
</t>
</xpath>
</template>
<template id="display_course" name="Profile Courses">
<t t-if="courses">
<div class="row">
<div class="col-lg-6 o_wprofile_slides_card" t-foreach="courses" t-as="course">
<div class="card-body o_wprofile_slides_course_card_body o_wprofile_bb_grey" t-attf-onclick="location.href='/slides/#{slug(course)}';">
<div class="row">
<div class="o_wprofile_slides_course_img">
<img class="o_wprofile_img_small mb-3" t-attf-src="/web/image/slide.channel/#{course.id}/image_small"/>
</div>
<div class="col-10 o_wprofile_slides_title">
<b t-field="course.name"/>
</div>
<div class="o_wprofile_slide_course_rating">
<t t-call="website_rating.rating_widget_stars_static">
<t t-set="rating_avg" t-value="course.rating_avg"/>
<t t-set="rating_count" t-value="course.rating_count"/>
</t>
<template id="display_course">
<div class="row">
<div class="col-lg-6 o_wprofile_slides_card" t-foreach="courses" t-as="course">
<div class="card-body o_wprofile_slides_course_card_body o_wprofile_bb_grey" t-attf-onclick="location.href='/slides/#{slug(course.channel_id)}';">
<div class="row">
<div class="o_wprofile_slides_course_img">
<img class="o_wprofile_img_small" t-attf-src="/web/image/slide.channel/#{course.channel_id.id}/image_small"/>
</div>
<div class="col-10 o_wprofile_slides_title">
<span class="font-weight-bold" t-field="course.channel_id.name"/>
</div>
<div class="o_wprofile_slide_course_rating">
<t t-call="website_rating.rating_widget_stars_static">
<t t-set="rating_avg" t-value="course.channel_id.rating_avg"/>
<t t-set="rating_count" t-value="course.channel_id.rating_count"/>
</t>
</div>
<div class="row o_wprofile_slide_course_progress">
<div class="col progress o_wprofile_slide_course_progress_bar">
<div class="progress-bar" style="background-color: #00A09D !important"
t-att-style="'width: '+ str(course.completion)+'%'"></div>
</div>
<span class="col-3 text-primary o_wprofile_slide_course_progress_value"><span t-field="course.completion"/> %</span>
</div>
</div>
</div>
</div>
</t>
<t t-else="">
<b>No followed course yet!</b>
</t>
</div>
</template>
</data></odoo>
@@ -15,6 +15,8 @@
'views/slide_channel_views.xml',
'views/website_slides_templates.xml',
'data/slide_channel_demo.xml',
'views/assets.xml',
'views/website_slides_forum_templates.xml'
],
'auto_install': True,
}
@@ -6,8 +6,21 @@ from odoo.addons.website_slides.controllers.main import WebsiteSlides
class WebsiteSlidesForum(WebsiteSlides):
def _slide_channel_prepare_values(self, **kwargs):
communication_type = kwargs.get('communication_type')
if communication_type:
if communication_type == 'forum':
forum = request.env['forum.forum'].create({
'name': kwargs.get('name')
})
kwargs['forum_id'] = forum.id
kwargs['allow_comment'] = communication_type == 'comment'
return super(WebsiteSlidesForum, self)._slide_channel_prepare_values(**kwargs)
# Profile
# ---------------------------------------------------
def _prepare_user_profile_parameters(self, **post):
post = super(WebsiteSlidesForum, self)._prepare_user_profile_parameters(**post)
if post.get('channel_id'):
+11 -2
View File
@@ -1,10 +1,19 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo import api, fields, models
class Forum(models.Model):
_inherit = 'forum.forum'
slide_channel_id = fields.Many2one('slide.channel', 'Course')
slide_channel_ids = fields.One2many('slide.channel', 'forum_id', 'Courses')
slide_channel_id = fields.Many2one('slide.channel', 'Course', compute='_compute_slide_channel_id')
@api.depends('slide_channel_ids')
def _compute_slide_channel_id(self):
for forum in self:
if forum.slide_channel_ids:
forum.slide_channel_id = forum.slide_channel_ids[0]
else:
forum.slide_channel_id = None
@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_forum_forum_website_publisher,access.forum.forum.website.publisher,model_forum_forum,base.group_website_publisher,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_forum_forum_website_publisher access.forum.forum.website.publisher model_forum_forum base.group_website_publisher 1 1 1 0
@@ -0,0 +1,13 @@
odoo.define('website_slides_forum.editor', function (require) {
"use strict";
var core = require('web.core');
var QWeb = core.qweb;
var WebsiteNewMenu = require('website.newMenu');
WebsiteNewMenu.include({
xmlDependencies: WebsiteNewMenu.prototype.xmlDependencies.concat(
['/website_slides_forum/static/src/xml/website_slides_forum_channel.xml']
),
});
});
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-extend="website.slide.channel.create">
<t t-jquery=".o_wslide_channel_communication_type" t-operation="replace">
<fieldset id="channel_create_forum" class="form-group">
<div class="row">
<legend class="col-form-label col-sm-2">Communication</legend>
<div class="col-sm-10">
<div class="form-check">
<input class="form-check-input" type="radio" name="communication_type" id="communication_type1" value="nothing" checked="checked"/>
<label class="form-check-label" for="communication_type1">
No communication
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="communication_type" id="communication_type2" value="forum"/>
<label class="form-check-label" for="communication_type2">
Create a Forum
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="communication_type" id="communication_type3" value="comment"/>
<label class="form-check-label" for="communication_type3">
Allow comments
</label>
</div>
</div>
</div>
</fieldset>
</t>
</t>
</templates>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_frontend" inherit_id="website.assets_frontend" name="Website slide forum frontend assets">
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/website_slides_forum/static/src/js/website_slides_forum.editor.js"></script>
</xpath>
</template>
</odoo>
@@ -5,12 +5,8 @@
<field name="model">slide.channel</field>
<field name="inherit_id" ref="website_slides.view_slide_channel_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Forum">
<group>
<field name="forum_id"/>
</group>
</page>
<xpath expr="//field[@name='allow_comment']" position="after">
<field name="forum_id"/>
</xpath>
</field>
</record>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data>
<template id="website_slides_forum_header" inherit_id="website_forum.header">
<xpath expr="//div[hasclass('container')]" position="before">
<t t-if="forum.slide_channel_id">
<div class="row" style="background-color: #66445e; height: 48px;">
<div class="container o_wprofile_navtabs_container">
<ul class="nav nav-tabs o_wprofile_nav_tabs" role="tablist" id="profile_extra_info_tablist">
<li class="nav-item">
<a t-att-href="'/slides/%s%s?%s' % (slug(forum.slide_channel_id), '/%s' % slug(category) if category else '', keep_query('debug'))"
t-att-class="'nav-link o_wprofile_navlink'" style="border-left: 0px">
<i class="fa fa-home"/> Home</a>
</li>
<li class="nav-item">
<a href="#" t-att-class="'nav-link active o_wprofile_navlink'" style="border-left: 0px">Forum</a>
</li>
<li class="nav-item">
<a t-att-href="'/slides/%s' % (slug(forum.slide_channel_id))"
t-att-class="'nav-link o_wprofile_navlink'" style="border-left: 0px">Review</a>
</li>
</ul>
</div>
</div>
</t>
</xpath>
</template>
</data></odoo>
+1
View File
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import controllers
+1 -1
View File
@@ -6,7 +6,6 @@
'description': """This module lets you use the full power of certifications within your courses.""",
'category': 'Hidden',
'version': '0.1',
'depends': ['website_slides', 'website_survey'],
'installable': True,
'auto_install': True,
@@ -17,6 +16,7 @@
'views/website_slides_templates.xml',
'views/website_slides_templates_homepage.xml',
'views/survey_templates.xml',
'views/website_profile.xml',
'data/gamification_data.xml',
],
'demo': [
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main
from . import slides
@@ -47,3 +47,30 @@ class WebsiteSlides(WebsiteSlides):
result['certification_url'] = user_input._get_survey_url()
return result
# Profile
# ---------------------------------------------------
def _prepare_user_slides_profile(self, user):
values = super(WebsiteSlides, self)._prepare_user_slides_profile(user)
values.update({
'certificates': self._get_user_certificates(user),
})
return values
# All Users Page
# ---------------------------------------------------
def _prepare_all_users_values(self, user, position):
result = super(WebsiteSlides, self)._prepare_all_users_values(user, position)
result.update({
'certification_count': len(self._get_user_certificates(user))
})
return result
def _get_user_certificates(self, user):
domain = [
('partner_id', '=', user.partner_id.id),
('quizz_passed', '=', True),
('slide_partner_id.survey_quizz_passed', '=', True)
]
certifications = request.env['survey.user_input'].sudo().search(domain)
return certifications
@@ -0,0 +1,26 @@
// Certification Card
.o_wprofile_slides_certif_card_body {
height: 66px;
> div > div:first-child {
position: absolute;
padding: 0px;
top: 8px;
left: 8px;
width: 66px;
height: 66px;
}
> div > div:nth-child(3) > span {
position: absolute;
right: 32px;
top: 13px;
}
> div > div > div:nth-child(2) {
position: absolute;
bottom: 0px;
right: 32px;
a {
margin-left: 8px;
color: grey;
}
}
}
@@ -5,8 +5,8 @@
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/website_slides_survey/static/src/js/slides_upload.js"/>
<script type="text/javascript" src="/website_slides_survey/static/src/js/slides_certification_download.js"/>
<link rel="stylesheet" type="text/scss" href="/website_slides_survey/static/src/scss/website_profile.scss"/>
</xpath>
</template>
</data>
</odoo>
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data>
<template id="user_profile_content" inherit_id="website_profile.user_profile_content">
<xpath expr="//div[@id='profile_about_badge']" position="before">
<t t-if="channel">
<div class="mb32">
<h4><i class="fa fa-trophy"/> Certifications</h4>
<t t-call="website_slides_survey.display_certificate"/>
</div>
</t>
</xpath>
</template>
<template id="display_certificate">
<t t-if="certificates">
<div class="row">
<div class="col-lg-6 o_wprofile_slides_card" t-foreach="certificates" t-as="certificate">
<div class="card-body o_wprofile_slides_certif_card_body o_wprofile_bb_grey">
<div class="row">
<div><img class="o_wprofile_img_small" t-attf-src="/web/image/slide.slide/#{certificate.slide_id.id}/image_small"/></div>
<div class="col-10 o_wprofile_slides_title">
<span class="font-weight-bold" t-field="certificate.survey_id.title"/>
</div>
<div class="float-right" t-if="user.id == uid">
<span class="row">Score : <b t-field="certificate.quizz_score"/>%</span>
<div class="row">
<a role="button"
t-att-data-survey_id="certificate.survey_id.id"
class="o_wslides_survey_download_certification"
href="#">
<i class="fa fa-download"/>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</t>
<t t-else="">
<b>No certifications yet!</b>
</t>
</template>
<template id="top3_user_card" inherit_id="website_profile.top3_user_card">
<xpath expr="//div[@id='top3_user_badge_count']" position="after">
<div class="col" style="border-left: 1px solid lightgrey"><span><span t-esc="user['certification_count']"/> Certifications</span></div>
</xpath>
</template>
<template id="all_user_card" inherit_id="website_profile.all_user_card">
<xpath expr="//div[@id='all_user_badge_count']" position="after">
<div class="col"><span><span t-esc="user['certification_count']"/> Certifications</span></div>
</xpath>
</template>
</data></odoo>