Skip to content

Commit

Permalink
Add a printer job queue via the printer plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
paroga committed Feb 5, 2019
1 parent 63e1541 commit c955a6e
Show file tree
Hide file tree
Showing 24 changed files with 561 additions and 1 deletion.
1 change: 1 addition & 0 deletions Gemfile
Expand Up @@ -64,6 +64,7 @@ gem 'foodsoft_discourse', path: 'plugins/discourse'

# plugins not enabled by default
#gem 'foodsoft_current_orders', path: 'plugins/current_orders'
#gem 'foodsoft_printer', path: 'plugins/printer'
#gem 'foodsoft_uservoice', path: 'plugins/uservoice'


Expand Down
22 changes: 22 additions & 0 deletions config/locales/de.yml
Expand Up @@ -649,6 +649,11 @@ de:
pdf_font_size: Schriftgrösse
pdf_page_size: Seitenformat
price_markup: Foodcoop Marge
printer_print_order_articles: Artikel PDF drucken
printer_print_order_fax: Fax PDF drucken
printer_print_order_groups: Gruppen PDF drucken
printer_print_order_matrix: Matrix PDF drucken
printer_token: Geheimer Token
stop_ordering_under: Apfelpunkte Minimum
tasks_period_days: Zeitintervall
tasks_upfront_days: Im Voraus
Expand All @@ -662,6 +667,7 @@ de:
use_iban: IBAN verwenden
use_messages: Nachrichten
use_nick: Benutzernamen verwenden
use_printer: Drucker verwenden
use_wiki: Wiki verwenden
webstats_tracking_code: Code für Websiteanalysetool
tabs:
Expand Down Expand Up @@ -1109,6 +1115,8 @@ de:
submit:
invite:
create: Einladung verschicken
printer_job:
create: Druckauftrag erstellen
message:
create: Nachricht verschicken
tasks:
Expand Down Expand Up @@ -1461,6 +1469,7 @@ de:
manage: Bestellverwaltung
ordering: Bestellen!
pickups: Abholtage
printer_jobs: Druckaufträge
title: Bestellungen
tasks: Aufgaben
wiki:
Expand Down Expand Up @@ -1565,6 +1574,7 @@ de:
confirm_end: |-
Willst Du wirklich die Bestellung %{order} beenden?
Es gibt kein zurück.
confirm_create_printer_job: Für diese Bestellung wurde bereits ein Druckauftrag erstellt. Willst du einen neuen erstellen?
confirm_send_to_supplier: Die Bestellung wurde bereit am %{when} zur Lieferantin geschickt. Willst du sie wirklich erneut schicken?
create_invoice: Rechnung anlegen
description1_order: "%{state} Bestellung von %{supplier} angelegt von %{who},"
Expand Down Expand Up @@ -1704,6 +1714,18 @@ de:
nojs: Achtung, Cookies und Javascript müssen aktiviert sein! %{link} bitte abschalten.
noscript: NoScript
title: Foodsoft anmelden
printer_jobs:
create:
notice: "%{count} Druckaufträge wurden erstellt."
destroy:
notice: Druckauftrag wurde gelöscht.
index:
finished: Beendet
pending: Ausstehend
queued: Warten auf den Bestellschluss
title: Druckaufträge
show:
title: Druckauftrag %{id}
shared:
articles:
ordered: Bestellt
Expand Down
22 changes: 22 additions & 0 deletions config/locales/en.yml
Expand Up @@ -652,6 +652,11 @@ en:
pdf_font_size: Font size
pdf_page_size: Page size
price_markup: Foodcoop margin
printer_print_order_articles: Print article PDF
printer_print_order_fax: Print fax PDF
printer_print_order_groups: Print group PDF
printer_print_order_matrix: Print matrix PDF
printer_token: Secret token
stop_ordering_under: Minimum apple points
tasks_period_days: Period
tasks_upfront_days: Create upfront
Expand All @@ -665,6 +670,7 @@ en:
use_iban: Use IBAN
use_messages: Messages
use_nick: Use nicknames
use_printer: Use printer
use_wiki: Enable wiki
webstats_tracking_code: Tracking code
tabs:
Expand Down Expand Up @@ -1126,6 +1132,8 @@ en:
submit:
invite:
create: send invitation
printer_job:
create: create printer job
message:
create: send message
tasks:
Expand Down Expand Up @@ -1487,6 +1495,7 @@ en:
manage: Manage orders
ordering: Place order!
pickups: Pickup days
printer_jobs: Printer jobs
title: Orders
tasks: Tasks
wiki:
Expand Down Expand Up @@ -1591,6 +1600,7 @@ en:
confirm_end: |-
Do you really want to close the order %{order}?
There is no going back.
confirm_create_printer_job: A printer job for this order has been created already. Do you want to create a new one?
confirm_send_to_supplier: The order has been sent to the supplier already on %{when}. Do you really want to send it again?
create_invoice: Add invoice
description1_order: "%{state} order from %{supplier} opened by %{who},"
Expand Down Expand Up @@ -1730,6 +1740,18 @@ en:
nojs: Attention, cookies and javascript have to be activated! Please switch off %{link}.
noscript: NoScript
title: Foodsoft login
printer_jobs:
create:
notice: Created %{count} printer jobs.
destroy:
notice: Printer job has been deleted.
index:
finished: Finished
pending: Pending
queued: Waiting for order to close
title: Printer jobs
show:
title: Printer job %{id}
shared:
articles:
ordered: Ordered
Expand Down
@@ -0,0 +1,20 @@
class CreatePrinterJobs < ActiveRecord::Migration
def change
create_table :printer_jobs do |t|
t.references :order
t.string :document, null: false
t.integer :created_by_user_id, null: false
t.integer :finished_by_user_id
t.datetime :finished_at, index: true
end

