Merge branch 'flectra-app-store' into 'master'

[IMP] Flectra App Store: User can direct install/download apps from flectraHQ store.

See merge request flectra-hq/flectra!158
This commit is contained in:
Parthiv Patel 2018-10-26 13:09:57 +00:00
commit b1929364fe
6 changed files with 666 additions and 134 deletions

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
import tarfile
import babel.messages.pofile import babel.messages.pofile
import base64 import base64
@ -45,6 +46,11 @@ from flectra.http import content_disposition, dispatch_rpc, request, \
from flectra.exceptions import AccessError, UserError from flectra.exceptions import AccessError, UserError
from flectra.models import check_method_name from flectra.models import check_method_name
from flectra.service import db from flectra.service import db
import requests
from flectra.tools import config
from flectra import release
from flectra.http import root
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -433,6 +439,12 @@ def binary_content(xmlid=None, model='ir.attachment', id=None, field='datas', un
#---------------------------------------------------------- #----------------------------------------------------------
# Flectra Web web Controllers # Flectra Web web Controllers
#---------------------------------------------------------- #----------------------------------------------------------
server_url = 'https://store.flectrahq.com'
tmp_dir_path = tempfile.gettempdir()
data_dir = os.path.join(config.options['data_dir'], 'addons', release.series)
class Home(http.Controller): class Home(http.Controller):
@http.route('/', type='http', auth="none") @http.route('/', type='http', auth="none")
@ -549,6 +561,104 @@ class Home(http.Controller):
options=context) options=context)
return True return True
@http.route(['/web/get_app_store_mode'], type='json', auth="user")
def get_app_store_mode(self, **kwargs):
if request.env.user.has_group('base.group_system'):
app_store = config.get('app_store')
return app_store if app_store in ['install', 'download', 'disable'] else 'download'
return False
@http.route(['/web/app_action'], type='json', auth="user")
def app_action(self, action='', module_name='', **kwargs):
if request.env.user.has_group('base.group_system'):
if module_name and action:
module = request.env['ir.module.module'].search([('state', '=', 'installed'), ('name', '=', module_name)], limit=1)
if module:
module.button_immediate_uninstall()
return {"success": "Module is successfully uninstalled."}
return {"error": "Module not found or Module already uninstalled."}
return False
@http.route(['/web/get_modules'], type='json', auth="user")
def get_modules(self, **kwargs):
if request.env.user.has_group('base.group_system'):
try:
modules = request.env['ir.module.module'].search_read([('state', '=', 'installed')], fields=['name'])
p = requests.post(server_url + '/flectrahq/get_modules', data=kwargs)
data = json.loads(p.content.decode('utf-8'))
data.update({
'installed_modules': [m['name'] for m in modules],
'store_url': server_url
})
return data
except:
return False
return False
@http.route(['/web/module_download/<int:id>'], type='http', auth="user", methods=['GET', 'POST'])
def app_download(self, id=None):
if request.env.user.has_group('base.group_system'):
dbuuid = request.env['ir.config_parameter'].get_param('database.uuid')
p = requests.get(server_url + '/flectrahq/get_module_zip/' + str(id) + '/1', params={'dbuuid': dbuuid})
try:
data = json.loads(p.content.decode('utf-8'))
if data.get('error', False):
return json.dumps(data)
except:
pass
name = p.headers.get('Content-Disposition', False)
if name and name.startswith('filename='):
zip = p.content
headers = [('Content-Type', 'application/zip'),
('Content-Length', len(zip)),
('Content-Disposition', p.headers.get('Content-Disposition', 'dummy') + '.tar.gz')]
response = request.make_response(zip, headers)
return response
else:
return request.not_found()
return request.not_found()
@http.route(['/web/app_download_install/<int:id>'], type='json', auth="user")
def app_download_install(self, id=None):
if request.env.user.has_group('base.group_system'):
IrModule = request.env['ir.module.module']
try:
res_get_details = requests.get(server_url + '/flectrahq/get_module_zip/' + str(id) + '/0')
module_file_details = json.loads(res_get_details.content.decode('utf-8'))
except:
return {"error": "Internal Server Error"}
finally:
res_download = requests.get(server_url + '/flectrahq/get_module_zip/' + str(id) + '/1')
downloaded_file_checksum = hashlib.sha1(res_download.content or b'').hexdigest()
if res_download.status_code == 200:
try:
data = json.loads(res_download.content.decode('utf-8'))
if data.get('error', False):
return data
except:
pass
if module_file_details['checksum'] == downloaded_file_checksum:
path = os.path.join(tmp_dir_path, module_file_details['name'])
try:
with open(path, 'wb') as f:
f.write(res_download.content)
with tarfile.open(path) as tar:
tar.extractall(data_dir)
except:
return {"error": "Internal Server Error"}
finally:
IrModule.update_list()
root.load_addons()
modules = IrModule.search([('name', '=', module_file_details['module_name'])], limit=1)
modules.button_immediate_install()
os.remove(path)
return {"success": "Module is successfully installed."}
else:
return {"error": "File Crash."}
return {"error": "Internal Server Error."}
return False
class WebClient(http.Controller): class WebClient(http.Controller):

