Add image editing to position dialog
This commit is contained in:
parent
62729088c5
commit
74eef23492
@ -1,5 +1,4 @@
|
||||
<?php
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once(__DIR__ . "/../lib/UloggerDatabaseTestCase.php");
|
||||
require_once(__DIR__ . "/../../helpers/track.php");
|
||||
@ -118,7 +117,7 @@ class PositionTest extends UloggerDatabaseTestCase {
|
||||
$this->assertEquals(2, $this->getConnection()->getRowCount('tracks'), "Wrong row count");
|
||||
$this->assertEquals(4, $this->getConnection()->getRowCount('positions'), "Wrong row count");
|
||||
$posArr = uPosition::getLastAllUsers();
|
||||
$this->assertEquals(2, count($posArr), "Wrong row count");
|
||||
$this->assertCount(2, $posArr, "Wrong row count");
|
||||
foreach ($posArr as $position) {
|
||||
/** @var uPosition $position */
|
||||
switch ($position->id) {
|
||||
@ -152,15 +151,15 @@ class PositionTest extends UloggerDatabaseTestCase {
|
||||
$this->assertEquals(3, $this->getConnection()->getRowCount('positions'), "Wrong row count");
|
||||
|
||||
$posArr = uPosition::getAll();
|
||||
$this->assertEquals(3, count($posArr), "Wrong row count");
|
||||
$this->assertCount(3, $posArr, "Wrong row count");
|
||||
$posArr = uPosition::getAll($userId);
|
||||
$this->assertEquals(2, count($posArr), "Wrong row count");
|
||||
$this->assertCount(2, $posArr, "Wrong row count");
|
||||
$posArr = uPosition::getAll($userId, $trackId);
|
||||
$this->assertEquals(1, count($posArr), "Wrong row count");
|
||||
$this->assertCount(1, $posArr, "Wrong row count");
|
||||
$posArr = uPosition::getAll(NULL, $trackId);
|
||||
$this->assertEquals(1, count($posArr), "Wrong row count");
|
||||
$this->assertCount(1, $posArr, "Wrong row count");
|
||||
$posArr = uPosition::getAll($userId3);
|
||||
$this->assertEquals(0, count($posArr), "Wrong row count");
|
||||
$this->assertCount(0, $posArr, "Wrong row count");
|
||||
}
|
||||
|
||||
public function testDistanceTo() {
|
||||
@ -169,7 +168,7 @@ class PositionTest extends UloggerDatabaseTestCase {
|
||||
$pos1 = $this->addTestPosition($userId, $trackId, $this->testTimestamp, 0, 0);
|
||||
$pos2 = $this->addTestPosition($userId, $trackId, $this->testTimestamp, 0, 1);
|
||||
$posArr = uPosition::getAll();
|
||||
$this->assertEquals(2, count($posArr), "Wrong row count");
|
||||
$this->assertCount(2, $posArr, "Wrong row count");
|
||||
$this->assertEquals(111195, round($posArr[0]->distanceTo($posArr[1])), "Wrong distance");
|
||||
}
|
||||
|
||||
@ -179,7 +178,7 @@ class PositionTest extends UloggerDatabaseTestCase {
|
||||
$pos1 = $this->addTestPosition($userId, $trackId, $this->testTimestamp);
|
||||
$pos2 = $this->addTestPosition($userId, $trackId, $this->testTimestamp + 1);
|
||||
$posArr = uPosition::getAll();
|
||||
$this->assertEquals(2, count($posArr), "Wrong row count");
|
||||
$this->assertCount(2, $posArr, "Wrong row count");
|
||||
$this->assertEquals(-1, $posArr[0]->secondsTo($posArr[1]), "Wrong time difference");
|
||||
}
|
||||
|
||||
|
@ -507,6 +507,10 @@ button > * {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* alert */
|
||||
|
||||
.alert {
|
||||
|
@ -194,9 +194,7 @@ require_once(ROOT_DIR . "/helpers/upload.php");
|
||||
$query = "DELETE FROM " . self::db()->table('positions') . " WHERE id = ?";
|
||||
$stmt = self::db()->prepare($query);
|
||||
$stmt->execute([ $this->id ]);
|
||||
if ($this->hasImage()) {
|
||||
uUpload::delete($this->image);
|
||||
}
|
||||
$this->removeImage();
|
||||
$ret = true;
|
||||
$this->id = NULL;
|
||||
$this->isValid = false;
|
||||
@ -373,15 +371,9 @@ require_once(ROOT_DIR . "/helpers/upload.php");
|
||||
*/
|
||||
public static function removeImages($userId, $trackId = NULL) {
|
||||
if (($positions = self::getAllWithImage($userId, $trackId)) !== false) {
|
||||
/** @var uUpload $position */
|
||||
foreach ($positions as $position) {
|
||||
try {
|
||||
$query = "UPDATE " . self::db()->table('positions') . "
|
||||
SET image = NULL WHERE id = ?";
|
||||
$stmt = self::db()->prepare($query);
|
||||
$stmt->execute([ $position->id ]);
|
||||
// ignore unlink errors
|
||||
uUpload::delete($position->image);
|
||||
$position->removeImage();
|
||||
} catch (PDOException $e) {
|
||||
// TODO: handle exception
|
||||
syslog(LOG_ERR, $e->getMessage());
|
||||
@ -392,6 +384,38 @@ require_once(ROOT_DIR . "/helpers/upload.php");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add uploaded image
|
||||
* @param array $imageMeta File metadata array
|
||||
*/
|
||||
public function setImage($imageMeta) {
|
||||
if (!empty($imageMeta)) {
|
||||
if ($this->hasImage()) {
|
||||
$this->removeImage();
|
||||
}
|
||||
$this->image = uUpload::add($imageMeta, $this->trackId);
|
||||
$query = "UPDATE " . self::db()->table('positions') . "
|
||||
SET image = ? WHERE id = ?";
|
||||
$stmt = self::db()->prepare($query);
|
||||
$stmt->execute([ $this->image, $this->id ]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete image
|
||||
*/
|
||||
public function removeImage() {
|
||||
if ($this->hasImage()) {
|
||||
$query = "UPDATE " . self::db()->table('positions') . "
|
||||
SET image = NULL WHERE id = ?";
|
||||
$stmt = self::db()->prepare($query);
|
||||
$stmt->execute([ $this->id ]);
|
||||
// ignore unlink errors
|
||||
uUpload::delete($this->image);
|
||||
$this->image = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate distance to target point using haversine formula
|
||||
*
|
||||
|
@ -210,7 +210,7 @@ export default class MapViewModel extends ViewModel {
|
||||
if (pos.hasImage()) {
|
||||
const image = node.querySelector('#pimage img');
|
||||
image.onclick = () => {
|
||||
const modal = new uDialog(`<img src="uploads/${pos.image}" alt="image">`);
|
||||
const modal = new uDialog(`<img src="${pos.getImagePath()}" alt="image">`);
|
||||
const closeEl = modal.element.querySelector('#modal-close');
|
||||
closeEl.onclick = () => modal.destroy();
|
||||
modal.element.classList.add('image');
|
||||
|
@ -85,6 +85,17 @@ export default class uPosition {
|
||||
return (this.image != null && this.image.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?string}
|
||||
*/
|
||||
getImagePath() {
|
||||
return this.hasImage() ? `uploads/${this.image}` : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total speed in m/s
|
||||
* @return {number}
|
||||
*/
|
||||
get totalSpeed() {
|
||||
return this.totalSeconds ? this.totalMeters / this.totalSeconds : 0;
|
||||
}
|
||||
@ -110,10 +121,37 @@ export default class uPosition {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Promise<void, Error>}
|
||||
*/
|
||||
imageDelete() {
|
||||
return uPosition.update({
|
||||
action: 'imagedel',
|
||||
posid: this.id
|
||||
}).then(() => { this.image = null; });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {File} imageFile
|
||||
* @return {Promise<void, Error>}
|
||||
*/
|
||||
imageAdd(imageFile) {
|
||||
const data = new FormData();
|
||||
data.append('image', imageFile);
|
||||
data.append('action', 'imageadd');
|
||||
data.append('posid', this.id);
|
||||
return uPosition.update(data).then(
|
||||
/**
|
||||
* @param {Object} result
|
||||
* @param {string} result.image
|
||||
*/
|
||||
(result) => { this.image = result.image; });
|
||||
}
|
||||
|
||||
/**
|
||||
* Save track data
|
||||
* @param {Object} data
|
||||
* @return {Promise<void, Error>}
|
||||
* @return {Promise<void|Object, Error>}
|
||||
*/
|
||||
static update(data) {
|
||||
return uAjax.post('utils/handleposition.php', data);
|
||||
|
@ -17,13 +17,15 @@
|
||||
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { lang as $ } from './initializer.js';
|
||||
import { lang as $, config } from './initializer.js';
|
||||
import ViewModel from './viewmodel.js';
|
||||
import uAlert from './alert.js';
|
||||
import uDialog from './dialog.js';
|
||||
import uObserve from './observe.js';
|
||||
import uUtils from './utils.js';
|
||||
|
||||
const hiddenClass = 'hidden';
|
||||
|
||||
/**
|
||||
* @class PositionDialogModel
|
||||
*/
|
||||
@ -38,14 +40,22 @@ export default class PositionDialogModel extends ViewModel {
|
||||
onPositionDelete: null,
|
||||
onPositionUpdate: null,
|
||||
onCancel: null,
|
||||
comment: ''
|
||||
comment: null,
|
||||
image: null,
|
||||
onImageDelete: null
|
||||
});
|
||||
this.state = state;
|
||||
this.positionIndex = positionIndex;
|
||||
this.position = this.state.currentTrack.positions[positionIndex];
|
||||
this.model.comment = this.position.hasComment() ? this.position.comment : '';
|
||||
this.model.image = this.position.image;
|
||||
this.model.onPositionDelete = () => this.onPositionDelete();
|
||||
this.model.onPositionUpdate = () => this.onPositionUpdate();
|
||||
this.model.onCancel = () => this.onCancel();
|
||||
this.model.onImageDelete = () => this.onImageDelete();
|
||||
this.onChanged('image', (image) => {
|
||||
if (image && image !== this.position.image) { this.readImage(); }
|
||||
});
|
||||
}
|
||||
|
||||
init() {
|
||||
@ -53,6 +63,61 @@ export default class PositionDialogModel extends ViewModel {
|
||||
this.dialog = new uDialog(html);
|
||||
this.dialog.show();
|
||||
this.bindAll(this.dialog.element);
|
||||
this.previewEl = this.getBoundElement('imagePreview');
|
||||
this.fileEl = this.getBoundElement('image');
|
||||
this.imageDeleteEl = this.getBoundElement('onImageDelete');
|
||||
this.initReader();
|
||||
}
|
||||
|
||||
initReader() {
|
||||
this.reader = new FileReader();
|
||||
this.reader.addEventListener('load', () => {
|
||||
this.showThumbnail();
|
||||
}, false);
|
||||
this.reader.addEventListener('error', () => {
|
||||
this.model.image = this.position.image;
|
||||
}, false);
|
||||
}
|
||||
|
||||
readImage() {
|
||||
const file = this.fileEl.files[0];
|
||||
if (file) {
|
||||
if (file.size > config.uploadMaxSize) {
|
||||
uAlert.error($._('isizefailure', config.uploadMaxSize));
|
||||
this.model.image = this.position.image;
|
||||
return;
|
||||
}
|
||||
this.reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
showThumbnail() {
|
||||
this.previewEl.onload = () => this.toggleImage();
|
||||
this.previewEl.onerror = () => {
|
||||
uAlert.error($._('iuploadfailure'));
|
||||
this.model.image = this.position.image;
|
||||
};
|
||||
this.previewEl.src = this.reader.result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle image visibility
|
||||
*/
|
||||
toggleImage() {
|
||||
if (this.previewEl.classList.contains(hiddenClass)) {
|
||||
this.previewEl.classList.remove(hiddenClass);
|
||||
this.imageDeleteEl.classList.remove(hiddenClass);
|
||||
this.fileEl.classList.add(hiddenClass);
|
||||
} else {
|
||||
this.previewEl.classList.add(hiddenClass);
|
||||
this.imageDeleteEl.classList.add(hiddenClass);
|
||||
this.fileEl.classList.remove(hiddenClass);
|
||||
}
|
||||
}
|
||||
|
||||
onImageDelete() {
|
||||
this.model.image = null;
|
||||
this.toggleImage();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -64,7 +129,13 @@ export default class PositionDialogModel extends ViewModel {
|
||||
<div style="clear: both; padding-bottom: 1em;"></div>
|
||||
<form id="positionForm">
|
||||
<label><b>${$._('comment')}</b></label><br>
|
||||
<textarea style="width:100%;" maxlength="255" rows="5" placeholder="${$._('comment')}" name="comment" data-bind="comment" autofocus>${this.position.hasComment() ? uUtils.htmlEncode(this.position.comment) : ''}</textarea>
|
||||
<textarea style="width:100%;" maxlength="255" rows="5" placeholder="${$._('comment')}" name="comment"
|
||||
data-bind="comment" autofocus>${uUtils.htmlEncode(this.model.comment)}</textarea>
|
||||
<br><br>
|
||||
<label><b>${$._('image')}</b></label><br>
|
||||
<input type="file" name="image" data-bind="image" accept="image/png, image/jpeg, image/gif, image/bmp"${this.position.hasImage() ? ' class="hidden"' : ''}>
|
||||
<img style="max-width:50px; max-height:50px" data-bind="imagePreview" ${this.position.hasImage() ? `src="${this.position.getImagePath()}"` : 'class="hidden"'}>
|
||||
<a data-bind="onImageDelete" ${this.position.hasImage() ? '' : ' class="hidden"'}>${$._('delimage')}</a>
|
||||
<div class="buttons">
|
||||
<button class="button-reject" data-bind="onCancel" type="button">${$._('cancel')}</button>
|
||||
<button class="button-resolve" data-bind="onPositionUpdate" type="submit">${$._('submit')}</button>
|
||||
@ -86,10 +157,27 @@ export default class PositionDialogModel extends ViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
updateImage() {
|
||||
let promise = Promise.resolve();
|
||||
if (this.model.image !== this.position.image) {
|
||||
if (this.model.image === null) {
|
||||
promise = this.position.imageDelete();
|
||||
} else {
|
||||
promise = this.position.imageAdd(this.fileEl.files[0]);
|
||||
}
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
||||
onPositionUpdate() {
|
||||
this.model.comment.trim();
|
||||
if (this.validate()) {
|
||||
this.position.comment = this.model.comment;
|
||||
this.position.save()
|
||||
this.updateImage()
|
||||
.then(() => this.position.save())
|
||||
.then(() => {
|
||||
uObserve.forceUpdate(this.state, 'currentTrack');
|
||||
this.dialog.destroy()
|
||||
@ -107,6 +195,7 @@ export default class PositionDialogModel extends ViewModel {
|
||||
* @return {boolean} True if valid
|
||||
*/
|
||||
validate() {
|
||||
return this.model.comment !== this.position.comment;
|
||||
return !(this.model.comment === this.position.comment && this.model.image === this.position.image);
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -241,6 +241,43 @@ describe('Position tests', () => {
|
||||
expect(uPosition.update).toHaveBeenCalledWith({ action: 'update', posid: posId, comment: comment });
|
||||
});
|
||||
|
||||
it('should delete image on server', (done) => {
|
||||
// given
|
||||
spyOn(uPosition, 'update').and.returnValue(Promise.resolve());
|
||||
const position = uPosition.fromJson(jsonPosition);
|
||||
// when
|
||||
position.imageDelete()
|
||||
// then
|
||||
setTimeout(() => {
|
||||
expect(uPosition.update).toHaveBeenCalledWith({ action: 'imagedel', posid: posId });
|
||||
expect(position.image).toBeNull();
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('should add image on server', (done) => {
|
||||
// given
|
||||
const newImage = 'new_image.jpg';
|
||||
const imageFile = 'imageFile';
|
||||
spyOn(uPosition, 'update').and.returnValue(Promise.resolve({ image: newImage }));
|
||||
const position = uPosition.fromJson(jsonPosition);
|
||||
// when
|
||||
position.imageAdd(imageFile);
|
||||
// then
|
||||
setTimeout(() => {
|
||||
expect(uPosition.update).toHaveBeenCalledWith(jasmine.any(FormData));
|
||||
|
||||
/** @var {FormData} */
|
||||
const data = uPosition.update.calls.mostRecent().args[0];
|
||||
|
||||
expect(data.get('image')).toBe(imageFile);
|
||||
expect(data.get('action')).toBe('imageadd');
|
||||
expect(data.get('posid')).toBe(posId.toString());
|
||||
expect(position.image).toBe(newImage);
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('should call ajax post with url and params', () => {
|
||||
// given
|
||||
const url = 'utils/handleposition.php';
|
||||
|
@ -116,7 +116,9 @@ $lang["edittrack"] = "Edit track";
|
||||
$lang["positiondelwarn"] = "Warning!\n\nYou are going to permanently delete position %d of track %s.\n\nAre you sure?"; // substitutes position index and track name
|
||||
$lang["editingposition"] = "You are editing position #%d of track %s"; // substitutes position index and track name
|
||||
$lang["delposition"] = "Remove position";
|
||||
$lang["delimage"] = "Remove image";
|
||||
$lang["comment"] = "Comment";
|
||||
$lang["image"] = "Image";
|
||||
$lang["editposition"] = "Edit position";
|
||||
$lang["passlenmin"] = "Password must be at least %d characters"; // substitutes password minimum length
|
||||
$lang["passrules_1"] = "It should contain at least one lower case letter, one upper case letter";
|
||||
|
@ -25,9 +25,9 @@ require_once(ROOT_DIR . "/helpers/config.php");
|
||||
|
||||
$auth = new uAuth();
|
||||
|
||||
$action = uUtils::postString('action');
|
||||
$positionId = uUtils::postInt('posid');
|
||||
$comment = uUtils::postString('comment');
|
||||
$action = uUtils::postString("action");
|
||||
$positionId = uUtils::postInt("posid");
|
||||
$comment = uUtils::postString("comment");
|
||||
|
||||
$config = uConfig::getInstance();
|
||||
$lang = (new uLang($config))->getStrings();
|
||||
@ -41,26 +41,52 @@ if (!$position->isValid ||
|
||||
uUtils::exitWithError($lang["servererror"]);
|
||||
}
|
||||
|
||||
$data = null;
|
||||
|
||||
switch ($action) {
|
||||
|
||||
case 'update':
|
||||
case "update":
|
||||
$position->comment = $comment;
|
||||
if ($position->update() === false) {
|
||||
uUtils::exitWithError($lang["servererror"]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
case "delete":
|
||||
if ($position->delete() === false) {
|
||||
uUtils::exitWithError($lang["servererror"]);
|
||||
}
|
||||
break;
|
||||
|
||||
case "imageadd":
|
||||
try {
|
||||
$fileMeta = uUtils::requireFile("image");
|
||||
if ($position->setImage($fileMeta) === false) {
|
||||
uUtils::exitWithError($lang["servererror"]);
|
||||
}
|
||||
$data = [ "image" => $position->image ];
|
||||
} catch (ErrorException $ee) {
|
||||
$message = $lang["servererror"];
|
||||
$message .= ": {$ee->getMessage()}";
|
||||
uUtils::exitWithError($message);
|
||||
} catch (Exception $e) {
|
||||
$message = $lang["iuploadfailure"];
|
||||
$message .= ": {$e->getMessage()}";
|
||||
uUtils::exitWithError($message);
|
||||
}
|
||||
break;
|
||||
|
||||
case "imagedel":
|
||||
if ($position->removeImage() === false) {
|
||||
uUtils::exitWithError($lang["servererror"]);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
uUtils::exitWithError($lang["servererror"]);
|
||||
break;
|
||||
}
|
||||
|
||||
uUtils::exitWithSuccess();
|
||||
uUtils::exitWithSuccess($data);
|
||||
|
||||
?>
|
Loading…
x
Reference in New Issue
Block a user