// This function runs the sprint burndown history logger for $bible.
// If no $bible is passed, it will do all Bibles.
// If $mail is true, it will mail the burndown chart to the subscribed users.
// If $mail is false, it decides on its own whether to mail the chart to the users.
void sprint_burndown (string bible, bool email)
{
  int localseconds = filter_date_local_seconds (filter_date_seconds_since_epoch ());
  int year = filter_date_numerical_year (localseconds);
  int month = filter_date_numerical_month (localseconds);
  int monthday = filter_date_numerical_month_day (localseconds); // 1 to 31.
  int weekday = filter_date_numerical_week_day (localseconds); // 0 (for Sunday) through 6 (for Saturday).
  int hour = filter_date_numerical_hour (localseconds);
  bool sprintstart = false;
  bool sprintfinish = false;

  // Every Friday at 2 PM (14:00h) it sends email about the sprint progress.
  if ((weekday == 5) && (hour == 14)) email = true;
  // On the first business day of the month, at 10 AM, send email about the start of the sprint.
  if (filter_date_is_first_business_day_of_month (monthday, weekday) && (hour == 10)) {
    email = true;
    sprintstart = true;
  }
  // On the last business day of the month, at 2 PM (14:00h), send email about the end of the sprint.
  if ((monthday == filter_date_get_last_business_day_of_month (year, month)) && (hour == 14)) {
    email = true;
    sprintfinish = true;
  }
  
  
  // Determine what to do, or to quit.
  if (!email && !sprintstart && !sprintfinish) {
    if (hour != 1) return;
  }
  

  Database_Logs::log ("Updating Sprint information", Filter_Roles::manager ());

  Webserver_Request request;
  Database_Sprint database_sprint = Database_Sprint ();

  
  // Determine year / month / day of the current sprint.
  // If this script runs from midnight till early morning,
  // it applies to the day before.
  // If the script runs during the day, it applies to today.
  if (hour <= 6) {
    localseconds -= (3600 * 6);
  }
  year = filter_date_numerical_year (localseconds);
  month = filter_date_numerical_month (localseconds);
  monthday = filter_date_numerical_month_day (localseconds); // 1 to 31.
  
  
  vector <string> bibles = {bible};
  if (bible == "") {
    bibles = request.database_bibles()->getBibles ();
  }
  

  for (auto bible : bibles) {

    
    // Get the total number of tasks for this sprint,
    // and the average percentage of completion of them,
    // and store this information in the sprint history table.
    vector <int> ids = database_sprint.getTasks (bible, year, month);
    vector <int> percentages;
    for (auto id : ids) {
      percentages.push_back (database_sprint.getComplete (id));
    }
    int tasks = ids.size ();
    int complete = 0;
    if (tasks != 0) {
      for (auto percentage : percentages) complete += percentage;
      complete = round ((float) complete / (float) tasks);
    }
    database_sprint.logHistory (bible, year, month, monthday, tasks, complete);
    
    
    // Send email if requested.
    if (email) {
      if (tasks) {
        // Only mail if the current sprint contains tasks.
        string scategories = Database_Config_Bible::getSprintTaskCompletionCategories (bible);
        vector <string> categories = filter_string_explode (scategories, '\n');
        int category_count = categories.size();
        int category_percentage = round (100 / category_count);
        vector <string> users = request.database_users ()->getUsers ();
        for (auto user : users) {
          if (request.database_config_user()->getUserSprintProgressNotification (user)) {
            
            string subject = translate("Team's progress in Sprint");
            if (sprintstart) subject = translate("Sprint has started");
            if (sprintfinish) subject = translate("Sprint has finished");
            subject +=  " | " + bible;
            
            vector <string> body;
            
            body.push_back ("<h3>" + translate("Sprint Planning and Team's Progress") + " | " + bible + "</h3>");
            body.push_back ("<table>");
            vector <int> tasks = database_sprint.getTasks (bible, year, month);
            for (auto id : tasks) {
              body.push_back ("<tr>");
              string title = database_sprint.getTitle (id);
              body.push_back ("<td>" + title + "</td>");
              int complete = database_sprint.getComplete (id);
              string text;
              for (int i = 0; i < round (complete / category_percentage); i++) text.append ("▓");
              for (int i = 0; i < category_count - round (complete / category_percentage); i++) text.append ("▁");
              body.push_back ("<td>" + text + "</td>");
              body.push_back ("</tr>");
            }
            body.push_back ("</table>");
            
            body.push_back ("<h3>" + translate("Sprint Burndown Chart - Remaining Tasks") + "</h3>");
            string burndownchart = sprint_create_burndown_chart (bible, year, month);
            body.push_back ("<p>" + burndownchart + "</p>");
            
            if (!body.empty ()) {
              string mailbody = filter_string_implode (body, "\n");
              email_schedule (user, subject, mailbody);
            }
            
          }
        }
      } else {
        
        // Since there are no tasks, no mail will be sent: Make a logbook entry.
        Database_Logs::log ("No tasks in this Sprint: No email was sent");
      }
    }
  }
}
Exemple #2
0
void sendreceive_changes ()
{
  if (sendreceive_changes_watchdog) {
    int time = filter_date_seconds_since_epoch ();
    if (time < (sendreceive_changes_watchdog + 900)) {
      Database_Logs::log (sendreceive_changes_text () + translate("Still busy"), Filter_Roles::translator ());
      return;
    }
    Database_Logs::log (sendreceive_changes_text () + translate("Watchdog timeout"), Filter_Roles::translator ());
  }
  sendreceive_changes_kick_watchdog ();
  config_globals_syncing_changes = true;

  
  Database_Logs::log (sendreceive_changes_sendreceive_text (), Filter_Roles::translator ());
  

  Webserver_Request request;
  Sync_Logic sync_logic = Sync_Logic (&request);
  Database_Modifications database_modifications;
  
  
  if (!database_modifications.healthy ()) {
    Database_Logs::log (sendreceive_changes_text () + translate("Recreate damaged modifications database"), Filter_Roles::translator ());
    database_modifications.erase ();
    database_modifications.create ();
  }
  
  
  string response = client_logic_connection_setup ();
  int iresponse = convert_to_int (response);
  if (iresponse < Filter_Roles::guest () || iresponse > Filter_Roles::admin ()) {
    Database_Logs::log (sendreceive_changes_text () + translate("Failure to initiate connection"), Filter_Roles::translator ());
    send_receive_changes_done ();
    return;
  }
  
  
  // Set the correct user in the session: The sole user on the Client.
  vector <string> users = request.database_users ()->getUsers ();
  if (users.empty ()) {
    Database_Logs::log (translate("No user found"), Filter_Roles::translator ());
    send_receive_changes_done ();
    return;
  }
  string user = users [0];
  request.session_logic ()->setUsername (user);
  string password = request.database_users ()->getmd5 (user);
  
  
  // The basic request to be POSTed to the server.
  // It contains the user's credentials.
  map <string, string> post;
  post ["u"] = bin2hex (user);
  post ["p"] = password;
  post ["l"] = convert_to_string (request.database_users ()->getUserLevel (user));
  
  
  // Error variables.
  string error;
  bool communication_errors = false;
  
  
  // Server URL to call.
  string address = Database_Config_General::getServerAddress ();
  int port = Database_Config_General::getServerPort ();
  string url = client_logic_url (address, port, sync_changes_url ());
  
  
  // Send the removed change notifications to the server.
  vector <int> ids = request.database_config_user ()->getRemovedChanges ();
  if (!ids.empty ()) Database_Logs::log (sendreceive_changes_text () + "Sending removed notifications: " + convert_to_string (ids.size()), Filter_Roles::translator ());
  for (auto & id : ids) {
    post ["a"] = convert_to_string (Sync_Logic::changes_delete_modification);
    post ["i"] = convert_to_string (id);
    response = sync_logic.post (post, url, error);
    if (!error.empty ()) {
      communication_errors = true;
      Database_Logs::log (sendreceive_changes_text () + "Failure sending removed notification: " + error, Filter_Roles::translator ());
    }
    else {
      request.database_config_user ()->removeRemovedChange (id);
    }
  }
  
  
  if (communication_errors) {
    Database_Logs::log (sendreceive_changes_text () + translate("Not downloading change notifications due to communication error"), Filter_Roles::translator ());
    send_receive_changes_done ();
    return;
  }
  
  
  // Compare the total checksum for the change notifications for the active user on client and server.
  // Checksum is cached for future re-use.
  // Take actions based on that.
  string client_checksum = request.database_config_user ()->getChangeNotificationsChecksum ();
  if (client_checksum.empty ()) {
    client_checksum = Sync_Logic::changes_checksum (user);
    request.database_config_user ()->setChangeNotificationsChecksum (client_checksum);
  }
  string server_checksum;
  post ["a"] = convert_to_string (Sync_Logic::changes_get_checksum);
  response = sync_logic.post (post, url, error);
  if (!error.empty ()) {
    Database_Logs::log (sendreceive_changes_text () + "Failure receiving checksum: " + error, Filter_Roles::translator ());
    send_receive_changes_done ();
    return;
  }
  server_checksum = response;
  if (client_checksum == server_checksum) {
    Database_Logs::log (sendreceive_changes_up_to_date_text (), Filter_Roles::translator ());
    send_receive_changes_done ();
    return;
  }
  
  
  // Get all identifiers for the notifications on the server for the user.
  // Get the identifiers on the client.
  vector <int> client_identifiers = database_modifications.getNotificationIdentifiers (user, false);
  vector <int> server_identifiers;
  post ["a"] = convert_to_string (Sync_Logic::changes_get_identifiers);
  response = sync_logic.post (post, url, error);
  if (!error.empty ()) {
    Database_Logs::log (sendreceive_changes_text () + "Failure receiving identifiers: " + error, Filter_Roles::translator ());
    send_receive_changes_done ();
    return;
  }
  {
    vector <string> ids = filter_string_explode (response, '\n');
    for (auto & id : ids) server_identifiers.push_back (convert_to_int (id));
  }

  
  // Any identifiers on the client, but not on the server, remove them from the client.
  vector <int> remove_identifiers = filter_string_array_diff (client_identifiers, server_identifiers);
  for (auto & id : remove_identifiers) {
    database_modifications.deleteNotification (id);
    request.database_config_user ()->setChangeNotificationsChecksum ("");
    Database_Logs::log (sendreceive_changes_text () + "Removing notification: " + convert_to_string (id), Filter_Roles::translator ());
  }

  
  // Any identifiers on the server, but not on the client, download them from the server.
  vector <int> download_identifiers = filter_string_array_diff (server_identifiers, client_identifiers);
  for (auto & id : download_identifiers) {
    sendreceive_changes_kick_watchdog ();
    Database_Logs::log (sendreceive_changes_text () + "Downloading notification: " + convert_to_string (id), Filter_Roles::translator ());
    post ["a"] = convert_to_string (Sync_Logic::changes_get_modification);
    post ["i"] = convert_to_string (id);
    response = sync_logic.post (post, url, error);
    if (!error.empty ()) {
      Database_Logs::log (sendreceive_changes_text () + "Failure downloading notification: " + error, Filter_Roles::translator ());
    }
    else {
      // The server has put all bits together, one bit per line.
      vector <string> lines = filter_string_explode (response, '\n');
      string category;
      if (!lines.empty ()) {
        category = lines [0];
        lines.erase (lines.begin ());
      }
      string bible;
      if (!lines.empty ()) {
        bible = lines [0];
        lines.erase (lines.begin ());
      }
      int book = 0;
      if (!lines.empty ()) {
        book = convert_to_int (lines [0]);
        lines.erase (lines.begin ());
      }
      int chapter = 0;
      if (!lines.empty ()) {
        chapter = convert_to_int (lines [0]);
        lines.erase (lines.begin ());
      }
      int verse = 0;
      if (!lines.empty ()) {
        verse = convert_to_int (lines [0]);
        lines.erase (lines.begin ());
      }
      string oldtext;
      if (!lines.empty ()) {
        oldtext = lines [0];
        lines.erase (lines.begin ());
      }
      string modification;
      if (!lines.empty ()) {
        modification = lines [0];
        lines.erase (lines.begin ());
      }
      string newtext;
      if (!lines.empty ()) {
        newtext = lines [0];
        lines.erase (lines.begin ());
      }
      database_modifications.storeClientNotification (id, user, category, bible, book, chapter, verse, oldtext, modification, newtext);
      request.database_config_user ()->setChangeNotificationsChecksum ("");
    }
  }
  

  // Done.
  Database_Logs::log (sendreceive_changes_text () + "Ready", Filter_Roles::translator ());
  send_receive_changes_done ();
}
Exemple #3
0
string manage_users (void * webserver_request)
{
  Webserver_Request * request = (Webserver_Request *) webserver_request;

  
  bool user_updated = false;
  bool privileges_updated = false;
  
  
  string page;
  Assets_Header header = Assets_Header (translate("Users"), webserver_request);
  header.addBreadCrumb (menu_logic_settings_menu (), menu_logic_settings_text ());
  page = header.run ();

  
  Assets_View view;


  int myLevel = request->session_logic ()->currentLevel ();
  
  
  // New user creation.
  if (request->query.count ("new")) {
    Dialog_Entry dialog_entry = Dialog_Entry ("users", translate("Please enter a name for the new user"), "", "new", "");
    page += dialog_entry.run ();
    return page;
  }
  if (request->post.count ("new")) {
    string user = request->post["entry"];
    if (request->database_users ()->usernameExists (user)) {
      page += Assets_Page::error (translate("User already exists"));
    } else {
      request->database_users ()->addNewUser(user, user, Filter_Roles::member (), "");
      user_updated = true;
      page += Assets_Page::success (translate("User created"));
    }
  }
  
  
  // The user to act on.
  string objectUsername = request->query["user"];
  int objectUserLevel = request->database_users ()->getUserLevel (objectUsername);
  
  
  // Delete a user.
  if (request->query.count ("delete")) {
    string role = Filter_Roles::text (objectUserLevel);
    string email = request->database_users ()->getUserToEmail (objectUsername);
    string message = "Deleted user " + objectUsername + " with role " + role + " and email " + email;
    Database_Logs::log (message, Filter_Roles::admin ());
    request->database_users ()->removeUser (objectUsername);
    user_updated = true;
    database_privileges_client_remove (objectUsername);
    page += Assets_Page::success (message);
    // Also remove any privileges for this user.
    // In particular for the Bible privileges this is necessary,
    // beause if old users remain in the privileges storage,
    // then a situation where no user has any privileges to any Bible,
    // and thus all relevant users have all privileges,
    // can never be achieved again.
    Database_Privileges::removeUser (objectUsername);
    // Remove any login tokens the user might have had: Just to clean things up.
    Database_Login::removeTokens (objectUsername);
    // Remove any settings for the user.
    // The advantage of this is that when a user is removed, all settings are gone,
    // so when the same user would be created again, all settings will go back to their defaults.
    request->database_config_user ()->remove (objectUsername);
  }
  
  
  // The user's role.
  if (request->query.count ("level")) {
    string level = request->query ["level"];
    if (level == "") {
      Dialog_List dialog_list = Dialog_List ("users", translate("Select a role for") + " " + objectUsername, "", "");
      dialog_list.add_query ("user", objectUsername);
      for (int i = Filter_Roles::lowest (); i <= Filter_Roles::highest (); i++) {
        if (i <= myLevel) {
          dialog_list.add_row (Filter_Roles::text (i), "level", convert_to_string (i));
        }
      }
      page += dialog_list.run ();
      return page;
    } else {
      request->database_users ()->updateUserLevel (objectUsername, convert_to_int (level));
      user_updated = true;
    }
  }
  
  
  // User's email address.
  if (request->query.count ("email")) {
    string email = request->query ["email"];
    if (email == "") {
      string question = translate("Please enter an email address for") + " " + objectUsername;
      string value = request->database_users ()->getUserToEmail (objectUsername);
      Dialog_Entry dialog_entry = Dialog_Entry ("users", question, value, "email", "");
      dialog_entry.add_query ("user", objectUsername);
      page += dialog_entry.run ();
      return page;
    }
  }
  if (request->post.count ("email")) {
    string email = request->post["entry"];
    if (filter_url_email_is_valid (email)) {
      page += Assets_Page::success (translate("Email address was updated"));
      request->database_users ()->updateUserEmail (objectUsername, email);
      user_updated = true;
    } else {
      page += Assets_Page::error (translate("The email address is not valid"));
    }
  }
  
  
  // Fetch all available Bibles.
  vector <string> allbibles = request->database_bibles ()->getBibles ();
  
  
  // Add Bible to user account.
  if (request->query.count ("addbible")) {
    string addbible = request->query["addbible"];
    if (addbible == "") {
      Dialog_List dialog_list = Dialog_List ("users", translate("Would you like to grant the user access to a Bible?"), "", "");
      dialog_list.add_query ("user", objectUsername);
      for (auto bible : allbibles) {
        dialog_list.add_row (bible, "addbible", bible);
      }
      page += dialog_list.run ();
      return page;
    } else {
      Assets_Page::success (translate("The user has been granted access to this Bible"));
      // Write access depends on whether it's a translator role or higher.
      bool write = (objectUserLevel >= Filter_Roles::translator ());
      Database_Privileges::setBible (objectUsername, addbible, write);
      user_updated = true;
      privileges_updated = true;
    }
  }
  
  
  // Remove Bible from user.
  if (request->query.count ("removebible")) {
    string removebible = request->query ["removebible"];
    Database_Privileges::removeBibleBook (objectUsername, removebible, 0);
    user_updated = true;
    privileges_updated = true;
    Assets_Page::success (translate("The user no longer has access to this Bible"));
  }
  
  
  // Login on behalf of another user.
  if (request->query.count ("login")) {
    request->session_logic ()->switchUser (objectUsername);
    redirect_browser (request, session_switch_url ());
    return "";
  }
  
  
  // User accounts to display.
  vector <string> tbody;
  // Retrieve assigned users.
  vector <string> users = access_user_assignees (webserver_request);
  for (auto & username : users) {
    // Gather details for this user account.
    objectUserLevel = request->database_users ()->getUserLevel (username);
    string namedrole = Filter_Roles::text (objectUserLevel);
    string email = request->database_users ()->getUserToEmail (username);
    if (email == "") email = "--";
    tbody.push_back ("<tr>");
    tbody.push_back ("<td><a href=\"?user="******"&delete\">✗</a> " + username + "</td>");
    tbody.push_back ("<td>│</td>");
    tbody.push_back ("<td><a href=\"?user="******"&level\">" + namedrole + "</a></td>");
    tbody.push_back ("<td>│</td>");
    tbody.push_back ("<td><a href=\"?user="******"&email\">" + email + "</a></td>");
    tbody.push_back ("<td>│</td>");
    tbody.push_back ("<td>");

    if (objectUserLevel < Filter_Roles::manager ()) {
      for (auto & bible : allbibles) {
        bool exists = Database_Privileges::getBibleBookExists (username, bible, 0);
        if (exists) {
          bool read, write;
          Database_Privileges::getBible (username, bible, read, write);
          if  (objectUserLevel >= Filter_Roles::translator ()) write = true;
          tbody.push_back ("<a href=\"?user="******"&removebible=" + bible + "\">✗</a>");
          tbody.push_back ("<a href=\"/bible/settings?bible=" + bible + "\">" + bible + "</a>");
          tbody.push_back ("<a href=\"write?user="******"&bible=" + bible + "\">");
          int readwritebooks = 0;
          vector <int> books = request->database_bibles ()->getBooks (bible);
          for (auto book : books) {
            Database_Privileges::getBibleBook (username, bible, book, read, write);
            if (write) readwritebooks++;
          }
          tbody.push_back ("(" + convert_to_string (readwritebooks) + "/" + convert_to_string (books.size ()) + ")");
          tbody.push_back ("</a>");
          tbody.push_back ("|");
        }
      }
    }
    if (objectUserLevel >= Filter_Roles::manager ()) {
      // Managers and higher roles have access to all Bibles.
      tbody.push_back ("(" + translate ("all") + ")");
    } else {
      tbody.push_back ("<a href=\"?user="******"&addbible=\">➕</a>");
    }
    tbody.push_back ("</td>");

    tbody.push_back ("<td>│</td>");

    tbody.push_back ("<td>");
    if (objectUserLevel >= Filter_Roles::manager ()) {
      // Managers and higher roles have all privileges.
      tbody.push_back ("(" + translate ("all") + ")");
    } else {
      tbody.push_back ("<a href=\"privileges?user="******"\">" + translate ("edit") + "</a>");
    }
    tbody.push_back ("</td>");
    
    // Logging for another user.
    if (myLevel > objectUserLevel) {
      tbody.push_back ("<td>│</td>");
      tbody.push_back ("<td>");
      tbody.push_back ("<a href=\"?user="******"&login\">" + translate ("Login") + "</a>");
      tbody.push_back ("</td>");
    }
    
    tbody.push_back ("</tr>");
  }

  view.set_variable ("tbody", filter_string_implode (tbody, "\n"));

  page += view.render ("manage", "users");

  page += Assets_Page::footer ();
  
  if (user_updated) notes_logic_maintain_note_assignees (true);
  if (privileges_updated) database_privileges_client_create (objectUsername, true);

  return page;
}
Exemple #4
0
void statistics_statistics ()
{
  Webserver_Request request;
  Database_Mail database_mail = Database_Mail (&request);
  Database_Modifications database_modifications;
  Database_Notes database_notes (&request);
  
  
  Database_Logs::log (translate("Sending statistics"), Filter_Roles::manager ());

  
  string siteUrl = config_logic_site_url ();
  
  
  vector <string> bibles = request.database_bibles()->getBibles ();
  
  
  vector <string> users = request.database_users ()->getUsers ();
  for (auto & user : users) {
    
    
    string subject = "Bibledit " + translate("statistics");
    vector <string> body;
    
    
    if (request.database_config_user()->getUserPendingChangesNotification (user)) {
      vector <int> ids = database_modifications.getNotificationIdentifiers (user);
      body.push_back ("<p><a href=\"" + siteUrl + changes_changes_url () + "\">" + translate("Number of change notifications") + "</a>: " + convert_to_string (ids.size()) + "</p>\n");
    }
    
    
    if (request.database_config_user()->getUserAssignedNotesStatisticsNotification (user)) {
      vector <int> ids = database_notes.selectNotes (
                                                     bibles, // Bibles.
                                                     0,      // Book
                                                     0,      // Chapter
                                                     0,      // Verse
                                                     3,      // Passage selector.
                                                     0,      // Edit selector.
                                                     0,      // Non-edit selector.
                                                     "",     // Status selector.
                                                     "",     // Bible selector.
                                                     user,   // Assignment selector.
                                                     0,      // Subscription selector.
                                                     -1,     // Severity selector.
                                                     0,      // Text selector.
                                                     "",     // Search text.
                                                     -1);     // Limit.
      body.push_back ("<p><a href=\"" + siteUrl + notes_index_url () + "?presetselection=assigned\">" + translate("Number of consultation notes assigned to you awaiting your response") + "</a>: " + convert_to_string (ids.size ()) + "</p>\n");
    }
    
    
    if (request.database_config_user()->getUserSubscribedNotesStatisticsNotification (user)) {
      body.push_back ("<p>" + translate("Number of consultation notes you are subscribed to") + ":</p>\n");
      body.push_back ("<ul>\n");
      request.session_logic ()->setUsername (user);
      
      vector <int> ids = database_notes.selectNotes (
                                                     bibles, // Bible.
                                                     0,      // Book
                                                     0,      // Chapter
                                                     0,      // Verse
                                                     3,      // Passage selector.
                                                     0,      // Edit selector.
                                                     0,      // Non-edit selector.
                                                     "",     // Status selector.
                                                     "",     // Bible selector.
                                                     "",     // Assignment selector.
                                                     1,      // Subscription selector.
                                                     -1,     // Severity selector.
                                                     0,      // Text selector.
                                                     "",     // Search text.
                                                     -1);     // Limit.
      body.push_back ("<li><a href=\"" + siteUrl + notes_index_url () + "?presetselection=subscribed\">" + translate("Total") + "</a>: " + convert_to_string (ids.size ()) + "</li>\n");
      ids = database_notes.selectNotes (
                                                     bibles, // Bible.
                                                     0,      // Book
                                                     0,      // Chapter
                                                     0,      // Verse
                                                     3,      // Passage selector.
                                                     0,      // Edit selector.
                                                     1,      // Non-edit selector.
                                                     "",     // Status selector.
                                                     "",     // Bible selector.
                                                     "",     // Assignment selector.
                                                     1,      // Subscription selector.
                                                     -1,     // Severity selector.
                                                     0,      // Text selector.
                                                     "",     // Search text.
                                                     -1);     // Limit.
      body.push_back ("<li><a href=\"" + siteUrl + notes_index_url () + "?presetselection=subscribeddayidle\">" + translate("Inactive for a day") + "</a>: " + convert_to_string (ids.size ()) + "</li>\n");
      ids = database_notes.selectNotes (
                                                     bibles, // Bible.
                                                     0,      // Book
                                                     0,      // Chapter
                                                     0,      // Verse
                                                     3,      // Passage selector.
                                                     0,      // Edit selector.
                                                     3,      // Non-edit selector.
                                                     "",     // Status selector.
                                                     "",     // Bible selector.
                                                     "",     // Assignment selector.
                                                     1,      // Subscription selector.
                                                     -1,     // Severity selector.
                                                     0,      // Text selector.
                                                     "",     // Search text.
                                                     -1);     // Limit.
      body.push_back ("<li><a href=\"" + siteUrl + notes_index_url () + "?presetselection=subscribedweekidle\">" + translate("Inactive for a week") + "</a>: " + convert_to_string (ids.size ()) + "</li>\n");
      body.push_back ("</ul>\n");
      request.session_logic ()->setUsername ("");
    }
    
    
    if (!body.empty ()) {
      string mailbody = filter_string_implode (body, "\n");
      database_mail.send (user, subject, mailbody);
    }
  }
}
Exemple #5
0
// Cleans and resets the data in the Bibledit installation.
void demo_clean_data ()
{
  Database_Logs::log ("Cleaning up the demo data");

  
  Webserver_Request request;
  
  
  // Set user to the demo credentials (admin) as this is the user who is always logged-in in a demo installation.
  request.session_logic ()->setUsername (session_admin_credentials ());
  
  
  // Delete empty stylesheet that may have been there.
  request.database_styles()->revokeWriteAccess ("", styles_logic_standard_sheet ());
  request.database_styles()->deleteSheet ("");
  styles_sheets_create_all ();
  
  
  // Set the export stylesheet to "Standard" for all Bibles and the admin.
  vector <string> bibles = request.database_bibles()->getBibles ();
  for (auto & bible : bibles) {
    Database_Config_Bible::setExportStylesheet (bible, styles_logic_standard_sheet ());
  }
  request.database_config_user()->setStylesheet (styles_logic_standard_sheet ());
  
  
  // Set the site language to "Default"
  Database_Config_General::setSiteLanguage ("");
  
  
  // Ensure the default users are there.
  map <string, int> users = {
    make_pair ("guest", Filter_Roles::guest ()),
    make_pair ("member", Filter_Roles::member ()),
    make_pair ("consultant", Filter_Roles::consultant ()),
    make_pair ("translator", Filter_Roles::translator ()),
    make_pair ("manager", Filter_Roles::manager ()),
    make_pair (session_admin_credentials (), Filter_Roles::admin ())
  };
  for (auto & element : users) {
    if (!request.database_users ()->usernameExists (element.first)) {
      request.database_users ()->addNewUser(element.first, element.first, element.second, "");
    }
    request.database_users ()->updateUserLevel (element.first, element.second);
  }

  
  // Create / update sample Bible.
  demo_create_sample_bible ();


  // Create sample notes.
  demo_create_sample_notes (&request);
  
  
  // Create samples for the workbenches.
  demo_create_sample_workbenches (&request);
  
  
  // Set navigator to John 3:16.
  Ipc_Focus::set (&request, 43, 3, 16);
  
  
  // Set and/or trim resources to display.
  // Too many resources crash the demo: Limit the amount.
  vector <string> resources = request.database_config_user()->getActiveResources ();
  bool reset_resources = false;
  if (resources.size () > 25) reset_resources = true;
  vector <string> defaults = demo_logic_default_resources ();
  for (auto & name : defaults) {
    if (!in_array (name, resources)) reset_resources = true;
  }
  if (reset_resources) {
    resources = demo_logic_default_resources ();
    request.database_config_user()->setActiveResources (resources);
  }
  
  
  // No flipped basic <> advanded mode.
  request.database_config_user ()->setFlipInterfaceMode (false);
}
Exemple #6
0
string user_account (void * webserver_request)
{
  Webserver_Request * request = (Webserver_Request *) webserver_request;
  
  string page;

  Assets_Header header = Assets_Header (translate("Account"), webserver_request);
  header.addBreadCrumb (menu_logic_settings_menu (), menu_logic_settings_text ());
  page = header.run ();

  Assets_View view;

  string username = request->session_logic()->currentUser ();
  string email = request->database_users()->getUserToEmail (username);

  bool actions_taken = false;
  vector <string> success_messages;

  // Form submission handler.
  if (request->post.count ("submit")) {
    bool form_is_valid = true;
    string currentpassword = request->post ["currentpassword"];
    string newpassword     = request->post ["newpassword"];
    string newpassword2    = request->post ["newpassword2"];
    string newemail        = request->post ["newemail"];
  
    if ((newpassword != "") || (newpassword2 != "")) {
      if (newpassword.length () < 4) {
        form_is_valid = false;
        view.set_variable ("new_password_invalid_message", translate("Password should be at least four characters long"));
      }
      if (newpassword2.length () < 4) {
        form_is_valid = false;
        view.set_variable ("new_password2_invalid_message", translate("Password should be at least four characters long"));
      }
      if (newpassword2 != newpassword) {
        form_is_valid = false;
        view.set_variable ("new_password2_invalid_message", translate("Passwords do not match"));
      }
      if (!request->database_users()->matchUsernamePassword (username, currentpassword)) {
        form_is_valid = false;
        view.set_variable ("current_password_invalid_message", translate("Current password is not valid"));
      }
      if (form_is_valid) {
        request->database_users()->updateUserPassword (username, newpassword);
        actions_taken = true;
        success_messages.push_back (translate("The new password was saved"));
      }
    }
  
    if (newemail != "") {
      if (!filter_url_email_is_valid (newemail)) {
        form_is_valid = false;
        view.set_variable ("new_email_invalid_message", translate("Email address is not valid"));
      }
      if (!request->database_users()->matchUsernamePassword (username, currentpassword)) {
        form_is_valid = false;
        view.set_variable ("current_password_invalid_message", translate("Current password is not valid"));
      }
      if (form_is_valid) {
        Confirm_Worker confirm_worker = Confirm_Worker (webserver_request);
        string initial_subject = translate("Email address verification");
        string initial_body = translate("Somebody requested to change the email address that belongs to your account.");
        string query = request->database_users()->updateEmailQuery (username, newemail);
        string subsequent_subject = translate("Email address change");
        string subsequent_body = translate("The email address that belongs to your account has been changed successfully.");
        confirm_worker.setup (newemail, initial_subject, initial_body, query, subsequent_subject, subsequent_body);
        actions_taken = true;
        success_messages.push_back (translate("A verification email was sent to ") + newemail);
      }
    }
  
    if (!actions_taken) {
      success_messages.push_back (translate("No changes were made"));
    }
  
  }

  view.set_variable ("username", filter_string_sanitize_html (username));
  view.set_variable ("email", filter_string_sanitize_html (email));
  string success_message = filter_string_implode (success_messages, "\n");
  view.set_variable ("success_messages", success_message);
  if (!actions_taken) view.enable_zone ("no_actions_taken");

  page += view.render ("user", "account");

  page += Assets_Page::footer ();

  return page;
}
Exemple #7
0
string changes_changes (void * webserver_request)
{
  Webserver_Request * request = (Webserver_Request *) webserver_request;
  Database_Modifications database_modifications;
  Database_Notes database_notes = Database_Notes (request);
  

  bool touch = request->session_logic ()->touchEnabled ();

  
  string page;
  Assets_Header header = Assets_Header (translate("Changes"), request);
  header.setStylesheet ();
  header.addBreadCrumb (menu_logic_translate_menu (), menu_logic_translate_text ());
  if (touch) header.jQueryMobileTouchOn ();
  page += header.run ();
  Assets_View view;
  
  
  string username = request->session_logic()->currentUser ();
  
  
  // Handle AJAX call to remove a change notification.
  if (request->post.count ("remove")) {
    int remove = convert_to_int (request->post["remove"]);
    trash_change_notification (request, remove);
    database_modifications.deleteNotification (remove);
#ifdef CLIENT_PREPARED
    request->database_config_user ()->addRemovedChange (remove);
#endif
    request->database_config_user ()->setChangeNotificationsChecksum ("");
    return "";
  }
  
  
  // Handle AJAX call to navigate to the passage belonging to the change notification.
  if (request->post.count ("navigate")) {
    string navigate = request->post["navigate"];
    int id = convert_to_int (navigate);
    Passage passage = database_modifications.getNotificationPassage (id);
    if (passage.book) {
      Ipc_Focus::set (request, passage.book, passage.chapter, convert_to_int (passage.verse));
      Navigation_Passage::recordHistory (request, passage.book, passage.chapter, convert_to_int (passage.verse));
    }
    // Set the correct default Bible for the user.
    string bible = database_modifications.getNotificationBible (id);
    if (!bible.empty ()) request->database_config_user()->setBible (bible);
    return "";
  }
  
  
  // Remove personal changes notifications and their matching change notifications in the Bible.
  if (request->query.count ("match")) {
    vector <int> ids = database_modifications.clearNotificationMatches (username, changes_personal_category (), changes_bible_category ());
#ifdef CLIENT_PREPARED
    // Client records deletions for sending to the Cloud.
    for (auto & id : ids) {
      request->database_config_user ()->addRemovedChange (id);
    }
#endif
    // Clear checksum cache.
    request->database_config_user ()->setChangeNotificationsChecksum ("");
  }
  
  
  // Remove all the personal change notifications.
  if (request->query.count ("personal")) {
    vector <int> ids = database_modifications.getNotificationTeamIdentifiers (username, changes_personal_category (), true);
    for (auto id : ids) {
      trash_change_notification (request, id);
      database_modifications.deleteNotification (id);
#ifdef CLIENT_PREPARED
      request->database_config_user ()->addRemovedChange (id);
#endif
      request->database_config_user ()->setChangeNotificationsChecksum ("");
    }
  }
  
  
  // Remove all the Bible change notifications.
  if (request->query.count ("bible")) {
    vector <int> ids = database_modifications.getNotificationTeamIdentifiers (username, changes_bible_category (), true);
    for (auto id : ids) {
      trash_change_notification (request, id);
      database_modifications.deleteNotification (id);
#ifdef CLIENT_PREPARED
      request->database_config_user ()->addRemovedChange (id);
#endif
      request->database_config_user ()->setChangeNotificationsChecksum ("");
    }
  }
  
  
  // Remove all the change notifications made by a certain user.
  if (request->query.count ("dismiss")) {
    string user = request->query ["dismiss"];
    vector <int> ids = database_modifications.getNotificationTeamIdentifiers (username, user, true);
    for (auto id : ids) {
      trash_change_notification (request, id);
      database_modifications.deleteNotification (id);
#ifdef CLIENT_PREPARED
      request->database_config_user ()->addRemovedChange (id);
#endif
      request->database_config_user ()->setChangeNotificationsChecksum ("");
    }
  }
  
  
  // Read the identifiers.
  // Limit the number of results to keep the page reasonabley fast even if there are many notifications.
  vector <int> personal_ids = database_modifications.getNotificationPersonalIdentifiers (username, changes_personal_category (), true);
  vector <int> bible_ids = database_modifications.getNotificationTeamIdentifiers (username, changes_bible_category (), true);
  vector <int> ids = database_modifications.getNotificationIdentifiers (username, true);

  
  string textblock;
  for (auto id : ids) {
    Passage passage = database_modifications.getNotificationPassage (id);
    string link = filter_passage_link_for_opening_editor_at (passage.book, passage.chapter, passage.verse);
    string category = database_modifications.getNotificationCategory (id);
    if (category == changes_personal_category ()) category = "😊";
    if (category == changes_bible_category ()) category = "📖";
    string modification = database_modifications.getNotificationModification (id);
    textblock.append ("<div id=\"entry" + convert_to_string (id) + "\">\n");
    textblock.append ("<a href=\"expand\" id=\"expand" + convert_to_string (id) + "\"> ⊞ </a>\n");
    textblock.append ("<a href=\"remove\" id=\"remove" + convert_to_string (id) + "\"> ✗ </a>\n");
    textblock.append (link + "\n");
    textblock.append (category + "\n");
    textblock.append (modification + "\n");
    textblock.append ("</div>\n");
  }
  view.set_variable ("textblock", textblock);
  
  
  string loading = "\"" + translate("Loading ...") + "\"";
  string script = "var loading = " + loading + ";";
  view.set_variable ("script", script);

  
  // Enable links to dismiss categories of notifications depending on whether there's anything to dismiss.
  if (!personal_ids.empty () && !bible_ids.empty ()) view.enable_zone ("matching");
  if (!personal_ids.empty ()) view.enable_zone ("personal");
  if (!bible_ids.empty ()) view.enable_zone ("bible");
  
  
  // Add links to clear the notifications from the individual contributors.
  string dismissblock;
  vector <string> users = request->database_users ()->getUsers ();
  for (auto & user : users) {
    vector <int> ids = database_modifications.getNotificationTeamIdentifiers (username, user, true);
    if (!ids.empty ()) {
      dismissblock.append ("<p>* <a href=\"?dismiss=");
      dismissblock.append (user);
      dismissblock.append ("\">");
      dismissblock.append (user);
      dismissblock.append (": ");
      dismissblock.append (translate("all of them"));
      dismissblock.append (": ");
      dismissblock.append (convert_to_string (ids.size ()));
      dismissblock.append ("</a></p>\n");
    }
  }
  view.set_variable ("dismissblock", dismissblock);
  
  
  view.set_variable ("VERSION", config_logic_version ());

  
  if (touch) view.enable_zone ("touch");

  
  page += view.render ("changes", "changes");
  
  
  page += Assets_Page::footer ();
  return page;
}
void changes_modifications ()
{
  Database_Logs::log ("Change notifications: Generating", Filter_Roles::translator ());

  
  // Notifications are not available to clients for the duration of processing them.
  config_globals_change_notifications_available = false;
  
  
  // Data objects.
  Webserver_Request request;
  Database_Modifications database_modifications;


  // Check on the health of the modifications database and (re)create it if needed.
  if (!database_modifications.healthy ()) database_modifications.erase ();
  database_modifications.create ();
  
  
  // Create online change notifications for users who made changes in Bibles
  // through the web editor or through a client.
  // It runs before the team changes.
  // This produces the desired order of the notifications in the GUI.
  // At the same time, produce change statistics per user.
  
  // Get the users who will receive the changes entered by the contributors.
  vector <string> recipients;
  {
    vector <string> users = request.database_users ()->getUsers ();
    for (auto & user : users) {
      if (request.database_config_user ()->getContributorChangesNotificationsOnline (user)) {
        recipients.push_back (user);
      }
    }
  }

  // Storage for the statistics.
  map <string, int> user_change_statistics;
  float modification_time_total = 0;
  int modification_time_count = 0;
  
  vector <string> users = database_modifications.getUserUsernames ();
  if (!users.empty ()) Database_Logs::log ("Change notifications: Per user", Filter_Roles::translator ());
  for (auto user : users) {

    // Total changes made by this user.
    int change_count = 0;
    
    // Go through the Bibles changed by the current user.
    vector <string> bibles = database_modifications.getUserBibles (user);
    for (auto bible : bibles) {
      
      // Body of the email to be sent.
      string email = "<p>" + translate("You have entered the changes below in a Bible editor.") + " " + translate ("You may check if it made its way into the Bible text.") + "</p>";
      size_t empty_email_length = email.length ();
      
      // Go through the books in that Bible.
      vector <int> books = database_modifications.getUserBooks (user, bible);
      for (auto book : books) {
        
        // Go through the chapters in that book.
        vector <int> chapters = database_modifications.getUserChapters (user, bible, book);
        for (auto chapter : chapters) {
          
          // Get the sets of identifiers for that chapter, and set some variables.
          vector <Database_Modifications_Id> IdSets = database_modifications.getUserIdentifiers (user, bible, book, chapter);
          //int referenceOldId = 0;
          int referenceNewId = 0;
          int newId = 0;
          int lastNewId = 0;
          bool restart = true;
          
          // Go through the sets of identifiers.
          for (auto IdSet : IdSets) {
            
            int oldId = IdSet.oldid;
            newId = IdSet.newid;
            
            if (restart) {
              changes_process_identifiers (&request, user, recipients, bible, book, chapter, referenceNewId, newId, email, change_count, modification_time_total, modification_time_count);
              //referenceOldId = oldId;
              referenceNewId = newId;
              lastNewId = newId;
              restart = false;
              continue;
            }
            
            if (oldId == lastNewId) {
              lastNewId = newId;
            } else {
              restart = true;
            }
          }
          
          // Process the last set of identifiers.
          changes_process_identifiers (&request, user, recipients, bible, book, chapter, referenceNewId, newId, email, change_count, modification_time_total, modification_time_count);
          
        }
      }

      // Check whether there's any email to be sent.
      if (email.length () != empty_email_length) {
        // Send the user email with the user's personal changes if the user opted to receive it.
        if (request.database_config_user()->getUserUserChangesNotification (user)) {
          string subject = translate("Changes you entered in") + " " + bible;
          if (!client_logic_client_enabled ()) email_schedule (user, subject, email);
        }
      }
    }
    
    // Store change statistics for this user.
    user_change_statistics [user] = change_count;

    // Clear the user's changes in the database.
    database_modifications.clearUserUser (user);
    
    
    // Clear checksum cache.
    request.database_config_user ()->setUserChangeNotificationsChecksum (user, "");
  }
  
  
  // Generate the notifications, online and by email,
  // for the changes in the Bibles entered by anyone since the previous notifications were generated.
  vector <string> bibles = database_modifications.getTeamDiffBibles ();
  for (auto bible : bibles) {
    
    
    string stylesheet = Database_Config_Bible::getExportStylesheet (bible);
    
    
    vector <string> changeNotificationUsers;
    vector <string> users = request.database_users ()->getUsers ();
    for (auto user : users) {
      if (access_bible_read (&request, bible, user)) {
        if (request.database_config_user()->getUserGenerateChangeNotifications (user)) {
          changeNotificationUsers.push_back (user);
        }
      }
    }
    users.clear ();
    
    
    // The number of changes processed so far for this Bible.
    int processedChangesCount = 0;
    
    
    // The files get stored at http://site.org:<port>/revisions/<Bible>/<date>
    int seconds = filter_date_seconds_since_epoch ();
    string timepath;
    timepath.append (convert_to_string (filter_date_numerical_year (seconds)));
    timepath.append ("-");
    timepath.append (filter_string_fill (convert_to_string (filter_date_numerical_month (seconds)), 2, '0'));
    timepath.append ("-");
    timepath.append (filter_string_fill (convert_to_string (filter_date_numerical_month_day (seconds)), 2, '0'));
    timepath.append (" ");
    timepath.append (filter_string_fill (convert_to_string (filter_date_numerical_hour (seconds)), 2, '0'));
    timepath.append (":");
    timepath.append (filter_string_fill (convert_to_string (filter_date_numerical_minute (seconds)), 2, '0'));
    timepath.append (":");
    timepath.append (filter_string_fill (convert_to_string (filter_date_numerical_second (seconds)), 2, '0'));
    string directory = filter_url_create_root_path ("revisions", bible, timepath);
    filter_url_mkdir (directory);
    
    
    // Produce the USFM and html files.
    filter_diff_produce_verse_level (bible, directory);
    
    
    // Create online page with changed verses.
    string versesoutputfile = filter_url_create_path (directory, "changed_verses.html");
    filter_diff_run_file (filter_url_create_path (directory, "verses_old.txt"), filter_url_create_path (directory, "verses_new.txt"), versesoutputfile);
    
    
    // Storage for body of the email with the changes.
    vector <string> email_changes;
    
    
    // Generate the online change notifications.
    vector <int> books = database_modifications.getTeamDiffBooks (bible);
    for (auto book : books) {
      vector <int> chapters = database_modifications.getTeamDiffChapters (bible, book);
      for (auto chapter : chapters) {
        Database_Logs::log ("Change notifications: " + bible + " " + filter_passage_display (book, chapter, ""), Filter_Roles::translator ());
        string old_chapter_usfm = database_modifications.getTeamDiff (bible, book, chapter);
        string new_chapter_usfm = request.database_bibles()->getChapter (bible, book, chapter);
        vector <int> old_verse_numbers = usfm_get_verse_numbers (old_chapter_usfm);
        vector <int> new_verse_numbers = usfm_get_verse_numbers (new_chapter_usfm);
        vector <int> verses = old_verse_numbers;
        verses.insert (verses.end (), new_verse_numbers.begin (), new_verse_numbers.end ());
        verses = array_unique (verses);
        sort (verses.begin (), verses.end());
        for (auto verse : verses) {
          string old_verse_usfm = usfm_get_verse_text (old_chapter_usfm, verse);
          string new_verse_usfm = usfm_get_verse_text (new_chapter_usfm, verse);
          if (old_verse_usfm != new_verse_usfm) {
            processedChangesCount++;
            // In case of too many change notifications, processing them would take too much time, so take a few shortcuts.
            string old_html = "<p>" + old_verse_usfm + "</p>";
            string new_html = "<p>" + new_verse_usfm + "</p>";
            string old_text = old_verse_usfm;
            string new_text = new_verse_usfm;
            if (processedChangesCount < 800) {
              Filter_Text filter_text_old = Filter_Text (bible);
              Filter_Text filter_text_new = Filter_Text (bible);
              filter_text_old.html_text_standard = new Html_Text ("");
              filter_text_new.html_text_standard = new Html_Text ("");
              filter_text_old.text_text = new Text_Text ();
              filter_text_new.text_text = new Text_Text ();
              filter_text_old.addUsfmCode (old_verse_usfm);
              filter_text_new.addUsfmCode (new_verse_usfm);
              filter_text_old.run (stylesheet);
              filter_text_new.run (stylesheet);
              old_html = filter_text_old.html_text_standard->getInnerHtml ();
              new_html = filter_text_new.html_text_standard->getInnerHtml ();
              old_text = filter_text_old.text_text->get ();
              new_text = filter_text_new.text_text->get ();
            }
            string modification = filter_diff_diff (old_text, new_text);
            database_modifications.recordNotification (changeNotificationUsers, changes_bible_category (), bible, book, chapter, verse, old_html, modification, new_html);
            string passage = filter_passage_display (book, chapter, convert_to_string (verse))   + ": ";
            if (old_text != new_text) {
              email_changes.push_back (passage  + modification);
            } else {
              email_changes.push_back (translate ("The following passage has no change in the text.") + " " + translate ("The change is in the formatting only.") + " " + translate ("The USFM code is given for reference."));
              email_changes.push_back (passage);
              email_changes.push_back (translate ("Old code:") + " " + old_verse_usfm);
              email_changes.push_back (translate ("New code:") + " " + new_verse_usfm);
            }
          }
        }
        // Delete the diff data for this chapter, for two reasons:
        // 1. New diffs for this chapter can be stored straightaway.
        // 2. In case of large amounts of diff data, and this function gets killed,
        //    then the next time it runs again, it will continue from where it was killed.
        database_modifications.deleteTeamDiffChapter (bible, book, chapter);
      }
    }
    
    
    // Email the changes to the subscribed users.
    if (!email_changes.empty ()) {
      string body;
      for (auto & line : email_changes) {
        body.append ("<div>");
        body.append (line);
        body.append ("</div>\n");
      }
      string subject = translate("Recent changes:") + " " + bible;
      vector <string> users = request.database_users ()->getUsers ();
      for (auto & user : users) {
        if (request.database_config_user()->getUserBibleChangesNotification (user)) {
          if (access_bible_read (&request, bible, user)) {
            if (!client_logic_client_enabled ()) {
              email_schedule (user, subject, body);
            }
          }
        }
      }
    }
    
    
  }

  
  // Index the data and remove expired notifications.
  Database_Logs::log ("Change notifications: Indexing", Filter_Roles::translator ());
  database_modifications.indexTrimAllNotifications ();

  
  // Remove expired downloadable revisions.
  string directory = filter_url_create_root_path ("revisions");
  int now = filter_date_seconds_since_epoch ();
  bibles = filter_url_scandir (directory);
  for (auto &bible : bibles) {
    string folder = filter_url_create_path (directory, bible);
    int time = filter_url_file_modification_time (folder);
    int days = (now - time) / 86400;
    if (days > 31) {
      filter_url_rmdir (folder);
    } else {
      vector <string> revisions = filter_url_scandir (folder);
      for (auto & revision : revisions) {
        string path = filter_url_create_path (folder, revision);
        int time = filter_url_file_modification_time (path);
        int days = (now - time) / 86400;
        if (days > 31) {
          filter_url_rmdir (path);
          Database_Logs::log ("Removing expired downloadable revision notification: " + bible + " " + revision, Filter_Roles::translator ());
        }
      }
    }
  }
  
  
  // Clear checksum caches.
  users = request.database_users ()->getUsers ();
  for (auto user : users) {
    request.database_config_user ()->setUserChangeNotificationsChecksum (user, "");
  }
  
  
  // Vacuum the modifications index, as it might have been updated.
  database_modifications.vacuum ();
  
  
  // Make the notifications available again to clients.
  config_globals_change_notifications_available = true;

  
  // Store the statistics in the database.
  if (modification_time_count) {
    // Take average timestamp of all timestamps.
    int timestamp = round (modification_time_total / modification_time_count);
    for (auto & element : user_change_statistics) {
      // Store dated change statistics per user.
      string user = element.first;
      int count = element.second;
      Database_Statistics::store_changes (timestamp, user, count);
    }
  }
  

  Database_Logs::log ("Change notifications: Ready", Filter_Roles::translator ());
}