View File

@ -2,63 +2,77 @@ flectra.define('web.Apps', function (require) {
"use strict"; "use strict";
var core = require('web.core'); var core = require('web.core');
var framework = require('web.framework');
var session = require('web.session');
var Widget = require('web.Widget'); var Widget = require('web.Widget');
var Dialog = require('web.Dialog');
var framework = require('web.framework');
var _t = core._t; var _t = core._t;
var QWeb = core.qweb;
var apps_client = null;
var Apps = Widget.extend({ var Apps = Widget.extend({
template: 'EmptyComponent', template: 'AppStore',
remote_action_tag: 'loempia.embed', events: {
failback_action_id: 'base.open_module_tree', 'click [app-action="download"]': '_onDownload',
'click [app-action="install"]': '_onInstall',
init: function(parent, action) { 'click [app-action="uninstall"]': '_onUninstall',
'click [app-action="view-info"]': '_onClickViewDetails',
'click .load-more': '_onLoadMore',
'click #try-again': '_onTryAgain',
'keypress #input-module-search': '_onEnterSearch',
'click #btn-module-search': '_onClickSearch',
'click .top': '_onClickTop',
},
init: function (parent, action) {
this._super(parent, action); this._super(parent, action);
var options = action.params || {}; var options = action.params || {};
this.params = options; // NOTE forwarded to embedded client action this.context = {};
this.params = options;
}, },
willStart: function () {
get_client: function() { var self = this;
// return the client via a deferred, resolved or rejected depending if var def = this._rpc({
// the remote host is available or not. route: '/web/get_app_store_mode',
var check_client_available = function(client) { });
var d = $.Deferred(); return $.when(def, this._super.apply(this, arguments)).then(function (mode) {
var i = new Image(); self.mode = mode;
i.onerror = function() { framework.blockUI();
d.reject(client); if (self.mode != 'disable') {
}; self._rpc({
i.onload = function() { route: '/web/get_modules',
d.resolve(client); }).done(function (data) {
}; if (data) {
var ts = new Date().getTime(); self.all_app = [];
i.src = _.str.sprintf('%s/web/static/src/img/sep-a.gif?%s', client.origin, ts); _.each(data.modules, function (categ) {
return d.promise(); self.all_app = self.all_app.concat(categ);
}; });
if (apps_client) { self.active_categ = data.categ[0][0];
return check_client_available(apps_client); self.store_url = data.store_url;
} else { self.search_categ = '';
return this._rpc({model: 'ir.module.module', method: 'get_apps_server'}) self.$el.html(QWeb.render('AppStore.Content', {
.then(function(u) { modules: data.modules,
var link = $(_.str.sprintf('<a href="%s"></a>', u))[0]; categ: data.categ,
var host = _.str.sprintf('%s//%s', link.protocol, link.host); installed_modules: data.installed_modules,
var dbname = link.pathname; mode: self.mode,
if (dbname[0] === '/') { store_url: data.store_url
dbname = dbname.substr(1); }));
if (data.banner) {
self.$el.find('.banner').html(data.banner);
} else {
self.$el.find('.banner').html('<h2 class="text-center">FlectraHQ Store</h2>');
}
self.$el.find('ul.nav.category a:first').tab('show');
self.$el.find('a[data-toggle="tab"]').on('shown.bs.tab', self._onChangeTab.bind(self));
self.context.categ = self.prepareContext(data.categ);
self.context.limit = data.limit;
} else {
self.$el.html(QWeb.render('AppStore.TryError', {}));
} }
var client = { framework.unblockUI();
origin: host,
dbname: dbname
};
apps_client = client;
return check_client_available(client);
}); });
} }
});
}, },
destroy: function () {
destroy: function() {
$(window).off("message." + this.uniq); $(window).off("message." + this.uniq);
if (this.$ifr) { if (this.$ifr) {
this.$ifr.remove(); this.$ifr.remove();
@ -66,97 +80,189 @@ var Apps = Widget.extend({
} }
return this._super(); return this._super();
}, },
start: function () {
_on_message: function($e) { if (this.mode == 'disable') {
var self = this, client = this.client, e = $e.originalEvent; this.$el.html(QWeb.render('AppStore.Disable', {}));
framework.unblockUI();
if (e.origin !== client.origin) {
return;
}
var dispatcher = {
'event': function(m) { self.trigger('message:' + m.event, m); },
'action': function(m) {
self.do_action(m.action).then(function(r) {
var w = self.$ifr[0].contentWindow;
w.postMessage({id: m.id, result: r}, client.origin);
});
},
'rpc': function(m) {
return self._rpc({route: m.args[0], params: m.args[1]}).then(function(r) {
var w = self.$ifr[0].contentWindow;
w.postMessage({id: m.id, result: r}, client.origin);
});
},
'Model': function(m) {
return self._rpc({model: m.model, method: m.args[0], args: m.args[1]})
.then(function(r) {
var w = self.$ifr[0].contentWindow;
w.postMessage({id: m.id, result: r}, client.origin);
});
},
};
// console.log(e.data);
if (!_.isObject(e.data)) { return; }
if (dispatcher[e.data.type]) {
dispatcher[e.data.type](e.data);
} }
return this._super.apply(this, arguments);
}, },
prepareContext: function (data) {
start: function() { var context = {};
var self = this; _.each(data, function (value) {
var def = $.Deferred(); context[value[0]] = {
self.get_client().then(function(client) { offset: 0,
self.client = client; search: ''
var qs = {db: client.dbname};
if (session.debug) {
qs.debug = session.debug;
} }
var u = $.param.querystring(client.origin + "/apps/embed/client", qs);
var css = {width: '100%', height: '750px'};
self.$ifr = $('<iframe>').attr('src', u);
self.uniq = _.uniqueId('apps');
$(window).on("message." + self.uniq, self.proxy('_on_message'));
self.on('message:ready', self, function(m) {
var w = this.$ifr[0].contentWindow;
var act = {
type: 'ir.actions.client',
tag: this.remote_action_tag,
params: _.extend({}, this.params, {
db: session.db,
origin: session.origin,
})
};
w.postMessage({type:'action', action: act}, client.origin);
});
self.on('message:set_height', self, function(m) {
this.$ifr.height(m.height);
});
self.on('message:blockUI', self, function() { framework.blockUI(); });
self.on('message:unblockUI', self, function() { framework.unblockUI(); });
self.on('message:warn', self, function(m) {self.do_warn(m.title, m.message, m.sticky); });
self.$ifr.appendTo(self.$el).css(css).addClass('apps-client');
def.resolve();
}, function() {
self.do_warn(_t('Flectra Apps will be available soon'), _t('Showing locally available modules'), true);
return self._rpc({
route: '/web/action/load',
params: {action_id: self.failback_action_id},
}).then(function(action) {
return self.do_action(action);
}).always(function () {
def.reject();
});
}); });
return def; return context
}, },
_openDialogAfterAction: function (data) {
if (!_.isEmpty(data)) {
var buttons = [];
if (data.success) {
buttons.push({
text: _t("Refresh"),
classes: 'btn-success',
click: function (e) {
window.location.reload();
},
close: true
});
}
buttons.push({
text: _t("Cancel"),
classes: 'btn-warning',
close: true,
});
var dialog = new Dialog(this, {
size: 'medium',
buttons: buttons,
$content: $("<h4>" + (data.error || data.success) + "</h4>"),
title: _t(data.error ? "Error" : "Message"),
});
dialog.open();
}
},
_onUninstall: function (e) {
e.preventDefault();
var self = this;
var id = $(e.target).data("module-id");
var data = _.findWhere(this.all_app, {id: id});
if (!_.isEmpty(data)) {
this._rpc({
route: '/web/app_action',
params: {
action: "uninstall",
module_name: data['technical_name'],
},
}).then(function (data) {
self._openDialogAfterAction(data);
})
}
},
_onDownload: function (e) {
e.preventDefault();
var href = $(e.currentTarget).attr('href').trim();
if (href) {
window.location = href;
}
},
_onInstall: function (e) {
e.preventDefault();
var self = this;
var id = $(e.target).data("module-id");
self._rpc({
route: '/web/app_download_install/' + id,
}).then(function (data) {
self._openDialogAfterAction(data);
});
},
_onChangeTab: function (e) {
this.active_categ = $(e.target).data("category-id");
this.$el.find('#input-module-search').val(this.context.categ[this.active_categ]['search']);
},
_onLoadMore: function (e) {
e.preventDefault();
var self = this;
framework.blockUI();
this.context.categ[this.active_categ]['offset'] += this.context.limit;
this._rpc({
route: '/web/get_modules',
params: {
offset: this.context.categ[this.active_categ]['offset'],
categ: this.active_categ,
search: this.context.categ[self.active_categ]['search']
}
}).done(function (data) {
if (data) {
self.$el.find('#' + self.active_categ + " .module-kanban:last")
.after(QWeb.render('AppStore.ModuleBoxContainer', {
modules: data.modules[self.active_categ],
installed_modules: data.installed_modules,
mode: self.mode,
store_url: data.store_url
}));
if (!_.isEmpty(data.modules[self.active_categ])) {
self.$el.find('#' + self.active_categ + " .load-more").show();
} else {
self.$el.find('#' + self.active_categ + " .load-more").hide();
}
} else {
self.$el.html(QWeb.render('AppStore.TryError', {}));
}
framework.unblockUI();
});
},
_onTryAgain: function (e) {
e.preventDefault();
var self = this;
this._rpc({
route: '/web/action/load',
params: {action_id: "base.modules_act_cl"},
}).then(function (action) {
self.do_action(action);
});
},
_onEnterSearch: function (e) {
if (e.keyCode == 13) {
e.preventDefault();
this._onModuleSearch(e);
}
},
_onClickSearch: function (e) {
e.preventDefault();
this._onModuleSearch(e);
},
_onModuleSearch: function (e) {
var search = this.$el.find('#input-module-search').val().trim();
var self = this;
framework.blockUI();
this._rpc({
route: '/web/get_modules',
params: {
offset: 0,
categ: this.active_categ,
search: search
}
}).done(function (data) {
if (data) {
self.$el.find('#' + self.active_categ).find('.module-kanban').remove();
self.context.categ[self.active_categ]['search'] = search;
$(QWeb.render('AppStore.ModuleBoxContainer', {
modules: data.modules[self.active_categ],
installed_modules: data.installed_modules,
mode: self.mode,
store_url: data.store_url
})).prependTo(self.$el.find('#' + self.active_categ + " .o_kanban_view"));
if (!_.isEmpty(data.modules[self.active_categ])) {
self.$el.find('#' + self.active_categ + " .load-more").show().next('h3').remove();
} else {
self.$el.find('#' + self.active_categ + " .load-more").hide().after('<h3>No such module(s) found.</h3>');
}
} else {
self.$el.html(QWeb.render('AppStore.TryError', {}));
}
framework.unblockUI();
});
},
_onClickViewDetails: function (e) {
e.preventDefault();
var id = $(e.currentTarget).data('module-id');
var data = _.findWhere(this.all_app, {id: id});
if (!_.isEmpty(data)) {
var dialog = new Dialog(this, {
size: 'medium',
$content: $(QWeb.render('AppStore.ViewDetails', {app: data, store_url: this.store_url})),
title: _t('Module'),
});
dialog.open();
}
},
_onClickTop: function (e) {
e.preventDefault();
this.$el.parents('.o_content').animate({scrollTop:0}, 500, 'swing');
}
}); });
var AppsUpdates = Apps.extend({ var AppsUpdates = Apps.extend({

View File

@ -0,0 +1,90 @@
.f_app_store_container {
.f_app_title {
background-color: mix(@brand-primary, #ffffff, 30%);
padding: 5px 10px;
margin: 0;
}
.top {
position: fixed;
bottom: 25px;
right: 25px;
background: @brand-primary;
padding: 10px 15px;
font-size: 20px;
z-index: 2;
color: #ffffff;
cursor: pointer;
}
ul.nav.category {
border-top: 1px solid #dddddd;
background-color: mix(@brand-primary, #ffffff, 30%);
overflow-x: auto;
overflow-y: hidden;
.o-flex-display();
li.active > a {
border-color: transparent;
border-bottom: 3px solid @brand-primary;
background-color: unset;
&:hover, &:focus {
border-bottom: 3px solid @brand-primary;
background-color: unset;
}
}
li {
float: none;
> a:hover, > a:focus {
border-color: transparent;
border-radius: 0;
background-color: mix(@brand-primary, #ffffff, 50%);
}
}
@media (max-width: @screen-sm-max) {
border-top: none;
}
}
ul.search-box {
background-color: mix(@brand-primary, #ffffff, 30%);
float: right;
border-top: 1px solid #dddddd;
> li {
padding: 5px 20px;
width: 260px;
#input-module-search {
border-radius: 0;
}
@media (max-width: @screen-sm-max) {
width: 100%;
}
}
@media (max-width: @screen-sm-max) {
float: none;
border-bottom: none;
}
}
.o_kanban_view {
width: 100%;
.o_kanban_record {
cursor: auto;
border-color: #777777;
.o_dropdown_kanban {
visibility: visible;
}
.oe_module_desc {
.oe_module_action {
.o_module_tech_name {
display: inline-block;
width: 140px;
white-space: nowrap;
overflow: hidden !important;
text-overflow: ellipsis;
}
}
}
.oe_module_footer {
border-top: 1px solid @flectra-color-silver-darker;
margin-top: 8px;
padding-top: 8px;
}
}
}
}

View File

@ -1443,4 +1443,194 @@
</div> </div>
</t> </t>
<t t-name="AppStore">
<div class="f_app_store_container">
<t t-call="AppStore.Content"/>
</div>
</t>
<t t-name="AppDownload">
<div>
<div class="progress" id="app_1">
<div class="progress-bar progress-bar-striped active"
role="progressbar"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
style="width:0%">
0%
</div>
</div>
</div>
</t>
<t t-name="AppStore.RatingStart">
<t t-set="val_integer"
t-value="m.rate"/>
<t t-set="val_decimal"
t-value="0"/>
<t t-set="empty_star"
t-value="5 - val_integer"/>
<div class="o_website_rating_static">
<t t-foreach="_.range(0,val_integer)"
t-as="num">
<i class="fa fa-star"/>
</t>
<t t-if="val_decimal">
<i class="fa fa-star-half-o"/>
</t>
<t t-foreach="_.range(0,empty_star)"
t-as="num">
<i class="fa fa-star-o"/>
</t>
</div>
</t>
<t t-name="AppStore.ModuleBox">
<div class="module-kanban oe_module_vignette oe_kanban_global_click o_kanban_record">
<div t-if="!m.price" class="o_dropdown_kanban dropdown">
<a class="dropdown-toggle btn" data-toggle="dropdown" href="#" aria-expanded="true">
<span class="fa fa-ellipsis-v"/>
</a>
<ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
<t t-set="is_install" t-value="installed_modules.includes(m.technical_name)"/>
<li><a t-if="is_install and mode == 'install'" app-action="uninstall" t-att-data-module-id="m.id" href="#" class=" oe_kanban_action oe_kanban_action_a">Uninstall</a></li>
<li><a app-action="download" t-att-data-module-id="m.id" t-att-href="m.price ? 'javascript:void(0);' : '/web/module_download/' + m.id" class=" oe_kanban_action oe_kanban_action_a">Download</a></li>
<li><a app-action="view-info" t-att-data-module-id="m.id" href="#" class="oe_kanban_action oe_kanban_action_a">View Info</a></li>
<li><a t-attf-href="{{ store_url }}/apps/{{ m.version }}/{{ m.technical_name }}" target="_blank" class=" oe_kanban_action oe_kanban_action_a">View More on Store</a></li>
</ul>
</div>
<img class="oe_module_icon" t-attf-src="{{ store_url }}/web/image/griffin.module/{{ m.id }}/module_icon/120x120"/>
<div class="oe_module_desc" t-att-title="m.name">
<h4 class="o_kanban_record_title"><span t-esc="m.name"/></h4>
<p class="oe_module_name">
<span><t t-esc="m.description or ''"/></span>
</p>
<div class="oe_module_action">
<span class="pull-left"><t t-esc="m.price ? m.price + ' ' + m.currency[1] : 'Free'"/></span><br/>
<code class="pull-left o_module_tech_name"><small><span><t t-esc="m.technical_name"/></span></small></code>
<div class="text-right">
<t t-if="is_install">
<span>Installed</span>
</t>
<t t-else="">
<t t-if="m.price">
<a t-attf-href="{{ store_url }}/apps/{{ m.version }}/{{ m.technical_name }}" target="_blank" class="btn btn-primary btn-sm oe_kanban_action oe_kanban_action_button">
<span><i class="fa fa-globe"/></span>
</a>
</t>
<t t-else="">
<button t-if="mode == 'install'" app-action="install" t-att-data-module-id="m.id" class="btn btn-primary btn-sm oe_kanban_action oe_kanban_action_button">Install</button>
<a t-if="mode == 'download'" app-action="download" t-att-href="m.price ? 'javascript:void(0);' : '/web/module_download/' + m.id " t-att-data-module-id="m.id" class="btn btn-primary btn-sm oe_kanban_action oe_kanban_action_button">
<span><i class="fa fa-download"/></span>
</a>
</t>
</t>
</div>
</div>
</div>
<div class="oe_module_footer">
<div>
<span>
<i class="fa fa-download"/> (<span t-esc="m.download_count"/>)
</span>
<span class="pull-right"><t t-call="AppStore.RatingStart"/></span>
</div>
<span><span><t t-esc="m.author"/></span></span>
</div>
</div>
</t>
<t t-name="AppStore.ModuleBoxContainer">
<t t-foreach="modules" t-as="m">
<t t-call="AppStore.ModuleBox"/>
</t>
</t>
<t t-name="AppStore.Content">
<div t-if="modules" class="o_view_manager_content">
<div class="container-fluid">
<div class="banner"/>
<ul class="nav nav-tabs search-box">
<li>
<div class="input-group">
<input id="input-module-search" type="text" class="form-control" name="input-module-search" placeholder="Search Modules"/>
<span class = "input-group-btn">
<button id="btn-module-search" class="btn btn-default" type = "button"><i class="fa fa-search"/></button>
</span>
</div>
</li>
</ul>
<ul class="nav nav-tabs category">
<t t-foreach="categ" t-as="c">
<li><a t-att-data-category-id="c[0]" data-toggle="tab" t-att-href="'#' + c[0]"><t t-esc="c[1]"/></a></li>
</t>
</ul>
<div class="tab-content">
<t t-foreach="categ" t-as="c">
<div t-att-id="c[0]" class="tab-pane fade">
<div class="o_kanban_view o_kanban_ungrouped">
<t t-foreach="modules[c[0]]" t-as="m">
<t t-call="AppStore.ModuleBox"/>
</t>
<div class="o_kanban_record o_kanban_ghost"/>
<div class="o_kanban_record o_kanban_ghost"/>
<div class="o_kanban_record o_kanban_ghost"/>
</div>
<div class="text-center mb16">
<div t-att-data-category-name="c[0]" class="btn btn-primary load-more">View More Modules</div>
</div>
</div>
</t>
</div>
<span class="top"><i class="fa fa-arrow-up"/></span>
</div>
</div>
</t>
<t t-name="AppStore.TryError">
<div class="text-center mt32 mb32">
<h3>Unable to connect with FlectraHQ store.</h3>
<a class="btn btn-danger" id="try-again">Try Again</a>
</div>
</t>
<t t-name="AppStore.ViewDetails">
<div>
<div class="container-fluid">
<t t-set="m" t-value="app"/>
<div class="col-md-12 col-xs-12">
<img class="oe_module_icon" t-attf-src="{{ store_url }}/web/image/griffin.module/{{ m.id }}/module_icon/120x120"/>
<div class="oe_module_desc">
<h4 class="o_kanban_record_title"><span t-esc="m.name"/></h4>
<p class="oe_module_name">
<span><t t-esc="m.description or ''"/></span>
</p>
<div class="oe_module_action">
<span><t t-esc="m.price ? m.price + ' ' + m.currency[1] : 'Free'"/></span><br/>
<code><small><span><t t-esc="m.technical_name"/></span></small></code>
</div>
</div>
<div class="oe_module_footer">
<span><b>Version: </b><t t-esc="m.version"/></span><br/>
<span><b>license: </b><t t-esc="m.license"/></span><br/>
<span><b>Dependent Module: </b><t t-esc="m.depends"/></span><br/>
<span><b>Develop By: </b><span><t t-esc="m.author"/></span></span>
<div>
<span class="pull-left mr8">
<i class="fa fa-download"/> (<span t-esc="m.download_count"/>)
</span>
<t t-call="AppStore.RatingStart"/>
</div>
</div>
</div>
</div>
</div>
</t>
<t t-name="AppStore.Disable">
<div class="text-center mt32 mb32">
<h2>App Store Disable by Administrator.</h2>
</div>
</t>
</templates> </templates>

View File

@ -252,6 +252,7 @@
<!-- @Flectra: Gantt View Assets ::: End --> <!-- @Flectra: Gantt View Assets ::: End -->
<link rel="stylesheet" type="text/less" href="/web/static/src/less/backend_theme/bootswatch_dark.less"/> <link rel="stylesheet" type="text/less" href="/web/static/src/less/backend_theme/bootswatch_dark.less"/>
<link rel="stylesheet" type="text/less" href="/web/static/src/less/apps.less"/>
<link rel="stylesheet" type="text/less" href="/web/static/src/less/backend_theme/menu_launcher.less"/> <link rel="stylesheet" type="text/less" href="/web/static/src/less/backend_theme/menu_launcher.less"/>
<link rel="stylesheet" type="text/less" href="/web/static/src/less/backend_theme/navbar.less"/> <link rel="stylesheet" type="text/less" href="/web/static/src/less/backend_theme/navbar.less"/>
<link rel="stylesheet" type="text/less" href="/web/static/src/less/backend_theme/flectra_style.less"/> <link rel="stylesheet" type="text/less" href="/web/static/src/less/backend_theme/flectra_style.less"/>

View File

@ -10,7 +10,9 @@ import logging
import optparse import optparse
import os import os
import sys import sys
import shutil
import flectra import flectra
from contextlib import closing
from .. import release, conf, loglevels from .. import release, conf, loglevels
from . import appdirs, pycompat from . import appdirs, pycompat
@ -276,6 +278,7 @@ class configmanager(object):
help="Use the unaccent function provided by the database when available.") help="Use the unaccent function provided by the database when available.")
group.add_option("--geoip-db", dest="geoip_database", my_default='/usr/share/GeoIP/GeoLiteCity.dat', group.add_option("--geoip-db", dest="geoip_database", my_default='/usr/share/GeoIP/GeoLiteCity.dat',
help="Absolute path to the GeoIP database file.") help="Absolute path to the GeoIP database file.")
group.add_option("--app-store", dest="app_store", help="specify the option to enable app store. Accepted values: [install|download|disable]", my_default='install')
parser.add_option_group(group) parser.add_option_group(group)
if os.name == 'posix': if os.name == 'posix':
@ -403,7 +406,8 @@ class configmanager(object):
'db_maxconn', 'import_partial', 'addons_path', 'db_maxconn', 'import_partial', 'addons_path',
'syslog', 'without_demo', 'syslog', 'without_demo',
'dbfilter', 'log_level', 'log_db', 'dbfilter', 'log_level', 'log_db',
'log_db_level', 'geoip_database', 'dev_mode', 'shell_interface' 'log_db_level', 'geoip_database', 'dev_mode', 'shell_interface',
'app_store'
] ]
for arg in keys: for arg in keys:
@ -602,6 +606,15 @@ class configmanager(object):
def __getitem__(self, key): def __getitem__(self, key):
return self.options[key] return self.options[key]
def copytree(self, src, dst, symlinks=False, ignore=None):
for item in os.listdir(src):
s = os.path.join(src, item)
d = os.path.join(dst, item)
if os.path.isdir(s):
shutil.copytree(s, d, symlinks, ignore)
else:
shutil.copy2(s, d)
@property @property
def addons_data_dir(self): def addons_data_dir(self):
add_dir = os.path.join(self['data_dir'], 'addons') add_dir = os.path.join(self['data_dir'], 'addons')
@ -612,9 +625,31 @@ class configmanager(object):
if not os.path.exists(add_dir): if not os.path.exists(add_dir):
os.makedirs(add_dir, 0o700) os.makedirs(add_dir, 0o700)
# try to make +rx placeholder dir, will need manual +w to activate it # try to make +rx placeholder dir, will need manual +w to activate it
os.makedirs(d, 0o500) os.makedirs(d, 0o700)
except OSError: except OSError:
logging.getLogger(__name__).debug('Failed to create addons data dir %s', d) logging.getLogger(__name__).debug('Failed to create addons data dir %s', d)
try:
if not os.listdir(os.path.join(d)):
from flectra.sql_db import db_connect
with closing(db_connect(self.get('db_name')).cursor()) as cr:
if flectra.tools.table_exists(cr, 'ir_module_module'):
cr.execute("SELECT latest_version FROM ir_module_module WHERE name=%s", ('base',))
base_version = cr.fetchone()
if base_version and base_version[0]:
tmp = base_version[0].split('.')[:2]
last_version = '.'.join(str(v) for v in tmp)
s = os.path.join(add_dir, last_version)
if float(last_version) < float(release.series) and os.listdir(os.path.join(s)):
self.copytree(s, d)
if self.get('app_store') == 'install':
if not os.access(d, os.W_OK):
os.chmod(d, 0o700)
else:
if os.access(d, os.W_OK):
os.chmod(d, 0o500)
except OSError:
logging.getLogger(__name__).debug("No such file or directory: %s", d)
return d return d
@property @property