7#include "../../libs/json.hpp"
23#define S_ISDIR(mode) (((mode) & _S_IFMT) == _S_IFDIR)
26#define S_ISREG(mode) (((mode) & _S_IFMT) == _S_IFREG)
29#define S_IFDIR _S_IFDIR
35using json = nlohmann::json;
36namespace fs = std::filesystem;
43 std::string fullPath = studyPath +
"/tests/" +
testPath;
45 return (stat(fullPath.c_str(), &info) == 0 && (info.st_mode & S_IFDIR));
49 std::vector<std::string> languages;
50 std::string translationsDir = studyPath +
"/tests/" +
testPath +
"/translations";
53 if (!fs::exists(translationsDir) || !fs::is_directory(translationsDir)) {
57 for (
const auto& entry : fs::directory_iterator(translationsDir)) {
58 if (!entry.is_regular_file())
continue;
60 std::string filename = entry.path().filename().string();
63 size_t dashPos = filename.rfind(
'-');
64 size_t jsonPos = filename.rfind(
".json");
66 if (dashPos != std::string::npos && jsonPos != std::string::npos) {
67 std::string lang = filename.substr(dashPos + 1, jsonPos - dashPos - 1);
69 languages.push_back(lang);
73 }
catch (
const fs::filesystem_error&) {
93 : mVersion(1), mUploadEnabled(false)
101 auto study = std::make_shared<Study>();
104 std::string jsonPath = path +
"/study-info.json";
105 printf(
" Looking for: %s\n", jsonPath.c_str());
108 std::ifstream testFile(jsonPath);
109 if (!testFile.good()) {
110 printf(
" File does not exist or cannot be opened\n");
115 if (!study->LoadFromJSON(jsonPath)) {
116 printf(
" Failed to parse JSON from file\n");
120 printf(
" Successfully loaded study: %s\n", study->GetName().c_str());
125 std::string jsonPath = mPath +
"/study-info.json";
126 return SaveToJSON(jsonPath);
130 const std::string& name,
131 const std::string& author) {
132 auto study = std::make_shared<Study>();
135 study->mAuthor = author;
137 study->mCreatedDate = study->GetCurrentISO8601Time();
138 study->mModifiedDate = study->mCreatedDate;
142 fs::create_directories(path);
143 fs::create_directories(fs::path(path) /
"chains");
144 fs::create_directories(fs::path(path) /
"tests");
146 }
catch (
const fs::filesystem_error& e) {
147 printf(
"Error creating study directories: %s\n", e.what());
151 std::string chainPath = path +
"/chains/Main.json";
152 auto defaultChain =
Chain::CreateNew(chainPath,
"Main",
"Default chain for this study");
154 defaultChain->Save();
155 printf(
"Created default chain: Main.json\n");
163 for (
const auto& t : mTests) {
169 mTests.push_back(test);
170 UpdateModifiedDate();
175 auto it = std::find_if(mTests.begin(), mTests.end(),
176 [&testName](
const Test& t) { return t.testName == testName; });
178 if (it != mTests.end()) {
180 UpdateModifiedDate();
188 auto it = std::find_if(mTests.begin(), mTests.end(),
189 [&testName](
const Test& t) { return t.testName == testName; });
191 if (it != mTests.end()) {
199 auto it = std::find_if(mTests.begin(), mTests.end(),
200 [&testName](
const Test& t) { return t.testName == testName; });
202 if (it != mTests.end()) {
210 std::vector<std::string> chains;
211 std::string chainsDir = mPath +
"/chains";
214 if (!fs::exists(chainsDir) || !fs::is_directory(chainsDir)) {
218 for (
const auto& entry : fs::directory_iterator(chainsDir)) {
219 if (!entry.is_regular_file())
continue;
221 std::string filename = entry.path().filename().string();
224 if (filename.size() > 5 && filename.substr(filename.size() - 5) ==
".json") {
225 chains.push_back(filename);
228 }
catch (
const fs::filesystem_error&) {
244 result.
errors.push_back(
"Study name is required");
248 result.
errors.push_back(
"Study path is required");
252 result.
errors.push_back(
"Study version must be >= 1");
257 if (stat(mPath.c_str(), &info) != 0 || !(info.st_mode & S_IFDIR)) {
258 result.
errors.push_back(
"Study directory does not exist: " + mPath);
261 std::string chainsDir = mPath +
"/chains";
262 if (stat(chainsDir.c_str(), &info) != 0 || !(info.st_mode & S_IFDIR)) {
263 result.
warnings.push_back(
"chains/ directory missing");
266 std::string testsDir = mPath +
"/tests";
267 if (stat(testsDir.c_str(), &info) != 0 || !(info.st_mode & S_IFDIR)) {
268 result.
warnings.push_back(
"tests/ directory missing");
273 for (
const auto& test : mTests) {
274 if (test.testName.empty()) {
275 result.
errors.push_back(
"Test name cannot be empty");
278 if (test.testPath.empty()) {
279 result.
errors.push_back(
"Test path cannot be empty for test: " + test.testName);
283 if (!test.Exists(mPath)) {
284 result.
warnings.push_back(
"Test directory not found: " + test.testPath);
291bool Study::LoadFromJSON(
const std::string& jsonPath) {
293 std::ifstream file(jsonPath);
294 if (!file.is_open()) {
302 mName = j.value(
"study_name",
"");
303 mVersion = j.value(
"version", 1);
306 mDescription = j.value(
"description",
"");
307 mAuthor = j.value(
"author",
"");
308 mStudyToken = j.value(
"study_token",
"");
309 mUploadServerURL = j.value(
"upload_server_url",
"");
310 mCreatedDate = j.value(
"created_date",
"");
311 mModifiedDate = j.value(
"modified_date",
"");
315 if (j.contains(
"tests") && j[
"tests"].is_array()) {
316 for (
const auto& testJson : j[
"tests"]) {
318 test.
testName = testJson.value(
"test_name",
"");
319 test.
displayName = testJson.value(
"display_name", testJson.value(
"test_name",
""));
320 test.
testPath = testJson.value(
"test_path",
"");
321 test.
included = testJson.value(
"included",
true);
324 if (testJson.contains(
"parameter_variants") && testJson[
"parameter_variants"].is_object()) {
325 for (
auto& [key, value] : testJson[
"parameter_variants"].items()) {
327 variant.
description = value.value(
"description",
"");
329 if (value.contains(
"file") && !value[
"file"].is_null()) {
330 variant.
file = value[
"file"].get<std::string>();
338 mTests.push_back(test);
344 }
catch (
const std::exception& e) {
350bool Study::SaveToJSON(
const std::string& jsonPath) {
355 j[
"study_name"] = mName;
356 j[
"description"] = mDescription;
357 j[
"version"] = mVersion;
358 j[
"author"] = mAuthor;
359 j[
"study_token"] = mStudyToken;
360 j[
"upload_server_url"] = mUploadServerURL;
361 j[
"created_date"] = mCreatedDate;
362 j[
"modified_date"] = mModifiedDate;
365 json testsArray = json::array();
366 for (
const auto& test : mTests) {
368 testJson[
"test_name"] = test.
testName;
370 testJson[
"test_path"] = test.
testPath;
371 testJson[
"included"] = test.
included;
376 for (
const auto& [key, variant] : test.parameterVariants) {
378 variantJson[
"description"] = variant.description;
379 if (!variant.file.empty()) {
380 variantJson[
"file"] = variant.file;
382 variantJson[
"file"] =
nullptr;
384 variantsJson[key] = variantJson;
386 testJson[
"parameter_variants"] = variantsJson;
389 testsArray.push_back(testJson);
391 j[
"tests"] = testsArray;
394 std::ofstream file(jsonPath);
395 if (!file.is_open()) {
404 }
catch (
const std::exception& e) {
409void Study::UpdateModifiedDate() {
410 mModifiedDate = GetCurrentISO8601Time();
413std::string Study::GetCurrentISO8601Time()
const {
414 auto now = std::time(
nullptr);
415 auto tm = *std::gmtime(&now);
417 std::ostringstream oss;
418 oss << std::put_time(&tm,
"%Y-%m-%dT%H:%M:%SZ");
427 for (
char c : mName) {
428 if (std::isalnum(
static_cast<unsigned char>(c))) {
429 code += std::toupper(
static_cast<unsigned char>(c));
430 if (code.length() >= 4) {
437 while (code.length() < 4) {
449 return mPath +
"/tests/" + testName +
"/upload.json";
455 return (stat(uploadPath.c_str(), &info) == 0);
460 if (mStudyToken.empty()) {
461 printf(
"Cannot create upload config: study token not set\n");
465 if (mUploadServerURL.empty()) {
466 printf(
"Cannot create upload config: upload server URL not set\n");
473 printf(
"Cannot create upload config: test not found: %s\n", testName.c_str());
478 std::string host, page;
481 std::string url = mUploadServerURL;
484 if (url.find(
"https://") == 0) {
487 }
else if (url.find(
"http://") == 0) {
493 size_t slashPos = url.find(
'/');
494 if (slashPos != std::string::npos) {
495 host = url.substr(0, slashPos);
496 page = url.substr(slashPos);
499 page =
"/api/upload.php";
503 size_t colonPos = host.find(
':');
504 if (colonPos != std::string::npos) {
505 std::string portStr = host.substr(colonPos + 1);
506 port = std::atoi(portStr.c_str());
507 host = host.substr(0, colonPos);
515 j[
"subnumpage"] =
"/api/getNewParticipantId.php";
517 j[
"token"] = mStudyToken;
518 j[
"taskname"] = testName;
519 j[
"username"] = mStudyToken;
520 j[
"uploadpassword"] = mStudyToken;
526 std::string testDir = mPath +
"/tests/" + testName;
528 fs::create_directories(testDir);
529 }
catch (
const fs::filesystem_error& e) {
530 printf(
"Error creating test directory: %s\n", e.what());
534 std::ofstream file(uploadPath);
535 if (!file.is_open()) {
536 printf(
"Failed to create upload config file: %s\n", uploadPath.c_str());
543 printf(
"Created upload config: %s\n", uploadPath.c_str());
546 }
catch (
const std::exception& e) {
547 printf(
"Error creating upload config: %s\n", e.what());
static std::shared_ptr< Chain > CreateNew(const std::string &path, const std::string &name, const std::string &description="")
bool TestHasUploadConfig(const std::string &testName) const
Test * GetTest(const std::string &testName)
bool CreateUploadConfigForTest(const std::string &testName)
std::string GetStudyCode() const
static std::shared_ptr< Study > CreateNew(const std::string &path, const std::string &name, const std::string &author="")
bool RemoveTest(const std::string &testName)
std::vector< std::string > GetChainFiles() const
ValidationResult Validate() const
bool AddTest(const Test &test)
int GetChainCount() const
static std::shared_ptr< Study > LoadFromDirectory(const std::string &path)
std::string GetUploadConfigPath(const std::string &testName) const
std::vector< std::string > warnings
std::vector< std::string > errors
std::vector< std::string > GetAvailableLanguages(const std::string &studyPath) const
bool Exists(const std::string &studyPath) const
std::map< std::string, ParameterVariant > parameterVariants
const ParameterVariant * GetVariant(const std::string &variantName) const