Skip to content

Commit 0cbd924

Browse files
author
malnvenshorn
committed
Finish 0.5.2
2 parents d32c419 + 6ae577b commit 0cbd924

File tree

17 files changed

+388
-282
lines changed

17 files changed

+388
-282
lines changed

README.md

+4-6
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,15 @@ If you have questions or encounter issues please take a look at the [Frequently
1616

1717
## Setup
1818

19-
1. Install dependencies with:
20-
21-
`pacman -Sy postgresql-libs` on Arch Linux
22-
23-
`apt-get install libpq-dev` on Debian/Raspbian
24-
2519
1. Install this plugin via the bundled [Plugin Manager](https://github.com/foosel/OctoPrint/wiki/Plugin:-Plugin-Manager)
2620
or manually using this URL:
2721

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

24+
1. For PostgreSQL support you need to install an additional dependency:
25+
26+
`pip install psycopg2`
27+
3028
## Screenshots
3129

3230
![FilamentManager Sidebar](screenshots/filamentmanager_sidebar.png?raw=true)

octoprint_filamentmanager/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def migrate_database_schema(self, target, current):
120120
def on_after_startup(self):
121121
# subscribe to the notify channel so that we get notified if another client has altered the data
122122
# notify is not available if we are connected to the internal sqlite database
123-
if self.filamentManager.notify is not None:
123+
if self.filamentManager is not None and self.filamentManager.notify is not None:
124124
def notify(pid, channel, payload):
125125
# ignore notifications triggered by our own connection
126126
if pid != self.filamentManager.conn.connection.get_backend_pid():

octoprint_filamentmanager/api/__init__.py

+31
Original file line numberDiff line numberDiff line change
@@ -373,3 +373,34 @@ def unzip(filename, extract_dir):
373373
.format(path=tempdir, message=str(e)))
374374

375375
return make_response("", 204)
376+
377+
@octoprint.plugin.BlueprintPlugin.route("/database/test", methods=["POST"])
378+
@restricted_access
379+
def test_database_connection(self):
380+
if "application/json" not in request.headers["Content-Type"]:
381+
return make_response("Expected content-type JSON", 400)
382+
383+
try:
384+
json_data = request.json
385+
except BadRequest:
386+
return make_response("Malformed JSON body in request", 400)
387+
388+
if "config" not in json_data:
389+
return make_response("No database configuration included in request", 400)
390+
391+
config = json_data["config"]
392+
393+
for key in ["uri", "name", "user", "password"]:
394+
if key not in config:
395+
return make_response("Configuration does not contain mandatory '{}' field".format(key), 400)
396+
397+
try:
398+
connection = self.filamentManager.connect(config["uri"],
399+
database=config["name"],
400+
username=config["user"],
401+
password=config["password"])
402+
except Exception as e:
403+
return make_response("Failed to connect to the database with the given configuration", 400)
404+
else:
405+
connection.close()
406+
return make_response("", 204)

octoprint_filamentmanager/data/__init__.py

+41-26
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
from multiprocessing import Lock
1010

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

2829
def __init__(self, config):
29-
if not set(("uri", "name", "user", "password")).issubset(config):
30-
raise ValueError("Incomplete config dictionary")
30+
self.notify = None
31+
self.conn = self.connect(config.get("uri", ""),
32+
database=config.get("name", ""),
33+
username=config.get("user", ""),
34+
password=config.get("password", ""))
3135

3236
# QUESTION thread local connection (pool) vs sharing a serialized connection, pro/cons?
3337
# from sqlalchemy.orm import sessionmaker, scoped_session
3438
# Session = scoped_session(sessionmaker(bind=engine))
3539
# when using a connection pool how do we prevent notifiying ourself on database changes?
3640
self.lock = Lock()
37-
self.notify = None
38-
39-
uri_parts = urisplit(config["uri"])
4041

41-
if self.DIALECT_SQLITE == uri_parts.scheme:
42-
self.engine = create_engine(config["uri"], connect_args={"check_same_thread": False})
43-
self.conn = self.engine.connect()
42+
if self.engine_dialect_is(self.DIALECT_SQLITE):
43+
# Enable foreign key constraints
4444
self.conn.execute(text("PRAGMA foreign_keys = ON").execution_options(autocommit=True))
45-
elif self.DIALECT_POSTGRESQL == uri_parts.scheme:
46-
uri = uricompose(scheme=uri_parts.scheme, host=uri_parts.host, port=uri_parts.getport(default=5432),
47-
path="/{}".format(config["name"]),
48-
userinfo="{}:{}".format(config["user"], config["password"]))
49-
self.engine = create_engine(uri)
50-
self.conn = self.engine.connect()
51-
self.notify = PGNotify(uri)
45+
elif self.engine_dialect_is(self.DIALECT_POSTGRESQL):
46+
# Create listener thread
47+
self.notify = PGNotify(self.conn.engine.url)
48+
49+
def connect(self, uri, database="", username="", password=""):
50+
uri_parts = urisplit(uri)
51+
52+
if uri_parts.scheme == self.DIALECT_SQLITE:
53+
engine = create_engine(uri, connect_args={"check_same_thread": False})
54+
elif uri_parts.scheme == self.DIALECT_POSTGRESQL:
55+
uri = URL(drivername=uri_parts.scheme,
56+
host=uri_parts.host,
57+
port=uri_parts.getport(default=5432),
58+
database=database,
59+
username=username,
60+
password=password)
61+
engine = create_engine(uri)
5262
else:
5363
raise ValueError("Engine '{engine}' not supported".format(engine=uri_parts.scheme))
5464

65+
return engine.connect()
66+
5567
def close(self):
5668
self.conn.close()
5769

70+
def engine_dialect_is(self, dialect):
71+
return self.conn.engine.dialect.name == dialect if self.conn is not None else False
72+
5873
def initialize(self):
5974
metadata = MetaData()
6075

@@ -91,7 +106,7 @@ def initialize(self):
91106
Column("changed_at", TIMESTAMP, nullable=False,
92107
server_default=text("CURRENT_TIMESTAMP")))
93108

