Implemented attachments for products

This commit is contained in:
Erik Thuning 2019-11-19 15:06:22 +01:00
parent 3cc2b91ed4
commit a3656604c0
11 changed files with 312 additions and 53 deletions

@ -18,4 +18,11 @@ $error_address = 'root@example.com';
#$notify_discard = 'inventory-tracking@example.com';
$discard_notify = false;
# Directory to save attachments to
# Interpreted as absolute path if beginning with /,
# otherwise assumed to be relative to the application root.
# Must be writable by the web server.
# Does not need to be within the web root.
$files_dir = 'attachments';
?>

@ -280,12 +280,6 @@
</button>
</form>
</div>
<div id="product-history"
class="¤hidden¤">
<h2>Artikelhistorik</h2>
¤history¤
</div>
<!--
<div id="product-attachments"
class="¤hidden¤">
<h2>Bilagor</h2>
@ -298,17 +292,21 @@
<input id="uploadfile"
name="uploadfile"
type="file"
onchange="show_file(event)"/>
onchange="showFile(event)"/>
<input id="filename"
name="filename"
type="text"
placeholder="Välj en fil..."
onclick="select_file(event)"
onclick="selectFile(event)"
readonly />
<button>Ladda upp</button>
</form>
</div>
-->
<div id="product-history"
class="¤hidden¤">
<h2>Artikelhistorik</h2>
¤history¤
</div>
<div id="product-label"
class="¤hidden¤">
<h2>Etikett</h2>
@ -316,13 +314,23 @@
</div>
¤¤ attachment_list ¤¤
<ul>
<ul class="attachment-list">
¤attachments¤
</ul>
¤¤ attachment ¤¤
<li>
<strong>¤name¤</strong>: <a href="./?page=dl&id=¤id¤">Ladda ner</a>
<strong>¤name¤</strong> (¤date¤): <a href="./?page=dl&id=¤id¤">Ladda ner</a>
<br/>
<form onSubmit="JavaScript:deleteAttachment(event)">
<input type="hidden"
name="id"
value="¤id¤" />
<input type="hidden"
name="name"
value="¤name¤" />
<button>Ta bort</button>
</form>
</li>
¤¤ product_label ¤¤

@ -59,6 +59,13 @@ class Ajax extends Responder {
break;
case 'toggleservice':
$out = $this->toggle_service();
break;
case 'addattachment':
$out = $this->add_attachment();
break;
case 'deleteattachment':
$out = $this->delete_attachment();
break;
}
print($out->toJson());
}
@ -381,5 +388,31 @@ class Ajax extends Responder {
.'på den här artikeln nu.');
}
}
private function add_attachment() {
try {
$product = new Product($_POST['id']);
$uploadfile = $_FILES['uploadfile'];
$attach = Attachment::create($uploadfile, $product->get_id());
$date = format_date($attach->get_uploadtime());
$fragment = replace(array('name' => $attach->get_filename(),
'id' => $attach->get_id(),
'date' => $date),
$this->fragments['attachment']);
return new Success($fragment);
} catch(Exception $e) {
return new Failure($e->getMessage());
}
}
private function delete_attachment() {
$attach = new Attachment($_POST['id']);
try {
$attach->delete();
return new Success('');
} catch(Exception $e) {
return new Failure($e->getMessage());
}
}
}
?>

109
include/Attachment.php Normal file

