Skip to content

Commit

Permalink
Finish 0.5.2
Browse files Browse the repository at this point in the history
  • Loading branch information
malnvenshorn committed Jan 11, 2018
2 parents d32c419 + 6ae577b commit 0cbd924
Show file tree
Hide file tree
Showing 17 changed files with 388 additions and 282 deletions.
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,15 @@ If you have questions or encounter issues please take a look at the [Frequently

## Setup

1. Install dependencies with:

`pacman -Sy postgresql-libs` on Arch Linux

`apt-get install libpq-dev` on Debian/Raspbian

1. Install this plugin via the bundled [Plugin Manager](https://github.com/foosel/OctoPrint/wiki/Plugin:-Plugin-Manager)
or manually using this URL:

`https://github.com/malnvenshorn/OctoPrint-FilamentManager/archive/master.zip`

1. For PostgreSQL support you need to install an additional dependency:

`pip install psycopg2`

## Screenshots

![FilamentManager Sidebar](screenshots/filamentmanager_sidebar.png?raw=true)
Expand Down
2 changes: 1 addition & 1 deletion octoprint_filamentmanager/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def migrate_database_schema(self, target, current):
def on_after_startup(self):
# subscribe to the notify channel so that we get notified if another client has altered the data
# notify is not available if we are connected to the internal sqlite database
if self.filamentManager.notify is not None:
if self.filamentManager is not None and self.filamentManager.notify is not None:
def notify(pid, channel, payload):
# ignore notifications triggered by our own connection
if pid != self.filamentManager.conn.connection.get_backend_pid():
Expand Down
31 changes: 31 additions & 0 deletions octoprint_filamentmanager/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,3 +373,34 @@ def unzip(filename, extract_dir):
.format(path=tempdir, message=str(e)))

return make_response("", 204)

@octoprint.plugin.BlueprintPlugin.route("/database/test", methods=["POST"])
@restricted_access
def test_database_connection(self):
if "application/json" not in request.headers["Content-Type"]:
return make_response("Expected content-type JSON", 400)

try:
json_data = request.json
except BadRequest:
return make_response("Malformed JSON body in request", 400)

if "config" not in json_data:
return make_response("No database configuration included in request", 400)

config = json_data["config"]

for key in ["uri", "name", "user", "password"]:
if key not in config:
return make_response("Configuration does not contain mandatory '{}' field".format(key), 400)

try:
connection = self.filamentManager.connect(config["uri"],
database=config["name"],
username=config["user"],
password=config["password"])
except Exception as e:
return make_response("Failed to connect to the database with the given configuration", 400)
else:
connection.close()
return make_response("", 204)
67 changes: 41 additions & 26 deletions octoprint_filamentmanager/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
from multiprocessing import Lock

from backports import csv
from uritools import uricompose, urisplit
from uritools import urisplit
from sqlalchemy.engine.url import URL
from sqlalchemy import create_engine, event, text
from sqlalchemy.schema import MetaData, Table, Column, ForeignKeyConstraint, DDL, PrimaryKeyConstraint
from sqlalchemy.sql import insert, update, delete, select, label
Expand All @@ -26,35 +27,49 @@ class FilamentManager(object):
DIALECT_POSTGRESQL = "postgresql"

def __init__(self, config):
if not set(("uri", "name", "user", "password")).issubset(config):
raise ValueError("Incomplete config dictionary")
self.notify = None
self.conn = self.connect(config.get("uri", ""),
database=config.get("name", ""),
username=config.get("user", ""),
password=config.get("password", ""))

# QUESTION thread local connection (pool) vs sharing a serialized connection, pro/cons?
# from sqlalchemy.orm import sessionmaker, scoped_session
# Session = scoped_session(sessionmaker(bind=engine))
# when using a connection pool how do we prevent notifiying ourself on database changes?
self.lock = Lock()
self.notify = None

uri_parts = urisplit(config["uri"])

if self.DIALECT_SQLITE == uri_parts.scheme:
self.engine = create_engine(config["uri"], connect_args={"check_same_thread": False})
self.conn = self.engine.connect()
if self.engine_dialect_is(self.DIALECT_SQLITE):
# Enable foreign key constraints
self.conn.execute(text("PRAGMA foreign_keys = ON").execution_options(autocommit=True))
elif self.DIALECT_POSTGRESQL == uri_parts.scheme:
uri = uricompose(scheme=uri_parts.scheme, host=uri_parts.host, port=uri_parts.getport(default=5432),
path="/{}".format(config["name"]),
userinfo="{}:{}".format(config["user"], config["password"]))
self.engine = create_engine(uri)
self.conn = self.engine.connect()
self.notify = PGNotify(uri)
elif self.engine_dialect_is(self.DIALECT_POSTGRESQL):
# Create listener thread
self.notify = PGNotify(self.conn.engine.url)

def connect(self, uri, database="", username="", password=""):
uri_parts = urisplit(uri)

if uri_parts.scheme == self.DIALECT_SQLITE:
engine = create_engine(uri, connect_args={"check_same_thread": False})
elif uri_parts.scheme == self.DIALECT_POSTGRESQL:
uri = URL(drivername=uri_parts.scheme,
host=uri_parts.host,
port=uri_parts.getport(default=5432),
database=database,
username=username,
password=password)
engine = create_engine(uri)
else:
raise ValueError("Engine '{engine}' not supported".format(engine=uri_parts.scheme))

return engine.connect()

def close(self):
self.conn.close()

def engine_dialect_is(self, dialect):
return self.conn.engine.dialect.name == dialect if self.conn is not None else False

def initialize(self):
metadata = MetaData()

Expand Down Expand Up @@ -91,7 +106,7 @@ def initialize(self):
Column("changed_at", TIMESTAMP, nullable=False,
server_default=text("CURRENT_TIMESTAMP")))

