From 88f328581bcac82dd92f180dbfcee9f27d9170f6 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Tue, 2 Dec 2025 13:32:11 +0000 Subject: [PATCH 01/11] Added Awards Settings --- application/config/migration.php | 2 +- application/controllers/Award.php | 61 +++++ .../language/bulgarian/awards_lang.php | 1 + .../chinese_simplified/awards_lang.php | 3 +- application/language/czech/awards_lang.php | 1 + application/language/dutch/awards_lang.php | 3 +- application/language/english/awards_lang.php | 1 + application/language/finnish/awards_lang.php | 1 + application/language/french/awards_lang.php | 1 + application/language/german/awards_lang.php | 1 + application/language/greek/awards_lang.php | 1 + application/language/italian/awards_lang.php | 1 + application/language/polish/awards_lang.php | 1 + .../language/portuguese/awards_lang.php | 1 + application/language/russian/awards_lang.php | 3 +- application/language/spanish/awards_lang.php | 3 +- application/language/swedish/awards_lang.php | 1 + application/language/turkish/awards_lang.php | 1 + application/migrations/237_add_awardxuser.php | 161 ++++++++++++ application/models/Awards_model.php | 180 +++++++++++++ application/models/User_model.php | 1 + application/views/awards/settings.php | 239 ++++++++++++++++++ application/views/interface_assets/footer.php | 4 + application/views/interface_assets/header.php | 60 ++++- assets/js/sections/award_settings.js | 37 +++ assets/js/sections/awards.js | 94 +++++++ 26 files changed, 853 insertions(+), 10 deletions(-) create mode 100644 application/controllers/Award.php create mode 100644 application/migrations/237_add_awardxuser.php create mode 100644 application/models/Awards_model.php create mode 100644 application/views/awards/settings.php create mode 100644 assets/js/sections/award_settings.js create mode 100644 assets/js/sections/awards.js diff --git a/application/config/migration.php b/application/config/migration.php index c0deb3066..8f2e83acc 100644 --- a/application/config/migration.php +++ b/application/config/migration.php @@ -22,7 +22,7 @@ | */ -$config['migration_version'] = 236; +$config['migration_version'] = 237; /* |-------------------------------------------------------------------------- diff --git a/application/controllers/Award.php b/application/controllers/Award.php new file mode 100644 index 000000000..a10c4aec0 --- /dev/null +++ b/application/controllers/Award.php @@ -0,0 +1,61 @@ +load->helper(array('form', 'url')); + + $this->load->model('user_model'); + if(!$this->user_model->authorize(2)) { $this->session->set_flashdata('notice', 'You\'re not allowed to do that!'); redirect('dashboard'); } + } + + public function index() + { + $this->load->model('awards_model'); + + $data['user_awards'] = $this->awards_model->get_user_awards(); + + // Render Page + $data['page_title'] = "Award Settings"; + $this->load->view('interface_assets/header', $data); + $this->load->view('awards/settings'); + $this->load->view('interface_assets/footer'); + } + + public function saveAward() { + // Get the award type and value from POST + $award_type = $this->security->xss_clean($this->input->post('award_type')); + $award_value = $this->security->xss_clean($this->input->post('award_value')); + + $this->load->model('awards_model'); + $result = $this->awards_model->save_single_award($award_type, $award_value); + + header('Content-Type: application/json'); + echo json_encode(array('message' => 'OK')); + return; + } + + public function activateall() { + $this->load->model('awards_model'); + $this->awards_model->activateall(); + + header('Content-Type: application/json'); + echo json_encode(array('message' => 'OK')); + return; + } + + public function deactivateall() { + $this->load->model('awards_model'); + $this->awards_model->deactivateall(); + + header('Content-Type: application/json'); + echo json_encode(array('message' => 'OK')); + return; + } +} diff --git a/application/language/bulgarian/awards_lang.php b/application/language/bulgarian/awards_lang.php index 23dfe8e4c..13407e637 100644 --- a/application/language/bulgarian/awards_lang.php +++ b/application/language/bulgarian/awards_lang.php @@ -2,6 +2,7 @@ defined('BASEPATH') OR exit('No direct script access allowed'); +$lang['awards_menu_settings'] = "Award Settings"; $lang['awards_info_button'] = "Award Info"; $lang['awards_show_worked'] = "Show worked"; $lang['awards_show_confirmed'] = "Show confirmed"; diff --git a/application/language/chinese_simplified/awards_lang.php b/application/language/chinese_simplified/awards_lang.php index 77222622b..9ebf749fb 100644 --- a/application/language/chinese_simplified/awards_lang.php +++ b/application/language/chinese_simplified/awards_lang.php @@ -2,7 +2,8 @@ defined('BASEPATH') OR exit('No direct script access allowed'); -$lang['awards_info_button'] = "奖状详情"; +$lang['awards_menu_settings'] = "奖项设置"; +$lang['awards_info_button'] = "奖项信息"; $lang['awards_show_worked'] = "显示已通联"; $lang['awards_show_confirmed'] = "显示已确认"; $lang['awards_show_not_worked'] = "显示未通联"; diff --git a/application/language/czech/awards_lang.php b/application/language/czech/awards_lang.php index b77ede943..130a0a872 100644 --- a/application/language/czech/awards_lang.php +++ b/application/language/czech/awards_lang.php @@ -2,6 +2,7 @@ defined('BASEPATH') OR exit('No direct script access allowed'); +$lang['awards_menu_settings'] = "Award Settings"; $lang['awards_info_button'] = "Award Info"; $lang['awards_show_worked'] = "Show worked"; $lang['awards_show_confirmed'] = "Show confirmed"; diff --git a/application/language/dutch/awards_lang.php b/application/language/dutch/awards_lang.php index 6115045cf..d20b0e8df 100644 --- a/application/language/dutch/awards_lang.php +++ b/application/language/dutch/awards_lang.php @@ -2,7 +2,8 @@ defined('BASEPATH') OR exit('No direct script access allowed'); -$lang['awards_info_button'] = "Award Info"; +$lang['awards_menu_settings'] = "Award-instellingen"; +$lang['awards_info_button'] = "Award informatie"; $lang['awards_show_worked'] = "Show worked"; $lang['awards_show_confirmed'] = "Show confirmed"; $lang['awards_show_not_worked'] = "Show not worked"; diff --git a/application/language/english/awards_lang.php b/application/language/english/awards_lang.php index 23dfe8e4c..13407e637 100644 --- a/application/language/english/awards_lang.php +++ b/application/language/english/awards_lang.php @@ -2,6 +2,7 @@ defined('BASEPATH') OR exit('No direct script access allowed'); +$lang['awards_menu_settings'] = "Award Settings"; $lang['awards_info_button'] = "Award Info"; $lang['awards_show_worked'] = "Show worked"; $lang['awards_show_confirmed'] = "Show confirmed"; diff --git a/application/language/finnish/awards_lang.php b/application/language/finnish/awards_lang.php index 05c6071be..c9e14cdbc 100644 --- a/application/language/finnish/awards_lang.php +++ b/application/language/finnish/awards_lang.php @@ -2,6 +2,7 @@ defined('BASEPATH') OR exit('No direct script access allowed'); +$lang['awards_menu_settings'] = "Award Settings"; $lang['awards_info_button'] = "Award Info"; $lang['awards_show_worked'] = "Show worked"; $lang['awards_show_confirmed'] = "Show confirmed"; diff --git a/application/language/french/awards_lang.php b/application/language/french/awards_lang.php index 9faef5f7f..2827d5017 100644 --- a/application/language/french/awards_lang.php +++ b/application/language/french/awards_lang.php @@ -2,6 +2,7 @@ defined('BASEPATH') OR exit('No direct script access allowed'); +$lang['awards_menu_settings'] = "Paramètres des diplômes"; $lang['awards_info_button'] = "Informations complémentaires"; $lang['awards_show_worked'] = "Voir les \"réalisés\""; $lang['awards_show_confirmed'] = "Voir les \"confirmés\""; diff --git a/application/language/german/awards_lang.php b/application/language/german/awards_lang.php index 4368e4da9..140418a49 100644 --- a/application/language/german/awards_lang.php +++ b/application/language/german/awards_lang.php @@ -2,6 +2,7 @@ defined('BASEPATH') OR exit('Direkter Skriptzugriff nicht erlaubt'); +$lang['awards_menu_settings'] = "Diplom-Einstellungen"; $lang['awards_info_button'] = "Diplom Info"; $lang['awards_show_worked'] = "Zeige gearbeitete"; $lang['awards_show_confirmed'] = "Zeige bestätigte"; diff --git a/application/language/greek/awards_lang.php b/application/language/greek/awards_lang.php index 23dfe8e4c..13407e637 100644 --- a/application/language/greek/awards_lang.php +++ b/application/language/greek/awards_lang.php @@ -2,6 +2,7 @@ defined('BASEPATH') OR exit('No direct script access allowed'); +$lang['awards_menu_settings'] = "Award Settings"; $lang['awards_info_button'] = "Award Info"; $lang['awards_show_worked'] = "Show worked"; $lang['awards_show_confirmed'] = "Show confirmed"; diff --git a/application/language/italian/awards_lang.php b/application/language/italian/awards_lang.php index 04a250898..8167650ac 100644 --- a/application/language/italian/awards_lang.php +++ b/application/language/italian/awards_lang.php @@ -2,6 +2,7 @@ defined('BASEPATH') OR exit('Non è consentito l\'accesso diretto allo script'); +$lang['awards_menu_settings'] = "Impostazioni premi"; $lang['awards_info_button'] = "Informazioni sull'Award"; $lang['awards_show_worked'] = "Mostra lavorati"; $lang['awards_show_confirmed'] = "Mostra confermati"; diff --git a/application/language/polish/awards_lang.php b/application/language/polish/awards_lang.php index 26339b643..21728ae4d 100644 --- a/application/language/polish/awards_lang.php +++ b/application/language/polish/awards_lang.php @@ -2,6 +2,7 @@ defined('BASEPATH') OR exit('Brak bezpośredniego dostępu do skryptu'); +$lang['awards_menu_settings'] = "Ustawienia nagród"; $lang['awards_info_button'] = "Informacje o nagrodzie"; $lang['awards_show_worked'] = "Pokaż wykonane"; $lang['awards_show_confirmed'] = "Pokaż potwierdzone"; diff --git a/application/language/portuguese/awards_lang.php b/application/language/portuguese/awards_lang.php index 8e37ad9e0..39134bbe8 100644 --- a/application/language/portuguese/awards_lang.php +++ b/application/language/portuguese/awards_lang.php @@ -1,6 +1,7 @@ db->table_exists('awardxuser')) { + $this->dbforge->add_field(array( + 'id' => array( + 'type' => 'INT', + 'constraint' => 20, + 'unsigned' => TRUE, + 'auto_increment' => TRUE, + 'unique' => TRUE + ), + + 'userid' => array( + 'type' => 'INT', + 'constraint' => 20, + 'unsigned' => TRUE, + 'auto_increment' => FALSE + ), + + 'cq' => array( + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 1 + ), + + 'dok' => array( + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 1 + ), + + 'dxcc' => array( + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 1 + ), + + 'ffma' => array( + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 1 + ), + + 'iota' => array( + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 1 + ), + + 'gridmaster_dl' => array( + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 1 + ), + + 'gridmaster_lx' => array( + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 1 + ), + + 'gridmaster_ja' => array( + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 1 + ), + + 'gridmaster_us' => array( + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 1 + ), + + 'gridmaster_uk' => array( + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 1 + ), + + 'gmdxsummer' => array( + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 1 + ), + + 'pota' => array( + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 1 + ), + + 'sig' => array( + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 1 + ), + + 'sota' => array( + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 1 + ), + + 'uscounties' => array( + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 1 + ), + + 'vucc' => array( + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 1 + ), + + 'wab' => array( + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 1 + ), + + 'waja' => array( + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 1 + ), + + 'was' => array( + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 1 + ), + + 'wwff' => array( + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 1 + ), + )); + + $this->dbforge->add_key('id', TRUE); + $this->dbforge->add_key('userid'); + + $this->dbforge->create_table('awardxuser'); + + // Insert default records for all existing users (all awards enabled by default) + $this->db->query("INSERT INTO awardxuser (userid, cq, dok, dxcc, ffma, iota, gridmaster_dl, gridmaster_lx, gridmaster_ja, gridmaster_us, gridmaster_uk, gmdxsummer, pota, sig, sota, uscounties, vucc, wab, waja, was, wwff) + SELECT user_id, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 FROM users;"); + } + } + + public function down() + { + $this->dbforge->drop_table('awardxuser'); + } +} diff --git a/application/models/Awards_model.php b/application/models/Awards_model.php new file mode 100644 index 000000000..72c1bbb46 --- /dev/null +++ b/application/models/Awards_model.php @@ -0,0 +1,180 @@ +db->from('awardxuser'); + $this->db->where('userid', $this->session->userdata('user_id')); + $result = $this->db->get(); + + if ($result->num_rows() > 0) { + return $result->row(); + } + + // If no preferences exist, create default ones + $this->create_default_awards(); + return $this->get_user_awards(); + } + + /** + * Create default award preferences for current user + */ + function create_default_awards() { + $data = array( + 'userid' => $this->session->userdata('user_id'), + 'cq' => 1, + 'dok' => 1, + 'dxcc' => 1, + 'ffma' => 1, + 'iota' => 1, + 'gridmaster_dl' => 1, + 'gridmaster_lx' => 1, + 'gridmaster_ja' => 1, + 'gridmaster_us' => 1, + 'gridmaster_uk' => 1, + 'gmdxsummer' => 1, + 'pota' => 1, + 'sig' => 1, + 'sota' => 1, + 'uscounties' => 1, + 'vucc' => 1, + 'wab' => 1, + 'waja' => 1, + 'was' => 1, + 'wwff' => 1, + ); + + $this->db->insert('awardxuser', $data); + } + + /** + * Save a single award preference for current user + * @param string $award_type The award type (e.g., 'cq', 'dxcc', 'pota') + * @param mixed $value The value (true/false or 1/0) + */ + function save_single_award($award_type, $value) { + // Validate award type to prevent SQL injection + $valid_awards = array( + 'cq', 'dok', 'dxcc', 'ffma', 'iota', + 'gridmaster_dl', 'gridmaster_lx', 'gridmaster_ja', 'gridmaster_us', 'gridmaster_uk', + 'gmdxsummer', 'pota', 'sig', 'sota', 'uscounties', 'vucc', 'wab', 'waja', 'was', 'wwff' + ); + + if (!in_array($award_type, $valid_awards)) { + return false; + } + + $data = array( + $award_type => ($value == "true" || $value == "1" || $value === true) ? '1' : '0' + ); + + $this->db->where('userid', $this->session->userdata('user_id')); + $this->db->update('awardxuser', $data); + + return true; + } + + /** + * Save award preferences for current user + * @param array $awards Array of award settings + */ + function save_awards($awards) { + $data = array( + 'cq' => $awards['cq'] == "true" ? '1' : '0', + 'dok' => $awards['dok'] == "true" ? '1' : '0', + 'dxcc' => $awards['dxcc'] == "true" ? '1' : '0', + 'ffma' => $awards['ffma'] == "true" ? '1' : '0', + 'iota' => $awards['iota'] == "true" ? '1' : '0', + 'gridmaster_dl' => $awards['gridmaster_dl'] == "true" ? '1' : '0', + 'gridmaster_lx' => $awards['gridmaster_lx'] == "true" ? '1' : '0', + 'gridmaster_ja' => $awards['gridmaster_ja'] == "true" ? '1' : '0', + 'gridmaster_us' => $awards['gridmaster_us'] == "true" ? '1' : '0', + 'gridmaster_uk' => $awards['gridmaster_uk'] == "true" ? '1' : '0', + 'gmdxsummer' => $awards['gmdxsummer'] == "true" ? '1' : '0', + 'pota' => $awards['pota'] == "true" ? '1' : '0', + 'sig' => $awards['sig'] == "true" ? '1' : '0', + 'sota' => $awards['sota'] == "true" ? '1' : '0', + 'uscounties' => $awards['uscounties'] == "true" ? '1' : '0', + 'vucc' => $awards['vucc'] == "true" ? '1' : '0', + 'wab' => $awards['wab'] == "true" ? '1' : '0', + 'waja' => $awards['waja'] == "true" ? '1' : '0', + 'was' => $awards['was'] == "true" ? '1' : '0', + 'wwff' => $awards['wwff'] == "true" ? '1' : '0', + ); + + $this->db->where('userid', $this->session->userdata('user_id')); + $this->db->update('awardxuser', $data); + + return true; + } + + /** + * Activate all awards for current user + */ + function activateall() { + $data = array( + 'cq' => 1, + 'dok' => 1, + 'dxcc' => 1, + 'ffma' => 1, + 'iota' => 1, + 'gridmaster_dl' => 1, + 'gridmaster_lx' => 1, + 'gridmaster_ja' => 1, + 'gridmaster_us' => 1, + 'gridmaster_uk' => 1, + 'gmdxsummer' => 1, + 'pota' => 1, + 'sig' => 1, + 'sota' => 1, + 'uscounties' => 1, + 'vucc' => 1, + 'wab' => 1, + 'waja' => 1, + 'was' => 1, + 'wwff' => 1, + ); + + $this->db->where('userid', $this->session->userdata('user_id')); + $this->db->update('awardxuser', $data); + + return true; + } + + /** + * Deactivate all awards for current user + */ + function deactivateall() { + $data = array( + 'cq' => 0, + 'dok' => 0, + 'dxcc' => 0, + 'ffma' => 0, + 'iota' => 0, + 'gridmaster_dl' => 0, + 'gridmaster_lx' => 0, + 'gridmaster_ja' => 0, + 'gridmaster_us' => 0, + 'gridmaster_uk' => 0, + 'gmdxsummer' => 0, + 'pota' => 0, + 'sig' => 0, + 'sota' => 0, + 'uscounties' => 0, + 'vucc' => 0, + 'wab' => 0, + 'waja' => 0, + 'was' => 0, + 'wwff' => 0, + ); + + $this->db->where('userid', $this->session->userdata('user_id')); + $this->db->update('awardxuser', $data); + + return true; + } +} diff --git a/application/models/User_model.php b/application/models/User_model.php index 4aa8c1969..ba778fb65 100644 --- a/application/models/User_model.php +++ b/application/models/User_model.php @@ -214,6 +214,7 @@ function add($username, $password, $email, $type, $firstname, $lastname, $callsi $this->db->insert($this->config->item('auth_table'), $data); $insert_id = $this->db->insert_id(); $this->db->query("insert into bandxuser (bandid, userid, active, cq, dok, dxcc, iota, pota, sig, sota, uscounties, was, wwff, vucc) select bands.id, " . $insert_id . ", 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 from bands;"); + $this->db->query("insert into awardxuser (userid, cq, dok, dxcc, ffma, iota, gridmaster_dl, gridmaster_lx, gridmaster_ja, gridmaster_us, gridmaster_uk, gmdxsummer, pota, sig, sota, uscounties, vucc, wab, waja, was, wwff) values (" . $insert_id . ", 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1);"); $this->db->query("insert into paper_types (user_id,paper_name,metric,width,orientation,height) SELECT ".$insert_id.", paper_name, metric, width, orientation,height FROM paper_types where user_id = -1;"); $this->db->query("insert into user_options (user_id, option_type, option_name, option_key, option_value) values (" . $insert_id . ", 'hamsat','hamsat_key','api','".xss_clean($user_hamsat_key)."');"); $this->db->query("insert into user_options (user_id, option_type, option_name, option_key, option_value) values (" . $insert_id . ", 'hamsat','hamsat_key','workable','".xss_clean($user_hamsat_workable_only)."');"); diff --git a/application/views/awards/settings.php b/application/views/awards/settings.php new file mode 100644 index 000000000..b17738e39 --- /dev/null +++ b/application/views/awards/settings.php @@ -0,0 +1,239 @@ +
+ +
+ session->flashdata('message')) { ?> + +
+

session->flashdata('message'); ?>

+
+ + + + +

Award Settings

+ + +
+
+
Award Preferences
+
+
+ + +

+ Using this list you can control which awards are shown in the Awards menu. +

+

+ Active awards will be shown in the menu, while inactive awards will be hidden. +

+
+
+ + +
+
+
+
Awards Configuration
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Show in MenuAward NameDescription
+ cq == 1) echo 'checked'; ?>> + CQ MagazineCQ Magazine DX Awards
+ dok == 1) echo 'checked'; ?>> + DOKDiplom Ortsverbände Kennung (German Districts)
+ dxcc == 1) echo 'checked'; ?>> + DXCCDX Century Club
+ ffma == 1) echo 'checked'; ?>> + FFMAFlora and Fauna in Marche Award
+ iota == 1) echo 'checked'; ?>> + IOTAIslands On The Air
Gridmaster Awards
+ gridmaster_dl == 1) echo 'checked'; ?>> + Gridmaster DLGerman Gridmaster Award
+ gridmaster_lx == 1) echo 'checked'; ?>> + Gridmaster LXLuxembourg Gridmaster Award
+ gridmaster_ja == 1) echo 'checked'; ?>> + Gridmaster JAJapanese Gridmaster Award
+ gridmaster_us == 1) echo 'checked'; ?>> + Gridmaster USUnited States Gridmaster Award
+ gridmaster_uk == 1) echo 'checked'; ?>> + Gridmaster UKUnited Kingdom Gridmaster Award
+ gmdxsummer == 1) echo 'checked'; ?>> + GMDX Summer ChallengeGMDX Summer Challenge Award
+ pota == 1) echo 'checked'; ?>> + POTAParks on the Air
+ sig == 1) echo 'checked'; ?>> + SIGSpecial Interest Group
+ sota == 1) echo 'checked'; ?>> + SOTASummits on the Air
+ uscounties == 1) echo 'checked'; ?>> + US CountiesUnited States Counties Award
+ vucc == 1) echo 'checked'; ?>> + VUCCVHF/UHF Century Club
+ wab == 1) echo 'checked'; ?>> + WABWorked All Britain
+ waja == 1) echo 'checked'; ?>> + WAJAWorked All Japan
+ was == 1) echo 'checked'; ?>> + WASWorked All States
+ wwff == 1) echo 'checked'; ?>> + WWFFWorld Wide Flora and Fauna
+
+
+