create_table :printer_job_updates do |t|
t.references :printer_job, null: false
t.datetime :created_at, null: false
t.string :state, null: false
t.text :message
end

add_index :printer_job_updates, [:printer_job_id, :created_at]
end
end
21 changes: 20 additions & 1 deletion db/schema.rb
Expand Up @@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 20171201000000) do
ActiveRecord::Schema.define(version: 20181201000000) do

create_table "article_categories", force: :cascade do |t|
t.string "name", limit: 255, default: "", null: false
Expand Down Expand Up @@ -378,6 +378,25 @@
t.datetime "updated_at", null: false
end

create_table "printer_job_updates", force: :cascade do |t|
t.integer "printer_job_id", null: false
t.datetime "created_at", null: false
t.string "state", null: false
t.text "message"
end

add_index "printer_job_updates", ["printer_job_id", "created_at"], name: "index_printer_job_updates_on_printer_job_id_and_created_at", using: :btree

create_table "printer_jobs", force: :cascade do |t|
t.integer "order_id"
t.string "document", null: false
t.integer "created_by_user_id", null: false
t.integer "finished_by_user_id"
t.datetime "finished_at"
end

add_index "printer_jobs", ["finished_at"], name: "index_printer_jobs_on_finished_at", using: :btree

create_table "settings", force: :cascade do |t|
t.string "var", limit: 255, null: false
t.text "value", limit: 65535
Expand Down
61 changes: 61 additions & 0 deletions plugins/printer/README.md
@@ -0,0 +1,61 @@
FoodsoftPrinter
=================

This plugin adds a printer queue to allow mebers to print PDF with one click.
Usually a mini computer with a printer at a room in the foodcoop will wait for
new printer jobs and prints them.

This plugin is not enabled by default. To install it, add uncomment the
corresponding line in the `Gemfile`, or add:

```Gemfile
gem 'foodsoft_printer', path: 'plugins/foodsoft_printer'
```

This plugin introduces the foodcoop config option `printer_token`, which takes
a random string for authentication at the endpoint. Additionally a set of
PDF files can be selected, which will be generated when a print is triggered.

The communication with the printer client happens via two endpoints, which both
require the `printer_token` as `Bearer` token in the `Authorization` header.
* `/:foodcoop/printer/socket`: main WebSocket communication
* `/:foodcoop/printer/:id`: HTTP GET for documents

The main communication happens via JSON messages via an WebSocket connection,
which sends an array of docuement ids to the client, which need to be printed.
Addionally the docuemnt can be downloaded as PDF via a separate endpoint.
The client can updated the status of the documents by sending an object with
the following keys to the server:
* `id` (NBR, REQUIRED): id of the document, which should be updated
* `state` (ENUM): the current sate of the printing progress.
* `message` (STR, REQUIRES `state`): detailed description of the current state
(e.g. download progress)
* `finish` (BOOL): when set to `true` the job will be marked as done

The following values are valid for the `state` property:
* `queued`: the document is not yet ready for printing
* `ready`: the document is ready to be downloaded
* `downloading`: transfer of the document is in progress
* `pending`: download completed, waiting for the printer
* `held`: e.g., for "PIN printing"
* `processing`: printing is in progress
* `stopped`: out of paper, etc.)
* `cancelled`: the user stopped the action
* `aborted`: the printer stopped the action
* `completed`: print was successful

