[FIX] website_slides: fix display of lessons in training course

Currently slides are displayed in a reorderable list, using current user
access rights. This means external customers see only published and free
lessons.

However purpose of course page is to display all lessons, and display
only available ones as clickable. Other one should be displayed as muted
to tease people.

Note that a specific lesson page does check for access rights. Here we
just display all lessons names. A label for free preview is also added.

Fetch of data when displaying a channel is therefore a bit modified as
there are less differences between training and documentation channels.
User progress is also updated to be easier to use in templates.

Commit linked to task ID 1941250 and PR #31453
This commit is contained in:
Thibault Delavallée
2019-03-04 16:28:27 +00:00
parent a488cf67ed
commit dc76c99a1e
5 changed files with 180 additions and 161 deletions
+81 -44
View File
@@ -19,8 +19,9 @@ _logger = logging.getLogger(__name__)
class WebsiteSlides(WebsiteProfile):
_slides_per_page = 12
_slides_per_list = 20
SLIDES_PER_PAGE = 12
SLIDES_PER_ASIDE = 20
SLIDES_PER_CATEGORY = 4
_channel_order_by_criterion = {
'vote': 'total_votes desc',
'view': 'total_views desc',
@@ -64,8 +65,8 @@ class WebsiteSlides(WebsiteProfile):
return True
def _get_slide_detail(self, slide):
most_viewed_slides = slide._get_most_viewed_slides(self._slides_per_list)
related_slides = slide._get_related_slides(self._slides_per_list)
most_viewed_slides = slide._get_most_viewed_slides(self.SLIDES_PER_ASIDE)
related_slides = slide._get_related_slides(self.SLIDES_PER_ASIDE)
values = {
'slide': slide,
'most_viewed_slides': most_viewed_slides,
@@ -90,12 +91,41 @@ class WebsiteSlides(WebsiteProfile):
possible_points = [slide.quiz_first_attempt_reward,slide.quiz_second_attempt_reward,slide.quiz_third_attempt_reward, slide.quiz_fourth_attempt_reward]
return possible_points[attempt_count] if attempt_count < len(possible_points) else possible_points[-1]
# CHANNEL UTILITIES
# --------------------------------------------------
def _get_user_progress(self, channel):
user_progress = { slide_partner.slide_id.id: slide_partner for slide_partner in request.env['slide.slide.partner'].sudo().search([('channel_id', '=', channel.id),('partner_id', '=', request.env.user.partner_id.id)])}
user_progress = {
slide_partner.slide_id.id: slide_partner
for slide_partner in request.env['slide.slide.partner'].sudo().search([
('channel_id', '=', channel.id),
('partner_id', '=', request.env.user.partner_id.id)
])
}
return {
'user_progress': user_progress
}
def _get_channel_progress(self, channel):
""" Replacement to user_progress. Both may exist in some transient state. """
slides = request.env['slide.slide'].sudo().search([('channel_id', '=', channel.id)])
channel_progress = dict((sid, dict()) for sid in slides.ids)
if not request.env.user._is_public() and channel.is_member:
slide_partners = request.env['slide.slide.partner'].sudo().search([
('channel_id', '=', channel.id),
('partner_id', '=', request.env.user.partner_id.id)
])
for slide_partner in slide_partners:
channel_progress[slide_partner.slide_id.id].update(slide_partner.read()[0])
if slide_partner.slide_id.question_ids:
gains = [slide_partner.slide_id.quiz_first_attempt_reward,
slide_partner.slide_id.quiz_second_attempt_reward,
slide_partner.slide_id.quiz_third_attempt_reward,
slide_partner.slide_id.quiz_fourth_attempt_reward]
channel_progress[slide_partner.slide_id.id]['quiz_gain'] = gains[slide_partner.quiz_attempts_count] if slide_partner.quiz_attempts_count < len(gains) else gains[-1]
return channel_progress
def _extract_channel_tag_search(self, **post):
tags = request.env['slide.channel.tag']
for key in (_key for _key in post if _key.startswith('channel_tag_group_id_')):
@@ -133,9 +163,6 @@ class WebsiteSlides(WebsiteProfile):
domain = expression.AND([domain, [('partner_ids', '=', request.env.user.partner_id.id)]])
return domain
def _prepare_channel_values(self, **kwargs):
return dict(**kwargs)
# --------------------------------------------------
# MAIN / SEARCH
# --------------------------------------------------
@@ -245,11 +272,6 @@ class WebsiteSlides(WebsiteProfile):
raise werkzeug.exceptions.NotFound()
domain = [('channel_id', '=', channel.id)]
if not channel.can_publish:
domain = expression.AND([
domain,
['&', ('website_published', '=', True), ('channel_id.website_published', '=', True)]
])
pager_url = "/slides/%s" % (channel.id)
pager_args = {}
@@ -278,13 +300,16 @@ class WebsiteSlides(WebsiteProfile):
pager_url += "?slide_type=%s" % slide_type
# sorting criterion
actual_sorting = sorting if sorting and sorting in request.env['slide.slide']._order_by_strategy else channel.promote_strategy
if channel.channel_type == 'documentation':
actual_sorting = sorting if sorting and sorting in request.env['slide.slide']._order_by_strategy else channel.promote_strategy
else:
actual_sorting = 'sequence'
order = request.env['slide.slide']._order_by_strategy[actual_sorting]
pager_args['sorting'] = actual_sorting
pager_count = request.env['slide.slide'].sudo().search_count(domain)
pager = request.website.pager(url=pager_url, total=pager_count, page=page,
step=self._slides_per_page, scope=self._slides_per_page,
step=self.SLIDES_PER_PAGE, scope=self.SLIDES_PER_PAGE,
url_args=pager_args)
values = {
@@ -323,24 +348,47 @@ class WebsiteSlides(WebsiteProfile):
'last_rating_value': last_message_data.get('rating_value'),
})
# Display uncategorized slides
# fetch slides; done as sudo because we want to display all of them but
# unreachable ones won't be clickable (+ slide controller will crash anyway)
# fetch slides and handle uncategorized slides; done as sudo because we want to display all
# of them but unreachable ones won't be clickable (+ slide controller will crash anyway)
# documentation mode may display less slides than content by category but overhead of
# computation is reasonable
if not category and not uncategorized:
category_data = []
for category in request.env['slide.slide'].sudo().read_group(domain, ['category_id'], ['category_id']):
category_id, name = category.get('category_id') or (False, _('Uncategorized'))
slides_sudo = request.env['slide.slide'].sudo().search(category['__domain'], limit=4, offset=0, order=order)
all_categories = request.env['slide.category'].search([('channel_id', '=', channel.id)])
all_slides = request.env['slide.slide'].sudo().search(domain, order=order)
uncategorized_slides = all_slides.filtered(lambda slide: not slide.category_id)
displayed_slides = uncategorized_slides
if channel.channel_type == 'documentation':
displayed_slides = uncategorized_slides[:self.SLIDES_PER_CATEGORY]
if channel.channel_type == 'training' or displayed_slides:
category_data.append({
'id': category_id,
'name': name,
'slug_name': slug((category_id, name)) if category_id else name,
'total_slides': category['category_id_count'],
'slides': slides_sudo,
'category': False, 'id': False,
'name': _('Uncategorized'), 'slug_name': _('Uncategorized'),
'total_slides': len(uncategorized_slides),
'slides': displayed_slides,
})
for category in all_categories:
category_slides = all_slides.filtered(lambda slide: slide.category_id == category)
displayed_slides = category_slides
if channel.channel_type == 'documentation':
displayed_slides = category_slides[:self.SLIDES_PER_CATEGORY]
if not displayed_slides:
continue
category_data.append({
'id': category.id,
'name': category.name,
'slug_name': slug(category),
'total_slides': len(category_slides),
'slides': displayed_slides,
})
elif uncategorized:
slides_sudo = request.env['slide.slide'].sudo().search(domain, limit=self._slides_per_page, offset=pager['offset'], order=order)
if channel.channel_type == 'documentation':
slides_sudo = request.env['slide.slide'].sudo().search(domain, limit=self.SLIDES_PER_PAGE, offset=pager['offset'], order=order)
else:
slides_sudo = request.env['slide.slide'].sudo().search(domain, order=order)
category_data = [{
'category': False,
'id': False,
'name': _('Uncategorized'),
'slug_name': _('Uncategorized'),
@@ -348,35 +396,24 @@ class WebsiteSlides(WebsiteProfile):
'slides': slides_sudo,
}]
else:
slides_sudo = request.env['slide.slide'].sudo().search(domain, limit=self._slides_per_page, offset=pager['offset'], order=order)
if channel.channel_type == 'documentation':
slides_sudo = request.env['slide.slide'].sudo().search(domain, limit=self.SLIDES_PER_PAGE, offset=pager['offset'], order=order)
else:
slides_sudo = request.env['slide.slide'].sudo().search(domain, order=order)
category_data = [{
'category': category,
'id': category.id,
'name': category.name,
'slug_name': slug(category),
'total_slides': len(slides_sudo),
'slides': slides_sudo,
}]
# post slide-fetch computation: promoted, user completion (separated because sudo-ed)
if not request.website.is_public_user() and channel.is_member:
displayed_slide_ids = list(set(sid for item in category_data for sid in item['slides'].ids))
done_slide_ids = request.env['slide.slide.partner'].sudo().search([
('slide_id', 'in', displayed_slide_ids),
('partner_id', '=', request.env.user.partner_id.id),
('completed', '=', True)
]).mapped('slide_id').ids
else:
done_slide_ids = []
values['done_slide_ids'] = done_slide_ids
values['slide_promoted'] = request.env['slide.slide'].sudo().search(domain, limit=1, order=order)
values['category_data'] = category_data
values['channel_progress'] = self._get_channel_progress(channel)
values = self._prepare_additional_channel_values(values, **kw)
if channel.channel_type == "training":
values.update(self._get_user_progress(channel))
values['uncategorized_slides'] = channel.slide_ids.filtered(lambda slide: not slide.category_id)
return request.render('website_slides.course_main', values)
@http.route(['/slides/channel/add'], type='http', auth='user', methods=['POST'], website=True)
@@ -88,6 +88,7 @@ class Slide(models.Model):
_description = 'Slides'
_mail_post_access = 'read'
_order_by_strategy = {
'sequence': 'category_sequence asc, sequence asc',
'most_viewed': 'total_views desc',
'most_voted': 'likes desc',
'latest': 'date_published desc',
@@ -28,17 +28,17 @@ sAnimations.registry.websiteSlidesCourseSlidesList = sAnimations.Class.extend({
* @private
*/
_bindSortable: function () {
this.$('ul.o_wslides_slides_list_container').sortable({
this.$('ul.o_wslides_js_slides_list_container').sortable({
handle: '.fa-arrows',
stop: this._reorderCategories.bind(this),
items: '.o_wslides_slide_list_category'
});
this.$('.o_wslides_slides_list_container ul').sortable({
this.$('.o_wslides_js_slides_list_container ul').sortable({
handle: '.fa-arrows',
connectWith: '.o_wslides_slides_list_container ul',
connectWith: '.o_wslides_js_slides_list_container ul',
stop: this._reorderSlides.bind(this),
items: '.o_wslides_slides_list_slide:not(.o_wslides_empty_category)'
items: '.o_wslides_slides_list_slide:not(.o_wslides_js_slides_list_empty)'
});
},
@@ -50,8 +50,8 @@ sAnimations.registry.websiteSlidesCourseSlidesList = sAnimations.Class.extend({
* @private
*/
_checkForEmptySections: function (){
this.$('.o_wslides_slides_list_container ul').each(function (){
var $emptyCategory = $(this).find('.o_wslides_empty_category');
this.$('.o_wslides_js_slides_list_container ul').each(function (){
var $emptyCategory = $(this).find('.o_wslides_js_slides_list_empty');
if ($(this).find('li.o_wslides_slides_list_slide[data-slide-id]').length === 0) {
$emptyCategory.removeClass('d-none').addClass('d-flex');
} else {
@@ -130,7 +130,7 @@ sAnimations.registry.websiteSlidesCourseSlidesList = sAnimations.Class.extend({
* @private
*/
_updateHref: function () {
this.$(".o_wslides_slides_list_slide_link").each(function (){
this.$(".o_wslides_js_slides_list_slide_link").each(function (){
var href = $(this).attr('href');
var operator = href.indexOf('?') !== -1 ? '&' : '?';
$(this).attr('href', href + operator + "fullscreen=1");
@@ -2,7 +2,7 @@
$nav-tabs-border-color: #dddddd !default;
$body-bg: #FFFFFF !default;
$MAX-Z-INDEX : 2147483647 !default;
$gray-50: #f4f4f4 !default;
// Common to new slides pages
// **************************************************
@@ -204,49 +204,33 @@ $MAX-Z-INDEX : 2147483647 !default;
}
.o_wslides_course_main {
.o_wslides_course_content_aside {
max-width: 286px;
.bg-white {
max-width: 256px;
background-color: white;
padding-bottom: 8px;
> div.row {
padding-left: 15px;
padding-right: 15px;
}
}
.o_wslides_course_content_aside {
max-width: 286px;
.bg-white {
max-width: 256px;
background-color: white;
padding-bottom: 8px;
> div.row {
padding-left: 15px;
padding-right: 15px;
}
}
}
}
// Slides list reordering widget
.o_wslides_slides_list {
padding: 0;
ul {
list-style: none;
padding: 0;
}
.o_wslides_slide_list_category_container {
margin: 0;
background-color: #ddd;
display: flex;
list-style: none;
font-size: 1.05rem;
border-bottom: 1px solid #ccc;
}
.o_wslides_slides_list_slide {
&.o_not_editable {
height: 0px;
}
&:nth-child(odd) {
background-color: #f6f6f6;
background-color: $gray-50;
}
&:nth-child(even) {
background-color: #f9f9f9;
background-color: $gray-100;
}
.o_wslides_slides_list_slide_controls {
@@ -264,39 +264,44 @@
<div class="row">
<t t-call="website_slides.course_sidebar"/>
<div class="col-lg-8 mt-3 mb-5 o_wslides_slides_list" t-att-data-channel-id="channel.id">
<ul class="o_wslides_slides_list_container">
<ul class="o_wslides_js_slides_list_container border-bottom list-unstyled">
<t t-set="j" t-value="0"/>
<li t-if="uncategorized_slides or channel.can_publish">
<div class="text-muted font-weight-bold pt-0 pb-0 pl-2 pr-2 d-flex justify-content-between align-items-center">
<t t-call="website_slides.course_slides_list_category"/>
</div>
<ul>
<li t-att-class="('text-muted font-italic o_wslides_slides_list_slide o_wslides_empty_category justify-content-between align-items-center p-2 %s') % ('d-none' if uncategorized_slides else 'd-flex')">
<div class="content-slide-infos ml-2">
<span>Empty category</span>
<t t-foreach="category_data" t-as="category">
<t t-set="category_id" t-value="category['id'] if category['id'] else None"/>
<li t-if="category['total_slides'] or channel.can_publish" class="o_wslides_slide_list_category"
t-att-data-category-id="category_id">
<div t-att-data-category-id="category_id"
t-att-class="'text-muted bg-white border-bottom font-weight-bold pt-0 pb-0 pl-2 pr-2 d-flex justify-content-between align-items-center %s' % ('o_wslides_js_category' if category_id else '')">
<div class="pt-2 pb-2 d-flex align-items-center">
<i t-if="channel.can_publish and category_id" class="fa fa-arrows mr-3"></i>
<t t-if="category_id">
<span t-esc="category['name']"/>
</t>
<t t-else="">
<span>Uncategorized</span>
</t>
</div>
</li>
<li class="o_wslides_slides_list_slide o_not_editable"></li>
<t t-foreach="uncategorized_slides" t-as="slide">
<t t-call="website_slides.course_slides_list_slide" />
<t t-set="j" t-value="j+1"/>
</t>
</ul>
</li>
<t t-foreach="channel.category_ids" t-as="category">
<li t-if="category.slide_ids or channel.can_publish" class="o_wslides_slide_list_category" t-att-data-category-id="category.id">
<div t-att-data-category-id="category.id"
class="o_wslides_js_category text-muted font-weight-bold pt-0 pb-0 pl-2 pr-2 d-flex justify-content-between align-items-center">
<t t-call="website_slides.course_slides_list_category" />
<a t-if="channel.can_upload"
class="mr-2 pt-2 pb-2 border-left o_wslides_js_slide_upload"
role="button"
aria-label="Upload Presentation"
href="#"
style="text-decoration: none;"
t-att-data-channel-id="channel.id"
t-att-data-category-id="category_id"
t-att-data-can-upload="channel.can_upload"
t-att-data-can-publish="channel.can_publish">
<i class="fa fa-plus ml-2"></i> Add content
</a>
</div>
<ul t-att-data-category-id="category.id">
<li t-att-class="('text-muted font-italic o_wslides_slides_list_slide o_wslides_empty_category justify-content-between align-items-center p-2 %s') % ('d-none' if category.slide_ids else 'd-flex')">
<div class="content-slide-infos ml-2">
<ul t-att-data-category-id="category_id" class="list-unstyled">
<li t-att-class="('text-muted font-italic o_wslides_slides_list_slide o_wslides_js_slides_list_empty justify-content-between align-items-center p-2 %s') % ('d-none' if category['total_slides'] else 'd-flex')">
<div class="ml-2">
<span>Empty category</span>
</div>
</li>
<li class="o_wslides_slides_list_slide o_not_editable"></li>
<t t-foreach="category.slide_ids" t-as="slide">
<t t-foreach="category['slides']" t-as="slide">
<t t-call="website_slides.course_slides_list_slide" />
<t t-set="j" t-value="j+1"/>
</t>
@@ -305,15 +310,16 @@
</t>
</ul>
<div t-if="channel.can_upload" class="o_wslides_content_actions">
<a class="mr-2 o_wslides_js_slide_upload"
<a class="o_wslides_js_slide_upload mr-2 border btn btn-light text-muted font-weight-bold"
role="button"
aria-label="Upload Presentation"
href="#"
t-att-data-channel-id="channel.id"
t-att-data-can-upload="channel.can_upload"
t-att-data-can-publish="channel.can_publish">Add Content</a>
<a class="o_wslides_js_slide_section_add" t-attf-channel_id="#{channel.id}" href="#"
groups="website.group_website_publisher">Add Section</a>
t-att-data-can-publish="channel.can_publish"><i class="fa fa-plus"></i> Add Content</a>
<a class="o_wslides_js_slide_section_add border btn btn-light text-muted font-weight-bold" t-attf-channel_id="#{channel.id}"
href="#" role="button"
groups="website.group_website_publisher"><i class="fa fa-folder-o"></i> Add Section</a>
</div>
</div>
<div t-field="channel.description_html"/>
@@ -321,58 +327,51 @@
</template>
<template id="course_slides_list_slide" name="Slide template for a training channel">
<li t-att-index="j" t-att-data-slide-id="slide.id" t-att-data-category-id="category.id if category else None"
class="o_wslides_slides_list_slide d-flex justify-content-between align-items-center p-2">
<div class="ml-2">
<i t-if="channel.can_publish" class="fa fa-arrows mr-2 text-muted"></i>
<t t-call="website_slides.slide_icon"/>
<a class="o_wslides_slides_list_slide_link" t-attf-href="/slides/slide/#{slug(slide)}"><span t-field="slide.name"/></a>
<li t-att-index="j" t-att-data-slide-id="slide.id" t-att-data-category-id="category_id"
class="o_wslides_slides_list_slide d-flex align-items-center px-2 py-0">
<div t-if="channel.can_publish" class="border-right pt-2 pb-2">
<i class="fa fa-arrows mr-2 text-muted"></i>
</div>
<div class="o_wslides_slides_list_slide_controls mr-2">
<i t-if="not slide.id in user_progress or not user_progress[slide.id].completed" class="check-done fa fa-check-circle text-muted mr-1"></i>
<i t-if="slide.id in user_progress and user_progress[slide.id].completed" class="check-done text-primary fa fa-check-circle mr-1"></i>
<a t-if="slide.is_preview or channel.is_member or channel.can_publish" class="o_wslides_js_slides_list_slide_link font-weight-bold mr-auto"
t-attf-href="/slides/slide/#{slug(slide)}">
<t t-call="website_slides.slide_icon">
<t t-set="icon_class" t-value="'pt-2 pb-2 ml-2 mr-2'"/>
</t>
<span t-field="slide.name"/>
</a>
<span t-else="" class="text-muted mr-auto">
<t t-call="website_slides.slide_icon">
<t t-set="icon_class" t-value="'pt-2 pb-2 ml-2 mr-2'"/>
</t>
<span t-esc="slide.name"/>
</span>
<t t-if="slide.question_ids">
<span class="badge badge-warning text-white font-weight-bold"><i class="fa fa-flag"></i><t t-esc="channel_progress[slide.id].get('quiz_gain', slide.quiz_first_attempt_reward)"/> xp</span>
</t>
<span class="badge badge-info ml-2" t-if="slide.is_preview">Free preview</span>
<div t-if="channel.is_member or channel.can_publish" class="pt-2 pb-2 border-left ml-2 mr-2 pl-2 d-flex align-items-center o_wslides_slides_list_slide_controls">
<t t-if="channel.is_member">
<i t-if="not channel_progress[slide.id].get('completed')" class="check-done fa fa-check-circle text-muted mr-2"></i>
<i t-if="channel_progress[slide.id].get('completed')" class="check-done text-primary fa fa-check-circle mr-2"></i>
</t>
<a t-if="channel.can_publish and not slide.slide_type == 'webpage'" t-attf-href="/web#id=#{slide.id}&amp;action=#{slide_action}&amp;model=slide.slide&amp;view_type=form">
<i class="fa fa-pencil text-muted mr-1"></i>
<i class="fa fa-pencil text-muted mr-2"></i>
</a>
<a t-if="channel.can_publish and slide.slide_type == 'webpage'" t-attf-href="/slides/slide/#{slug(slide)}?enable_editor=1">
<i class="fa fa-pencil text-muted mr-1"></i>
<i class="fa fa-pencil text-muted mr-2"></i>
</a>
<i t-if="channel.can_publish" t-att-data-slide-id="slide.id" class="fa fa-trash text-muted o_wslides_js_slide_archive"></i>
</div>
</li>
</template>
<template id="course_slides_list_category" name="Category template for a training channel">
<div style="width:50%;" class="d-flex align-items-center">
<i t-if="channel.can_publish and category" class="fa fa-arrows mr-3 text-muted"></i>
<t t-if="category">
<span t-field="category.name"/>
</t>
<t t-else="">
<span>Uncategorized</span>
</t>
</div>
<a t-if="channel.can_upload"
class="mr-2 o_wslides_js_slide_upload"
role="button"
aria-label="Upload Presentation"
href="#"
style="font-size: 1.5rem;text-decoration: none;"
t-att-data-channel-id="channel.id"
t-att-data-category-id="category.id if category else None"
t-att-data-can-upload="channel.can_upload"
t-att-data-can-publish="channel.can_publish">+</a>
</template>
<template id="course_slides_cards" name="Documentation Course content: cards / categories">
<div t-if="not search" class="row mb-5">
<t t-call="website_slides.course_sidebar"/>
<div class="col-lg-8">
<div class="row align-items-center mt-3">
<div class="col">
<h5 class="m-0"> Featured lesson</h5>
<hr class="mt-2"/>
</div>
<div class="mt-3 w-100">
<h5 class="m-0"> Featured lesson</h5>
<hr class="mt-2"/>
</div>
<div class="row" t-if="slide_promoted">
<div class="col-4">
@@ -381,17 +380,15 @@
t-att-alt="slide_promoted.name"/>
</div>
<div class="col-8">
<div class="row">
<h3 class="col-6" t-att-title="slide_promoted.name">
<t t-if="slide_promoted.is_preview or channel.is_member or is_slides_publisher">
<a t-attf-href="/slides/slide/#{slug(slide_promoted)}" class="font-weight-bold text-muted" t-field="slide_promoted.name"/>
</t>
<t t-else="">
<span class="font-weight-bold text-muted" t-field="slide_promoted.name"/>
</t>
</h3>
<div class="col-12" t-field="slide_promoted.description"/>
</div>
<h3 class="w-100" t-att-title="slide_promoted.name">
<t t-if="slide_promoted.is_preview or channel.is_member or is_slides_publisher">
<a t-attf-href="/slides/slide/#{slug(slide_promoted)}" class="font-weight-bold text-muted" t-field="slide_promoted.name"/>
</t>
<t t-else="">
<span class="font-weight-bold text-muted" t-field="slide_promoted.name"/>
</t>
</h3>
<div t-field="slide_promoted.description"/>
</div>
</div>
</div>
@@ -470,7 +467,7 @@
<span t-esc="slide.dislikes"/>
</span>
</div>
<t t-if="channel.is_member and slide.id in done_slide_ids">
<t t-if="channel.is_member and channel_progress[slide.id].get('completed')">
<span class="badge badge-pill badge-success"><i class="fa fa-check"/> Completed</span>
</t>
</div>