[IMP] Flectra App Store: User can direct install/download apps from flectraHQ store.
This commit is contained in:
parent
02c5ecba43
commit
77b8adfaab
@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
import tarfile
|
||||
|
||||
import babel.messages.pofile
|
||||
import base64
|
||||
@ -45,6 +46,11 @@ from flectra.http import content_disposition, dispatch_rpc, request, \
|
||||
from flectra.exceptions import AccessError, UserError
|
||||
from flectra.models import check_method_name
|
||||
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__)
|
||||
|
||||
@ -433,6 +439,12 @@ def binary_content(xmlid=None, model='ir.attachment', id=None, field='datas', un
|
||||
#----------------------------------------------------------
|
||||
# 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):
|
||||
|
||||
@http.route('/', type='http', auth="none")
|
||||
@ -549,6 +561,104 @@ class Home(http.Controller):
|
||||
options=context)
|
||||
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):
|
||||
|
@ -2,62 +2,76 @@ flectra.define('web.Apps', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var framework = require('web.framework');
|
||||
var session = require('web.session');
|
||||
var Widget = require('web.Widget');
|
||||
var Dialog = require('web.Dialog');
|
||||
var framework = require('web.framework');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
var apps_client = null;
|
||||
var QWeb = core.qweb;
|
||||
|
||||
var Apps = Widget.extend({
|
||||
template: 'EmptyComponent',
|
||||
remote_action_tag: 'loempia.embed',
|
||||
failback_action_id: 'base.open_module_tree',
|
||||
|
||||
template: 'AppStore',
|
||||
events: {
|
||||
'click [app-action="download"]': '_onDownload',
|
||||
'click [app-action="install"]': '_onInstall',
|
||||
'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);
|
||||
var options = action.params || {};
|
||||
this.params = options; // NOTE forwarded to embedded client action
|
||||
this.context = {};
|
||||
this.params = options;
|
||||
},
|
||||
|
||||
get_client: function() {
|
||||
// return the client via a deferred, resolved or rejected depending if
|
||||
// the remote host is available or not.
|
||||
var check_client_available = function(client) {
|
||||
var d = $.Deferred();
|
||||
var i = new Image();
|
||||
i.onerror = function() {
|
||||
d.reject(client);
|
||||
};
|
||||
i.onload = function() {
|
||||
d.resolve(client);
|
||||
};
|
||||
var ts = new Date().getTime();
|
||||
i.src = _.str.sprintf('%s/web/static/src/img/sep-a.gif?%s', client.origin, ts);
|
||||
return d.promise();
|
||||
};
|
||||
if (apps_client) {
|
||||
return check_client_available(apps_client);
|
||||
willStart: function () {
|
||||
var self = this;
|
||||
var def = this._rpc({
|
||||
route: '/web/get_app_store_mode',
|
||||
});
|
||||
return $.when(def, this._super.apply(this, arguments)).then(function (mode) {
|
||||
self.mode = mode;
|
||||
framework.blockUI();
|
||||
if (self.mode != 'disable') {
|
||||
self._rpc({
|
||||
route: '/web/get_modules',
|
||||
}).done(function (data) {
|
||||
if (data) {
|
||||
self.all_app = [];
|
||||
_.each(data.modules, function (categ) {
|
||||
self.all_app = self.all_app.concat(categ);
|
||||
});
|
||||
self.active_categ = data.categ[0][0];
|
||||
self.store_url = data.store_url;
|
||||
self.search_categ = '';
|
||||
self.$el.html(QWeb.render('AppStore.Content', {
|
||||
modules: data.modules,
|
||||
categ: data.categ,
|
||||
installed_modules: data.installed_modules,
|
||||
mode: self.mode,
|
||||
store_url: data.store_url
|
||||
}));
|
||||
if (data.banner) {
|
||||
self.$el.find('.banner').html(data.banner);
|
||||
} else {
|
||||
return this._rpc({model: 'ir.module.module', method: 'get_apps_server'})
|
||||
.then(function(u) {
|
||||
var link = $(_.str.sprintf('<a href="%s"></a>', u))[0];
|
||||
var host = _.str.sprintf('%s//%s', link.protocol, link.host);
|
||||
var dbname = link.pathname;
|
||||
if (dbname[0] === '/') {
|
||||
dbname = dbname.substr(1);
|
||||
self.$el.find('.banner').html('<h2 class="text-center">FlectraHQ Store</h2>');
|
||||
}
|
||||
var client = {
|
||||
origin: host,
|
||||
dbname: dbname
|
||||
};
|
||||
apps_client = client;
|
||||
return check_client_available(client);
|
||||
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', {}));
|
||||
}
|
||||
framework.unblockUI();
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
$(window).off("message." + this.uniq);
|
||||
if (this.$ifr) {
|
||||
@ -66,97 +80,189 @@ var Apps = Widget.extend({
|
||||
}
|
||||
return this._super();
|
||||
},
|
||||
|
||||
_on_message: function($e) {
|
||||
var self = this, client = this.client, e = $e.originalEvent;
|
||||
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
start: function () {
|
||||
var self = this;
|
||||
var def = $.Deferred();
|
||||
self.get_client().then(function(client) {
|
||||
self.client = client;
|
||||
|
||||
var qs = {db: client.dbname};
|
||||
if (session.debug) {
|
||||
qs.debug = session.debug;
|
||||
if (this.mode == 'disable') {
|
||||
this.$el.html(QWeb.render('AppStore.Disable', {}));
|
||||
framework.unblockUI();
|
||||
}
|
||||
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 this._super.apply(this, arguments);
|
||||
},
|
||||
prepareContext: function (data) {
|
||||
var context = {};
|
||||
_.each(data, function (value) {
|
||||
context[value[0]] = {
|
||||
offset: 0,
|
||||
search: ''
|
||||
}
|
||||
});
|
||||
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({
|
||||
|
90
addons/web/static/src/less/apps.less
Normal file
90
addons/web/static/src/less/apps.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1443,4 +1443,194 @@
|
||||
</div>
|
||||
</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>
|
||||
|
@ -252,6 +252,7 @@
|
||||
<!-- @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/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/navbar.less"/>
|
||||
<link rel="stylesheet" type="text/less" href="/web/static/src/less/backend_theme/flectra_style.less"/>
|
||||
|
@ -10,7 +10,9 @@ import logging
|
||||
import optparse
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import flectra
|
||||
from contextlib import closing
|
||||
from .. import release, conf, loglevels
|
||||
from . import appdirs, pycompat
|
||||
|
||||
@ -276,6 +278,7 @@ class configmanager(object):
|
||||
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',
|
||||
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)
|
||||
|
||||
if os.name == 'posix':
|
||||
@ -403,7 +406,8 @@ class configmanager(object):
|
||||
'db_maxconn', 'import_partial', 'addons_path',
|
||||
'syslog', 'without_demo',
|
||||
'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:
|
||||
@ -602,6 +606,15 @@ class configmanager(object):
|
||||
def __getitem__(self, 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
|
||||
def addons_data_dir(self):
|
||||
add_dir = os.path.join(self['data_dir'], 'addons')
|
||||
@ -612,9 +625,31 @@ class configmanager(object):
|
||||
if not os.path.exists(add_dir):
|
||||
os.makedirs(add_dir, 0o700)
|
||||
# try to make +rx placeholder dir, will need manual +w to activate it
|
||||
os.makedirs(d, 0o500)
|
||||
os.makedirs(d, 0o700)
|
||||
except OSError:
|
||||
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
|
||||
|
||||
@property
|
||||
|
Loading…
Reference in New Issue
Block a user