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 | |