94-
if self.DIALECT_POSTGRESQL == self.engine.dialect.name:
109+
if self.engine_dialect_is(self.DIALECT_POSTGRESQL):
95110
def should_create_function(name):
96111
row = self.conn.execute("select proname from pg_proc where proname = '%s'" % name).scalar()
97112
return not bool(row)
@@ -128,7 +143,7 @@ def should_create_trigger(name):
128143
if should_create_trigger(name):
129144
event.listen(metadata, "after_create", trigger)
130145

131-
elif self.DIALECT_SQLITE == self.engine.dialect.name:
146+
elif self.engine_dialect_is(self.DIALECT_SQLITE):
132147
for table in [self.profiles.name, self.spools.name]:
133148
for action in ["INSERT", "UPDATE", "DELETE"]:
134149
name = "{table}_on_{action}".format(table=table, action=action.lower())
@@ -293,10 +308,10 @@ def get_selection(self, identifier, client_id):
293308
def update_selection(self, identifier, client_id, data):
294309
with self.lock, self.conn.begin():
295310
values = dict()
296-
if self.engine.dialect.name == self.DIALECT_SQLITE:
311+
if self.engine_dialect_is(self.DIALECT_SQLITE):
297312
stmt = insert(self.selections).prefix_with("OR REPLACE")\
298313
.values(tool=identifier, client_id=client_id, spool_id=data["spool"]["id"])
299-
elif self.engine.dialect.name == self.DIALECT_POSTGRESQL:
314+
elif self.engine_dialect_is(self.DIALECT_POSTGRESQL):
300315
stmt = pg_insert(self.selections)\
301316
.values(tool=identifier, client_id=client_id, spool_id=data["spool"]["id"])\
302317
.on_conflict_do_update(constraint="selections_pkey", set_=dict(spool_id=data["spool"]["id"]))
@@ -327,23 +342,23 @@ def from_csv(table):
327342
for row in csv_reader:
328343
values = dict(zip(header, row))
329344

330-
if self.engine.dialect.name == self.DIALECT_SQLITE:
345+
if self.engine_dialect_is(self.DIALECT_SQLITE):
331346
identifier = values[table.c.id]
332347
# try to update entry
333348
stmt = update(table).values(values).where(table.c.id == identifier)
334349
if self.conn.execute(stmt).rowcount == 0:
335350
# identifier doesn't match any => insert new entry
336351
stmt = insert(table).values(values)
337352
self.conn.execute(stmt)
338-
elif self.engine.dialect.name == self.DIALECT_POSTGRESQL:
353+
elif self.engine_dialect_is(self.DIALECT_POSTGRESQL):
339354
stmt = pg_insert(table).values(values)\
340355
.on_conflict_do_update(index_elements=[table.c.id], set_=values)
341356
self.conn.execute(stmt)
342357