if self.DIALECT_POSTGRESQL == self.engine.dialect.name:
if self.engine_dialect_is(self.DIALECT_POSTGRESQL):
def should_create_function(name):
row = self.conn.execute("select proname from pg_proc where proname = '%s'" % name).scalar()
return not bool(row)
Expand Down Expand Up @@ -128,7 +143,7 @@ def should_create_trigger(name):
if should_create_trigger(name):
event.listen(metadata, "after_create", trigger)

elif self.DIALECT_SQLITE == self.engine.dialect.name:
elif self.engine_dialect_is(self.DIALECT_SQLITE):
for table in [self.profiles.name, self.spools.name]:
for action in ["INSERT", "UPDATE", "DELETE"]:
name = "{table}_on_{action}".format(table=table, action=action.lower())
Expand Down Expand Up @@ -293,10 +308,10 @@ def get_selection(self, identifier, client_id):
def update_selection(self, identifier, client_id, data):
with self.lock, self.conn.begin():
values = dict()
if self.engine.dialect.name == self.DIALECT_SQLITE:
if self.engine_dialect_is(self.DIALECT_SQLITE):
stmt = insert(self.selections).prefix_with("OR REPLACE")\
.values(tool=identifier, client_id=client_id, spool_id=data["spool"]["id"])
elif self.engine.dialect.name == self.DIALECT_POSTGRESQL:
elif self.engine_dialect_is(self.DIALECT_POSTGRESQL):
stmt = pg_insert(self.selections)\
.values(tool=identifier, client_id=client_id, spool_id=data["spool"]["id"])\
.on_conflict_do_update(constraint="selections_pkey", set_=dict(spool_id=data["spool"]["id"]))
Expand Down Expand Up @@ -327,23 +342,23 @@ def from_csv(table):
for row in csv_reader:
values = dict(zip(header, row))

if self.engine.dialect.name == self.DIALECT_SQLITE:
if self.engine_dialect_is(self.DIALECT_SQLITE):
identifier = values[table.c.id]
# try to update entry
stmt = update(table).values(values).where(table.c.id == identifier)
if self.conn.execute(stmt).rowcount == 0:
# identifier doesn't match any => insert new entry
stmt = insert(table).values(values)
self.conn.execute(stmt)
elif self.engine.dialect.name == self.DIALECT_POSTGRESQL:
elif self.engine_dialect_is(self.DIALECT_POSTGRESQL):
stmt = pg_insert(table).values(values)\
.on_conflict_do_update(index_elements=[table.c.id], set_=values)
self.conn.execute(stmt)