**Example**:
A server sending `{"unfinished_jobs":[12,16]}` via WebSocket indicates that the
two documents `12` and `16` are ready for printing. The client will request the
first document from `/foodcoop/printer/12` and send it to the printer. The
status can be updated by sending `{"id":12,"state":"pending"}` via WebSocket to
the server. Sending `{"id":12,"state":"completed","finish":true}` as soon as
when the printing succeded will mark the job done.

To use this plugin the webserver must support WebSockets. The current
implementation uses socket hijack, which is not supported by `thin`. `puma`
supports that, but might lead to other problems, since it's not well tested
in combination with foodsoft. Please be careful when switching the webserver!

This plugin is part of the foodsoft package and uses the AGPL-3 license (see
foodsoft's LICENSE for the full license text).
40 changes: 40 additions & 0 deletions plugins/printer/Rakefile
@@ -0,0 +1,40 @@
#!/usr/bin/env rake
begin
require 'bundler/setup'
rescue LoadError
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
end
begin
require 'rdoc/task'
rescue LoadError
require 'rdoc/rdoc'
require 'rake/rdoctask'
RDoc::Task = Rake::RDocTask
end

RDoc::Task.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'FoodsoftPrinter'
rdoc.options << '--line-numbers'
rdoc.rdoc_files.include('README.rdoc')
rdoc.rdoc_files.include('lib/**/*.rb')
end

APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
load 'rails/tasks/engine.rake'



Bundler::GemHelper.install_tasks

require 'rake/testtask'

Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.libs << 'test'
t.pattern = 'test/**/*_test.rb'
t.verbose = false
end


task :default => :test
58 changes: 58 additions & 0 deletions plugins/printer/app/controllers/printer_controller.rb
@@ -0,0 +1,58 @@
class PrinterController < ApplicationController
include Concerns::SendOrderPdf
include Tubesock::Hijack

skip_before_filter :authenticate
before_filter :authenticate_printer
before_filter -> { require_plugin_enabled FoodsoftPrinter }

def socket
hijack do |tubesock|
tubesock.onopen do
tubesock.send_data unfinished_jobs
end

tubesock.onmessage do |data|
update_job data
tubesock.send_data unfinished_jobs
end
end
end

def show
job = PrinterJob.find(params[:id])
send_order_pdf job.order, job.document
end

private

def unfinished_jobs
{
unfinished_jobs: PrinterJob.pending.map(&:id)
}.to_json
end

def update_job(data)
json = JSON.parse data, symbolize_names: true
job = PrinterJob.unfinished.find_by_id(json[:id])
return unless job
if json[:state]
job.add_update! json[:state], json[:message]
end
job.finish! if json[:finish]
end

protected

def bearer_token
pattern = /^Bearer /
header = request.headers['Authorization']
header.gsub(pattern, '') if header && header.match(pattern)
end

def authenticate_printer
return head(:unauthorized) unless bearer_token
return head(:forbidden) if bearer_token != FoodsoftConfig[:printer_token]
end

end
44 changes: 44 additions & 0 deletions plugins/printer/app/controllers/printer_jobs_controller.rb
@@ -0,0 +1,44 @@
class PrinterJobsController < ApplicationController
include Concerns::SendOrderPdf

before_filter -> { require_plugin_enabled FoodsoftPrinter }

def index
jobs = PrinterJob.includes(:printer_job_updates)
@pending_jobs = jobs.pending
@queued_jobs = jobs.queued
@finished_jobs = jobs.finished.order(finished_at: :desc).page(params[:page]).per(@per_page)
end

def create
order = Order.find(params[:order])
state = order.open? ? 'queued' : 'ready'
count = 0
PrinterJob.transaction do
%w(articles fax groups matrix).each do |document|
next unless FoodsoftConfig["printer_print_order_#{document}"]
job = PrinterJob.create! order: order, document: document, created_by: current_user
job.add_update! state
count += 1
end
end
redirect_to order, notice: t('.notice', count: count)
end

def show
@job = PrinterJob.find(params[:id])
end

def document
job = PrinterJob.find(params[:id])
send_order_pdf job.order, job.document
end

def destroy
job = PrinterJob.find(params[:id])
job.finish! current_user
redirect_to printer_jobs_path, notice: t('.notice')
rescue => error
redirect_to printer_jobs_path, t('errors.general_msg', msg: error.message)
end
end

0 comments on commit c955a6e

Please sign in to comment.