343-
if self.DIALECT_POSTGRESQL == self.engine.dialect.name:
344-
# update sequences
345-
self.conn.execute(text("SELECT setval('profiles_id_seq', max(id)) FROM profiles"))
346-
self.conn.execute(text("SELECT setval('spools_id_seq', max(id)) FROM spools"))
358+
if self.engine_dialect_is(self.DIALECT_POSTGRESQL):
359+
# update sequence
360+
sql = "SELECT setval('{table}_id_seq', max(id)) FROM {table}".format(table=table.name)
361+
self.conn.execute(text(sql))
347362

348363
tables = [self.profiles, self.spools]
349364
for t in tables:

octoprint_filamentmanager/static/js/filamentmanager.bundled.js

+62-33
Original file line numberDiff line numberDiff line change
@@ -73,20 +73,17 @@ FilamentManager.prototype.core.bridge = function pluginBridge() {
7373
self.core.bridge = {
7474
allViewModels: {},
7575

76-
REQUIRED_VIEWMODELS: ['settingsViewModel', 'printerStateViewModel', 'loginStateViewModel', 'temperatureViewModel'],
76+
REQUIRED_VIEWMODELS: ['settingsViewModel', 'printerStateViewModel', 'loginStateViewModel', 'temperatureViewModel', 'filesViewModel'],
7777

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

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

84-
self.viewModels.profiles.call(self);
85-
self.viewModels.spools.call(self);
86-
self.viewModels.selections.call(self);
87-
self.viewModels.config.call(self);
88-
self.viewModels.import.call(self);
89-
self.viewModels.confirmation.call(self);
84+
Object.values(self.viewModels).forEach(function (viewModel) {
85+
return viewModel.call(self);
86+
});
9087

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

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

111107
self.onStartup = function onStartupCallback() {
112108
self.viewModels.warning.replaceFilamentView();
113-
self.viewModels.confirmation.replacePrintStart();
114-
self.viewModels.confirmation.replacePrintResume();
115109
};
116110

117111
self.onBeforeBinding = function onBeforeBindingCallback() {
@@ -220,11 +214,20 @@ FilamentManager.prototype.core.client = function apiClient() {
220214
return OctoPrint.patchJson(selectionUrl(id), data, opts);
221215
}
222216
};
217+
218+
self.database = {
219+
test: function test(config, opts) {
220+
var url = pluginUrl + '/database/test';
221+
var data = { config: config };
222+
return OctoPrint.postJson(url, data, opts);
223+
}
224+
};
223225
};
224226
/* global FilamentManager ko $ */
225227

226228
FilamentManager.prototype.viewModels.config = function configurationViewModel() {
227229
var self = this.viewModels.config;
230+
var api = this.core.client;
228231
var settingsViewModel = this.core.bridge.allViewModels.settingsViewModel;
229232

230233

@@ -267,14 +270,33 @@ FilamentManager.prototype.viewModels.config = function configurationViewModel()
267270
var pluginSettings = settingsViewModel.settings.plugins.filamentmanager;
268271
ko.mapping.fromJS(ko.toJS(pluginSettings), self.config);
269272
};
273+
274+
self.connectionTest = function runExternalDatabaseConnectionTest(viewModel, event) {
275+
var target = $(event.target);
276+
target.removeClass('btn-success btn-danger');
277+
target.prepend('<i class="fa fa-spinner fa-spin"></i> ');
278+
target.prop('disabled', true);
279+
280+
var data = ko.mapping.toJS(self.config.database);
281+
282+
api.database.test(data).done(function () {
283+
target.addClass('btn-success');
284+
}).fail(function () {
285+
target.addClass('btn-danger');
286+
}).always(function () {
287+
$('i.fa-spinner', target).remove();
288+
target.prop('disabled', false);
289+
});
290+
};
270291
};
271-
/* global FilamentManager gettext $ ko Utils */
292+
/* global FilamentManager gettext $ ko Utils OctoPrint */
272293

