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

This commit is contained in:
Chintan Ambaliya 2018-10-12 18:24:18 +05:30 committed by chintan-ambaliya
parent 02c5ecba43
commit 77b8adfaab
6 changed files with 666 additions and 134 deletions

View File

@ -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):

View File

@ -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({

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>
</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>

View File

@ -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"/>

View File

@ -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