+ + +

+
+
+ +
diff --git a/application/views/interface_assets/footer.php b/application/views/interface_assets/footer.php index a3f794cd6..4f9fc5de1 100644 --- a/application/views/interface_assets/footer.php +++ b/application/views/interface_assets/footer.php @@ -3187,6 +3187,10 @@ function displayActivatorsContacts(call, band, leogeo) { +uri->segment(1) == "award") { ?> + + + uri->segment(1) == "accumulated") { ?> diff --git a/application/views/interface_assets/header.php b/application/views/interface_assets/header.php index 2dde290d7..3ccffa617 100644 --- a/application/views/interface_assets/header.php +++ b/application/views/interface_assets/header.php @@ -144,53 +144,101 @@ - -
  • + config->item('disable_open_registration')) { ?> +
  • +
  • diff --git a/application/views/user/login.php b/application/views/user/login.php index 8396487a4..78cc2e0eb 100644 --- a/application/views/user/login.php +++ b/application/views/user/login.php @@ -61,6 +61,9 @@

    + optionslib->get_option('open_registration') == 'true' && !$this->config->item('disable_open_registration')) { ?> +

    Don't have an account? Sign up

    + diff --git a/application/views/user/signup.php b/application/views/user/signup.php new file mode 100644 index 000000000..b8d7c7508 --- /dev/null +++ b/application/views/user/signup.php @@ -0,0 +1,201 @@ + +
    + Cloudlog +
    +
    +

    + + Create your Cloudlog account +

    +

    Sign up with your callsign to get started.

    +
    +
    + load->view('layout/messages'); ?> + + 'novalidate']); ?> + form_validation->set_error_delimiters('', ''); ?> + +
    +
    + +
    + + +
    +
    +
    +
    + +
    + + +
    +
    +
    +
    + +
    +
    + +
    + + +
    +
    +
    +
    +
    + Start typing a password… +
    +
    + +
    + + +
    +
    + +
    +
    + +
    +
    + +
    + + +
    +
    +
    +
    + +
    + + +
    +
    +
    +
    + +
    +
    + +
    + + +
    +
    +
    +
    + +
    + + +
    +
    +
    +
    + +
    + +
    + + + +
    +
    +
    + + + +
    +
    + + +
    From 307c87ea48b53cc96a01295a9cb0d734f1cd0745 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Tue, 2 Dec 2025 22:43:19 +0000 Subject: [PATCH 05/11] Optimize LOTW user join in logbook query Replaces direct join with lotw_users table by joining a subquery that selects the latest lastupload per callsign. This improves performance and ensures only the most recent LOTW upload date is included for each callsign. --- application/models/Logbook_model.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/application/models/Logbook_model.php b/application/models/Logbook_model.php index bfdd2f920..8e9e3833a 100755 --- a/application/models/Logbook_model.php +++ b/application/models/Logbook_model.php @@ -1819,7 +1819,7 @@ function get_qsos($num, $offset, $StationLocationsArray = null) $location_list = "'" . implode("','", $logbooks_locations_array) . "'"; // Use optimized subquery approach for better performance - $sql = "SELECT qsos.*, station_profile.*, dxcc_entities.*, lotw_users.callsign, lotw_users.lastupload + $sql = "SELECT qsos.*, station_profile.*, dxcc_entities.*, lotw.callsign, lotw.lastupload FROM ( SELECT DISTINCT COL_PRIMARY_KEY, COL_TIME_ON FROM " . $this->config->item('table_name') . " qsos_inner @@ -1831,7 +1831,11 @@ function get_qsos($num, $offset, $StationLocationsArray = null) INNER JOIN " . $this->config->item('table_name') . " qsos ON qsos.COL_PRIMARY_KEY = FilteredIDs.COL_PRIMARY_KEY INNER JOIN station_profile ON qsos.station_id = station_profile.station_id LEFT JOIN dxcc_entities ON qsos.col_dxcc = dxcc_entities.adif - LEFT OUTER JOIN lotw_users ON qsos.col_call = lotw_users.callsign + LEFT OUTER JOIN ( + SELECT callsign, MAX(lastupload) AS lastupload + FROM lotw_users + GROUP BY callsign + ) AS lotw ON qsos.col_call = lotw.callsign ORDER BY qsos.COL_TIME_ON DESC, qsos.COL_PRIMARY_KEY DESC"; return $this->db->query($sql); From 7bdd3386b7cfd4a56ba667a640429d38e524425d Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Tue, 2 Dec 2025 22:47:19 +0000 Subject: [PATCH 06/11] Use latest LoTW upload per callsign in joins Replaces direct joins to lotw_users with subqueries that select the latest (MAX) lastupload per callsign in Distances_model, Logbook_model, and Logbookadvanced_model. This ensures only the most recent LoTW upload per callsign is joined, improving data accuracy and preventing duplicate join results. --- application/models/Distances_model.php | 6 +++- application/models/Logbook_model.php | 34 ++++++++++++++++---- application/models/Logbookadvanced_model.php | 18 ++++++++--- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/application/models/Distances_model.php b/application/models/Distances_model.php index d2e26e001..17a8514d7 100644 --- a/application/models/Distances_model.php +++ b/application/models/Distances_model.php @@ -254,7 +254,11 @@ public function qso_details($distance, $band, $sat, $mode, $power, $propag){ $this->db->join('station_profile', 'station_profile.station_id = '.$this->config->item('table_name').'.station_id'); $this->db->join('dxcc_entities', 'dxcc_entities.adif = '.$this->config->item('table_name').'.COL_DXCC', 'left outer'); - $this->db->join('lotw_users', 'lotw_users.callsign = '.$this->config->item('table_name').'.col_call', 'left outer'); + $this->db->join('( + SELECT callsign, MAX(lastupload) AS lastupload + FROM lotw_users + GROUP BY callsign + ) lotw', 'lotw.callsign = '.$this->config->item('table_name').'.col_call', 'left', false); $this->db->where('COL_DISTANCE >=', $distarray[0]); $this->db->where('COL_DISTANCE <=', $distarray[1]); $this->db->where('LENGTH(col_gridsquare) >', 0); diff --git a/application/models/Logbook_model.php b/application/models/Logbook_model.php index 8e9e3833a..897b958fe 100755 --- a/application/models/Logbook_model.php +++ b/application/models/Logbook_model.php @@ -404,7 +404,11 @@ public function qso_details($searchphrase, $band, $mode, $type, $qsl, $searchmod $this->db->join('station_profile', 'station_profile.station_id = ' . $this->config->item('table_name') . '.station_id'); $this->db->join('dxcc_entities', 'dxcc_entities.adif = ' . $this->config->item('table_name') . '.COL_DXCC', 'left outer'); - $this->db->join('lotw_users', 'lotw_users.callsign = ' . $this->config->item('table_name') . '.col_call', 'left outer'); + $this->db->join('( + SELECT callsign, MAX(lastupload) AS lastupload + FROM lotw_users + GROUP BY callsign + ) lotw', 'lotw.callsign = ' . $this->config->item('table_name') . '.col_call', 'left', false); switch ($type) { case 'DXCC': $this->db->where('COL_COUNTRY', $searchphrase); @@ -547,7 +551,7 @@ public function activated_grids_qso_details($searchphrase, $band, $mode) $sql .= 'INNER JOIN ' . $this->config->item('table_name') . ' qsos ON qsos.COL_PRIMARY_KEY = FilteredIDs.COL_PRIMARY_KEY '; $sql .= 'JOIN `station_profile` ON station_profile.station_id = qsos.station_id '; $sql .= 'LEFT OUTER JOIN `dxcc_entities` ON dxcc_entities.adif = qsos.COL_DXCC '; - $sql .= 'LEFT OUTER JOIN `lotw_users` ON lotw_users.callsign = qsos.COL_CALL '; + $sql .= 'LEFT OUTER JOIN (SELECT callsign, MAX(lastupload) AS lastupload FROM lotw_users GROUP BY callsign) lotw ON lotw.callsign = qsos.COL_CALL '; $sql .= 'ORDER BY qsos.COL_TIME_ON DESC'; return $this->db->query($sql); @@ -597,7 +601,11 @@ public function activator_details($call, $band, $leogeo) $this->db->join('station_profile', 'station_profile.station_id = ' . $this->config->item('table_name') . '.station_id'); $this->db->join('dxcc_entities', 'dxcc_entities.adif = ' . $this->config->item('table_name') . '.COL_DXCC', 'left outer'); - $this->db->join('lotw_users', 'lotw_users.callsign = ' . $this->config->item('table_name') . '.col_call', 'left outer'); + $this->db->join('( + SELECT callsign, MAX(lastupload) AS lastupload + FROM lotw_users + GROUP BY callsign + ) lotw', 'lotw.callsign = ' . $this->config->item('table_name') . '.col_call', 'left', false); $this->db->where('COL_CALL', $call); if ($band != 'All') { if ($band == 'SAT') { @@ -1844,13 +1852,17 @@ function get_qsos($num, $offset, $StationLocationsArray = null) function get_qso($id, $trusted = false) { if ($trusted || ($this->logbook_model->check_qso_is_accessible($id))) { - $this->db->select($this->config->item('table_name') . '.*, station_profile.*, dxcc_entities.*, coalesce(dxcc_entities_2.name, "- NONE -") as station_country, dxcc_entities_2.end as station_end, eQSL_images.image_file as eqsl_image_file, lotw_users.callsign as lotwuser, lotw_users.lastupload'); + $this->db->select($this->config->item('table_name') . '.*, station_profile.*, dxcc_entities.*, coalesce(dxcc_entities_2.name, "- NONE -") as station_country, dxcc_entities_2.end as station_end, eQSL_images.image_file as eqsl_image_file, lotw.callsign as lotwuser, lotw.lastupload'); $this->db->from($this->config->item('table_name')); $this->db->join('dxcc_entities', $this->config->item('table_name') . '.col_dxcc = dxcc_entities.adif', 'left'); $this->db->join('station_profile', 'station_profile.station_id = ' . $this->config->item('table_name') . '.station_id', 'left'); $this->db->join('dxcc_entities as dxcc_entities_2', 'station_profile.station_dxcc = dxcc_entities_2.adif', 'left outer'); $this->db->join('eQSL_images', $this->config->item('table_name') . '.COL_PRIMARY_KEY = eQSL_images.qso_id', 'left outer'); - $this->db->join('lotw_users', $this->config->item('table_name') . '.COL_CALL = lotw_users.callsign', 'left outer'); + $this->db->join('( + SELECT callsign, MAX(lastupload) AS lastupload + FROM lotw_users + GROUP BY callsign + ) lotw', $this->config->item('table_name') . '.COL_CALL = lotw.callsign', 'left', false); $this->db->where('COL_PRIMARY_KEY', $id); return $this->db->get(); @@ -4980,7 +4992,11 @@ function county_qso_details($state, $county) $logbooks_locations_array = $CI->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook')); $this->db->join('station_profile', 'station_profile.station_id = ' . $this->config->item('table_name') . '.station_id'); - $this->db->join('lotw_users', 'lotw_users.callsign = ' . $this->config->item('table_name') . '.col_call', 'left outer'); + $this->db->join('( + SELECT callsign, MAX(lastupload) AS lastupload + FROM lotw_users + GROUP BY callsign + ) lotw', 'lotw.callsign = ' . $this->config->item('table_name') . '.col_call', 'left', false); $this->db->where_in($this->config->item('table_name') . '.station_id', $logbooks_locations_array); $this->db->where('COL_STATE', $state); $this->db->where('COL_CNTY', $county); @@ -4996,7 +5012,11 @@ function wab_qso_details($wab) $logbooks_locations_array = $CI->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook')); $this->db->join('station_profile', 'station_profile.station_id = ' . $this->config->item('table_name') . '.station_id'); - $this->db->join('lotw_users', 'lotw_users.callsign = ' . $this->config->item('table_name') . '.col_call', 'left outer'); + $this->db->join('( + SELECT callsign, MAX(lastupload) AS lastupload + FROM lotw_users + GROUP BY callsign + ) lotw', 'lotw.callsign = ' . $this->config->item('table_name') . '.col_call', 'left', false); $this->db->where_in($this->config->item('table_name') . '.station_id', $logbooks_locations_array); $this->db->where('COL_SIG', "WAB"); $this->db->where('COL_SIG_INFO', $wab); diff --git a/application/models/Logbookadvanced_model.php b/application/models/Logbookadvanced_model.php index 1b830e00a..feae66d71 100644 --- a/application/models/Logbookadvanced_model.php +++ b/application/models/Logbookadvanced_model.php @@ -220,7 +220,7 @@ public function searchDb($searchCriteria) { } $sql = " - SELECT qsos.*, station_profile.*, dxcc_entities.*, lotw_users.callsign, lotw_users.lastupload, x.qslcount + SELECT qsos.*, station_profile.*, dxcc_entities.*, lotw.callsign, lotw.lastupload, x.qslcount FROM ( SELECT qsos_inner.COL_PRIMARY_KEY FROM " . $this->config->item('table_name') . " qsos_inner @@ -233,7 +233,11 @@ public function searchDb($searchCriteria) { INNER JOIN " . $this->config->item('table_name') . " qsos ON qsos.COL_PRIMARY_KEY = FilteredIDs.COL_PRIMARY_KEY INNER JOIN station_profile ON qsos.station_id = station_profile.station_id LEFT OUTER JOIN dxcc_entities ON qsos.col_dxcc = dxcc_entities.adif - LEFT OUTER JOIN lotw_users ON qsos.col_call = lotw_users.callsign + LEFT OUTER JOIN ( + SELECT callsign, MAX(lastupload) AS lastupload + FROM lotw_users + GROUP BY callsign + ) AS lotw ON qsos.col_call = lotw.callsign LEFT OUTER JOIN ( select count(*) as qslcount, qsoid from qsl_images @@ -276,13 +280,17 @@ public function getQsosForAdif($ids, $user_id, $sortorder = null) : object { $order = $this->getSortorder($sortorder); - $sql = " - SELECT qsos.*, d2.*, lotw_users.*, station_profile.*, x.qslcount, dxcc_entities.name AS station_country + $sql = " + SELECT qsos.*, d2.*, lotw.*, station_profile.*, x.qslcount, dxcc_entities.name AS station_country FROM " . $this->config->item('table_name') . " qsos INNER JOIN station_profile ON qsos.station_id = station_profile.station_id LEFT OUTER JOIN dxcc_entities ON qsos.COL_MY_DXCC = dxcc_entities.adif LEFT OUTER JOIN dxcc_entities d2 ON qsos.COL_DXCC = d2.adif - LEFT OUTER JOIN lotw_users ON qsos.col_call=lotw_users.callsign + LEFT OUTER JOIN ( + SELECT callsign, MAX(lastupload) AS lastupload + FROM lotw_users + GROUP BY callsign + ) AS lotw ON qsos.col_call = lotw.callsign LEFT OUTER JOIN ( select count(*) as qslcount, qsoid from qsl_images From 9fe5b808a5f1e3a55851191376fc3fd7ee97d939 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Tue, 2 Dec 2025 22:58:56 +0000 Subject: [PATCH 07/11] Deduplicate LoTW users by callsign during import Changed the import logic to deduplicate LoTW users by callsign, keeping only the latest timestamp for each callsign. Updated batch insertion to process only unique callsigns and improved reporting to show both total records read and unique callsigns inserted. --- application/controllers/Update.php | 58 +++++++++++++++++------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/application/controllers/Update.php b/application/controllers/Update.php index 16c27ad2b..26428219f 100644 --- a/application/controllers/Update.php +++ b/application/controllers/Update.php @@ -389,9 +389,10 @@ public function lotw_users() { // Only truncate table AFTER we've validated the remote file $this->db->query("TRUNCATE TABLE lotw_users"); - $i = 0; + $i = 0; // raw rows read $batch_count = 0; - $lotwdata = array(); + // Use a map to deduplicate by callsign and keep the latest timestamp + $lotw_map = array(); $batch_size = 500; // Smaller batch size for better performance and memory usage // Skip CSV header row @@ -403,36 +404,42 @@ public function lotw_users() { $callsign = strtoupper($data[0]); // Validate callsign format (basic check) if (preg_match('/^[A-Z0-9\/]+$/', $callsign)) { - $lotwdata[] = array( - 'callsign' => $callsign, - 'lastupload' => $data[1] . ' ' . $data[2] - ); $i++; - - // Insert batch when we reach batch_size - if (count($lotwdata) >= $batch_size) { - if (!$this->db->insert_batch('lotw_users', $lotwdata)) { - echo "FAILED: Database error during batch insert"; - log_message('error', 'Database error during LoTW batch insert'); - fclose($handle); - return; - } - $batch_count++; - $lotwdata = array(); // Reset array + // Compose timestamp string and compare; keep the latest per callsign + $ts = $data[1] . ' ' . $data[2]; + // If we haven't seen this callsign, or this row is newer, store it + if (!isset($lotw_map[$callsign]) || strtotime($ts) > strtotime($lotw_map[$callsign])) { + $lotw_map[$callsign] = $ts; } } } } fclose($handle); - // Insert any remaining records in final batch - if (!empty($lotwdata)) { - if (!$this->db->insert_batch('lotw_users', $lotwdata)) { - echo "FAILED: Database error during final batch insert"; - log_message('error', 'Database error during LoTW final batch insert'); - return; + // Insert deduplicated records in batches + if (!empty($lotw_map)) { + $lotwdata = array(); + foreach ($lotw_map as $cs => $lu) { + $lotwdata[] = array('callsign' => $cs, 'lastupload' => $lu); + if (count($lotwdata) >= $batch_size) { + if (!$this->db->insert_batch('lotw_users', $lotwdata)) { + echo "FAILED: Database error during batch insert"; + log_message('error', 'Database error during LoTW batch insert while inserting deduped map'); + return; + } + $batch_count++; + $lotwdata = array(); + } + } + // Final batch + if (!empty($lotwdata)) { + if (!$this->db->insert_batch('lotw_users', $lotwdata)) { + echo "FAILED: Database error during final batch insert"; + log_message('error', 'Database error during LoTW final batch insert while inserting deduped map'); + return; + } + $batch_count++; } - $batch_count++; } // Verify we actually imported data @@ -454,7 +461,8 @@ public function lotw_users() { $endtime = $mtime; $totaltime = ($endtime - $starttime); echo "This page was created in ".$totaltime." seconds
    "; - echo "Records inserted: " . $i . "
    "; + echo "Records read: " . $i . "
    "; + echo "Unique callsigns inserted: " . $final_count . "
    "; } public function lotw_check() { From 2bd503cdcd5ef6fe9931cddd063eda80df056191 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Tue, 9 Dec 2025 16:09:15 +0000 Subject: [PATCH 08/11] Add support for managed email configuration Introduces logic to detect and enforce centrally managed email settings. When managed, email options are displayed as read-only and cannot be changed by users; attempts to save changes are blocked with a notice. The email options view now conditionally renders a read-only summary and test email form if management is enabled. --- application/controllers/Options.php | 6 +++ application/libraries/OptionsLib.php | 22 +++++++++++ application/views/options/email.php | 55 +++++++++++++++++++++++++++- 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/application/controllers/Options.php b/application/controllers/Options.php index 89c304b1e..c6905794d 100644 --- a/application/controllers/Options.php +++ b/application/controllers/Options.php @@ -274,6 +274,7 @@ function email() { $data['page_title'] = $this->lang->line('options_cloudlog_options'); $data['sub_heading'] = $this->lang->line('options_email'); + $data['is_managed'] = ($this->config->item('managed_service') || $this->config->item('managed_email_protocol')) ? true : false; $this->load->view('interface_assets/header', $data); $this->load->view('options/email'); @@ -282,6 +283,11 @@ function email() { // Handles saving the radio options to the options system. function email_save() { + // Check if email is managed - if so, redirect with message + if ($this->config->item('managed_service') || $this->config->item('managed_email_protocol')) { + $this->session->set_flashdata('notice', 'Email settings are centrally managed and cannot be changed here.'); + redirect('options'); + } // Get Language Options diff --git a/application/libraries/OptionsLib.php b/application/libraries/OptionsLib.php index 6ad036b72..19bc75ae5 100644 --- a/application/libraries/OptionsLib.php +++ b/application/libraries/OptionsLib.php @@ -48,6 +48,28 @@ function __construct() function get_option($option_name) { // Make Codeigniter functions available to library $CI =& get_instance(); + + // Check for managed email configuration overrides + $email_settings_map = array( + 'emailProtocol' => 'managed_email_protocol', + 'smtpEncryption' => 'managed_email_smtp_encryption', + 'emailAddress' => 'managed_email_address', + 'emailSenderName' => 'managed_email_sender_name', + 'smtpHost' => 'managed_email_smtp_host', + 'smtpPort' => 'managed_email_smtp_port', + 'smtpUsername' => 'managed_email_smtp_username', + 'smtpPassword' => 'managed_email_smtp_password' + ); + + // If this is an email setting and managed email is configured, return managed value + if (array_key_exists($option_name, $email_settings_map)) { + $managed_key = $email_settings_map[$option_name]; + $managed_value = $CI->config->item($managed_key); + if ($managed_value !== NULL && $managed_value !== FALSE) { + return $managed_value; + } + } + if (strpos($option_name, 'option_') !== false) { if(!$CI->config->item($option_name)) { //Load the options model diff --git a/application/views/options/email.php b/application/views/options/email.php index ffa8ce942..9837194e1 100644 --- a/application/views/options/email.php +++ b/application/views/options/email.php @@ -38,8 +38,60 @@ session->flashdata('testmailSuccess'); ?> + + + +
    + Email settings are centrally managed and cannot be changed here. +
    + +
    Current Email Configuration
    + + + + + + optionslib->get_option('emailProtocol') == 'smtp') { ?> + + + + + + + + + + + + + + + + + + + + + +
    :optionslib->get_option('emailProtocol'); ?>
    :optionslib->get_option('smtpEncryption') ?: 'None'; ?>
    :optionslib->get_option('smtpHost'); ?>
    :optionslib->get_option('smtpPort'); ?>
    :optionslib->get_option('emailAddress'); ?>
    :optionslib->get_option('emailSenderName'); ?>
    + +
    +
    Test Email
    + 'testMailForm']); ?> + + + + - + + + +
    @@ -124,6 +176,7 @@ document.getElementById('testMailSpinner').style.display = 'inline-block'; }); +
    From f99a10e13b39773860becb41aaaa730ded083b37 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Tue, 9 Dec 2025 16:23:21 +0000 Subject: [PATCH 09/11] Enhance options UI with icons and improved layout Added FontAwesome icons to options pages and sidebar for better visual distinction. Improved the options index page with overview cards for each settings category. Updated CSS for active and hover states in the options sidebar, providing a more modern and user-friendly interface. Added a subscription management link to the header if configured. --- application/views/interface_assets/header.php | 4 + application/views/options/appearance.php | 6 +- application/views/options/dxcluster.php | 2 +- application/views/options/email.php | 2 +- application/views/options/index.php | 74 ++++++++++++++++++- application/views/options/oqrs.php | 2 +- application/views/options/radios.php | 2 +- application/views/options/registration.php | 2 +- application/views/options/sidebar.php | 43 +++++++++-- application/views/options/version_dialog.php | 2 +- assets/css/general.css | 34 +++++++++ 11 files changed, 154 insertions(+), 19 deletions(-) diff --git a/application/views/interface_assets/header.php b/application/views/interface_assets/header.php index 386cdc9d0..05b2ee5da 100644 --- a/application/views/interface_assets/header.php +++ b/application/views/interface_assets/header.php @@ -367,6 +367,10 @@ function handleKeyPress(event) {