273294
FilamentManager.prototype.viewModels.confirmation = function spoolSelectionConfirmationViewModel() {
274295
var self = this.viewModels.confirmation;
275296
var _core$bridge$allViewM = this.core.bridge.allViewModels,
276297
printerStateViewModel = _core$bridge$allViewM.printerStateViewModel,
277-
settingsViewModel = _core$bridge$allViewM.settingsViewModel;
298+
settingsViewModel = _core$bridge$allViewM.settingsViewModel,
299+
filesViewModel = _core$bridge$allViewM.filesViewModel;
278300
var selections = this.viewModels.selections;
279301

280302

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

307-
printerStateViewModel.fmPrint = function confirmSpoolSelectionBeforeStartPrint() {
329+
var startPrint = printerStateViewModel.print;
330+
331+
printerStateViewModel.print = function confirmSpoolSelectionBeforeStartPrint() {
308332
if (settingsViewModel.settings.plugins.filamentmanager.confirmSpoolSelection()) {
309333
showDialog();
310334
button.html(gettext('Start Print'));
311-
self.print = function startPrint() {
335+
self.print = function continueToStartPrint() {
312336
dialog.modal('hide');
313-
printerStateViewModel.print();
337+
startPrint();
314338
};
315339
} else {
316-
printerStateViewModel.print();
340+
startPrint();
317341
}
318342
};
319343

320-
printerStateViewModel.fmResume = function confirmSpoolSelectionBeforeResumePrint() {
344+
var resumePrint = printerStateViewModel.resume;
345+
346+
printerStateViewModel.resume = function confirmSpoolSelectionBeforeResumePrint() {
321347
if (settingsViewModel.settings.plugins.filamentmanager.confirmSpoolSelection()) {
322348
showDialog();
323349
button.html(gettext('Resume Print'));
324-
self.print = function resumePrint() {
350+
self.print = function continueToResumePrint() {
325351
dialog.modal('hide');
326-
printerStateViewModel.onlyResume();
352+
resumePrint();
327353
};
328354
} else {
329-
printerStateViewModel.onlyResume();
355+
resumePrint();
330356
}
331357
};
332358

333-
self.replacePrintStart = function replacePrintStartButtonBehavior() {
334-
// Modifying print button action to invoke 'fmPrint'
335-
var element = $('#job_print');
336-
var dataBind = element.attr('data-bind');
337-
dataBind = dataBind.replace(/click:(.*?)(?=,|$)/, 'click: fmPrint');
338-
element.attr('data-bind', dataBind);
339-
};
359+
filesViewModel.loadFile = function confirmSpoolSelectionOnLoadAndPrint(data, printAfterLoad) {
360+
if (!data) {
361+
return;
362+
}
363+
364+
if (printAfterLoad && filesViewModel.listHelper.isSelected(data) && filesViewModel.enablePrint(data)) {
365+
// file was already selected, just start the print job
366+
printerStateViewModel.print();
367+
} else {
368+
// select file, start print job (if requested and within dimensions)
369+
var withinPrintDimensions = filesViewModel.evaluatePrintDimensions(data, true);
370+
var print = printAfterLoad && withinPrintDimensions;
340371

341-
self.replacePrintResume = function replacePrintResumeButtonBehavior() {
342-
// Modifying resume button action to invoke 'fmResume'
343-
var element = $('#job_pause');
344-
var dataBind = element.attr('data-bind');
345-
dataBind = dataBind.replace(/click:(.*?)(?=,|$)/, 'click: function() { isPaused() ? fmResume() : onlyPause(); }');
346-
element.attr('data-bind', dataBind);
372+
OctoPrint.files.select(data.origin, data.path, false).done(function () {
373+
if (print) printerStateViewModel.print();
374+
});
375+
}
347376
};
348377
};
349378
/* global FilamentManager ko $ PNotify gettext */

octoprint_filamentmanager/templates/settings_configdialog.jinja2

+4
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@
103103
<input type="password" class="input-block-level" data-bind="value: viewModels.config.config.database.password, enable: viewModels.config.config.database.useExternal">
104104
</div>
105105
</div>
106+
<!-- connection test -->
107+
<div class="control-group">
108+
<button class="btn pull-right" data-bind="click: viewModels.config.connectionTest">{{ _("Test connection") }}</button>
109+
</div>
106110

107111
<span>{{ _("Note: If you change these settings you must restart your OctoPrint instance for the changes to take affect.") }}</span>
108112
</form>
Binary file not shown.

0 commit comments

Comments
 (0)