| 38 | | **2) Временско печатење на учество** |
| | 55 | |
| | 56 | Каде се користи: |
| | 57 | |
| | 58 | DatabaseManager.track_experiment_participation(...): ON CONFLICT (user_id, experiment_id) DO NOTHING. |
| | 59 | |
| | 60 | **1.4 Уникатни бизнис-клучеви (email/symbol/equipment_name)** |
| | 61 | |
| | 62 | Цел: да нема дупликат email/симбол/име на опрема. |
| | 63 | |
| | 64 | |
| | 65 | |
| | 66 | {{{ |
| | 67 | ALTER TABLE "User" ADD CONSTRAINT uq_user_email UNIQUE (email); |
| | 68 | ALTER TABLE elements ADD CONSTRAINT uq_elements_symbol UNIQUE (symbol); |
| | 69 | ALTER TABLE labequipment ADD CONSTRAINT uq_equipment_name UNIQUE (equipment_name); |
| | 70 | }}} |
| | 71 | |
| | 72 | |
| | 73 | |
| | 74 | Каде се користи: |
| | 75 | |
| | 76 | Регистрација (DatabaseManager.register_user): фаќа UniqueViolation за email. |
| | 77 | |
| | 78 | Додавање/уредување елемент (add_element/update_element): _norm_symbol() → UniqueViolation ако постои. |
| | 79 | |
| | 80 | Додавање опрема (add_lab_equipment): UniqueViolation ако постои. |
| | 81 | |
| | 82 | **1.5 Уникатна тројка за Reaction (element1, element2, conditions)** |
| | 83 | |
| | 84 | Цел: иста реакција под исти услови да не се дуплира. |
| | 85 | |
| | 86 | |
| | 87 | |
| | 88 | {{{ |
| | 89 | CREATE UNIQUE INDEX IF NOT EXISTS uq_reaction_e1_e2_cond |
| | 90 | ON reaction (element1_id, element2_id, COALESCE(conditions, '')); |
| | 91 | }}} |
| | 92 | |
| | 93 | |
| | 94 | |
| | 95 | Каде се користи: |
| | 96 | |
| | 97 | add_reaction/update_reaction: фаќа UniqueViolation и враќа None/False со лог порака „duplicate (element1, element2, conditions)“. |
| | 98 | |
| | 99 | **2) Временско печатење на учество (TIMESTAMPTZ)** |
| | 100 | |
| | 101 | Цел: да се бележи точниот момент (со временска зона) кога студентот учествувал. |
| | 102 | |
| | 103 | |
| 141 | | }}} |
| 142 | | |
| 143 | | |
| 144 | | **5) Stored function: create_reaction_and_experiment_fn(...)** |
| 145 | | |
| 146 | | |
| 147 | | {{{ |
| 148 | | create_reaction_and_experiment_fn( |
| 149 | | p_teacher_id INT, |
| 150 | | p_element1_id INT, |
| 151 | | p_element2_id INT, |
| 152 | | p_product TEXT, |
| 153 | | p_conditions TEXT, |
| 154 | | p_exp_result TEXT, |
| 155 | | p_safety TEXT, |
| 156 | | p_equipment_ids INT[] |
| | 224 | |
| | 225 | |
| | 226 | }}} |
| | 227 | |
| | 228 | Каде се користи: |
| | 229 | |
| | 230 | Backend методи: vw_students_experiments_detailed(), vw_students_experiments_for_teacher(teacher_id), get_user_activity_summary(). |
| | 231 | |
| | 232 | **5) Складирана функција: create_reaction_and_experiment_fn(...)** |
| | 233 | |
| | 234 | Цел: атомично креирање Reaction → Experiment → (N:M) Equipment во една транскација на ниво на база. |
| | 235 | Код (скратено): |
| | 236 | |
| | 237 | |
| | 238 | {{{ |
| | 239 | CREATE OR REPLACE FUNCTION create_reaction_and_experiment_fn( |
| | 240 | p_teacher_id INT, p_element1_id INT, p_element2_id INT, |
| | 241 | p_product TEXT, p_conditions TEXT, p_exp_result TEXT, |
| | 242 | p_safety TEXT, p_equipment_ids INT[] |
| 158 | | |
| 159 | | }}} |
| 160 | | |
| 161 | | |
| 162 | | Логика (атомично во една функција): |
| 163 | | |
| 164 | | INSERT во reaction → reaction_id. |
| 165 | | |
| 166 | | Ако p_exp_result е празен → авто-генерира текст „Експеримент со X + Y…“. |
| 167 | | |
| 168 | | INSERT во experiment (time_stamp = CURRENT_TIMESTAMP, safety_warning = p_safety). |
| 169 | | |
| 170 | | Ако има p_equipment_ids → INSERT во experimentlabequipment со unnest(p_equipment_ids). |
| 171 | | |
| 172 | | Враќа {reaction_id, experiment_id}. |
| 173 | | |
| 174 | | Во апликацијата, оваа функција се повикува ако е достапна; ако не, има Python fallback со транскација (исто поведение). |
| | 244 | LANGUAGE plpgsql AS $$ |
| | 245 | DECLARE v_reaction_id INT; v_experiment_id INT; v_result TEXT; |
| | 246 | BEGIN |
| | 247 | INSERT INTO reaction(...) RETURNING reaction_id INTO v_reaction_id; |
| | 248 | |
| | 249 | IF p_exp_result IS NULL OR p_exp_result = '' THEN |
| | 250 | -- авто-опис: „Експеримент со X и Y под услови...“ |
| | 251 | SELECT 'Експеримент со ' || e1.symbol || ' и ' || e2.symbol || |
| | 252 | CASE WHEN p_conditions IS NULL OR p_conditions = '' THEN |
| | 253 | ' под услови: стандардни. ' ELSE ' под услови: ' || p_conditions || '. ' END || |
| | 254 | 'Очекуван производ: ' || COALESCE(p_product, 'непознат') || '.' |
| | 255 | INTO v_result |
| | 256 | FROM elements e1, elements e2 |
| | 257 | WHERE e1.element_id = p_element1_id AND e2.element_id = p_element2_id; |
| | 258 | ELSE v_result := p_exp_result; END IF; |
| | 259 | |
| | 260 | INSERT INTO experiment(...) VALUES (...) RETURNING experiment_id INTO v_experiment_id; |
| | 261 | |
| | 262 | IF p_equipment_ids IS NOT NULL AND array_length(p_equipment_ids,1) > 0 THEN |
| | 263 | INSERT INTO experimentlabequipment(experiment_id, equipment_id) |
| | 264 | SELECT v_experiment_id, unnest(p_equipment_ids) ON CONFLICT DO NOTHING; |
| | 265 | END IF; |
| | 266 | |
| | 267 | RETURN QUERY SELECT v_reaction_id, v_experiment_id; |
| | 268 | END; $$; |
| | 269 | }}} |
| | 270 | |
| | 271 | |
| | 272 | |
| | 273 | Каде се користи: |
| | 274 | |
| | 275 | DatabaseManager.create_reaction_and_experiment(...): прво повикува DB-функција; ако не постои → Python fallback со транскација. |
| | 276 | |
| | 277 | Route /reactions/add: UI формата креира Reaction + Experiment + Equipment во еден чекор. |
| 178 | | {{{ |
| 179 | | CREATE INDEX IF NOT EXISTS idx_reaction_elements ON reaction (element1_id, element2_id); |
| 180 | | CREATE INDEX IF NOT EXISTS idx_exp_reaction ON experiment (reaction_id); |
| 181 | | CREATE INDEX IF NOT EXISTS idx_up_user ON userparticipatesinexperiment (user_id); |
| 182 | | CREATE INDEX IF NOT EXISTS idx_up_experiment ON userparticipatesinexperiment (experiment_id); |
| 183 | | }}} |
| 184 | | |
| 185 | | |
| | 281 | Цел: побрзи пребарувања и сортирања. |
| | 282 | |
| | 283 | |
| | 284 | {{{ |
| | 285 | -- филтрирање по E1/E2: |
| | 286 | CREATE INDEX IF NOT EXISTS idx_reaction_elements ON reaction (element1_id, element2_id); |
| | 287 | -- експерименти по реакција (JOIN/филтер): |
| | 288 | CREATE INDEX IF NOT EXISTS idx_exp_reaction ON experiment (reaction_id); |
| | 289 | -- учества по корисник/експеримент: |
| | 290 | CREATE INDEX IF NOT EXISTS idx_up_user ON userparticipatesinexperiment (user_id); |
| | 291 | CREATE INDEX IF NOT EXISTS idx_up_experiment ON userparticipatesinexperiment (experiment_id); |
| | 292 | -- реални ORDER BY по време: |
| | 293 | CREATE INDEX IF NOT EXISTS idx_up_user_time ON userparticipatesinexperiment (user_id, participation_timestamp DESC); |
| | 294 | CREATE INDEX IF NOT EXISTS idx_exp_reaction_time ON experiment (reaction_id, time_stamp DESC); |
| | 295 | |
| | 296 | |
| | 297 | }}} |
| | 298 | |
| | 299 | Каде се користат: |
| | 300 | |
| | 301 | get_experiments_by_reaction(reaction_id) → idx_exp_reaction(_time) |
| | 302 | |
| | 303 | get_user_experiments(user_id) и get_student_participation_experiments(student_id) → idx_up_user(_time) |
| | 304 | |
| | 305 | get_reaction_by_symbols(sym1,sym2) → elements(symbol) + idx_reaction_elements (бидејќи симболите се нормализирани во кодот, индексот е употреблив). |
| | 306 | |