if self.DIALECT_POSTGRESQL == self.engine.dialect.name:
# update sequences
self.conn.execute(text("SELECT setval('profiles_id_seq', max(id)) FROM profiles"))
self.conn.execute(text("SELECT setval('spools_id_seq', max(id)) FROM spools"))
if self.engine_dialect_is(self.DIALECT_POSTGRESQL):
# update sequence
sql = "SELECT setval('{table}_id_seq', max(id)) FROM {table}".format(table=table.name)
self.conn.execute(text(sql))

tables = [self.profiles, self.spools]
for t in tables:
Expand Down
95 changes: 62 additions & 33 deletions octoprint_filamentmanager/static/js/filamentmanager.bundled.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,20 +73,17 @@ FilamentManager.prototype.core.bridge = function pluginBridge() {
self.core.bridge = {
allViewModels: {},

REQUIRED_VIEWMODELS: ['settingsViewModel', 'printerStateViewModel', 'loginStateViewModel', 'temperatureViewModel'],
REQUIRED_VIEWMODELS: ['settingsViewModel', 'printerStateViewModel', 'loginStateViewModel', 'temperatureViewModel', 'filesViewModel'],

BINDINGS: ['#settings_plugin_filamentmanager', '#settings_plugin_filamentmanager_profiledialog', '#settings_plugin_filamentmanager_spooldialog', '#settings_plugin_filamentmanager_configurationdialog', '#sidebar_plugin_filamentmanager_wrapper', '#plugin_filamentmanager_confirmationdialog'],

viewModel: function FilamentManagerViewModel(viewModels) {
self.core.bridge.allViewModels = _.object(self.core.bridge.REQUIRED_VIEWMODELS, viewModels);
self.core.callbacks.call(self);

self.viewModels.profiles.call(self);
self.viewModels.spools.call(self);
self.viewModels.selections.call(self);
self.viewModels.config.call(self);
self.viewModels.import.call(self);
self.viewModels.confirmation.call(self);
Object.values(self.viewModels).forEach(function (viewModel) {
return viewModel.call(self);
});

self.viewModels.profiles.updateCallbacks.push(self.viewModels.spools.requestSpools);
self.viewModels.profiles.updateCallbacks.push(self.viewModels.selections.requestSelectedSpools);
Expand All @@ -95,7 +92,6 @@ FilamentManager.prototype.core.bridge = function pluginBridge() {
self.viewModels.import.afterImportCallbacks.push(self.viewModels.spools.requestSpools);
self.viewModels.import.afterImportCallbacks.push(self.viewModels.selections.requestSelectedSpools);

self.viewModels.warning.call(self);
self.selectedSpools = self.viewModels.selections.selectedSpools; // for backwards compatibility
return self;
}
Expand All @@ -110,8 +106,6 @@ FilamentManager.prototype.core.callbacks = function octoprintCallbacks() {

self.onStartup = function onStartupCallback() {
self.viewModels.warning.replaceFilamentView();
self.viewModels.confirmation.replacePrintStart();
self.viewModels.confirmation.replacePrintResume();
};

self.onBeforeBinding = function onBeforeBindingCallback() {
Expand Down Expand Up @@ -220,11 +214,20 @@ FilamentManager.prototype.core.client = function apiClient() {
return OctoPrint.patchJson(selectionUrl(id), data, opts);
}
};

self.database = {
test: function test(config, opts) {
var url = pluginUrl + '/database/test';
var data = { config: config };
return OctoPrint.postJson(url, data, opts);
}
};
};
/* global FilamentManager ko $ */

FilamentManager.prototype.viewModels.config = function configurationViewModel() {
var self = this.viewModels.config;
var api = this.core.client;
var settingsViewModel = this.core.bridge.allViewModels.settingsViewModel;


Expand Down Expand Up @@ -267,14 +270,33 @@ FilamentManager.prototype.viewModels.config = function configurationViewModel()
var pluginSettings = settingsViewModel.settings.plugins.filamentmanager;
ko.mapping.fromJS(ko.toJS(pluginSettings), self.config);
};

self.connectionTest = function runExternalDatabaseConnectionTest(viewModel, event) {
var target = $(event.target);
target.removeClass('btn-success btn-danger');
target.prepend('<i class="fa fa-spinner fa-spin"></i> ');
target.prop('disabled', true);

var data = ko.mapping.toJS(self.config.database);

api.database.test(data).done(function () {
target.addClass('btn-success');
}).fail(function () {
target.addClass('btn-danger');
}).always(function () {
$('i.fa-spinner', target).remove();
target.prop('disabled', false);
});
};
};
/* global FilamentManager gettext $ ko Utils */
/* global FilamentManager gettext $ ko Utils OctoPrint */

FilamentManager.prototype.viewModels.confirmation = function spoolSelectionConfirmationViewModel() {
var self = this.viewModels.confirmation;
var _core$bridge$allViewM = this.core.bridge.allViewModels,
printerStateViewModel = _core$bridge$allViewM.printerStateViewModel,
settingsViewModel = _core$bridge$allViewM.settingsViewModel;
settingsViewModel = _core$bridge$allViewM.settingsViewModel,
filesViewModel = _core$bridge$allViewM.filesViewModel;
var selections = this.viewModels.selections;


Expand Down Expand Up @@ -304,46 +326,53 @@ FilamentManager.prototype.viewModels.confirmation = function spoolSelectionConfi
dialog.modal('show');
};

printerStateViewModel.fmPrint = function confirmSpoolSelectionBeforeStartPrint() {
var startPrint = printerStateViewModel.print;

printerStateViewModel.print = function confirmSpoolSelectionBeforeStartPrint() {
if (settingsViewModel.settings.plugins.filamentmanager.confirmSpoolSelection()) {
showDialog();
button.html(gettext('Start Print'));
self.print = function startPrint() {
self.print = function continueToStartPrint() {
dialog.modal('hide');
printerStateViewModel.print();
startPrint();
};
} else {
printerStateViewModel.print();
startPrint();
}
};

printerStateViewModel.fmResume = function confirmSpoolSelectionBeforeResumePrint() {
var resumePrint = printerStateViewModel.resume;

printerStateViewModel.resume = function confirmSpoolSelectionBeforeResumePrint() {
if (settingsViewModel.settings.plugins.filamentmanager.confirmSpoolSelection()) {
showDialog();
button.html(gettext('Resume Print'));
self.print = function resumePrint() {
self.print = function continueToResumePrint() {
dialog.modal('hide');
printerStateViewModel.onlyResume();
resumePrint();
};
} else {
printerStateViewModel.onlyResume();
resumePrint();
}
};

self.replacePrintStart = function replacePrintStartButtonBehavior() {
// Modifying print button action to invoke 'fmPrint'
var element = $('#job_print');
var dataBind = element.attr('data-bind');
dataBind = dataBind.replace(/click:(.*?)(?=,|$)/, 'click: fmPrint');
element.attr('data-bind', dataBind);
};
filesViewModel.loadFile = function confirmSpoolSelectionOnLoadAndPrint(data, printAfterLoad) {
if (!data) {
return;
}

if (printAfterLoad && filesViewModel.listHelper.isSelected(data) && filesViewModel.enablePrint(data)) {
// file was already selected, just start the print job
printerStateViewModel.print();
} else {
// select file, start print job (if requested and within dimensions)
var withinPrintDimensions = filesViewModel.evaluatePrintDimensions(data, true);
var print = printAfterLoad && withinPrintDimensions;

self.replacePrintResume = function replacePrintResumeButtonBehavior() {
// Modifying resume button action to invoke 'fmResume'
var element = $('#job_pause');
var dataBind = element.attr('data-bind');
dataBind = dataBind.replace(/click:(.*?)(?=,|$)/, 'click: function() { isPaused() ? fmResume() : onlyPause(); }');
element.attr('data-bind', dataBind);
OctoPrint.files.select(data.origin, data.path, false).done(function () {
if (print) printerStateViewModel.print();
});
}
};
};
/* global FilamentManager ko $ PNotify gettext */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@
<input type="password" class="input-block-level" data-bind="value: viewModels.config.config.database.password, enable: viewModels.config.config.database.useExternal">
</div>
</div>
<!-- connection test -->
<div class="control-group">
<button class="btn pull-right" data-bind="click: viewModels.config.connectionTest">{{ _("Test connection") }}</button>
</div>

<span>{{ _("Note: If you change these settings you must restart your OctoPrint instance for the changes to take affect.") }}</span>
</form>
Expand Down
Binary file modified octoprint_filamentmanager/translations/de/LC_MESSAGES/messages.mo
Binary file not shown.
Loading

0 comments on commit 0cbd924

Please sign in to comment.