@ -0,0 +1,109 @@
<?php
class Attachment {
private $id;
private $product;
private $filename;
private $uploadtime;
public static function create($file, $prodid) {
begin_trans();
try {
if(!isset($file['error']) || is_array($file['error'])) {
throw new Exception('Ogiltigt anrop.');
}
switch($file['error']) {
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_NO_FILE:
throw new Exception('Ingen fil skickades.');
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
throw new Exception('Filen är för stor.');
default:
throw new Exception('Ett okänt fel har inträffat.');
}
$filename = $file['name'];
$insert = prepare('insert into `attachment`
(`product`, `filename`, `uploadtime`)
values (?, ?, ?)');
bind($insert, 'isi', $prodid, $filename, time());
execute($insert);
$attachid = $insert->insert_id;
global $files_dir;
$savepath = $files_dir;
if(substr($savepath, -1) !== '/') {
$savepath .= '/';
}
$savepath .= $attachid;
$tmp_name = $file['tmp_name'];
if(file_exists($savepath)) {
throw new Exception('Filens plats är upptagen. '
.'Det här borde aldrig inträffa.');
}
if(!move_uploaded_file($tmp_name, $savepath)) {
throw new Exception('Filen kunde inte sparas.');
}
commit_trans();
return new Attachment($attachid);
} catch(Exception $e) {
revert_trans();
throw $e;
}
}
public function __construct($id) {
$search = prepare('select * from `attachment` where `id`=?');
bind($search, 'i', $id);
execute($search);
$result = result_single($search);
if($result === null) {
throw new Exception('Attachment does not exist.');
}
$this->id = $result['id'];
$this->product = $result['product'];
$this->filename = $result['filename'];
$this->uploadtime = $result['uploadtime'];
}
public function delete() {
$delete = prepare('update `attachment` set `deletetime`=?
where `id`=?');
bind($delete, 'ii', time(), $this->get_id());
execute($delete);
$path = $this->get_filepath();
if(file_exists($path)) {
unlink($path);
}
return true;
}
public function get_id() {
return $this->id;
}
public function get_product() {
return new Product($this->product);
}
public function get_filename() {
return $this->filename;
}
public function get_uploadtime() {
return $this->uploadtime;
}
public function get_filepath() {
global $files_dir;
$path = $files_dir;
if(substr($path, -1) !== '/') {
$path .= '/';
}
$path .= $this->get_id();
if(!file_exists($path)) {
throw new Exception('Filen har försvunnit.');
}
return $path;
}
}
?>

25
include/Download.php Normal file

@ -0,0 +1,25 @@
<?php
class Download extends Responder {
private $attachment;
public function __construct() {
parent::__construct();
if(isset($_GET['id'])) {
$this->attachment = new Attachment($_GET['id']);
}
}
public function render() {
$filename = $this->attachment->get_filename();
$filepath = $this->attachment->get_filepath();
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="'.$filename.'"');
header('Expires: 0');
header('Cache-Control: no-cache');
header('Content-length: '.filesize($filepath));
readfile($filepath);
exit(0);
}
}
?>

@ -21,8 +21,8 @@ class Product {
$now = time();
begin_trans();
try {
$stmt = 'insert into
`product`(`brand`, `name`, `invoice`, `serial`, `createtime`)
$stmt = 'insert into `product`
(`brand`, `name`, `invoice`, `serial`, `createtime`)
values (?, ?, ?, ?, ?)';
$ins_prod = prepare($stmt);
bind($ins_prod, 'ssssi', $brand, $name, $invoice, $serial, $now);
@ -403,7 +403,17 @@ class Product {
}
public function get_attachments() {
return array();
$out = array();
$find = prepare('select `id` from `attachment`
where `product`=? and `deletetime` is NULL
order by `uploadtime` asc');
bind($find, 'i', $this->id);
execute($find);
$items = result_list($find);
foreach($items as $item) {
$out[] = new Attachment($item['id']);
}
return $out;
}
}
?>

@ -89,7 +89,6 @@ class ProductPage extends Page {
'service' => 'Starta service',
'history' => $history,
'attachments' => $attachments);
$attachments = $this->product->get_attachments();
if(class_exists('QRcode')) {
$fields['label'] = replace($fields,
$this->fragments['product_label']);
@ -144,12 +143,14 @@ class ProductPage extends Page {
private function build_attachment_list($attachments) {
if(!$attachments) {
return 'Inga bilagor.';
return '<p>Inga bilagor.</p>';
}
$items = '';
foreach($attachments as $attachment) {
$items .= replace(array('name' => $attachment->get_name(),
'id' => $attachments->get_id()),
$date = format_date($attachment->get_uploadtime());
$items .= replace(array('name' => $attachment->get_filename(),
'id' => $attachment->get_id(),
'date' => $date),
$this->fragments['attachment']);
}
return replace(array('attachments' => $items),

@ -5,27 +5,35 @@ abstract class Responder {
public function __construct() {
$this->fragments = get_fragments('./html/fragments.html');
}
final protected function escape_tags($tags) {
foreach($tags as $key => $tag) {
$tags[$key] = str_replace(array("'",
'"'),
array('&#39;',
'&#34;'),
strtolower($tag));
$tags[$key] = $this->escape_string(strtolower($tag));
}
return $tags;
}
final protected function unescape_tags($tags) {
foreach($tags as $key => $tag) {
$tags[$key] = str_replace(array('&#39;',
'&#34;'),
array("'",
'"'),
strtolower($tag));
$tags[$key] = $this->unescape_string(strtolower($tag));
}
return $tags;
}
final protected function escape_string($string) {
return str_replace(array("'",
'"'),
array('&#39;',
'&#34;'),
$string);
}
final protected function unescape_string($string) {
return str_replace(array('&#39;',
'&#34;'),
array("'",
'"'),
$string);
}
}
?>

@ -97,6 +97,8 @@ function make_page($page) {
return new QR();
case 'print':
return new Printer();
case 'dl':
return new Download();
}
}

100
script.js

@ -2,18 +2,7 @@ function ajaxRequest(action, datalist, callback) {
var request = false
request = new XMLHttpRequest()
request.open('POST', "./?page=ajax&action=" + action, true)
request.setRequestHeader('Content-Type',
'application/x-www-form-urlencoded')
var datastring = ''
var first = true
for(let [key, value] of datalist) {
if(!first) {
datastring += '&'
}
datastring += key + '=' + encodeURIComponent(value)
first = false
}
request.send(datastring)
request.send(datalist)
request.onreadystatechange = function() {
if (request.readyState == 4) {
@ -83,12 +72,18 @@ function fixDuplicateInputNames(fields) {
}
function dataListFromForm(form, filter = function(field) {return true}) {
var out = []
var out = new FormData()
var fields = form.querySelectorAll('input,textarea')
fixDuplicateInputNames(fields)
for(var i = 0; i < fields.length; i++) {
if(filter(fields[i])) {
out.push([fields[i].name, fields[i].value])
var field = fields[i]
if(filter(field)) {
if(field.type == 'file') {
var file = field.files[0]
out.append(field.name, file, file.name)
} else {
out.append(field.name, field.value)
}
}
}
return out
@ -103,9 +98,9 @@ function getFragment(name, callback) {
}
}
ajaxRequest('getfragment',
[['fragment', name]],
unpack)
var data = new FormData()
data.append('fragment', name)
ajaxRequest('getfragment', data, unpack)
}
function replace(fragment, replacements) {
@ -158,12 +153,12 @@ function extendLoan(event) {
function startInventory(event) {
event.preventDefault()
ajaxRequest('startinventory', [], reloadOrError)
ajaxRequest('startinventory', new FormData(), reloadOrError)
}
function endInventory(event) {
event.preventDefault()
ajaxRequest('endinventory', [], reloadOrError)
ajaxRequest('endinventory', new FormData(), reloadOrError)
}
function inventoryProduct(event) {
@ -220,7 +215,9 @@ function suggest(input, type) {
suggestlist.appendChild(next)
}
}
ajaxRequest('suggest', [['type', type]], render)
data = new FormData()
data.append('type', type)
ajaxRequest('suggest', data, render)
}
function suggestContent(input) {
@ -236,7 +233,9 @@ function suggestContent(input) {
suggestlist.appendChild(next)
}
}
ajaxRequest('suggestcontent', [['fieldname', input.name]], render)
data = new FormData()
data.append('fieldname', input.name)
ajaxRequest('suggestcontent', data, render)
}
function addField(event) {
@ -414,6 +413,59 @@ function updateUser(event) {
ajaxRequest('updateuser', dataListFromForm(form), reloadOrError)
}
function uploadAttachment(event) {
event.preventDefault()
var form = event.currentTarget
var render = function(result) {
if(result.type != 'success') {
showResult(result)
return
}
var classvalue = 'attachment-list'
var list = form.parentNode.querySelector('.'+classvalue)
if(list == null) {
list = document.createElement('ul')
list.classList.add(classvalue)
var p = form.parentNode.querySelector('p')
p.replaceWith(list)
}
var temp = document.createElement('template')
temp.innerHTML = result.message
list.appendChild(temp.content.firstChild)
}
var filter = function(input) {
if(input.name == 'filename') {
return false;
}
return true;
}
ajaxRequest('addattachment', dataListFromForm(form, filter), render)
}
function deleteAttachment(event) {
event.preventDefault()
var form = event.currentTarget
var node = form.parentNode
var name = form.name.value
if(window.confirm("Är du säker på att du vill ta bort bilagan '"
+name+"'?")) {
var render = function(result) {
if(result.type == 'success') {
var list = node.parentNode
list.removeChild(node)
if(list.childElementCount == 0) {
var p = document.createElement('p')
p.append('Inga bilagor.')
list.replaceWith(p)
}
} else {
showResult(result);
}
}
ajaxRequest('deleteattachment', dataListFromForm(form), render)
}
}
function productDataList(form) {
var filter = function(input) {
var name = input.name
@ -513,11 +565,11 @@ function removeTerm(event) {
}
function selectFile(event) {
var fileinput = document.getElementById("uploadfile")
var fileinput = event.currentTarget.parentNode.uploadfile
fileinput.click()
}
function showFile(event) {
var filefield = document.getElementById("filename")
var filefield = event.currentTarget.parentNode.filename
filefield.value = event.currentTarget.files[0].name
}

@ -2,6 +2,10 @@ textarea {
height: 80px;
}
ul {
padding-left: 15px;
}
#message {
position: absolute;
top: 5px;