1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +02:00
This commit is contained in:
Laurent Cozic 2016-12-27 21:25:07 +01:00
parent 999a2c5ef2
commit a409f994cd
34 changed files with 528 additions and 270 deletions

3
.gitignore vendored
View File

@ -24,4 +24,5 @@ TODO.md
QtClient/data/
app/data/uploads/
!app/data/uploads/.gitkeep
sparse_test.php
sparse_test.php
INFO.md

View File

@ -1,4 +1,4 @@
QT += qml quick sql quickcontrols2
QT += qml quick sql quickcontrols2 network
CONFIG += c++11
@ -16,7 +16,10 @@ SOURCES += \
application.cpp \
models/notecollection.cpp \
services/notecache.cpp \
models/qmlnote.cpp
models/qmlnote.cpp \
webapi.cpp \
synchronizer.cpp \
settings.cpp
RESOURCES += qml.qrc \
database.qrc
@ -44,7 +47,10 @@ HEADERS += \
models/notecollection.h \
services/notecache.h \
sparsevector.hpp \
models/qmlnote.h
models/qmlnote.h \
webapi.h \
synchronizer.h \
settings.h
DISTFILES +=

View File

@ -4,11 +4,20 @@
#include "database.h"
#include "models/foldermodel.h"
#include "services/folderservice.h"
#include "settings.h"
using namespace jop;
Application::Application(int &argc, char **argv) : QGuiApplication(argc, argv) {
db_ = Database("D:/Web/www/joplin/QtClient/data/notes.sqlite");
Application::Application(int &argc, char **argv) : QGuiApplication(argc, argv), db_("D:/Web/www/joplin/QtClient/data/notes.sqlite"), api_("http://joplin.local"), synchronizer_(api_, db_) {
// This is linked to where the QSettings will be saved. In other words,
// if these values are changed, the settings will be reset and saved
// somewhere else.
QCoreApplication::setOrganizationName("Cozic");
QCoreApplication::setOrganizationDomain("cozic.net");
QCoreApplication::setApplicationName("Joplin");
Settings settings;
folderService_ = FolderService(db_);
folderModel_.setService(folderService_);
@ -29,6 +38,31 @@ Application::Application(int &argc, char **argv) : QGuiApplication(argc, argv) {
connect(rootObject, SIGNAL(currentNoteChanged()), this, SLOT(view_currentNoteChanged()));
view_.show();
connect(&api_, SIGNAL(requestDone(const QJsonObject&, const QString&)), this, SLOT(api_requestDone(const QJsonObject&, const QString&)));
QString sessionId = settings.value("sessionId").toString();
if (sessionId == "") {
QUrlQuery postData;
postData.addQueryItem("email", "laurent@cozic.net");
postData.addQueryItem("password", "12345678");
postData.addQueryItem("client_id", "B6E12222B6E12222");
api_.post("sessions", QUrlQuery(), postData, "getSession");
} else {
afterSessionInitialization();
}
}
void Application::api_requestDone(const QJsonObject& response, const QString& tag) {
// TODO: handle errors
if (tag == "getSession") {
QString sessionId = response.value("id").toString();
Settings settings;
settings.setValue("sessionId", sessionId);
afterSessionInitialization();
return;
}
}
QString Application::selectedFolderId() const {
@ -47,6 +81,17 @@ QString Application::selectedNoteId() const {
return noteModel_.data(modelIndex, NoteModel::IdRole).toString();
}
void Application::afterSessionInitialization() {
// TODO: rather than saving the session id, save the username/password and
// request a new session everytime on startup.
Settings settings;
QString sessionId = settings.value("sessionId").toString();
qDebug() << "Session:" << sessionId;
api_.setSessionId(sessionId);
synchronizer_.start();
}
void Application::view_currentFolderChanged() {
QString folderId = selectedFolderId();
noteCollection_ = NoteCollection(db_, folderId, "title ASC");

View File

@ -11,6 +11,8 @@
#include "services/notecache.h"
#include "models/notemodel.h"
#include "models/qmlnote.h"
#include "webapi.h"
#include "synchronizer.h"
namespace jop {
@ -35,11 +37,16 @@ private:
QString selectedNoteId() const;
NoteCache noteCache_;
QmlNote selectedQmlNote_;
WebApi api_;
Synchronizer synchronizer_;
void afterSessionInitialization();
public slots:
void view_currentFolderChanged();
void view_currentNoteChanged();
void api_requestDone(const QJsonObject& response, const QString& tag);
};

View File

@ -5,7 +5,7 @@ using namespace jop;
Database::Database(const QString &path) {
version_ = -1;
// QFile::remove(path);
//QFile::remove(path);
db_ = QSqlDatabase::addDatabase("QSQLITE");
db_.setDatabaseName(path);

View File

@ -12,7 +12,6 @@ public:
Database(const QString& path);
Database();
QSqlQuery query(const QString& sql) const;
//QSqlQuery exec(const QString& sql, const QMap<QString, QVariant> &parameters);
private:

View File

@ -14,8 +14,6 @@ public:
private:
};
}

View File

@ -5,16 +5,7 @@ using namespace jop;
Item::Item() {
isPartial_ = true;
}
void Item::fromSqlQuery(const QSqlQuery &q) {
int i_id = q.record().indexOf("id");
int i_title = q.record().indexOf("title");
int i_created_time = q.record().indexOf("created_time");
id_ = q.value(i_id).toString();
title_ = q.value(i_title).toString();
createdTime_ = q.value(i_created_time).toInt();
synced_ = false;
}
QString Item::id() const {
@ -29,6 +20,10 @@ int Item::createdTime() const {
return createdTime_;
}
int Item::updatedTime() const {
return updatedTime_;
}
void Item::setId(const QString& v) {
id_ = v;
}
@ -48,3 +43,17 @@ void Item::setIsPartial(bool v) {
bool Item::isPartial() const {
return isPartial_;
}
QStringList Item::dbFields() {
QStringList output;
output << "id" << "title" << "created_time" << "updated_time" << "synced";
return output;
}
void Item::fromSqlQuery(const QSqlQuery &q) {
id_ = q.value(0).toString();
title_ = q.value(1).toString();
createdTime_ = q.value(2).toInt();
updatedTime_ = q.value(3).toInt();
synced_ = q.value(4).toBool();
}

View File

@ -14,7 +14,9 @@ public:
QString id() const;
QString title() const;
int createdTime() const;
int updatedTime() const;
bool isPartial() const;
static QStringList dbFields();
void setId(const QString &v);
void setTitle(const QString& v);
@ -27,8 +29,11 @@ private:
QString id_;
QString title_;
int createdTime_;
bool isPartial_;
time_t createdTime_;
time_t updatedTime_;
bool synced_;
bool isPartial_;
};

View File

@ -14,3 +14,15 @@ QString Note::body() const {
void Note::setBody(const QString &v) {
body_ = v;
}
QStringList Note::dbFields() {
QStringList output = Item::dbFields();
output << "body";
return output;
}
void Note::fromSqlQuery(const QSqlQuery &q) {
Item::fromSqlQuery(q);
int idx = Item::dbFields().size();
body_ = q.value(idx).toString();
}

View File

@ -13,6 +13,8 @@ public:
Note();
QString body() const;
void setBody(const QString& v);
static QStringList dbFields();
void fromSqlQuery(const QSqlQuery &q);
private:

View File

@ -2,7 +2,8 @@ CREATE TABLE folders (
id TEXT PRIMARY KEY,
title TEXT,
created_time INT,
updated_time INT
updated_time INT,
synced BOOLEAN DEFAULT 0
);
CREATE TABLE notes (
@ -23,20 +24,23 @@ CREATE TABLE notes (
todo_completed INT,
source_application TEXT,
application_data TEXT,
`order` INT
`order` INT,
synced BOOLEAN DEFAULT 0
);
CREATE TABLE tags (
id TEXT PRIMARY KEY,
title TEXT,
created_time INT,
updated_time INT
updated_time INT,
synced BOOLEAN DEFAULT 0
);
CREATE TABLE note_tags (
id INTEGER PRIMARY KEY,
note_id TEXT,
tag_id TEXT
tag_id TEXT,
synced BOOLEAN DEFAULT 0
);
CREATE TABLE resources (
@ -45,13 +49,15 @@ CREATE TABLE resources (
mime TEXT,
filename TEXT,
created_time INT,
updated_time INT
updated_time INT,
synced BOOLEAN DEFAULT 0
);
CREATE TABLE note_resources (
id INTEGER PRIMARY KEY,
note_id TEXT,
resource_id TEXT
resource_id TEXT,
synced BOOLEAN DEFAULT 0
);
CREATE TABLE version (

View File

@ -0,0 +1,3 @@
#include "settings.h"
using namespace jop;

View File

@ -0,0 +1,19 @@
#ifndef SETTINGS_H
#define SETTINGS_H
#include <stable.h>
namespace jop {
class Settings : public QSettings {
Q_OBJECT
//public:
// Settings();
};
}
#endif // SETTINGS_H

View File

@ -10,8 +10,6 @@
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlRecord>
//#include <QUuid>
//#include <vector>
#include <QList>
#include <QGuiApplication>
#include <QQmlApplicationEngine>
@ -20,6 +18,13 @@
#include <QQmlContext>
#include <QQmlProperty>
#include <QSqlError>
#include <QNetworkAccessManager>
#include <QUrlQuery>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QSettings>
#include <QJsonObject>
#include <QJsonParseError>
#endif // __cplusplus

View File

@ -0,0 +1,83 @@
#include "synchronizer.h"
#include "models/folder.h"
#include "models/note.h"
using namespace jop;
Synchronizer::Synchronizer(WebApi& api, Database &database) : api_(api), db_(database) {
qDebug() << api_.baseUrl();
connect(&api_, SIGNAL(requestDone(QJsonObject,QString)), this, SLOT(api_requestDone(QJsonObject,QString)));
}
void Synchronizer::start() {
qDebug() << "Starting synchronizer...";
QSqlQuery query;
std::vector<Folder> folders;
query = db_.query("SELECT " + Folder::dbFields().join(',') + " FROM folders WHERE synced = 0");
query.exec();
while (query.next()) {
Folder folder;
folder.fromSqlQuery(query);
folders.push_back(folder);
}
QList<Note> notes;
query = db_.query("SELECT " + Note::dbFields().join(',') + " FROM notes WHERE synced = 0");
query.exec();
while (query.next()) {
Note note;
note.fromSqlQuery(query);
notes << note;
}
for (size_t i = 0; i < folders.size(); i++) {
Folder folder = folders[i];
QUrlQuery data;
data.addQueryItem("id", folder.id());
data.addQueryItem("title", folder.title());
data.addQueryItem("created_time", QString::number(folder.createdTime()));
data.addQueryItem("updated_time", QString::number(folder.updatedTime()));
api_.put("folders/" + folder.id(), QUrlQuery(), data, "putFolder:" + folder.id());
}
for (size_t i = 0; i < notes.size(); i++) {
Note note = notes[i];
QUrlQuery data;
data.addQueryItem("id", note.id());
data.addQueryItem("title", note.title());
data.addQueryItem("body", note.body());
data.addQueryItem("created_time", QString::number(note.createdTime()));
data.addQueryItem("updated_time", QString::number(note.updatedTime()));
api_.put("notes/" + note.id(), QUrlQuery(), data, "putNote:" + note.id());
}
}
void Synchronizer::api_requestDone(const QJsonObject& response, const QString& tag) {
QSqlQuery query;
QStringList parts = tag.split(':');
QString action = tag;
QString id = "";
if (parts.size() == 2) {
action = parts[0];
id = parts[1];
}
if (action == "putFolder") {
// qDebug() << "Done folder" << id;
// query = db_.query("UPDATE folders SET synced = 1 WHERE id = ?");
// query.addBindValue(id);
// query.exec();
}
if (action == "putNote") {
// qDebug() << "Done note" << id;
// query = db_.query("UPDATE notes SET synced = 1 WHERE id = ?");
// query.addBindValue(id);
// query.exec();
}
}

View File

@ -0,0 +1,32 @@
#ifndef SYNCHRONIZER_H
#define SYNCHRONIZER_H
#include <stable.h>
#include "webapi.h"
#include "database.h"
namespace jop {
class Synchronizer : public QObject {
Q_OBJECT
public:
Synchronizer(WebApi &api, Database& database);
void start();
private:
WebApi& api_;
Database& db_;
public slots:
void api_requestDone(const QJsonObject& response, const QString& tag);
};
}
#endif // SYNCHRONIZER_H

View File

@ -0,0 +1,110 @@
#include <stable.h>
#include "webapi.h"
using namespace jop;
WebApi::WebApi(const QString &baseUrl) {
baseUrl_ = baseUrl;
sessionId_ = "";
connect(&manager_, SIGNAL(finished(QNetworkReply*)), this, SLOT(request_finished(QNetworkReply*)));
}
QString WebApi::baseUrl() const {
return baseUrl_;
}
void WebApi::execRequest(QNetworkAccessManager::Operation method, const QString &path, const QUrlQuery &query, const QUrlQuery &data, const QString& tag) {
QueuedRequest r;
r.method = method;
r.path = path;
r.query = query;
r.data = data;
r.tag = tag;
queuedRequests_ << r;
processQueue();
}
void WebApi::post(const QString& path,const QUrlQuery& query, const QUrlQuery& data, const QString& tag) { execRequest(QNetworkAccessManager::PostOperation, path, query, data, tag); }
void WebApi::get(const QString& path,const QUrlQuery& query, const QUrlQuery& data, const QString& tag) { execRequest(QNetworkAccessManager::GetOperation, path, query, data, tag); }
void WebApi::put(const QString& path,const QUrlQuery& query, const QUrlQuery& data, const QString& tag) { execRequest(QNetworkAccessManager::PutOperation, path, query, data, tag); }
//void patch(const QString& path,const QUrlQuery& query = QUrlQuery(), const QUrlQuery& data = QUrlQuery(), const QString& tag = "") { execRequest(QNetworkAccessManager::PatchOperation, query, data, tag); }
void WebApi::setSessionId(const QString &v) {
sessionId_ = v;
}
void WebApi::processQueue() {
if (!queuedRequests_.size() || inProgressRequests_.size() >= 50) return;
QueuedRequest& r = queuedRequests_.takeFirst();
QString url = baseUrl_ + "/" + r.path;
QNetworkRequest* request = new QNetworkRequest(url);
request->setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
QNetworkReply* reply = NULL;
if (r.method == QNetworkAccessManager::GetOperation) {
// TODO
//manager->get(QNetworkRequest(QUrl("http://qt-project.org")));
}
if (r.method == QNetworkAccessManager::PostOperation) {
reply = manager_.post(*request, r.data.toString(QUrl::FullyEncoded).toUtf8());
}
if (r.method == QNetworkAccessManager::PutOperation) {
reply = manager_.put(*request, r.data.toString(QUrl::FullyEncoded).toUtf8());
}
if (!reply) {
qWarning() << "WebApi::processQueue(): reply object was not created - invalid request method";
return;
}
r.reply = reply;
r.request = request;
connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(request_error(QNetworkReply::NetworkError)));
QStringList cmd;
cmd << "curl";
if (r.method == QNetworkAccessManager::PutOperation) {
cmd << "-X" << "PUT";
cmd << "--data" << "'" + r.data.toString(QUrl::FullyEncoded) + "'";
cmd << url;
}
//qDebug().noquote() << cmd.join(" ");
inProgressRequests_.push_back(r);
}
void WebApi::request_finished(QNetworkReply *reply) {
QByteArray responseBodyBA = reply->readAll();
QJsonObject response;
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(responseBodyBA, &err);
if (err.error != QJsonParseError::NoError) {
qWarning() << "Could not parse JSON:" << err.errorString();
qWarning().noquote() << QString(responseBodyBA);
} else {
response = doc.object();
}
for (size_t i = 0; i < inProgressRequests_.size(); i++) {
QueuedRequest r = inProgressRequests_[i];
if (r.reply == reply) {
inProgressRequests_.erase(inProgressRequests_.begin() + i);
emit requestDone(response, r.tag);
break;
}
}
processQueue();
}
void WebApi::request_error(QNetworkReply::NetworkError e) {
qDebug() << "Network error" << e;
}

View File

@ -0,0 +1,55 @@
#ifndef WEBAPI_H
#define WEBAPI_H
#include <stable.h>
namespace jop {
class WebApi : public QObject {
Q_OBJECT
public:
struct QueuedRequest {
QNetworkAccessManager::Operation method;
QString path;
QUrlQuery query;
QUrlQuery data;
QNetworkReply* reply;
QNetworkRequest* request;
QString tag;
};
WebApi(const QString& baseUrl);
QString baseUrl() const;
void execRequest(QNetworkAccessManager::Operation method, const QString& path,const QUrlQuery& query = QUrlQuery(), const QUrlQuery& data = QUrlQuery(), const QString& tag = "");
void post(const QString& path,const QUrlQuery& query = QUrlQuery(), const QUrlQuery& data = QUrlQuery(), const QString& tag = "");
void get(const QString& path,const QUrlQuery& query = QUrlQuery(), const QUrlQuery& data = QUrlQuery(), const QString& tag = "");
void put(const QString& path,const QUrlQuery& query = QUrlQuery(), const QUrlQuery& data = QUrlQuery(), const QString& tag = "");
//void patch(const QString& path,const QUrlQuery& query = QUrlQuery(), const QUrlQuery& data = QUrlQuery(), const QString& tag = "");
void setSessionId(const QString& v);
private:
QString baseUrl_;
QList<QueuedRequest> queuedRequests_;
QList<QueuedRequest> inProgressRequests_;
void processQueue();
QString sessionId_;
QNetworkAccessManager manager_;
public slots:
void request_finished(QNetworkReply* reply);
void request_error(QNetworkReply::NetworkError e);
signals:
void requestDone(const QJsonObject& response, const QString& tag);
};
}
#endif // WEBAPI_H

0
app/data/uploads/.gitkeep Normal file → Executable file
View File

View File

@ -66,7 +66,11 @@ abstract class ApiController extends Controller {
protected function session() {
if ($this->useTestUserAndSession) {
$session = Session::find(Session::unhex('BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'));
if ($session) $session->delete();
if ($session) return $session;
// if ($session) {
// $ok = $session->delete();
// if (!$ok) throw new \Exception("Cannot delete session");
// }
$session = new Session();
$session->id = Session::unhex('BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB');
$session->owner_id = Session::unhex('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
@ -75,7 +79,6 @@ abstract class ApiController extends Controller {
return $session;
}
if ($this->session) return $this->session;
$request = $this->container->get('request_stack')->getCurrentRequest();
$this->session = Session::find(BaseModel::unhex($request->query->get('session')));
@ -142,37 +145,49 @@ abstract class ApiController extends Controller {
protected function patchParameters() {
$output = array();
$input = file_get_contents('php://input');
preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches);
$boundary = $matches[1];
$blocks = preg_split("/-+$boundary/", $input);
array_pop($blocks);
foreach ($blocks as $id => $block) {
if (empty($block)) continue;
// you'll have to var_dump $block to understand this and maybe replace \n or \r with a visibile char
// Two content types are supported:
//
// multipart/form-data; boundary=------------------------68670b1a1565e787
// application/x-www-form-urlencoded
// parse uploaded files
if (strpos($block, 'application/octet-stream') !== FALSE) {
// match "name", then everything after "stream" (optional) except for prepending newlines
preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches);
} else {
// match "name" and optional value in between newline sequences
preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches);
}
if (!isset($matches[2])) {
// Regex above will not find anything if the parameter has not value. For example
// "parent_id" below:
if (!isset($_SERVER['CONTENT_TYPE']) || $_SERVER['CONTENT_TYPE'] == 'application/x-www-form-urlencoded') {
parse_str($input, $output);
} else {
if (!isset($_SERVER['CONTENT_TYPE'])) throw new \Exception("Cannot decode input data");
preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches);
if (!isset($matches[1])) throw new \Exception("Cannot decode input data");
$boundary = $matches[1];
$blocks = preg_split("/-+$boundary/", $input);
array_pop($blocks);
foreach ($blocks as $id => $block) {
if (empty($block)) continue;
// Content-Disposition: form-data; name="parent_id"
//
//
// Content-Disposition: form-data; name="id"
//
// 54ad197be333c98778c7d6f49506efcb
// you'll have to var_dump $block to understand this and maybe replace \n or \r with a visibile char
$output[$matches[1]] = '';
} else {
$output[$matches[1]] = $matches[2];
// parse uploaded files
if (strpos($block, 'application/octet-stream') !== FALSE) {
// match "name", then everything after "stream" (optional) except for prepending newlines
preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches);
} else {
// match "name" and optional value in between newline sequences
preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches);
}
if (!isset($matches[2])) {
// Regex above will not find anything if the parameter has no value. For example
// "parent_id" below:
// Content-Disposition: form-data; name="parent_id"
//
//
// Content-Disposition: form-data; name="id"
//
// 54ad197be333c98778c7d6f49506efcb
$output[$matches[1]] = '';
} else {
$output[$matches[1]] = $matches[2];
}
}
}

View File

@ -36,18 +36,10 @@ class FoldersController extends ApiController {
}
if ($request->isMethod('PUT')) {
// TODO: call fromPublicArray() - handles unhex conversion
$data = $this->putParameters();
$isNew = !$folder;
if ($isNew) $folder = new Folder();
foreach ($data as $n => $v) {
if ($n == 'parent_id') $v = Folder::unhex($v);
$folder->{$n} = $v;
}
$folder->owner_id = $this->user()->id;
if (!$folder) $folder = new Folder();
$folder->fromPublicArray($this->putParameters());
$folder->id = Folder::unhex($id);
$folder->setIsNew($isNew);
$folder->owner_id = $this->user()->id;
$folder->save();
return static::successResponse($folder);
}

View File

@ -37,16 +37,10 @@ class NotesController extends ApiController {
}
if ($request->isMethod('PUT')) {
$data = $this->putParameters();
$isNew = !$note;
if ($isNew) $note = new Note();
foreach ($data as $n => $v) {
if ($n == 'parent_id') $v = Note::unhex($v);
$note->{$n} = $v;
}
if (!$note) $note = new Note();
$note->fromPublicArray($this->putParameters());
$note->id = Note::unhex($id);
$note->owner_id = $this->user()->id;
$note->setIsNew($isNew);
$note->save();
return static::successResponse($note);
}

View File

@ -20,7 +20,7 @@ class SessionsController extends ApiController {
* @Route("/sessions")
*/
public function allAction(Request $request) {
if ($request->isMethod('POST')) {
if ($request->isMethod('POST')) {
$data = $request->request->all();
// Note: the login method will throw an exception in case of failure
$session = Session::login($data['email'], $data['password'], Session::unhex($data['client_id']));

View File

@ -25,161 +25,6 @@ class UsersController extends ApiController {
* @Route("/users")
*/
public function allAction(Request $request) {
$source = "This is the first line.\n\nThis is the second line.";
$target1 = "This is the first line XXX.\n\nThis is the second line.";
$target2 = "This is the first line.\n\nThis is the second line YYY.";
$r = Diff::merge3($source, $target1, $target2);
var_dump($r);die();
// $dmp = new DiffMatchPatch();
// $patches = $dmp->patch_make($source, $target1);
// // @@ -1,11 +1,12 @@
// // Th
// // -e
// // +at
// // quick b
// // @@ -22,18 +22,17 @@
// // jump
// // -s
// // +ed
// // over
// // -the
// // +a
// // laz
// $result = $dmp->patch_apply($patches, $target2);
// var_dump($result);
// die();
// $dmp = new DiffMatchPatch();
// $source = "This is the first line.\n\nThis is the second line.";
// $target1 = "This is the first line XXX.\n\nThis is the second line.";
// $target2 = "edsùfrklq lkzerlmk zemlkrmzlkerm lze.";
// $diff1 = $dmp->patch_make($source, $target1);
// $diff2 = $dmp->patch_make($source, $target2);
// //var_dump($dmp->patch_toText($diff1));
// // //var_dump($diff1[0]->patch_toText());
// $r = $dmp->patch_apply($diff1, $source);
// $r = $dmp->patch_apply($diff1, $target2);
// var_dump($r);die();
// $r = $dmp->patch_apply($diff2, $r[0]);
// var_dump($r);
// $dmp = new DiffMatchPatch();
// $patches = $dmp->patch_make($source, $target1);
// // @@ -1,11 +1,12 @@
// // Th
// // -e
// // +at
// // quick b
// // @@ -22,18 +22,17 @@
// // jump
// // -s
// // +ed
// // over
// // -the
// // +a
// // laz
// $result = $dmp->patch_apply($patches, $target2);
// var_dump($result);
// die();
// $r = Diff::merge($source, $target1, $target2);
// var_dump($r);die();
// $diff1 = xdiff_string_diff($source, $target1);
// $diff2 = xdiff_string_diff($source, $target2);
// $errors = array();
// $t = xdiff_string_merge3($source , $target1, $target2, $errors);
// var_dump($errors);
// var_dump($t);die();
// var_dump($diff1);
// var_dump($diff2);
// $errors = array();
// $t = xdiff_string_patch($source, $diff1, XDIFF_PATCH_NORMAL, $errors);
// var_dump($t);
// var_dump($errors);
// $errors = array();
// $t = xdiff_string_patch($t, $diff2, XDIFF_PATCH_NORMAL, $errors);
// var_dump($t);
// var_dump($errors);
// var_dump($diff1);
// var_dump($diff2);
// $change = new Change();
// $change->user_id = BaseItem::unhex('204705F2E2E698036034FDC709840B80');
// $change->client_id = BaseItem::unhex('11111111111111111111111111111111');
// $change->item_type = BaseItem::enumId('type', 'note');
// $change->item_field = BaseItem::enumId('field', 'title');
// $change->item_id = BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
// $change->delta = 'salut ca va';
// $change->save();
// $change = new Change();
// $change->user_id = BaseItem::unhex('204705F2E2E698036034FDC709840B80');
// $change->client_id = BaseItem::unhex('11111111111111111111111111111111');
// $change->item_type = BaseItem::enumId('type', 'note');
// $change->item_field = BaseItem::enumId('field', 'title');
// $change->item_id = BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
// $change->createDelta('salut, ça va ? oui très bien');
// $change->save();
// $change = new Change();
// $change->user_id = BaseItem::unhex('204705F2E2E698036034FDC709840B80');
// $change->client_id = BaseItem::unhex('11111111111111111111111111111111');
// $change->item_type = BaseItem::enumId('type', 'note');
// $change->item_field = BaseItem::enumId('field', 'title');
// $change->item_id = BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
// $change->createDelta('salut - oui très bien');
// $change->save();
// $change = new Change();
// $change->user_id = BaseItem::unhex('204705F2E2E698036034FDC709840B80');
// $change->client_id = BaseItem::unhex('11111111111111111111111111111111');
// $change->item_type = BaseItem::enumId('type', 'note');
// $change->item_field = BaseItem::enumId('field', 'title');
// $change->item_id = BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
// $change->createDelta('salut, ça va ? oui bien');
// $change->save();
$d = Change::fullFieldText(BaseItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD'), BaseItem::enumId('field', 'title'));
var_dump($d);die();
die();
// $fineDiff = $this->get('app.fine_diff');
// $opcodes = $fineDiff->getDiffOpcodes('salut ca va', 'salut va?');
// var_dump($opcodes);
// $merged = $fineDiff->renderToTextFromOpcodes('salut ca va', $opcodes);
// var_dump($merged);
// die();
if ($request->isMethod('POST')) {
$user = new User();
$data = $request->request->all();

View File

@ -14,7 +14,7 @@ class Eloquent {
'host' => 'localhost',
'database' => 'notes',
'username' => 'root',
'password' => 'pass',
'password' => '',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',

View File

@ -8,7 +8,7 @@ class BaseItem extends BaseModel {
public $incrementing = false;
static protected $enums = array(
'type' => array('folder', 'note', 'todo', 'tag'),
'type' => array('folder', 'note', 'tag'),
);
public function itemTypeId() {

View File

@ -322,7 +322,7 @@ class BaseModel extends \Illuminate\Database\Eloquent\Model {
$this->updated_time = time(); // TODO: maybe only update if one of the fields, or if some of versioned data has changed
if ($isNew) $this->created_time = time();
parent::save($options);
$output = parent::save($options);
$this->isNew = null;
@ -330,14 +330,18 @@ class BaseModel extends \Illuminate\Database\Eloquent\Model {
$this->recordChanges($isNew ? 'create' : 'update', $this->changedVersionedFieldValues);
}
$this->changedVersionedFieldValues = array();
return $output;
}
public function delete() {
parent::delete();
$output = parent::delete();
if (count($this->versionedFields)) {
$this->recordChanges('delete');
}
return $output;
}
protected function recordChanges($type, $versionedData = array()) {

View File

@ -80,12 +80,14 @@ class Change extends BaseModel {
$revId = 0;
for ($i = 0; $i < count($changes); $i++) {
$change = $changes[$i];
$result = Diff::patch($output, $change->delta);
if (!count($result[1])) throw new \Exception('Unexpected result format for patch operation: ' . json_encode($result));
if (!$result[1][0]) {
// Could not patch the string. TODO: handle conflict
if (!empty($change->delta)) {
$result = Diff::patch($output, $change->delta);
if (!count($result[1])) throw new \Exception('Unexpected result format for patch operation: ' . json_encode($result));
if (!$result[1][0]) {
// Could not patch the string. TODO: handle conflict
}
$output = $result[0];
}
$output = $result[0];
$revId = $change->id;
}

View File

@ -8,7 +8,7 @@ class FolderItem extends BaseModel {
public $incrementing = false;
static protected $enums = array(
'type' => array('folder', 'note', 'todo'),
'type' => array('folder', 'note'),
);
}

View File

@ -29,7 +29,7 @@ class User extends BaseModel {
return $output;
}
public function byEmail($email) {
static public function byEmail($email) {
return self::where('email', '=', $email)->first();
}

View File

@ -1,18 +0,0 @@
<?php
namespace Tests\AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class DefaultControllerTest extends WebTestCase
{
public function testIndex()
{
$client = static::createClient();
$crawler = $client->request('GET', '/');
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$this->assertContains('Welcome to Symfony', $crawler->filter('#container h1')->text());
}
}

View File

@ -48,6 +48,27 @@ class ChangeTest extends BaseTestCase {
$this->assertEquals($r, $text2);
}
public function testSame() {
$note = new Note();
$note->fromPublicArray(array('body' => 'test'));
$note->owner_id = $this->userId();
$note->save();
$noteId = $note->id;
$note = Note::find($noteId);
$this->assertEquals('test', $note->versionedFieldValue('body'));
$note->fromPublicArray(array('body' => 'test'));
$note->owner_id = $this->userId();
$note->save();
$note = Note::find($noteId);
$this->assertEquals('test', $note->versionedFieldValue('body'));
}
public function testDiff3Ways() {
// Scenario where two different clients change the same note at the same time.
//

View File

@ -2,23 +2,29 @@
require_once dirname(__FILE__) . '/BaseTestCase.php';
$dbName = 'notes_test';
$dbConfig = array(
'dbName' => 'notes_test',
'user' => 'root',
'password' => '',
'host' => '127.0.0.1',
);
$structureFile = dirname(dirname(__FILE__)) . '/structure.sql';
$cmd = sprintf("mysql -u root -ppass -e 'DROP DATABASE IF EXISTS %s; CREATE DATABASE %s;'", $dbName, $dbName);
$cmd = sprintf("mysql -u %s %s -h %s -e 'DROP DATABASE IF EXISTS %s; CREATE DATABASE %s;'", $dbConfig['user'], empty($dbConfig['password']) ? '' : '-p' . $dbConfig['password'], $dbConfig['host'], $dbConfig['dbName'], $dbConfig['dbName']);
exec($cmd);
$cmd = sprintf("mysql -u root -ppass %s < '%s'", $dbName, $structureFile);
$cmd = sprintf("mysql -u %s %s -h %s %s < '%s'", $dbConfig['user'], empty($dbConfig['password']) ? '' : '-p' . $dbConfig['password'], $dbConfig['host'], $dbConfig['dbName'], $structureFile);
exec($cmd);
$capsule = new \Illuminate\Database\Capsule\Manager();
$capsule->addConnection([
'driver' => 'mysql',
'host' => 'localhost',
'database' => $dbName,
'username' => 'root',
'password' => 'pass',
'host' => $dbConfig['host'],
'database' => $dbConfig['dbName'],
'username' => $dbConfig['user'],
'password' => $dbConfig['password'],
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',