From 13737315df9f48bb216cc1647d2566c7e2ae2661 Mon Sep 17 00:00:00 2001 From: ODONG JEFERSON CLINTON Date: Fri, 13 Mar 2026 11:57:17 +0300 Subject: [PATCH] Add files via upload --- evoting_refactored (1)/evoting_data.json | 34 + .../evoting_refactored/evoting/README.md | 125 ++ .../__pycache__/config.cpython-313.pyc | Bin 0 -> 1139 bytes .../evoting_refactored/evoting/config.py | 37 + .../evoting/data/__init__.py | 1 + .../data/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 194 bytes .../data/__pycache__/storage.cpython-313.pyc | Bin 0 -> 8059 bytes .../evoting/data/storage.py | 138 ++ .../evoting_refactored/evoting/main.py | 63 + .../evoting/models/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 196 bytes .../models/__pycache__/admin.cpython-313.pyc | Bin 0 -> 1350 bytes .../__pycache__/candidate.cpython-313.pyc | Bin 0 -> 1824 bytes .../models/__pycache__/poll.cpython-313.pyc | Bin 0 -> 4658 bytes .../__pycache__/station.cpython-313.pyc | Bin 0 -> 1600 bytes .../models/__pycache__/vote.cpython-313.pyc | Bin 0 -> 1372 bytes .../models/__pycache__/voter.cpython-313.pyc | Bin 0 -> 1775 bytes .../evoting/models/admin.py | 26 + .../evoting/models/candidate.py | 40 + .../evoting_refactored/evoting/models/poll.py | 111 ++ .../evoting/models/station.py | 32 + .../evoting_refactored/evoting/models/vote.py | 26 + .../evoting/models/voter.py | 36 + .../evoting/services/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 198 bytes .../__pycache__/admin_service.cpython-313.pyc | Bin 0 -> 3499 bytes .../__pycache__/audit_service.cpython-313.pyc | Bin 0 -> 818 bytes .../__pycache__/auth_service.cpython-313.pyc | Bin 0 -> 3500 bytes .../candidate_service.cpython-313.pyc | Bin 0 -> 7492 bytes .../__pycache__/poll_service.cpython-313.pyc | Bin 0 -> 12144 bytes .../results_service.cpython-313.pyc | Bin 0 -> 10433 bytes .../station_service.cpython-313.pyc | Bin 0 -> 4545 bytes .../__pycache__/vote_service.cpython-313.pyc | Bin 0 -> 4161 bytes .../__pycache__/voter_service.cpython-313.pyc | Bin 0 -> 8702 bytes .../evoting/services/admin_service.py | 53 + .../evoting/services/audit_service.py | 15 + .../evoting/services/auth_service.py | 64 + .../evoting/services/candidate_service.py | 111 ++ .../evoting/services/poll_service.py | 179 +++ .../evoting/services/results_service.py | 125 ++ .../evoting/services/station_service.py | 65 + .../evoting/services/vote_service.py | 74 + .../evoting/services/voter_service.py | 139 ++ .../evoting_refactored/evoting/ui/__init__.py | 1 + .../ui/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 192 bytes .../ui/__pycache__/colors.cpython-313.pyc | Bin 0 -> 1759 bytes .../ui/__pycache__/components.cpython-313.pyc | Bin 0 -> 3142 bytes .../ui/__pycache__/prompts.cpython-313.pyc | Bin 0 -> 3367 bytes .../evoting_refactored/evoting/ui/colors.py | 61 + .../evoting/ui/components.py | 65 + .../evoting_refactored/evoting/ui/prompts.py | 72 + .../evoting/views/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 195 bytes .../__pycache__/admin_view.cpython-313.pyc | Bin 0 -> 86061 bytes .../__pycache__/auth_view.cpython-313.pyc | Bin 0 -> 7383 bytes .../__pycache__/voter_view.cpython-313.pyc | Bin 0 -> 20381 bytes .../evoting/views/admin_view.py | 1311 +++++++++++++++++ .../evoting/views/auth_view.py | 142 ++ .../evoting/views/voter_view.py | 294 ++++ 59 files changed, 3443 insertions(+) create mode 100644 evoting_refactored (1)/evoting_data.json create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/README.md create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/__pycache__/config.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/config.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/data/__init__.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/data/__pycache__/__init__.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/data/__pycache__/storage.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/data/storage.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/main.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/models/__init__.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/models/__pycache__/__init__.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/models/__pycache__/admin.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/models/__pycache__/candidate.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/models/__pycache__/poll.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/models/__pycache__/station.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/models/__pycache__/vote.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/models/__pycache__/voter.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/models/admin.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/models/candidate.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/models/poll.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/models/station.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/models/vote.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/models/voter.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/services/__init__.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/__init__.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/admin_service.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/audit_service.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/auth_service.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/candidate_service.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/poll_service.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/results_service.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/station_service.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/vote_service.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/voter_service.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/services/admin_service.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/services/audit_service.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/services/auth_service.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/services/candidate_service.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/services/poll_service.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/services/results_service.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/services/station_service.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/services/vote_service.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/services/voter_service.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/ui/__init__.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/ui/__pycache__/__init__.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/ui/__pycache__/colors.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/ui/__pycache__/components.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/ui/__pycache__/prompts.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/ui/colors.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/ui/components.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/ui/prompts.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/views/__init__.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/views/__pycache__/__init__.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/views/__pycache__/admin_view.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/views/__pycache__/auth_view.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/views/__pycache__/voter_view.cpython-313.pyc create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/views/admin_view.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/views/auth_view.py create mode 100644 evoting_refactored (1)/evoting_refactored/evoting/views/voter_view.py diff --git a/evoting_refactored (1)/evoting_data.json b/evoting_refactored (1)/evoting_data.json new file mode 100644 index 0000000..ef78d42 --- /dev/null +++ b/evoting_refactored (1)/evoting_data.json @@ -0,0 +1,34 @@ +{ + "candidates": {}, + "candidate_id_counter": 1, + "voting_stations": {}, + "station_id_counter": 1, + "polls": {}, + "poll_id_counter": 1, + "positions": {}, + "position_id_counter": 1, + "voters": {}, + "voter_id_counter": 1, + "admins": { + "1": { + "id": 1, + "username": "admin", + "password": "240be518fabd2724ddb6f04eeb1da5967448d7e831c08c8fa822809f74c720a9", + "full_name": "System Administrator", + "email": "admin@evote.com", + "role": "super_admin", + "created_at": "2026-03-12 23:39:29.971270", + "is_active": true + } + }, + "admin_id_counter": 2, + "votes": [], + "audit_log": [ + { + "timestamp": "2026-03-12 23:39:53.962079", + "action": "LOGIN_FAILED", + "user": "rfe244", + "details": "Invalid voter card number or password" + } + ] +} \ No newline at end of file diff --git a/evoting_refactored (1)/evoting_refactored/evoting/README.md b/evoting_refactored (1)/evoting_refactored/evoting/README.md new file mode 100644 index 0000000..7d0bc51 --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/README.md @@ -0,0 +1,125 @@ +# National E-Voting System — Refactored + +**Course:** Software Construction +**Year:** 3, Semester 2 — Easter 2026 +**Program:** BSc Computer Science, Uganda Christian University + +--- + +## Overview + +This project is a full refactor of a monolithic National E-Voting console application into a clean, modular, object-oriented Python codebase. The application behaviour is identical to the original — same menus, same prompts, same outputs — but the internal architecture has been completely restructured to follow four key software engineering principles: + +1. Modular Design +2. Object-Oriented Design +3. Separation of Concerns +4. Clean Code + +--- + +## How to Run + +```bash +cd evoting +python3 main.py +``` + +Default admin credentials: `admin` / `admin123` + +--- + +## Project Structure + +``` +evoting/ +├── main.py # Entry point only — wires everything together +├── config.py # Application-wide constants +│ +├── ui/ +│ ├── colors.py # ANSI color codes and theme constants +│ ├── components.py # Reusable display functions (header, table, menu) +│ └── prompts.py # Input helpers (prompt, masked_input, pause) +│ +├── models/ +│ ├── candidate.py # Candidate data class +│ ├── voter.py # Voter data class +│ ├── admin.py # Admin data class +│ ├── poll.py # Poll, PollPosition, and Position classes +│ ├── station.py # VotingStation data class +│ └── vote.py # Vote data class +│ +├── services/ +│ ├── auth_service.py # Login validation and session management +│ ├── audit_service.py # Audit log recording +│ ├── candidate_service.py # Candidate CRUD and eligibility checks +│ ├── voter_service.py # Voter registration, verification, password change +│ ├── station_service.py # Voting station CRUD +│ ├── poll_service.py # Poll and Position lifecycle management +│ ├── vote_service.py # Ballot casting with duplicate prevention +│ ├── admin_service.py # Admin account creation and deactivation +│ └── results_service.py # Vote tallying, turnout, and statistics +│ +├── data/ +│ └── storage.py # Central store, JSON save/load, data seeding +│ +└── views/ + ├── auth_view.py # Login menu and voter self-registration screen + ├── admin_view.py # All admin-facing menus and screens + └── voter_view.py # All voter-facing menus and screens +``` + +--- + +## Design Decisions + +### 1. Modular Design + +The original monolith had all code — constants, global state, UI, and logic — in a single 700+ line file. We split this into **29 focused files** across 5 packages, each with a single clear responsibility. The `config.py` file centralises all constants, eliminating magic numbers and hard-coded strings scattered through the original. + +### 2. Object-Oriented Design + +The original used plain Python dictionaries to represent every entity. We replaced these with proper classes in the `models/` package: + +- Each model (`Candidate`, `Voter`, `Admin`, `Poll`, `VotingStation`, `Vote`) encapsulates its own data and provides `to_dict()` / `from_dict()` methods for serialisation. +- The `Store` class in `data/storage.py` encapsulates all application state and persistence logic. +- The `AuthService` class manages the user session with clear `login_admin()`, `login_voter()`, and `logout()` methods. +- View classes (`AdminView`, `VoterView`, `AuthView`) encapsulate all UI behaviour per role. +- Service classes receive the store via constructor injection (dependency injection), making each testable in isolation. + +### 3. Separation of Concerns + +The architecture enforces three strict layers: + +| Layer | Package | Responsibility | +|---|---|---| +| Presentation | `views/` | Reads input, prints output. Never contains business logic. | +| Business Logic | `services/` | Validates rules, computes results. Never prints or reads input directly. | +| Data | `models/` + `data/` | Defines data shape and handles persistence. No logic, no UI. | + +A key example is `cast_vote()`. In the original, one function handled input collection, duplicate checking, vote recording, voter state update, and file saving. In the refactored version, `VoterView.cast_vote()` handles only input/output, and delegates to `VoteService.cast_ballot()` for all logic. The service raises a `ValueError` on any violation; the view catches it and displays the message. + +### 4. Clean Code + +- All functions and methods have a single clear purpose. +- Meaningful names replace cryptic abbreviations (e.g. `tc`, `ac`, `vc`). +- `ValueError` exceptions replace inline `error()` + `return` patterns in business logic. +- Magic values like `25`, `75`, `18`, and `6` are constants in `config.py`. +- No duplicated code — shared display logic lives in `ui/components.py` and is reused across all views. +- A singleton `store` and `session` are injected into views and services, avoiding global variables. + +--- + +## Features Preserved + +All original features work identically after refactoring: + +- Candidate CRUD with age, education, and criminal record eligibility checks +- Voting station management +- Position and poll lifecycle (draft → open → closed) +- Voter registration and verification +- Ballot casting with duplicate prevention +- Result tallying with ASCII bar charts and turnout statistics +- Station-wise result breakdowns +- Role-based access for 4 admin types and voters +- Audit log with filtering +- JSON persistence diff --git a/evoting_refactored (1)/evoting_refactored/evoting/__pycache__/config.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..22529b14743606cc86d009102e9601fcb29659fa GIT binary patch literal 1139 zcmZuwTW`}y6dvcPt9(k2A7!qrL(L295BUJydKwcCx>xipio3yNecdlC$e9gUNs z_yOfl@FRHUl^;>wta;^$w?hBG9=nOSTJo7W-}%m&@653;a=8qG=dTa%tv7LmK84Qu zNv<{*&tdZxVMGz;D2HP-hT}Aj6Etxf;p7QNlXwHCa2oSC(@4+_oW+|shx1qfF9nBplPt+YV z^hVZrP9s7i^SK7PKK~P%QfM~7)DwR3KsR0x+od-=Q6^(bh%dBsZ$`cWEZ%(~GhTL$ znG4kj%X;%Ep_*<^EQb|{O^i^3=8i^|K`0YILxFXN|#TBC({>Vj6!FZ76R?z)b> zSXHbiXc}6id}zY4(QMQ)OC1x3P~B#!F102)UHI99#qPQbmg$iZp~NxglX{$`ov0OH zrzsgjm#|CPuA$q0k-*WGGuA+(-gxP`4ohBMNjh5g?(X|rcmY>A5vA~|EN=$a=TcvY z;*Z08(lFdfyDqSlrkSpxX{^|4wzWg4EjMMUuV~V-0+96IPf#voJ?Zq7Uc@MeZdbzi z-%d}~RHc2~|C`l%%E_SFQ)Eq%2ZvI>*=a-QiK1ebmEW}=RfvRdzLixvl z(vq6$J9Gh4L+qLHU^wt#(8D>{VaFjdwNA-UCf=)=I~}fPLCHuDKT~3gH}~qlf!EOR z^7m*=7cBj7;+k`tJfgSYC=On43FbV`aom?!j7yxS(RTF=6@#rxXbYufE~o%d{+dYs z5j#V*^8(6L1O8gTUk&)u7d|P(mRSI|c7iMSgX)7o*a2`GsKu{&R8qKQo|X2OLa=## d$pHEWac(FFn2}H7zl@B)=#%MM1+*6N@lRC?+Mb uBrzsFJ~J<~BtBlRpz;=nO>TZlX-=wL5i8I}kc)~zjE~HWjEqIhKo$TC2{sS_ literal 0 HcmV?d00001 diff --git a/evoting_refactored (1)/evoting_refactored/evoting/data/__pycache__/storage.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/data/__pycache__/storage.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1382095e0d2613133ff53e848cc466b6ba127f17 GIT binary patch literal 8059 zcmcgxeQX;?cAq7e6iHDOsV_^^T3UTtv}EeTmSTyKQ&y5AQKIaKR;(+9V|n6I+L|Wi zS;|g~B1lo->Yy&-%blCp2`an&!}9fyvr(W4(EgF!T@$;#3&f8c={*NE;se_J=U6FH z-?c#B47p2Dmac#UdnC@hnRzqs&CHuOznR@AD>ESo?f?7_qP7yD`y`_THcxE)BM=vn z4`JkEe40rHGc?s;4NbLJ3#oRJ_2{t9qsMwr2`=#%u)$-*MxL}`CrzGGTuN!(q}fx3 z%P6g%w0O#KIi*V`tsWb;QQ9zB;i<%xo+?~L^Tx?)PYtf2v}tmerxw@ps1`LNU#T7W z%>A02rnqjtt{L^`)yDQkjiYSk&mn6X4aS4K6p!KHqQFOjuL_v|;`JZ#OR-2u;)9V0 zA6#CJhzr5E7>fd5FfQ=HXoz2eC?ZHEkMGDdzbs%$l;T2kL2&ZNmPCn%|M-%?OJZ~} z0!eHIF9>`r%*XLce2EXoFzq@7vYn)fG}ViXAD2n<97 zMFdj%ax5-jAj*IM)qOBr7`8(rvqxi*h-^3-lSEp=$SmO{tV=;@DX<)rq%$!bTFB2Z zO_+RUqX&qKXdZ!eAYvI{6&lR?w04AbJ{DrVPX}>{PYOr$80#q*e7|5yI zv@UNm2<#K;NJ|=Monra9I$cqnUdhpRB}$IAGblM)XH;^uj_xy3LhDMEx^AMUxhPkr zErSh6aydyhmm8P!Rw2oi3c0yn51qpXk*zc>stFP+gc_P|uF(-*Nj3&oLSj4+ zi7m>N0Z9--fshamu0-Mi+TuExp(I4Y5*a8j|M9}m{Hz3`=2sw|KN1y2v3OdT9~Gn% z@!0abkRK(6J{E{+h4_wM$2P@E(L6D>`Mh&*F25=p0s%1!3I${fwN0mz-vtUtXCQhV zWqbP0vFFRPj=ksDw4v$Vpo^qjc25UM8}GG4dft?6ZKK8aoIQ}WWIG%r%irsOR_9Bz zZS5qxduz6-g|=Jz%*g*rBa)rWCNPWy6$oO=y z1yt#adPm9G10z@cqr{$fcAoXFA6m z23aG9WCJmkXz-*U8x$WV8^bGL;j~aEoD7N)nZ>b)0Q-oF4h4d7*(gc@Fd{IoFUf39 zJ4!K`FHHOcE+-M0T1;jY6Hg#(r8t%i#L>n@kfx2s&frE;%af=TqMQ}uc9L&_NUqD< z*yD!ZP1uh@^g8-YT|>6qdbhgsZgq3IWiVAebhl>T-HNuXt>Nx2`+Em7^<9bju1tMT zqP{0p-4fe$9`tmjUCWwksXnBq;~WL4DBMg zq!}pK8fH`{S*TG~yAr+3ysVM5JxJ12qVZ~kR$#`KE9NR@RO@@ZRY2o~JynW$Bx$3X zC^+I7)j|v0P^wyKq4b$(qgrJ_E@-2=>q6->(Po<+2-;W+S}4>r(Po?d4BC)AxFtLj zZECg%g)C@O`*`Uy(Z>F`T+oJlycG70M$&ZWb`8vEtoirtm!ldl%rD(9ny3bPY{fQL@fD0?;x zx_?U=#u7$Ns0(%EbqK-f6oy_K!7~tP!`em^W}1*+VL&A)2x}YF`8yT0efn9Q1?|@; z=MHJ@(?xY|-6H5t(l8T7xt`#Jary)-1|#BGfRup$bC^FK<0;f9*#8U`$pIAOL!xxT z>9An%X$TWD!vqIlVp_P4L}amXJ&DKyV*=SQS(r_ppR5HY%OEpn@Bv5xQ|rZO2te)h z*=n92!x(N0Qt&k3aSDEict;JDDCFLj2onz{Zw^C?>vKX8%NrMP)X$ za&lQVj=i!VEE8ZT>z1(?jXU(XjcR;CW=;bWpd^_I%ZwlqAI;N1&oO{!!P9VX<_0K2siSM4cd?+#CP1>ik ztsR-x;Y90j*7ZWhbtK_Bl5x!@T(fD{@pR){(slgifg8P7m}`Sq2mj9Xx0ZzKc(QRW z+uWXM9!xY3W_$bJx8Jm9dfka$ce-~ZT|JuY9l2h$X1fqvt-DnBZv9))MDIwldNkYE zl4;zZXxyLeAAJA2H@}-HSAuVgKM?==#Py*>^+2|6_iDqXhOc&)Tj6Mbz_Z5g zySC~diB~Gqwzf6fU2E0b#J(Y|hLd$iGn>W?sw?l;aox?er|=ziVQ zqx&_7>F0k86sHp+O7Ib@QbVpn_*S}%nm8C{y#kdM1x*= z5Y2)&Q$h3&a%}~EW-P?80N(|FT4V8 z;F5D%X7DCNOpcy!1B=hrC>S^b`fLKh&9WvY$?S44z9j2&vMF@Mhls=%Ad*?qMAj|} zar`{VYXQ4ra;C!*4~!s(nhW5ayu5BwmE7ew!FY1AZNl>8{=W&(6Ew03)sy=;R-rw1 zlk6Wtlt<3^Few-#5m_EQN}?Ab+6tiY5hzsfGa1b+VJZXM11~@2wl~kbapuj}-gxaj zF;m`=DDTLcD;}_2#-1YJx2yi`SKoT|itUGgdV!_5?|}*JZdx6`G=63NMqi2>$Z*dm zxaZT{p|5q?M#~r6p&yK1aDS~uwyF#6AL`#TW@86)JtTbRM$aF4 zU_g$2KQUZ4toOcn%a-bxJU5y)w?S#H|Mj+k4>T#xmEqh8&Yk8)KtL-97%3J|mZ|Md z)^^`uQnh_aYu_Di1O&8#fV7#1zV>upTe`Zm-aYc+OR4r3Nok8J;mYx~@1?lj4EJ1u zdoIllf&e=R7%UdRu39czuAEBMI+9k$9c~c1w-fz6&`)c}wS!j=u6GT8a4OZ}R%GVq z$A6)I<< zUs>-N`|wn1&jjhewV?gn8cuP28SY?$JDBE%p#OU4f2g>B-D>&e@->*8y-DldJKPZT zUQc?Tgnrr_*T%1oukU-|LtCnKg!JC@bbG%C_oBI*a~~{zh|~R^RNZ8<-*ba`uXWXV z*|}!>mAy03?@82w9h~=MVG`#q&0RSGMz(w4+~{9TfAbOp_laNG+Wu!l4;J)j!wkzz zVmpl+Xk4$v56#Fv#r*fzdStC5myaHL`G9#|Ygw;wX3X6Qb2q(w^!!%(3)mKaZ=Ken zf7<0WXg{jx1oETdF0WnxQ-@m%nV)rad3pWMYiZ_R>btz{`j1O!<`Ywww@d%Y0L|PQ z?DF>MZ@1CRzjkza59HxI=g@N}TVt>)m2=p%vChxA2Usm)2{i)V8-KPDt%rebu zKjn2m!+QV^+T@%80dJVT3{W>i-vj8&0DTXjF9YC*l{d=Zv{TH1OnF)efXt!c1$iEG$O8BBHDf*-C++tWNUOS4|~kI=7(I3uKT}w zdUZXyR-}QW9?y|>10*8O7f(SX>lb3tu(+6W%q%G;Z@!zJ#mz7(AtE{H<2`*sm#z6C z^-ERGWhHEWac(FFn2}H7zl@B)=#%MM1+*6N@lRC?+>Q vB{iowCO$qhFS8^*Uaz3?7KaT`urw#tu80+AC&*33AjU^#Mn=XWW*`dykAgP- literal 0 HcmV?d00001 diff --git a/evoting_refactored (1)/evoting_refactored/evoting/models/__pycache__/admin.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/models/__pycache__/admin.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c22ed360977d2040b12b0c47ccbd475beecd16e1 GIT binary patch literal 1350 zcmZuwL2DaF6rNqJq?K%WlQ^c1lETC`A%bAsLvme9L&e^joN{xlL#};qR#Hgm1ATfk@6Fq9-uq_WYBdSitv`NG zUb=+5#l_ORg)_ea&Kcg62kO8gZ`jG}~`>6@8SOBo)R`sfG#vimFte&^XE>3TJYj z8AVuZ}uyIC0pa-r?5h`~$gUK>FWGJYlW>gIqR~ z^dHLVYJV(iD}N&Kv(|>>mDBC`g`32MkBv&BktqCeWXz9B$3^`5H})RuKE+lfq{Vl#wx3 zY}o$qFW1P`+yR)7bDwN(P3yC5I&GD3ZD+bM+iFjnv#ZPvt{JySNN zXq9(aEVnQXVM1oX<}WW!U;G{1{4==uSFroq-_@HC;VbRH0ySb4cLf~ej1TXC965*1 zJ!cQ7bYBtOh8U2laA^t;pNhc}bit%C&}vk22}&-jjcrIXIO_@dC%AI@{MQGk&tLm( zOQYkLQn1Rf8nL>;>{2&YEjXGkC<_{bI)sKa8H2&*68&|?p5~EUL;}VJi8h&p_nj)n z^kuB|89=edC5Uc?G;VdgWm}*M*kVVUCJJU0dkW;)VZpd694QEl9`NnyUck V6~}SjyA{X#u)gVBzaRj%>c43DIMM(B literal 0 HcmV?d00001 diff --git a/evoting_refactored (1)/evoting_refactored/evoting/models/__pycache__/candidate.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/models/__pycache__/candidate.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e6ec1c0ee3051fc8f13bbcf3e6fd6affbff22193 GIT binary patch literal 1824 zcmZux&2Jk;6rc6y+DYmpCLeX$vQ5%ZbBIEv<^TvGksDG#HIaOHIm5L#B(5Cz6M~3@H76*i-qQNOmG@@tjre%dzI|Ua^WK}^oAKOSnIUcc z^rQGL$Jn2wObK;E<4ydXvjzj!uo{^UEwEPEI%{NWtdV<|NrE8L$<*`x6V%Edcwry{ zFXm1+3b=HhpML8A?*UgljFsa#{xskX)?v@A{raNLh(MRxeJS10>vBC4n%;ZTMf)7x zx*N6KLjm!T&UziL3mqN?9CV2Xec#0De9wb;pi5mZ6m72JNEf{zKzt~49YrD6dEWJe z)YT(Txju-l2=M^meguJ@A8-$p%fIO1<@3a52$fK9Rq3=P>+CG1Jn_qM!+I5f&jcmY>C)1u@z#g#BL%} z6yp4sbl|_;IzZ5>Rv&rmqmb_brrp}(>a#fNwfJ!qi?D;Gwy~fHc;MWBtNuUcsi|c~ zOtsRH+U^as?YbfqvFqx^6usM18(X8w-G1}Zu8pe8 zLwmG*YdDj*mTJRtBCXsR&W?;~*v9Uq?TnnAOZ)b8lP5XarndPcH>{Z7*Of6I#{6^~ zP)P@87nO5%z#dV^zvxjqDA$hZfNnCNv8ZRL)j17)K^V zFI+ccV%nfky2Mj9s#L#ueD?Ua%H3ZocYm#HT-Y0M2R(i|x~R~Int>-AP~P|qT088( z+Ou}8O-yJ18H0Q1!^E<_R60k`o1!h8Nj9Z6MBOBx>UBP;aSKBgA@7v^QCU1YdHTuO z$%S1rsniz}5E_W4SW94Qz-oYL@N58VAZjpv4N(JSgLZ>#gJ){Me!WDkq>;u}mR%Qd z)t4mCy6)$FPfk1)*KG?>vBc*&j9eFP;bK@vre}QVDb?liQ53*^vMjl-igDz99KaaF zVL#>ymr#RDLFRsd-ax}BbLAlWioMFLF=Xb9 FzX7=Pt}6fl literal 0 HcmV?d00001 diff --git a/evoting_refactored (1)/evoting_refactored/evoting/models/__pycache__/poll.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/models/__pycache__/poll.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d283f93bc798e975162f2786d190398a9a2e7dfe GIT binary patch literal 4658 zcmcInO>7&-6`oy^(vlKISu$mbvgDO4%ckW}mL0iD?K+VwGzHoyvRDOd%3wn+SMt`Y zU3O+=*AC+9RK%!)T0jh2m)fTy%B2Q+X@LTDZ#kNkT5a~!pObI4bTE4Bd$T{J3A!i{ zm`3%Iq2Daw#JPm(yT}{gBeMTGeBLERLIv2Tfh2`#srljp!ey$mj913zGy6;0KbbQNq3 zBd5mvsjUTa}`A&9ZfKS)tleNwqDimE{u?*(T~h zRrCU+$Tz2Q!<7_8H+5T4+;shh=K?h@iX8>EMx6f99jVtDJhdbBJGt}K+}oQUd^!I~ zbVtfS*WaYxeJ~?xxQ+0Ej_?it5QMw1ne)@^FMuJwU7xHdH<&d^f#>5F+G7abv4 zah777aJD7Tc90paMy5&NZmVr6WnH!1gD-jNYdg{UG_W=DkNzXO{bSYsF=udicW|OQ zIN=No?GB7r2gaSC>v=rZnRrHG@dU+@YP|<|q}=Q8fq-5QlLa9!;ATK8dhLAuK0yys zJ~B>%YYvuCw9LoeK;K$sn?(wDH7mH0WzF6a-H2*1H;R{);d*4>@%R$8Ry@~xUUEIu z_kuM$2W*Y}Gu7qv9=`uUwRa4heIlQ(_D?vO!3QTcPdt3{(WULow}S45EyzY|5nNmA z>{bF}%q*>F5Mv2Nf%scBP;OV0pRJY*zbBldbKX2NZHCay(Mnb=(=Ne% zG?xK)HQAzo=YY2XtgxJlksJ^%hsW7N5MB*uh11O;tT1PUH>~iXfx*V9P%*+gwjgmR zlzb?}J@jnu7#QWA{Ln!B^5jPbPlNbTcx)h=aasnb2Vtw-6#*w>9z3>{HKtO%f*Hh3 z21!b>Z&Wll0q06>1+mkWG_&kU@o*>HHh%Dy$&s}MKUSIBXW0f+JG zCh?R*5Iu(G?*LmP&m_{FUT<~Mne~JZr~@YvK{F%kDW_LnPda_W>mAO?$?D1J&GU~b z;%l$db9lYYkx%bP1D;`KVX%Q9wX~1G|9bzu#FI=GY#@OCkJv%jdSw7*MIM8$P^m|259)aJlk-y(0yA3! z2C1p5=ic#+<4(Ho-uoNxhuNr>p8%=HH^%{@A0#)E&iL8i-TLCzcHhM>Zhb!VAiJ6U z&DhqhYTw1$z@E&#s~cAxd1P0ftjd#!^f=yYp7$j&$tB~7|Lk>;%$p4F@Lvw}T^j9t zDdFMS@wn^o;!bFq+@P8Yk{g`A7@j}ux+(r7z*aIJ0ujvW(}FnZ$2r6TAV0=}88Jma zwfZSFAHktP;)FKlf4BH;D{mtU&DST@)(2zb-k7GIB=T*6Ct%s`&z{6K-|3fO^l;#j zxZQi!={|H{+PDa@`k=5`crrTmhoS9(3*j(-czw)8NIEa@YWVJvOJX$JUbiPTyyj5N zx0mBJhws{ca&SlFo~4I@(-C9{Al?RzqJ#`Ha#$W**o&rO$M%LhpgfRyd5{Xr1CYG& zI7)?INI?jHo$^4M^Iv99e+={9oSBAa+$t+A8k$!sShQq>^?+Q~mauxqhhM{k=#>1n zbxotDMYB1z9+$(Sp#t-NGetvT&1hOE_(ecA-T<9$%+RiB27mjw2Uc_wQg`rfDlKbn zTxSZrZs6ThJO6B=X#$@Hr}#Yd5=!4iR?FD94|q2`s!P`gf?W?&b`eY&j%=E;>$3OI znH_1+%h^1IH_y!Bt|OMeDl^X`hBi=;14q09Dl1|&jjtFek)x_rxj`}Q)je~({LvXY zAg2hUN{ZlIwPDD zytfv=Nt_)5o0oDJhn=o)4I%Wbpdl7W{GD<8qdPyk{m~OC!~Y(_B0MRt8sm4GA7R6> z;+<}Ooi9QUMTWO0yw08|?Stf9XB;&PpgM$8pz9nK_+$;B z;Yt_0SIkBF1F#d|#}J~QMFc^3E=Gjti}qe2@tgpAaqe}LfW0t<9-)7a0ONo7Ux$97 AI{*Lx literal 0 HcmV?d00001 diff --git a/evoting_refactored (1)/evoting_refactored/evoting/models/__pycache__/station.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/models/__pycache__/station.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..11d8729a84e4dba0078e73d8ca7de47290eef09a GIT binary patch literal 1600 zcmZux&2Jk;6rc6y+OgxNY0|`r!X!0Stx8cqY7c-Aha5N=R1@TbrR`=saaT-s-8bt< zrmEx$e6EOqRh5YxnOMuY=iBwwpC~C%!5oE1*@NJ=7uA*%3lVd=l9#88+w7y zPlKLR{Lk_C9OM9`mVT(Y%Lh+J+7qtgXC|BLIDNm|3i$C*d%o0~tDx_R4|(9L5%0O7 zi&-!f;SeNu{T|m6JXd*Nn;-3$MFP}(5?pL{faV43GO!SNrfosCovAa(wX-Pm-9kH8 zXAhE{P;BSH?iZTHsJhU_-$l$2%Sjr2s6I}L&0eCoV4WKZzwzpDWu^Lonj>MIPZaGi ztMj><&y{r^t9pF$Me0=aiaAFrtHuC)RX=c#h6`{+3MP)IK8zo zDNQ%{q&i*So-D=M?)$IpooT%>sZDS1Oe)jud#~-yL~ia)mg7*p@ojB}wU|hmg+|K3 zhiF`|Bld_={<9vXoO>IX^S!}<*w)NYJ3UWOJ2?>yMokNDVL;R0IIN+JieX?sXw zgw%S);2n%1xU5h*%FzR$sRU0Dl};cyjWOL`#FPehFx@a@%>Jm|Jb(7$i}PofcHO{f zR%45Q-h-O%6_-Pnu_?c3()hyBE=uY9}%8r9P87k5%j`MWrsw7f#oD&ap zsPI|y1IK}N#0B)8MoU7uTA#}BNzj8m@~kD`MoM{mcJP=sd6k04E*&oG?d<^$7 cok*aG4p&*rvfgB}R_>q5rnPm=P?)O!0-?cvrT_o{ literal 0 HcmV?d00001 diff --git a/evoting_refactored (1)/evoting_refactored/evoting/models/__pycache__/vote.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/models/__pycache__/vote.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..468213ed7ec263750e8a97515e4b9d9f1bd772c6 GIT binary patch literal 1372 zcmZuw&2Jk;6rZ)%_S&)IwxO!)6qq(qR9~X>&>jFG4mlfClgfvchRyEStQhZ@H{(ia z70D;0NI`HQe?-q5;g2Yy64u^;6Sp)zaOJ)6M+D+Y``h<9`NGRX)F0iCsOf0ray3yV~|E> z*6@g%G#e_(k0YhD<>-VHKG48*Ciz&K)TTPyuXQy)o?xmphATZx;}0=Go$w^Gc!Y&X zY$qUjlqK9qNF$ZLh!&NxW3z-MH#R>&#{~hk zD?uxb%F-LCW4_TUr8?F55gmyf(a*x z;is8Ag!H)#52g9a>TxKM2@YL=97M6zAQS%ZQRjc$D_7`N$Aol){qad22$5!~6(Vmu zBiG6#re+^u$}R)edjGsWyYn8x_THPIHM{rlO>lSC;^&Q7d-uFKyZhjL6XDy%~dF>&={JwEAp(8PeDuW`76m{+WJ zDS6#pW@+7~EP^SUHMhPyJv;radH3quY(=f!brScCNyYi+zQ@8g(%bic)*Ul zL+^>Vi%hz&7_>2loQknBxlg~2p(FT`NOb^uw4k!-sI2W4W*M6Fl>Oekb@uZ6&(B`I z4%&`ICvZEkSdQymx!w@BM6@+s+$(HH7L6pfqb3B7b)rbG3-R?NQp-qFh(QX*D%{0P z3jsH=815kC8?lOv8Agc&119ip$kuete?$I7J=fx$dBUAD#yvEiUpg>|r`h*1Uso WS@b;bZK3G-f7iFX?JI`hPW=bB%0gTK literal 0 HcmV?d00001 diff --git a/evoting_refactored (1)/evoting_refactored/evoting/models/__pycache__/voter.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/models/__pycache__/voter.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71f06eb801fbf1121f8e24796932adbd05a3e753 GIT binary patch literal 1775 zcmZuxO>7%Q6rT0(#);!NY0|`L!{$dRMpXz%tq>rDL{12$sK%8KmbUBNv9mDVHE-5I zp(@G=HIflMkOMcay&-Ysz>#ArB4N#`loPkKM&inQv-Vc}jI?jwn>X|Jy>GrZqhhhZ zaBTnhgCAuW`<;x*KwFQ|6`W34jR9L`8?0tjSS@uU6+6M`q^p_!7ibmkgb@d;8+u$= zPmjN`fcJn)9z@b|(CGLQ{oJz-Y2P(zc~0b5PT*O-hnxK9Xs56rTCKkH11_Z{!jA90 zWrcw_inq!L`fk(*t`A8Lz9an4oyZS^oq`+Z6U%XOY;+UtQ`Tf)VEU<=0qHeX%hysB z_OSt(TDrm@+sxH66?S8M9`dy;n4MhJR9RinA9?h+^J6zn6g`dK=n0Bb)|eAG$D?5h zBhh!oBoEejSI`MP8Xt-Yw5)MgWRvxt$R+x*px^4zcqq(7|4vLL`u9R#9@&kb#B{QL zCT0@-i6~C=dZ~r^)JwOFS6Q8j*<^hVtx{r@(VClB9rnoBua;HH_taFoFN7U9U9P4B zjk+UjTrc81ayx9>Egzx-m3BH@a;3~_zjxUtibENz%1W%PU z8En@9&kp+C7M>}{NJppG)4sG1@uJ&4#{|%D+{i!VYUaQ}2lB|X{Xi98kjr)=m4Q(3 z>b%NGF4{mi0s#nw91vtcSOI|qgbolCKnMU84rLN_;jlpB5{X3;OC&C%kmR-gGt^!8 z&1M~mDVu$i&07KA15CHM$K@wc*lTi3*$+B4@HQ3_QZu~#TJ?XHCreG;O|q#`+UXrB z)5e1R$hOt&9crVUiA@zsBH}T-D6oaaL4LThF_;~$Zl9aW!>g~Ho0Z|h(!d-pT^URd zFRu+|hKrR!Ar{Nlpg3HA{oGs|R<;JE@gaTw`Wuh0?|u8h&l?Xi=jI9qqPa4>wEQ6R z)zk?8$J~VRXe7N6cV3;{rY-*(x07CY;h6N_76a;nYKltre3weixMA<8YQQo&B>l~T z6_hF;g?fuB##oHamuMSZ*fATH7QViJa{t%T`s332FQv^ha}($}bCb?QgE~<6Gu`Sb z_69w2yR2^P88?kBOlAKOgH7~dQfXI6m7$kQQV46D$y6J{Zk$Ug&Lvscz-^KkcFcY& z&7a(R_|eI|Gqa-8sFq^_YCdXaY8Gm?YI+!f^8$6`hxTs+y1ogh_R<++if3YB=F@8LfeK4Mga?jnsx>DYL`a`p$FTv zvS{17LtX5(2>ez*;u4P#K)NV>H$Z@DLD|b|D86JDCVS6#-}p1Xm|c0kV`R53ax7PV zmNK%dqcmzG(&89&d>`@7BMCwm!|Qne{D(5%j_Y{`c5yoqLlR6EX~Qs{rP4;`ubHdH K>T`xdNBtX2GoN(; literal 0 HcmV?d00001 diff --git a/evoting_refactored (1)/evoting_refactored/evoting/models/admin.py b/evoting_refactored (1)/evoting_refactored/evoting/models/admin.py new file mode 100644 index 0000000..410be52 --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/models/admin.py @@ -0,0 +1,26 @@ +""" +Admin model — represents an administrative user account. +No business logic; only data structure and serialization. +""" + + +class Admin: + def __init__( + self, id, username, password, full_name, email, + role, created_at, is_active, + ): + self.id = id + self.username = username + self.password = password + self.full_name = full_name + self.email = email + self.role = role + self.created_at = created_at + self.is_active = is_active + + def to_dict(self): + return self.__dict__.copy() + + @classmethod + def from_dict(cls, data): + return cls(**data) diff --git a/evoting_refactored (1)/evoting_refactored/evoting/models/candidate.py b/evoting_refactored (1)/evoting_refactored/evoting/models/candidate.py new file mode 100644 index 0000000..31a1786 --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/models/candidate.py @@ -0,0 +1,40 @@ +""" +Candidate model — represents a candidate's data and identity. +No business logic; only data structure and serialization. +""" + +import datetime + + +class Candidate: + def __init__( + self, id, full_name, national_id, date_of_birth, age, gender, + education, party, manifesto, address, phone, email, + has_criminal_record, years_experience, is_active, is_approved, + created_at, created_by, + ): + self.id = id + self.full_name = full_name + self.national_id = national_id + self.date_of_birth = date_of_birth + self.age = age + self.gender = gender + self.education = education + self.party = party + self.manifesto = manifesto + self.address = address + self.phone = phone + self.email = email + self.has_criminal_record = has_criminal_record + self.years_experience = years_experience + self.is_active = is_active + self.is_approved = is_approved + self.created_at = created_at + self.created_by = created_by + + def to_dict(self): + return self.__dict__.copy() + + @classmethod + def from_dict(cls, data): + return cls(**data) diff --git a/evoting_refactored (1)/evoting_refactored/evoting/models/poll.py b/evoting_refactored (1)/evoting_refactored/evoting/models/poll.py new file mode 100644 index 0000000..9f50d73 --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/models/poll.py @@ -0,0 +1,111 @@ +""" +Poll and Position models — represent elections and the positions within them. +No business logic; only data structure and serialization. +""" + + +class PollPosition: + """A position (e.g. President) within a specific poll.""" + + def __init__(self, position_id, position_title, candidate_ids, max_winners): + self.position_id = position_id + self.position_title = position_title + self.candidate_ids = candidate_ids if candidate_ids is not None else [] + self.max_winners = max_winners + + def to_dict(self): + return { + "position_id": self.position_id, + "position_title": self.position_title, + "candidate_ids": self.candidate_ids, + "max_winners": self.max_winners, + } + + @classmethod + def from_dict(cls, data): + return cls( + position_id=data["position_id"], + position_title=data["position_title"], + candidate_ids=data.get("candidate_ids", []), + max_winners=data["max_winners"], + ) + + +class Poll: + """An election poll containing one or more positions.""" + + def __init__( + self, id, title, description, election_type, start_date, end_date, + positions, station_ids, status, total_votes_cast, created_at, created_by, + ): + self.id = id + self.title = title + self.description = description + self.election_type = election_type + self.start_date = start_date + self.end_date = end_date + self.positions = positions # list of PollPosition objects + self.station_ids = station_ids if station_ids is not None else [] + self.status = status + self.total_votes_cast = total_votes_cast + self.created_at = created_at + self.created_by = created_by + + def to_dict(self): + return { + "id": self.id, + "title": self.title, + "description": self.description, + "election_type": self.election_type, + "start_date": self.start_date, + "end_date": self.end_date, + "positions": [p.to_dict() for p in self.positions], + "station_ids": self.station_ids, + "status": self.status, + "total_votes_cast": self.total_votes_cast, + "created_at": self.created_at, + "created_by": self.created_by, + } + + @classmethod + def from_dict(cls, data): + positions = [PollPosition.from_dict(p) for p in data.get("positions", [])] + return cls( + id=data["id"], + title=data["title"], + description=data["description"], + election_type=data["election_type"], + start_date=data["start_date"], + end_date=data["end_date"], + positions=positions, + station_ids=data.get("station_ids", []), + status=data["status"], + total_votes_cast=data.get("total_votes_cast", 0), + created_at=data["created_at"], + created_by=data["created_by"], + ) + + +class Position: + """A reusable electoral position definition (e.g. President, Governor).""" + + def __init__( + self, id, title, description, level, max_winners, + min_candidate_age, is_active, created_at, created_by, + ): + self.id = id + self.title = title + self.description = description + self.level = level + self.max_winners = max_winners + self.min_candidate_age = min_candidate_age + self.is_active = is_active + self.created_at = created_at + self.created_by = created_by + + def to_dict(self): + return self.__dict__.copy() + + @classmethod + def from_dict(cls, data): + return cls(**data) diff --git a/evoting_refactored (1)/evoting_refactored/evoting/models/station.py b/evoting_refactored (1)/evoting_refactored/evoting/models/station.py new file mode 100644 index 0000000..6e6146b --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/models/station.py @@ -0,0 +1,32 @@ +""" +VotingStation model — represents a physical voting station. +No business logic; only data structure and serialization. +""" + + +class VotingStation: + def __init__( + self, id, name, location, region, capacity, registered_voters, + supervisor, contact, opening_time, closing_time, + is_active, created_at, created_by, + ): + self.id = id + self.name = name + self.location = location + self.region = region + self.capacity = capacity + self.registered_voters = registered_voters + self.supervisor = supervisor + self.contact = contact + self.opening_time = opening_time + self.closing_time = closing_time + self.is_active = is_active + self.created_at = created_at + self.created_by = created_by + + def to_dict(self): + return self.__dict__.copy() + + @classmethod + def from_dict(cls, data): + return cls(**data) diff --git a/evoting_refactored (1)/evoting_refactored/evoting/models/vote.py b/evoting_refactored (1)/evoting_refactored/evoting/models/vote.py new file mode 100644 index 0000000..f7159f2 --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/models/vote.py @@ -0,0 +1,26 @@ +""" +Vote model — represents a single ballot entry for one position. +No business logic; only data structure and serialization. +""" + + +class Vote: + def __init__( + self, vote_id, poll_id, position_id, candidate_id, + voter_id, station_id, timestamp, abstained, + ): + self.vote_id = vote_id + self.poll_id = poll_id + self.position_id = position_id + self.candidate_id = candidate_id + self.voter_id = voter_id + self.station_id = station_id + self.timestamp = timestamp + self.abstained = abstained + + def to_dict(self): + return self.__dict__.copy() + + @classmethod + def from_dict(cls, data): + return cls(**data) diff --git a/evoting_refactored (1)/evoting_refactored/evoting/models/voter.py b/evoting_refactored (1)/evoting_refactored/evoting/models/voter.py new file mode 100644 index 0000000..d109922 --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/models/voter.py @@ -0,0 +1,36 @@ +""" +Voter model — represents a registered voter's data and identity. +No business logic; only data structure and serialization. +""" + + +class Voter: + def __init__( + self, id, full_name, national_id, date_of_birth, age, gender, + address, phone, email, password, voter_card_number, station_id, + is_verified, is_active, has_voted_in, registered_at, role="voter", + ): + self.id = id + self.full_name = full_name + self.national_id = national_id + self.date_of_birth = date_of_birth + self.age = age + self.gender = gender + self.address = address + self.phone = phone + self.email = email + self.password = password + self.voter_card_number = voter_card_number + self.station_id = station_id + self.is_verified = is_verified + self.is_active = is_active + self.has_voted_in = has_voted_in if has_voted_in is not None else [] + self.registered_at = registered_at + self.role = role + + def to_dict(self): + return self.__dict__.copy() + + @classmethod + def from_dict(cls, data): + return cls(**data) diff --git a/evoting_refactored (1)/evoting_refactored/evoting/services/__init__.py b/evoting_refactored (1)/evoting_refactored/evoting/services/__init__.py new file mode 100644 index 0000000..0274469 --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/services/__init__.py @@ -0,0 +1 @@ +# services package diff --git a/evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/__init__.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f4c5630b63edcf5685679b24d614af2eefb34aa9 GIT binary patch literal 198 zcmey&%ge<81nPS>X9@%9#~=<2FhUuhS%8eG4CxG-jD9N_ikN`B&mgH=smWF`p~b01 z#WAHoGR8kI)ukx2EH%a@wK%&ZzaS>HEWac(FFn2}H7zl@B)=#%MM1+*6N@lRCKEC3YYH{$>R literal 0 HcmV?d00001 diff --git a/evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/admin_service.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/admin_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f440a15de9244c05aa693c64adf06d8d8faf157d GIT binary patch literal 3499 zcma)8|4$pq6`!@&W?7r~3z!6xgbkE}DL7C_bU+#%1+EuJ$jOVZL|9pE>;*P8yJU7v ziYrz3Q$?g!0jW`_bW-^-_uEPR<$kz7Aoo+(XHr`u(jVGNRemGKN%w2t?0RiLoYax_ z&71dj-n{qb&1dGgrG-b(e){#_RMLddzv;kjY>hcy0_Fu05k?{-dgd9-K<}OR%z3eQ zj>W7%IKad3TS1n>Nt`2gMVZ=u85x;wg;X0RH?{~TNvfurl0;%BLrS{h z5cLZx4}FHJvG||(V_*?sbFwKD-rjO0Cq3FCeyo@UtYvA`ZVLiqXW>JjFHjoF(g&jc znm!jmXqj<;7Ckp=yuzeW>SQ)+zoguPcc(y~qOP53H)`Wn^zk)>G;i9Q;?Dd6L)V)L zI6XO7?cSwR-gq0{1dxE1!B?ljJeEe`>X&JhcAJPIB)6Wj;%jIxAkja1!YbHlqIQ;P z@z^cpmZ~d&z8ksGOhR_#(;4Gj9~5Re;!gUJ+<|Xro11~WmQDRxkNpq!AEHBC2#M~a}VZb$SX4N#h{Q~lQ94b08o;!SkEiPmOI*}oL6%K9VJQgN+F+@=<>v; ztjlVCF~Jd!n!~g%Ff9Zer-W`l?f}w&3fUyhs$m25rt*%ScoLjW%}y=ON;c9LL(}vg z0Z6l8|6JS-@N?E~@V6%AAD!8Gxh z7B2}q;HTy!`w~+yrU#CA4HFX%08KGf2*|7HPcf}8Orgc`o;Bo6CE>&LsA4$b2)6+t zj7)sa;?%xC8>V=j??87;@g zs_pT;$x?e?HQK)Obax0m-&xtce0VYb`3IHG!BXd7rE|E{Ib81idHLcf{QN%B9O1Wp zM{THY@HzjCuW%hDu4AwFul;}NFL6CpF6c;mzU6xMpOm=%H(bvl7u^|qIk7vj_xo4H zucX)Mzd!uup>;Q7WpbACxT5GKMYnPr*2WWS5+86z&B*i6v(N$8Q4O_i2Qke!;oQw9 z;cNk8MKh(k)xKYqt5kGWYb>J2Pjp)u^R4OK%<^h7}ZeKxfHGK zGp&J9@UHPM6vKAK3lx!7;H8*L7JH^0fNJDe*wPDF(4W?ylgW=rcxHBLdhyPu4IF5l zai5S4EJ$$nk;fD)iFZ{oaW`=IDkb)5wee>FhVEg#PF2H{j8HNN#5i+U`G#lY;;94M zPz=#|1t4%}!)-75UA_{&RtjIMgnLWj-u<<5`1-c*pP@GUBwsH_hppgnHPBiP^x2YM zmZQ^FaJm}r+V=f1RIh2PYvjw@*6fn?;9=##YU#nMwX|kUs|VcU6J`8{>#!>r(_LFg z1nG;7A}_qycu88$RgOc>5Am?kEkR+|B*f8^|rf9k(Vfqe*SDTtOaUZZYAYvL7 z@v54M={Ay>BpD{W*t3#snpk~QFcm|RzCh0Mbn`L2D{VrN(3k}B2lSnX@pT^gJAARd zu_FZCE79)0On*;DM>jcNe1G%^LHAXU-Muz`pd-gFH3?BQNm5SFf~WLU0D$V+in`iB za1w+_mAsL(53PgevDz~hgavcWebhRj-hM)tgXe8+=v0Xv#cf!p5gFlbIjOvjXFx&k qC8HO}k(XhZ_Z~099`lF^{}+wEMOWXV>u=G}5B@$T{v85hoB1E-lJ~&? literal 0 HcmV?d00001 diff --git a/evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/audit_service.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/audit_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a5c2a8a5c462d24385ec7a6f3684fdfc84ae1fd4 GIT binary patch literal 818 zcmZuv&1(}u6rb78BvK8=LJ~a8shS>A3-+LhQ0Pr*AWKeNSeD&soSN*0nMov`;>nvQ z3m*If^d@-qp9no93?4*J-cmyEzS-_pDL$C@e(%kj-@cDsZ){W$$kw;7Y=#l~rJX{Y z9IUqhoFWf#>{g7p)2%%U)XoT-p>NWw_6G!aCmM82RT%-#>ue!KEpS8=GE%M4|WfmOLN zl_495$kMmDxL{2<>vy0|Q4e+Vi+@d5XIzsby9gW=J)`awD((>KmjB`c?d4z(rl+Nc zdq&UnaN8;(1i@U)*E{>85lvzeCV6)>k$hS-EM&^*Js8hb<1I^-B^y#9!{LbQs=02K z>sk+6wkl_-0n7+ylTj;ENUln8D5-v^%p`pUEOy3yjX`KG0-{c$$9@lHDttXzzmw1d z&L-49pyH)WM?RgTk|q6s(_>fxR2h@|yRH9(i;$nMUHF$3=5Bj5RTZc&xn$WvzK$w(mL@ z$hO@zJVwx@8w7D01p&VSKG*+r_JPlcS2ex~N|l}r(|A0fPk9Yoy)V%MwK6fr=Z1l; abqV3>%5ky#AzC3YpK6);A}+L-QT+i`;l?Wf literal 0 HcmV?d00001 diff --git a/evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/auth_service.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/auth_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a0c9f72b5fab6abeac71f893a52187e2680ecd5 GIT binary patch literal 3500 zcmcgvO>Er86&`ZA++Rs6*|K6;wnwh4h_$?KZCSCM#)xbsaL`0;MQ;%*<&u-p947c7&Wxl<@SNYGmsl!Zbfo59gU5x{e*XflmeF zORmG{KbHSQ=WV~{F+mq?A$}9^8ZgAX>(7l+FPL+Ex@3E9O>P^_gD3?>)Twh0>S!Gl zfifXLX_-z1wDJlK>fvHNe4cU^*10dB7j-WbGz_R?dmiJWoc{_+X(~CYINj>#xCq8 ztD3}BRBed2mCVYESECNJ&1$-v7vOxr9ub2;mdU>gJs+HZ@BFQiPfmP%;$Hu;dxg=> z!uWb&{9fVAns#OzgouNr+4N+v{Yx0!AXNhRBwRJ&dZx-XGu2Oc8qlhnY53{Y%$ROw zBx+Z(+n9&v@XmRVC5{ z^Je4Va!m3lYgw-EhL#oWZM#Fc$?XOi(F0_eYz@%s+85dVTO*^_wTYF6|E)ntdz;NEhfW}q(@DZ%qegD)NAFdseW%}?)F@p53dk9a z<*Z=lZRUhVId7z1zP4w=ae_KLYc&S><}TSGt4&2|sqylEa<4Pb2^L^$qzp>&SA6+4 zQU@{vvtq2Fv5n;_aVErf%l_; zj)}`^d;lf_Jw#=T;Z=|bx1@nlUpvCu79eOuEC5+153{7_@U1KNx<{_-TfGNvzV*kq zKC-UAwAHougLnS$&dM_zUB+$pslV#q)i=s7Z}kn_eD~(N_Yb_VHDr8I8r&K>_MwJ- z-+^0KZ})zBc=hnw$k{vHYX_gdQ(5b~@b}V%2OVVJ(MBgJos99DhZl@-%dvS4Jc8q= zfOi03`<=lHm-~N%7vL$YIJmW~?SQxn5N`uDK)h9aOsL|Zqtv!J83M%hZ$La_WZNJv z9r-OF9y|XDi0}V727%hU039LygK0MBijc=&34RdLt1P5T@&#qJGQAl1Az&C@*EoO5 zkRI}s%lshN&kq4XJx80V5u1wfr+$55296GRt7uLcIhic{Fl>vuV{=i~ie+WqaEgls zNS>w{kJE<_;(1Bp9EEYbV;DzAkYGTbmNoLY-Lgh5d=Gd27Rdj~n$51^^{(OD-{0sO zyVLu(Lw`B+`S`}!pSAMlB8)csM&R%Mfj7T>4s~M=^^nq}B9rL5mn@F;g=2l;@)3X> zBQ$Cakmk4Q-g(p$U?vTUjZUxI!aX);_rKaSw85Kl*5ndtrOd6@j@QF06+u!g_ zCt$5@N7Uc$qbJv%0~5pqkY(~^v9wt{xL!QCa(S(2JR)kxnXS^^&C=j{X>jHB_0s60 z6tKQ*vNlV@>!sn_$Ja~GH8NT*FGFppvMNXmr;JYge8AEGpAUJ|z{9Bc&+H12v^++j*jNHFQ-udkc((Zsp=b>>lOI UO8x;sQd3A_-}}dZmyvq^3xc922LJ#7 literal 0 HcmV?d00001 diff --git a/evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/candidate_service.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/candidate_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ccdf1a95bfad24a4fba9005b2d60f2b232c77e6b GIT binary patch literal 7492 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 QIq;Ul(eo7|u|Bi^1sYCd7ytkO literal 0 HcmV?d00001 diff --git a/evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/poll_service.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/poll_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a309ad034c49d5360e3b6f7d694952b27b19e62c GIT binary patch literal 12144 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 literal 0 HcmV?d00001 diff --git a/evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/results_service.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/results_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3af9f72690ebce00b67459a472de02e6a779a5ee GIT binary patch literal 10433 zcmd^FZ%kWPmcP&TGsc)0^FRCv4?`eMz~F??lqRI)&orSW2{`UB4O*{bzkpj~llx3k z$YfQuqY=?=)nqyqWIEast#$!vXGuFBnyyA_`k`vRjE0A4Z9PS+YPZrV-$;qJlYZKB z?t2g0V7iIa+5NCL;lBIl-2dnN&bjA$t+3EaAT<%W$e)A*Z^vkr*TcqA?fl1MLwgQD>7Z~R&a#zx1+SDeh4WMlycUXGjskHY8WKX{Xl$4U z#zw+HK^h+&4N&OEEu4x8zLNs<1cSJ(L2btDW7aZje-DZG$Pl3%EWzOAsL^YvBGlwH z4)IMP4=0JXVS&CT|`qv9J=$I$b_1&1bC)UMtI4wOj$q6+q6`Ver~m zu28EhWVvlxZX3(ly>?nOT;y^n+qBK|s(be!4<;Sd_LwaM(?=MM)>w$*^+l}2E?nXO_8ogZ9 z1b@7b4CquD-+|1%)XGc9i5MYKv(G%B^Lj=LAJ;WVNC?{Xl@S6oIzwgB7}jZ;frj@Q zS_`2^9;k7J8^SIvAAp7XOd74J?-=Ba0|gt1Aq&0OHkW!iUG|^{s2=RhMFrC#eHRHS zAOacefSDQH#zx{YO-k0IvGHh}$)hA(h*2RRNO1HKQP4MC2uGn1&%zBdje9Q%SA-yl zVK@|si*P1gAlJ}Ii_q*3oCmoLN1HQ>;xUkOy+F9?wm!gBJb;rTE~8>*O(i8yYpL*z z-j&A~R^bsSB)ScVsU0FEU!xO4ig8$sE4IvuK&yPDOq277F%wFm*2>RDqvhC>iFJ)! zC#ZoU_$k|13%|a4rFH|)W>@1mNq+~?8)Vf=93@kHs&e~OVXDY6#eZE|Io*4sH(A=a zQrb9mEM+fw=jYdcK6Cj?d((VJa?f+}p6Bjd`EpNhs;F$nal?9@PnA?mcTabxO6zC) z<0mfOj7&nJ_Y%oXj2!W6AN;NTGeq570}%kpd<0PGBwz^V<8tBH z`qm>Z0apW64rI%saxtnL2zSzA0;}rtxmCY(KqZInB_B0^5-+ zosFx*^IoF}NrtC^q!RM7Q8KiFIRnxG9fOb?pFn3DbMg1tY@Ka9W9ZwXa(C1eHJ;0d z8M1H=k)8##czveL5VQ_O{5iy57JLQ(J}@u01U}SM8^VBNLjh!UxZ6A`+ACh;I>5cm z0M4WV93Rc|nSH!34?xcBHZ{VI3Gf#+UI4zG?AQQzn)V+PgCM|QSEDf=iwc*+@sYF% zkcC-@H4L*3Kq3RNz$lG5a^aPR9{`eQi3}6O4~m@N;uH%L7=Q%;6gUDxTOiUbRwe+{ z4F!s41K;a#vNas+#eQH&f@rYhJpR$u+x2r@|J3!-)yI+vir=iP;7Y{`~21Boms zKD;#)XD|{V#uETBpfXV51U7^$xCZ=Phuxk`*riR-%w=SPMVlc_u^kkw?;Wd>a4|Zj zvK^!cp#+yFbwi{>WMdj4(MXw;?JgA?-EE7bzZ+H4&-Z*$@Oi-|a4hYya_`$A`WK8o z27yIhO51=}5~ zyz2-b_3sBw5`MIUQ`NYZdOXW>+dG}oa>0?VHuT5FMu4+q_?MziQrplZT^Z6p{D&h03 zkH0O0u_e4&{-*Ruhv}nj5PfDcL!2chXpbXztu;Zs6@Ev#b4bHNZg$skGi@$1gv}`$ z1M7IK_xVHEm^CuJKL)1%rLcH8hedu2<|$P|#8hyjZ@K;Wyys8V zf2^MCztsptHwYoS}bK)gu(=sV;?oY60ed5mkW%uEE#}|h_ zKQ!mL<$=s&`XW&LuN@9O0i)5ar~5+IbcMdAEydGDz{< zdweo$(#}_w+fL3G|7qxtLu`-RPUe_23LUf1a6d8x08T2m0C>r|YYrUSUQRPat2FjE zpdkvkQB!P8jH=gJ@SvK}1Q?ezdsN}Za?c+*K&pj^F?a2mW!JN_J)aJKGI+0CK6Ltn zz|8=ZXdGVRBT#X-qq`ZADZar<9t@_dK}F2tW~?tNKv-!ZVfyKA=p(~(a` z?lsHZ{U2Psc@av~xw~xoMSR_FC@BurO#!!Z&8I0Tw)AbCN{U@~YA{O@3JSFrs*MRg zL-cX#7A4SY)tU(@DFrG4^ARXkWHN9tg2Mycm+XwRTmf6Oo=HzZ(k*1Te-Oo*o~LRV zqfyMvW06_b?L%9#{ixi2bfx`xqS2SA_ua3lo8{&#w=D1X-LG@X z&i?tp;>g0tVr(I{;y#>k_9yE4@7D^l&^O<+cw*ti;^~FcD=mi-LSLe`@BWVZ+4`h& zpX}V1*wMk-zcPO@+4j8L_I$GK1-b2om9~=!=PQZ2SMFEun7R1D<(rq^KX|{YR<1oW z?^vu@s94;wuw%vbY@+r|qUsFHMXozL?^%3i;hDwmh3=I-&n4>4CTh+?V|mBGeEnkc zLi3_~!M(D(E3soBQ9Y2+>%;AZXek%wTUAFZ?8bUPiv>ku?Hed%FA?YvP5(2v%~8V^ zPEeG~4I!8FHSG&H@voZi18>k9!XYkG=L^JZjGDmS$zeZ(Zy^1KU_#H+U}-bN;8V$A z2XE>Z-&o+$tg8!LtZ*X$cdPl3;gA8n#$FS?e@Nh~!T$Pw2DQ95FN4yu1!a^`=4Vh^ zx1fBBQ5IxS+P0vCyF83um_fO13(DUCrNL|8qV^A}$`~y67P$>z(K#mDHH!}1l_A|h zEU{-w@CXaTG&Vjagy7~&!{^7*qoO*AUFPq3&wpmULqLiC^kQ?N_q0iG9ycPsMX8BC2~ss~f>+CwOX2wniq z>eLh~OvukBB!Tx4O|JBJdbME0tC7fKn&Sdv0I6>$!Jv>1VIVW#{0s>L{D8x>1#M zIAw=3>2S#o*H?}`Q$6t3GHtnGnYPayg-l&bvaVgOYrpNd-IJ*6yyKATx~BSMTTQCC zWP0a~ozuH#JX1$g_Tua3|8`>5I{!>U*mwJ)EFAb!*q3sY&KTa^{^OSlyXW@sOM^4R9f$^}{&J!lob2U9b>VT$PnYSq1E&`}j^LnKj=N3IAi9SN*OBAWH4Gg{ z_!Pi=d`qPR%R<+T=hMh1k$XX~i>CW;^h3#G=KFf9M7?^Y-FD!y%hLu>3RHn*kEe30}CjmLkzs=<1a-h z92-##mxA<{kf3iulm-|52_$pLo1Q>2D}&g48X|lGfp=MRsz&&r_hxUhrc@V~P4wsE;~-_n40>O{&`{7&DszErg{RaXxWAgLYj9&$4Zj(jR0mx&Ii6-Fd7E-$frIb<}wlt zxr~7f!$JM=L2)iBru>hlC2+xbN@X*=nCN;4nU#5WnfF1}L;P5P#wQkx%yOk5Muf7v zqAD4>zF|cEOa;41-)ts9?I?L6f?u34?$DJ>lk8|pI$C8%>sO98&?oR@c;%fd*RG_T zyXJP^+CAUD_|t`--VP+32kv-e=fUgd>B~1RziWTUlj3%8`~E*aT6!I}^oNc#Ukr^0 z*}D?`6&^v>QI@Y)W*_Iy4dz2`ve&V{vrluRO)Yb;-g@=69dOrm}&w^hxMFXQE{G!R~_m9Rx;}J~T{QisMfrwUP_xmq|sT7Zd zqhd7X_tOGstQf=5xMB*0gK>(;Ogk|;hS94S{S8ElDT2o0+n9eBBSbN!%&%TAgYa~r zT9TsV_b(9jbrlVDihIc-oTh|Le@VVIaD4Tu*}=ca%?4Kqe(xC7?_JX)mS1hM@n^W% zqpJkJ^T!^s?`n;Mx6VpPYE@fU)dtqprXr{*eMI21+E~ONDgxVROHQ3n;`0*P~bts$1Jl{{gaK2oVYJ ZiL4qqj(cb@a;EQWmpHEcTY@ob_uu<^NB95$ literal 0 HcmV?d00001 diff --git a/evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/station_service.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/station_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a555bbf558c8df86a4dd3be0256e45dfa8b79acf GIT binary patch literal 4545 zcmb_fU2Idy6`t$spBu-q6Nel$6EoOsBAlU^gutg**egpVo5){K-W8b7^9bYr| zI$feZAXTcuLj{pGB2-mLpPGHBgw&T-s?fH=bo84=bJNIZEaox<=x-?I`gX*LjHywyEv`J);Kg)i9{%oILUF2qa4)y zImcO^@*?t_=bUF<)FqOgWG|6iAtJfQ9Oi85o_B;jg>%sJN_tYys5wzn=wc?Nh=0BN znRv6HWpaw9iCJ|%lM?4tDlV#eCO0o?mh`Dud?MnVQpNZgQ7!2Cf*$d{K;=_mjmLG*(H1z#<5BNF3!QM~G0T#3x*mGvSt83D2loa)-z* zClw@5qD2y5q{SK$B(E{zHAY$!?op@YGdQgV$CYRSPMg8;Nq*Wk9|*TIzh$6guKa}$ zj7V8KWEh5R9=@%1Agg4iJ!`O$%#$!T^*P5}nyykM>|jn!$<9$cHw~wWBFmqP`)e3n;Ax))>Fi%R&>3|8$r{1xc|&VRAgKj?D1Yr zW(&$Br7ER(@&+8Tpp90Uw4724Ik1QE^NP-RO{Yu%{85bM@j3MeEMS46HAkcUs0R<& zSSi44a#0C8DFz8e|252I9$OwIMJN;_hPh2xV_nv%N+!}rOg2u}CSauih=I-rAKm}q z^Wd&x|9Clgyy!cQth1HCZi9EU96VO^9jmqm9(YTweQUSNt)ek-tQ?FMebK)McZ2-> zrF%=2z)&eLRPE?`Ft@U++SR*upwzXm+S#)vtemKI2X1436ZMcGr9Yo3i~!XO2UCJg?-P3 z;DgbJ(UoX%_n{}D;)j=tmuD)Mua_=gFJ8J)ypb*{*@~i;6t$Sn7xTA^$LTXcvozfg z+zUJtdaHr%Wj{qY0zWqM138msojJwAykQ2!T2&+~il3BcJjnaHgk|WMbeB2oWU5y1G%^Bx_x*CA3Q4snu4Aq8W zuK<9}3Bcx_`Zj1ezlcya2LYIlFn|2g#J0$e8)ucSM>ZV<#q=E@;T9u~>3hh)n}F^E z!dw|$SgS5&bNfYe-{A$w1Ro=qLoZ-OQO&Z z4S)X^H2#nN&d2$hhk|F$HuZP(xoj=^>K^oqN6bBBj%r&JU{u`9n&>Y-@v*zhJ|K6NOyyZM#r=62E+02jb!+ohAXeTn74jP ztheEAs1F^5Ia(UX|Kw_WmOX%p+hBE|cxtA2?c>TdrF2ax&de20&OZ~r{~CyXDfAln zx$X6VAZNG>g~knT^|cH;&W%C_4$m0@j;|R&)(#q`S|wI<3!6iT4HliXmJOl+9ge9K zW&w6VQ;xv5br#48ZV+$|eHf4uatpPi8FpWBL}v&|BWgmLB9TS1>H?GmWJQ#m=RV-) zxUhrn0fq2vqgFSe|!Fmv!(H?4@Xu;fb&mH0FfVc4st(j8RWuF z$WICjW(wmrvnb{^Aq#9a@((oFeC>T~TG;xU9W3-_8^4DW5lX+gOP&XU&w|ry>BqMo z-Kq>6Dh(W}47^_&c>mb|BpuV`z;scVHk^av17#}g$EO}pXAxxbnVe4H$wlagNbq=Bmn>^|lc!|F z_RP%!+~czRIWY}EP@wqCG6?pxPk{WGeC6O=eVgt+SI^q$CV^^0GO8!nUtw=E+T!Y2 z@7yF%ZRkez2ebD@|0`r}cDr4nb(jOyZw6mswb{`X jn{M(P$Gvp$oO8=dxb}aL{eL0{zjlvtGu&4MsiETE#H7R? literal 0 HcmV?d00001 diff --git a/evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/vote_service.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/vote_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..54d71025b3c249f05a70e17793f3294f350c1aa8 GIT binary patch literal 4161 zcmbssOKcR`@%2p4OwY$N#vbr|>~$NP*bV+*uoq&&3W7<%2=8i|0Y|pzZO^prX2;W$ z*F6S|qHv0AMG0&+2wRF|ANF7_2~sW`a>*?@#f>2|>TDvEY!s1`S);6SOI6P_W2`|@ zlqYq)diCC`s#mXGRrO9oLx4c*{>`t|ANmRT8xEYtR*{`iKpqg0P$Dv->k>m5yLV9+ z^zKXUF_yAC&asy~V_xdzNsJsMq9;K_@35=Fh5BY)g0Gl@QQ)d(D7>!F1vR7af4%#2 zeooG3bBfMS%ekCp@EKV*)ch=eQ#I!JY_X72GcxQbP-Q{M8>*J?4~%Pk`Xa9tjY84r z54=Pf;{s!`$Qt@UPMekF46e;o2g;PkDx1BAXyL zSn3r$Q$Eo<#SO8d&!+fA4k$=<1Jx_~ZE8Ra(BN!P2w6dtFIGiqGBJPH7 zrxCydGSQf`$p~i2BWC;&V|jE#ql)0NSY64@&>&3dxQ*v6H8V1m2CL~)MSxQu=9N*Z zE+|u@ivEeA6{eI04Xq|oWd=?TT(W%6piqbGkfy3vpijY0MXHYa3%4y!lGMCvNRk!Z zb)bHS8b+;r0Pd3V{=`oLJAe|)OBJI?^KYFPUC2K1KLL3_j*`49P0$DSw$yGrRVP!P zy{iZru9BL`g$slfVOL=&&8D1t-L*kvMkWZ!!ivi+giH*kxLuP~^b}KF(L+d@N!d@~ z?Nz`#^fm=Yr{J?W!^@cXcG}$ylt}Y_D7KM~;4Zfh|ayoC!DLfjPf^X+} z@a|mB9`kCx-(t0bk{5jTWm_J5gXK{TWnQg@nze#8 zTo*hPm9;#!D9bM|$ZAfW&MB6=083e$H5!%2M$td5=2^v%-Yk?V(%X06dYuLPV5cqv zxJzC{4=nl1@wTS(1u6Sgk*ZzJH)L zcCaw`GJKveIqpuy9cr!LD<_q#%{iE}Mr1_~OY+{d8(!`mu&nPLchu_ato5n(8t!0| z9S)ZE)@+rAXpR2uoV*Y4w9BEUef1)W=yO)rw*pWPIoalE_o*4Kh=kVsq zmRE#cUY>?{t>%?%fTHCVJFd*Bx}79==<`tgU^3umUF+fNbE1{*J&|K1>my z8|6U@if%*?0kAkbb>Kt{hkiqyS3u+W0!1fyiE+{LpF1mFk*h(V$okv}Z!v-pvr`(ykYl+ajZh3SR^Y0fD=OzH)hLC`3{$#6Ee?%^(kypgyJ`6# z;mzgLX~=+c^0DFfEf++8uaeJbS;g|tDYvrfEGSk1twuvL zTE4Y(W2I%qzb>pN zH$#1;(2yA#+6ukD<96*2{3Fo3Ed2P${mgy3?Q!i-F8Oyj5`Ay!TshQuZ)|z26lyg? zt))<>8S4Cev>faB{M?3nkVdWf4PlcyS935{rXnhP&xkI zT5L5|PV%MX5i@zDoaihk+Sa;PyVs9w6t@z?ulD&5h5tqT4Ut!FkhdKp{&0y)m|UX7 zb(mbodiOK#@Cz<S5mF(J$)f+2Q8?nd2IZS-T2Z2jqXLLYm1{x9%E%cnaAO*lNd9)rjY?#y(xE zXyHkv-Sw*8wChdP3lBE!{>XJ1VlLLDx-0MQ0$9Oj2*p}OU@K4b1mO00Y-a~5){!-W z#VqKkP|dp|SQS>@5JKHb?)LCKlNVvDei{J$FC^R7Mpj4Q?w=-)Y(*r1_vK$LK3aS- zVGf-CjQL&1AB5ivo7b+JW7i)t;2&m0+T^5vKaEH_M!41HvqAQ87{G5`UVs#9_Hp?0 zf?t-rJg>knOF)w5wQMnmcu11IUzBsznTRCKs8ly{5P*h&{gPzY*cnMS4605S;kc6Yh}fm6{njb|e%Ju3HvK$+ACa$Ij3>G64S3=k zqpt~c+p)udcWx8to(#RlZhL}ZJRdU~?rnno<4mQ0lKt8qS0o^FjM$Qt)iR*;Q51Fz zfE9#uLhR|c?*SdpI^g`$^~=R9{J>R*cFe12IgY#0l9edMeXC-t4=DbXVR+ZO0BpM% jhI!?3Gwe=)FpYmA;lGpCKa;+%y{DOvnXd>!o6COyQt^Br literal 0 HcmV?d00001 diff --git a/evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/voter_service.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/services/__pycache__/voter_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1649c87e06bf3bf838fc93a288e502d96650d7ae GIT binary patch literal 8702 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 Nr)&8Nk(e&|zW}{>=)3>` literal 0 HcmV?d00001 diff --git a/evoting_refactored (1)/evoting_refactored/evoting/services/admin_service.py b/evoting_refactored (1)/evoting_refactored/evoting/services/admin_service.py new file mode 100644 index 0000000..59af6a7 --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/services/admin_service.py @@ -0,0 +1,53 @@ +""" +Admin account service — handles creating and deactivating admin accounts. +No UI output. +""" + +import datetime + +from config import MIN_PASSWORD_LENGTH, ADMIN_ROLES +from models.admin import Admin +from services.auth_service import hash_password +from services.audit_service import log_action + + +class AdminService: + def __init__(self, store): + self.store = store + + def create(self, data: dict, created_by: str) -> Admin: + if not data["username"]: + raise ValueError("Username cannot be empty.") + for a in self.store.admins.values(): + if a.username == data["username"]: + raise ValueError("Username already exists.") + if len(data["password"]) < MIN_PASSWORD_LENGTH: + raise ValueError(f"Password must be at least {MIN_PASSWORD_LENGTH} characters.") + if data["role"] not in ADMIN_ROLES.values(): + raise ValueError("Invalid role.") + + admin = Admin( + id=self.store.admin_id_counter, + username=data["username"], + password=hash_password(data["password"]), + full_name=data["full_name"], + email=data["email"], + role=data["role"], + created_at=str(datetime.datetime.now()), + is_active=True, + ) + self.store.admins[admin.id] = admin + self.store.admin_id_counter += 1 + log_action(self.store, "CREATE_ADMIN", created_by, f"Created admin: {admin.username} (Role: {admin.role})") + self.store.save() + return admin + + def deactivate(self, admin_id: int, current_admin_id: int, deactivated_by: str): + admin = self.store.admins.get(admin_id) + if not admin: + raise ValueError("Admin not found.") + if admin_id == current_admin_id: + raise ValueError("Cannot deactivate your own account.") + admin.is_active = False + log_action(self.store, "DEACTIVATE_ADMIN", deactivated_by, f"Deactivated admin: {admin.username}") + self.store.save() diff --git a/evoting_refactored (1)/evoting_refactored/evoting/services/audit_service.py b/evoting_refactored (1)/evoting_refactored/evoting/services/audit_service.py new file mode 100644 index 0000000..65e0475 --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/services/audit_service.py @@ -0,0 +1,15 @@ +""" +Audit service — records all significant system actions to the audit log. +""" + +import datetime + + +def log_action(store, action: str, user: str, details: str): + """Append an audit entry to the store's audit log.""" + store.audit_log.append({ + "timestamp": str(datetime.datetime.now()), + "action": action, + "user": user, + "details": details, + }) diff --git a/evoting_refactored (1)/evoting_refactored/evoting/services/auth_service.py b/evoting_refactored (1)/evoting_refactored/evoting/services/auth_service.py new file mode 100644 index 0000000..4897dcd --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/services/auth_service.py @@ -0,0 +1,64 @@ +""" +Authentication service — handles password hashing, login validation, +and the current user session. No UI output; returns results to callers. +""" + +import hashlib + + +def hash_password(password: str) -> str: + """Return the SHA-256 hex digest of a plaintext password.""" + return hashlib.sha256(password.encode()).hexdigest() + + +class AuthService: + """Manages the current user session and login verification.""" + + def __init__(self): + self.current_user = None + self.current_role: str | None = None + + def login_admin(self, username: str, password: str, admins: dict): + """ + Attempt admin login. Returns (success, message, admin_object). + """ + hashed = hash_password(password) + for admin in admins.values(): + if admin.username == username and admin.password == hashed: + if not admin.is_active: + return False, "Account deactivated", None + self.current_user = admin + self.current_role = "admin" + return True, f"Welcome, {admin.full_name}!", admin + return False, "Invalid credentials", None + + def login_voter(self, voter_card: str, password: str, voters: dict): + """ + Attempt voter login. Returns (success, message, voter_object). + """ + hashed = hash_password(password) + for voter in voters.values(): + if voter.voter_card_number == voter_card and voter.password == hashed: + if not voter.is_active: + return False, "Voter account deactivated", None + if not voter.is_verified: + return False, "Registration not yet verified. Contact an admin.", None + self.current_user = voter + self.current_role = "voter" + return True, f"Welcome, {voter.full_name}!", voter + return False, "Invalid voter card number or password", None + + def logout(self): + self.current_user = None + self.current_role = None + + def is_super_admin(self) -> bool: + return ( + self.current_user is not None + and self.current_role == "admin" + and self.current_user.role == "super_admin" + ) + + +# Singleton session used across the application +session = AuthService() diff --git a/evoting_refactored (1)/evoting_refactored/evoting/services/candidate_service.py b/evoting_refactored (1)/evoting_refactored/evoting/services/candidate_service.py new file mode 100644 index 0000000..297a31e --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/services/candidate_service.py @@ -0,0 +1,111 @@ +""" +Candidate service — business logic for candidate CRUD and eligibility validation. +No UI output; raises ValueError or returns results to callers. +""" + +import datetime + +from config import MIN_CANDIDATE_AGE, MAX_CANDIDATE_AGE, REQUIRED_EDUCATION_LEVELS +from models.candidate import Candidate +from services.audit_service import log_action + + +def calculate_age(dob_str: str) -> int: + dob = datetime.datetime.strptime(dob_str, "%Y-%m-%d") + return (datetime.datetime.now() - dob).days // 365 + + +class CandidateService: + def __init__(self, store): + self.store = store + + def validate_eligibility(self, national_id: str, dob_str: str, criminal_record: str): + """Validate all eligibility rules. Raises ValueError on failure.""" + for c in self.store.candidates.values(): + if c.national_id == national_id: + raise ValueError("A candidate with this National ID already exists.") + + age = calculate_age(dob_str) + if age < MIN_CANDIDATE_AGE: + raise ValueError(f"Candidate must be at least {MIN_CANDIDATE_AGE} years old. Current age: {age}") + if age > MAX_CANDIDATE_AGE: + raise ValueError(f"Candidate must not be older than {MAX_CANDIDATE_AGE}. Current age: {age}") + if criminal_record.lower() == "yes": + raise ValueError("Candidates with criminal records are not eligible.") + return age + + def create(self, data: dict, created_by: str) -> Candidate: + """Create and store a new candidate. Returns the created Candidate.""" + age = self.validate_eligibility(data["national_id"], data["date_of_birth"], data["criminal_record"]) + + candidate = Candidate( + id=self.store.candidate_id_counter, + full_name=data["full_name"], + national_id=data["national_id"], + date_of_birth=data["date_of_birth"], + age=age, + gender=data["gender"], + education=data["education"], + party=data["party"], + manifesto=data["manifesto"], + address=data["address"], + phone=data["phone"], + email=data["email"], + has_criminal_record=False, + years_experience=data.get("years_experience", 0), + is_active=True, + is_approved=True, + created_at=str(datetime.datetime.now()), + created_by=created_by, + ) + self.store.candidates[candidate.id] = candidate + self.store.candidate_id_counter += 1 + log_action(self.store, "CREATE_CANDIDATE", created_by, f"Created candidate: {candidate.full_name} (ID: {candidate.id})") + self.store.save() + return candidate + + def update(self, candidate_id: int, updates: dict, updated_by: str) -> Candidate: + """Apply non-empty updates to a candidate. Returns updated Candidate.""" + candidate = self.store.candidates.get(candidate_id) + if not candidate: + raise ValueError("Candidate not found.") + + for field, value in updates.items(): + if value: + setattr(candidate, field, value) + + log_action(self.store, "UPDATE_CANDIDATE", updated_by, f"Updated candidate: {candidate.full_name} (ID: {candidate_id})") + self.store.save() + return candidate + + def deactivate(self, candidate_id: int, deleted_by: str): + """Soft-delete a candidate by deactivating them.""" + candidate = self.store.candidates.get(candidate_id) + if not candidate: + raise ValueError("Candidate not found.") + + for poll in self.store.polls.values(): + if poll.status == "open": + for pos in poll.positions: + if candidate_id in pos.candidate_ids: + raise ValueError(f"Cannot delete — candidate is in active poll: {poll.title}") + + candidate.is_active = False + log_action(self.store, "DELETE_CANDIDATE", deleted_by, f"Deactivated candidate: {candidate.full_name} (ID: {candidate_id})") + self.store.save() + + def search(self, field: str, term) -> list: + """Search candidates by field. Returns a list of matching Candidates.""" + results = [] + for c in self.store.candidates.values(): + if field == "name" and term.lower() in c.full_name.lower(): + results.append(c) + elif field == "party" and term.lower() in c.party.lower(): + results.append(c) + elif field == "education" and c.education == term: + results.append(c) + elif field == "age_range": + min_age, max_age = term + if min_age <= c.age <= max_age: + results.append(c) + return results diff --git a/evoting_refactored (1)/evoting_refactored/evoting/services/poll_service.py b/evoting_refactored (1)/evoting_refactored/evoting/services/poll_service.py new file mode 100644 index 0000000..f5ba740 --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/services/poll_service.py @@ -0,0 +1,179 @@ +""" +Poll and Position service — business logic for creating, updating, +opening/closing polls and managing positions. No UI output. +""" + +import datetime + +from config import MIN_CANDIDATE_AGE +from models.poll import Poll, PollPosition, Position +from services.audit_service import log_action + + +class PositionService: + def __init__(self, store): + self.store = store + + def create(self, data: dict, created_by: str) -> Position: + if not data["title"]: + raise ValueError("Title cannot be empty.") + if data["level"].lower() not in ["national", "regional", "local"]: + raise ValueError("Invalid level. Choose: National, Regional, or Local.") + if data["max_winners"] <= 0: + raise ValueError("Must have at least 1 winner seat.") + + position = Position( + id=self.store.position_id_counter, + title=data["title"], + description=data.get("description", ""), + level=data["level"].capitalize(), + max_winners=data["max_winners"], + min_candidate_age=data.get("min_candidate_age", MIN_CANDIDATE_AGE), + is_active=True, + created_at=str(datetime.datetime.now()), + created_by=created_by, + ) + self.store.positions[position.id] = position + self.store.position_id_counter += 1 + log_action(self.store, "CREATE_POSITION", created_by, f"Created position: {position.title} (ID: {position.id})") + self.store.save() + return position + + def update(self, position_id: int, updates: dict, updated_by: str) -> Position: + position = self.store.positions.get(position_id) + if not position: + raise ValueError("Position not found.") + for field, value in updates.items(): + if value: + setattr(position, field, value) + log_action(self.store, "UPDATE_POSITION", updated_by, f"Updated position: {position.title}") + self.store.save() + return position + + def deactivate(self, position_id: int, deactivated_by: str): + position = self.store.positions.get(position_id) + if not position: + raise ValueError("Position not found.") + for poll in self.store.polls.values(): + for pp in poll.positions: + if pp.position_id == position_id and poll.status == "open": + raise ValueError(f"Cannot delete — in active poll: {poll.title}") + position.is_active = False + log_action(self.store, "DELETE_POSITION", deactivated_by, f"Deactivated position: {position.title}") + self.store.save() + + +class PollService: + def __init__(self, store): + self.store = store + + def create(self, data: dict, created_by: str) -> Poll: + if not data["title"]: + raise ValueError("Title cannot be empty.") + try: + sd = datetime.datetime.strptime(data["start_date"], "%Y-%m-%d") + ed = datetime.datetime.strptime(data["end_date"], "%Y-%m-%d") + except ValueError: + raise ValueError("Invalid date format. Use YYYY-MM-DD.") + if ed <= sd: + raise ValueError("End date must be after start date.") + + poll_positions = [] + for pid in data["position_ids"]: + pos = self.store.positions.get(pid) + if not pos or not pos.is_active: + continue + poll_positions.append(PollPosition( + position_id=pid, + position_title=pos.title, + candidate_ids=[], + max_winners=pos.max_winners, + )) + if not poll_positions: + raise ValueError("No valid positions selected.") + + poll = Poll( + id=self.store.poll_id_counter, + title=data["title"], + description=data.get("description", ""), + election_type=data["election_type"], + start_date=data["start_date"], + end_date=data["end_date"], + positions=poll_positions, + station_ids=data["station_ids"], + status="draft", + total_votes_cast=0, + created_at=str(datetime.datetime.now()), + created_by=created_by, + ) + self.store.polls[poll.id] = poll + self.store.poll_id_counter += 1 + log_action(self.store, "CREATE_POLL", created_by, f"Created poll: {poll.title} (ID: {poll.id})") + self.store.save() + return poll + + def update(self, poll_id: int, updates: dict, updated_by: str) -> Poll: + poll = self.store.polls.get(poll_id) + if not poll: + raise ValueError("Poll not found.") + if poll.status == "open": + raise ValueError("Cannot update an open poll. Close it first.") + if poll.status == "closed" and poll.total_votes_cast > 0: + raise ValueError("Cannot update a poll with recorded votes.") + for field, value in updates.items(): + if value: + setattr(poll, field, value) + log_action(self.store, "UPDATE_POLL", updated_by, f"Updated poll: {poll.title}") + self.store.save() + return poll + + def delete(self, poll_id: int, deleted_by: str): + poll = self.store.polls.get(poll_id) + if not poll: + raise ValueError("Poll not found.") + if poll.status == "open": + raise ValueError("Cannot delete an open poll. Close it first.") + title = poll.title + del self.store.polls[poll_id] + self.store.votes = [v for v in self.store.votes if v.poll_id != poll_id] + log_action(self.store, "DELETE_POLL", deleted_by, f"Deleted poll: {title}") + self.store.save() + + def open(self, poll_id: int, opened_by: str) -> Poll: + poll = self.store.polls.get(poll_id) + if not poll: + raise ValueError("Poll not found.") + if not any(pos.candidate_ids for pos in poll.positions): + raise ValueError("Cannot open — no candidates assigned.") + poll.status = "open" + log_action(self.store, "OPEN_POLL", opened_by, f"Opened poll: {poll.title}") + self.store.save() + return poll + + def close(self, poll_id: int, closed_by: str) -> Poll: + poll = self.store.polls.get(poll_id) + if not poll: + raise ValueError("Poll not found.") + poll.status = "closed" + log_action(self.store, "CLOSE_POLL", closed_by, f"Closed poll: {poll.title}") + self.store.save() + return poll + + def reopen(self, poll_id: int, reopened_by: str) -> Poll: + poll = self.store.polls.get(poll_id) + if not poll: + raise ValueError("Poll not found.") + poll.status = "open" + log_action(self.store, "REOPEN_POLL", reopened_by, f"Reopened poll: {poll.title}") + self.store.save() + return poll + + def assign_candidates(self, poll_id: int, position_index: int, candidate_ids: list, assigned_by: str): + poll = self.store.polls.get(poll_id) + if not poll: + raise ValueError("Poll not found.") + if poll.status == "open": + raise ValueError("Cannot modify candidates of an open poll.") + poll.positions[position_index].candidate_ids = candidate_ids + log_action(self.store, "ASSIGN_CANDIDATES", assigned_by, f"Updated candidates for poll: {poll.title}") + self.store.save() diff --git a/evoting_refactored (1)/evoting_refactored/evoting/services/results_service.py b/evoting_refactored (1)/evoting_refactored/evoting/services/results_service.py new file mode 100644 index 0000000..0d55160 --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/services/results_service.py @@ -0,0 +1,125 @@ +""" +Results service — computes vote tallies, turnout statistics, +station-wise breakdowns, and demographic summaries. No UI output. +""" + + +class ResultsService: + def __init__(self, store): + self.store = store + + def tally_position(self, poll_id: int, position_id: int) -> dict: + """ + Count votes for a single position in a poll. + Returns { candidate_id: count, '_abstain': count, '_total': count }. + """ + tally = {} + abstain_count = 0 + total = 0 + + for vote in self.store.votes: + if vote.poll_id == poll_id and vote.position_id == position_id: + total += 1 + if vote.abstained: + abstain_count += 1 + else: + tally[vote.candidate_id] = tally.get(vote.candidate_id, 0) + 1 + + return {"counts": tally, "abstain": abstain_count, "total": total} + + def poll_turnout(self, poll_id: int) -> dict: + """Return turnout info: eligible voter count, votes cast, percentage.""" + poll = self.store.polls.get(poll_id) + if not poll: + return {"eligible": 0, "voted": 0, "percentage": 0.0} + + eligible = sum( + 1 for v in self.store.voters.values() + if v.is_verified and v.is_active and v.station_id in poll.station_ids + ) + voted = poll.total_votes_cast + percentage = (voted / eligible * 100) if eligible > 0 else 0.0 + return {"eligible": eligible, "voted": voted, "percentage": percentage} + + def station_turnout(self, poll_id: int, station_id: int) -> dict: + """Return turnout for a specific station within a poll.""" + station_votes = [ + v for v in self.store.votes + if v.poll_id == poll_id and v.station_id == station_id + ] + voters_who_voted = len(set(v.voter_id for v in station_votes)) + registered_at_station = sum( + 1 for v in self.store.voters.values() + if v.station_id == station_id and v.is_verified and v.is_active + ) + percentage = (voters_who_voted / registered_at_station * 100) if registered_at_station > 0 else 0.0 + return { + "registered": registered_at_station, + "voted": voters_who_voted, + "percentage": percentage, + "votes": station_votes, + } + + def system_overview(self) -> dict: + """Return high-level counts for the statistics dashboard.""" + s = self.store + return { + "total_candidates": len(s.candidates), + "active_candidates": sum(1 for c in s.candidates.values() if c.is_active), + "total_voters": len(s.voters), + "verified_voters": sum(1 for v in s.voters.values() if v.is_verified), + "active_voters": sum(1 for v in s.voters.values() if v.is_active), + "total_stations": len(s.voting_stations), + "active_stations": sum(1 for st in s.voting_stations.values() if st.is_active), + "total_polls": len(s.polls), + "open_polls": sum(1 for p in s.polls.values() if p.status == "open"), + "closed_polls": sum(1 for p in s.polls.values() if p.status == "closed"), + "draft_polls": sum(1 for p in s.polls.values() if p.status == "draft"), + "total_votes": len(s.votes), + } + + def voter_demographics(self) -> dict: + """Return gender counts and age group distribution.""" + gender_counts = {} + age_groups = {"18-25": 0, "26-35": 0, "36-45": 0, "46-55": 0, "56-65": 0, "65+": 0} + + for v in self.store.voters.values(): + g = v.gender or "?" + gender_counts[g] = gender_counts.get(g, 0) + 1 + age = v.age or 0 + if age <= 25: age_groups["18-25"] += 1 + elif age <= 35: age_groups["26-35"] += 1 + elif age <= 45: age_groups["36-45"] += 1 + elif age <= 55: age_groups["46-55"] += 1 + elif age <= 65: age_groups["56-65"] += 1 + else: age_groups["65+"] += 1 + + return {"gender": gender_counts, "age_groups": age_groups} + + def station_load(self) -> list: + """Return load info for every station.""" + result = [] + for sid, station in self.store.voting_stations.items(): + vc = sum(1 for v in self.store.voters.values() if v.station_id == sid) + load_pct = (vc / station.capacity * 100) if station.capacity > 0 else 0 + result.append({ + "station": station, + "voter_count": vc, + "load_pct": load_pct, + "overloaded": load_pct > 100, + }) + return result + + def party_distribution(self) -> dict: + counts = {} + for c in self.store.candidates.values(): + if c.is_active: + counts[c.party] = counts.get(c.party, 0) + 1 + return counts + + def education_distribution(self) -> dict: + counts = {} + for c in self.store.candidates.values(): + if c.is_active: + counts[c.education] = counts.get(c.education, 0) + 1 + return counts diff --git a/evoting_refactored (1)/evoting_refactored/evoting/services/station_service.py b/evoting_refactored (1)/evoting_refactored/evoting/services/station_service.py new file mode 100644 index 0000000..8a30e4b --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/services/station_service.py @@ -0,0 +1,65 @@ +""" +Station service — business logic for voting station CRUD. +No UI output. +""" + +import datetime + +from models.station import VotingStation +from services.audit_service import log_action + + +class StationService: + def __init__(self, store): + self.store = store + + def create(self, data: dict, created_by: str) -> VotingStation: + if not data["name"]: + raise ValueError("Station name cannot be empty.") + if not data["location"]: + raise ValueError("Location cannot be empty.") + if data["capacity"] <= 0: + raise ValueError("Capacity must be positive.") + + station = VotingStation( + id=self.store.station_id_counter, + name=data["name"], + location=data["location"], + region=data["region"], + capacity=data["capacity"], + registered_voters=0, + supervisor=data.get("supervisor", ""), + contact=data.get("contact", ""), + opening_time=data.get("opening_time", ""), + closing_time=data.get("closing_time", ""), + is_active=True, + created_at=str(datetime.datetime.now()), + created_by=created_by, + ) + self.store.voting_stations[station.id] = station + self.store.station_id_counter += 1 + log_action(self.store, "CREATE_STATION", created_by, f"Created station: {station.name} (ID: {station.id})") + self.store.save() + return station + + def update(self, station_id: int, updates: dict, updated_by: str) -> VotingStation: + station = self.store.voting_stations.get(station_id) + if not station: + raise ValueError("Station not found.") + for field, value in updates.items(): + if value: + setattr(station, field, value) + log_action(self.store, "UPDATE_STATION", updated_by, f"Updated station: {station.name} (ID: {station_id})") + self.store.save() + return station + + def deactivate(self, station_id: int, deactivated_by: str): + station = self.store.voting_stations.get(station_id) + if not station: + raise ValueError("Station not found.") + station.is_active = False + log_action(self.store, "DELETE_STATION", deactivated_by, f"Deactivated station: {station.name}") + self.store.save() + + def voter_count(self, station_id: int) -> int: + return sum(1 for v in self.store.voters.values() if v.station_id == station_id) diff --git a/evoting_refactored (1)/evoting_refactored/evoting/services/vote_service.py b/evoting_refactored (1)/evoting_refactored/evoting/services/vote_service.py new file mode 100644 index 0000000..9724e3c --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/services/vote_service.py @@ -0,0 +1,74 @@ +""" +Vote service — handles ballot casting with duplicate prevention. +No UI output. +""" + +import datetime +import hashlib + +from models.vote import Vote +from services.audit_service import log_action + + +class VoteService: + def __init__(self, store): + self.store = store + + def get_available_polls(self, voter) -> dict: + """Return polls the voter can still vote in.""" + available = {} + for pid, poll in self.store.polls.items(): + if ( + poll.status == "open" + and pid not in voter.has_voted_in + and voter.station_id in poll.station_ids + ): + available[pid] = poll + return available + + def cast_ballot(self, voter, poll_id: int, selections: list): + """ + Record a voter's ballot selections. + + selections: list of dicts with keys: + position_id, position_title, candidate_id (or None), abstained + """ + poll = self.store.polls.get(poll_id) + if not poll or poll.status != "open": + raise ValueError("This poll is not open for voting.") + if poll_id in voter.has_voted_in: + raise ValueError("You have already voted in this poll.") + + timestamp = str(datetime.datetime.now()) + vote_hash = hashlib.sha256( + f"{voter.id}{poll_id}{timestamp}".encode() + ).hexdigest()[:16] + + for sel in selections: + self.store.votes.append(Vote( + vote_id=vote_hash + str(sel["position_id"]), + poll_id=poll_id, + position_id=sel["position_id"], + candidate_id=sel.get("candidate_id"), + voter_id=voter.id, + station_id=voter.station_id, + timestamp=timestamp, + abstained=sel.get("abstained", False), + )) + + voter.has_voted_in.append(poll_id) + # Sync the change back to the store + self.store.voters[voter.id].has_voted_in.append(poll_id) + poll.total_votes_cast += 1 + + log_action( + self.store, + "CAST_VOTE", + voter.voter_card_number, + f"Voted in poll: {poll.title} (Hash: {vote_hash})", + ) + self.store.save() + return vote_hash + + def get_voter_votes(self, voter_id: int, poll_id: int) -> list: + return [v for v in self.store.votes if v.voter_id == voter_id and v.poll_id == poll_id] diff --git a/evoting_refactored (1)/evoting_refactored/evoting/services/voter_service.py b/evoting_refactored (1)/evoting_refactored/evoting/services/voter_service.py new file mode 100644 index 0000000..d6dbd1f --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/services/voter_service.py @@ -0,0 +1,139 @@ +""" +Voter service — business logic for voter registration, verification, +deactivation, and search. No UI output. +""" + +import datetime +import random +import string + +from config import MIN_VOTER_AGE, MIN_PASSWORD_LENGTH +from models.voter import Voter +from services.auth_service import hash_password +from services.audit_service import log_action + + +def generate_voter_card_number() -> str: + return "".join(random.choices(string.ascii_uppercase + string.digits, k=12)) + + +class VoterService: + def __init__(self, store): + self.store = store + + def register(self, data: dict) -> Voter: + """Validate and register a new voter. Returns created Voter.""" + if not data["full_name"]: + raise ValueError("Name cannot be empty.") + if not data["national_id"]: + raise ValueError("National ID cannot be empty.") + + for v in self.store.voters.values(): + if v.national_id == data["national_id"]: + raise ValueError("A voter with this National ID already exists.") + + try: + dob = datetime.datetime.strptime(data["date_of_birth"], "%Y-%m-%d") + except ValueError: + raise ValueError("Invalid date format. Use YYYY-MM-DD.") + + age = (datetime.datetime.now() - dob).days // 365 + if age < MIN_VOTER_AGE: + raise ValueError(f"You must be at least {MIN_VOTER_AGE} years old to register.") + + if data["gender"] not in ["M", "F", "OTHER"]: + raise ValueError("Invalid gender selection.") + + if len(data["password"]) < MIN_PASSWORD_LENGTH: + raise ValueError(f"Password must be at least {MIN_PASSWORD_LENGTH} characters.") + + if data["password"] != data["confirm_password"]: + raise ValueError("Passwords do not match.") + + station_id = data["station_id"] + station = self.store.voting_stations.get(station_id) + if not station or not station.is_active: + raise ValueError("Invalid station selection.") + + voter_card = generate_voter_card_number() + voter = Voter( + id=self.store.voter_id_counter, + full_name=data["full_name"], + national_id=data["national_id"], + date_of_birth=data["date_of_birth"], + age=age, + gender=data["gender"], + address=data.get("address", ""), + phone=data.get("phone", ""), + email=data.get("email", ""), + password=hash_password(data["password"]), + voter_card_number=voter_card, + station_id=station_id, + is_verified=False, + is_active=True, + has_voted_in=[], + registered_at=str(datetime.datetime.now()), + role="voter", + ) + self.store.voters[voter.id] = voter + self.store.voter_id_counter += 1 + log_action(self.store, "REGISTER", voter.full_name, f"New voter registered with card: {voter_card}") + self.store.save() + return voter + + def verify(self, voter_id: int, verified_by: str): + voter = self.store.voters.get(voter_id) + if not voter: + raise ValueError("Voter not found.") + if voter.is_verified: + raise ValueError("Voter is already verified.") + voter.is_verified = True + log_action(self.store, "VERIFY_VOTER", verified_by, f"Verified voter: {voter.full_name}") + self.store.save() + + def verify_all_pending(self, verified_by: str) -> int: + count = 0 + for voter in self.store.voters.values(): + if not voter.is_verified: + voter.is_verified = True + count += 1 + log_action(self.store, "VERIFY_ALL_VOTERS", verified_by, f"Verified {count} voters") + self.store.save() + return count + + def deactivate(self, voter_id: int, deactivated_by: str): + voter = self.store.voters.get(voter_id) + if not voter: + raise ValueError("Voter not found.") + if not voter.is_active: + raise ValueError("Voter is already deactivated.") + voter.is_active = False + log_action(self.store, "DEACTIVATE_VOTER", deactivated_by, f"Deactivated voter: {voter.full_name}") + self.store.save() + + def change_password(self, voter_id: int, old_password: str, new_password: str, confirm_password: str): + voter = self.store.voters.get(voter_id) + if not voter: + raise ValueError("Voter not found.") + if hash_password(old_password) != voter.password: + raise ValueError("Incorrect current password.") + if len(new_password) < MIN_PASSWORD_LENGTH: + raise ValueError(f"Password must be at least {MIN_PASSWORD_LENGTH} characters.") + if new_password != confirm_password: + raise ValueError("Passwords do not match.") + voter.password = hash_password(new_password) + log_action(self.store, "CHANGE_PASSWORD", voter.voter_card_number, "Password changed") + self.store.save() + + def search(self, field: str, term) -> list: + results = [] + for v in self.store.voters.values(): + if field == "name" and term.lower() in v.full_name.lower(): + results.append(v) + elif field == "card" and term == v.voter_card_number: + results.append(v) + elif field == "national_id" and term == v.national_id: + results.append(v) + elif field == "station" and v.station_id == term: + results.append(v) + return results diff --git a/evoting_refactored (1)/evoting_refactored/evoting/ui/__init__.py b/evoting_refactored (1)/evoting_refactored/evoting/ui/__init__.py new file mode 100644 index 0000000..67e55b5 --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/ui/__init__.py @@ -0,0 +1 @@ +# ui package diff --git a/evoting_refactored (1)/evoting_refactored/evoting/ui/__pycache__/__init__.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/ui/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..acee355c38e75e96db865ce6d5f0e714287a6026 GIT binary patch literal 192 zcmey&%ge<81nPS>XYvB+#~=<2FhUuhS%8eG4CxG-jD9N_ikN`B&mgH=@yS*(p~b01 z#WAHoGR8kI)ukx2EH%a@wK%&ZzaS>HEWac(FFn2}H7zl@B)=#%MM1+*6N@lRD5f+s sCO$qhFS8^*Uaz3?7Kcr4eoARhs$CH)&_0lRib0Hz%#4hTMa)1J0FkRTQ2+n{ literal 0 HcmV?d00001 diff --git a/evoting_refactored (1)/evoting_refactored/evoting/ui/__pycache__/colors.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/ui/__pycache__/colors.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26a092ef9f200fa1204889014e4506791d1417e8 GIT binary patch literal 1759 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 WG0ey}BEwvw?Dc<%!;IUgqy7V{orOmL literal 0 HcmV?d00001 diff --git a/evoting_refactored (1)/evoting_refactored/evoting/ui/colors.py b/evoting_refactored (1)/evoting_refactored/evoting/ui/colors.py new file mode 100644 index 0000000..6e00d1d --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/ui/colors.py @@ -0,0 +1,61 @@ +""" +ANSI color codes and theme constants for terminal output. +""" + +import os +import sys + +if sys.platform == "win32": + os.system("") + +# Base styles +RESET = "\033[0m" +BOLD = "\033[1m" +DIM = "\033[2m" +ITALIC = "\033[3m" +UNDERLINE = "\033[4m" + +# Foreground colors +BLACK = "\033[30m" +RED = "\033[31m" +GREEN = "\033[32m" +YELLOW = "\033[33m" +BLUE = "\033[34m" +MAGENTA = "\033[35m" +CYAN = "\033[36m" +WHITE = "\033[37m" +GRAY = "\033[90m" +BRIGHT_RED = "\033[91m" +BRIGHT_GREEN = "\033[92m" +BRIGHT_YELLOW = "\033[93m" +BRIGHT_BLUE = "\033[94m" +BRIGHT_MAGENTA = "\033[95m" +BRIGHT_CYAN = "\033[96m" +BRIGHT_WHITE = "\033[97m" + +# Background colors +BG_RED = "\033[41m" +BG_GREEN = "\033[42m" +BG_YELLOW = "\033[43m" +BG_BLUE = "\033[44m" +BG_MAGENTA = "\033[45m" +BG_CYAN = "\033[46m" +BG_WHITE = "\033[47m" +BG_GRAY = "\033[100m" + +# Theme colors per context +THEME_LOGIN = BRIGHT_CYAN +THEME_ADMIN = BRIGHT_GREEN +THEME_ADMIN_ACCENT = YELLOW +THEME_VOTER = BRIGHT_BLUE +THEME_VOTER_ACCENT = MAGENTA + + +def colored(text, color): + """Wrap text in a color code.""" + return f"{color}{text}{RESET}" + + +def status_badge(text, is_good): + """Return a green or red colored status label.""" + return f"{GREEN}{text}{RESET}" if is_good else f"{RED}{text}{RESET}" diff --git a/evoting_refactored (1)/evoting_refactored/evoting/ui/components.py b/evoting_refactored (1)/evoting_refactored/evoting/ui/components.py new file mode 100644 index 0000000..140e1f2 --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/ui/components.py @@ -0,0 +1,65 @@ +""" +Reusable UI display components: headers, tables, menus, messages. +""" + +from ui.colors import ( + RESET, BOLD, DIM, GRAY, + RED, GREEN, YELLOW, + THEME_ADMIN, +) + + +def header(title, theme_color): + width = 58 + top = f" {theme_color}{'═' * width}{RESET}" + mid = f" {theme_color}{BOLD} {title.center(width - 2)} {RESET}{theme_color} {RESET}" + bot = f" {theme_color}{'═' * width}{RESET}" + print(top) + print(mid) + print(bot) + + +def subheader(title, theme_color): + print(f"\n {theme_color}{BOLD}▸ {title}{RESET}") + + +def table_header(format_str, theme_color): + print(f" {theme_color}{BOLD}{format_str}{RESET}") + + +def table_divider(width, theme_color): + print(f" {theme_color}{'─' * width}{RESET}") + + +def menu_item(number, text, color): + print(f" {color}{BOLD}{number:>3}.{RESET} {text}") + + +def error(msg): + print(f" {RED}{BOLD} {msg}{RESET}") + + +def success(msg): + print(f" {GREEN}{BOLD} {msg}{RESET}") + + +def warning(msg): + print(f" {YELLOW}{BOLD} {msg}{RESET}") + + +def info(msg): + print(f" {GRAY}{msg}{RESET}") +def status_badge(text, status="info"): + """ + Displays a small label showing the status of something + like ACTIVE, CLOSED, VERIFIED, etc. + """ + + if status == "success": + return f"[✓ {text}]" + elif status == "error": + return f"[✗ {text}]" + elif status == "warning": + return f"[! {text}]" + else: + return f"[{text}]" \ No newline at end of file diff --git a/evoting_refactored (1)/evoting_refactored/evoting/ui/prompts.py b/evoting_refactored (1)/evoting_refactored/evoting/ui/prompts.py new file mode 100644 index 0000000..432b803 --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/ui/prompts.py @@ -0,0 +1,72 @@ +""" +User input helpers: prompt, masked password input, pause, clear screen. +""" + +import os +import sys + +from ui.colors import RESET, DIM, BRIGHT_WHITE, YELLOW + + +def clear_screen(): + os.system("cls" if os.name == "nt" else "clear") + + +def pause(): + input(f"\n {DIM}Press Enter to continue...{RESET}") + + +def prompt(text): + return input(f" {BRIGHT_WHITE}{text}{RESET}").strip() + + +def masked_input(prompt_text="Password: "): + """Read a password from stdin, masking each character with '*'.""" + print(f" {BRIGHT_WHITE}{prompt_text}{RESET}", end="", flush=True) + password = "" + + if sys.platform == "win32": + import msvcrt + while True: + ch = msvcrt.getwch() + if ch in ("\r", "\n"): + print() + break + elif ch in ("\x08", "\b"): + if password: + password = password[:-1] + sys.stdout.write("\b \b") + sys.stdout.flush() + elif ch == "\x03": + raise KeyboardInterrupt + else: + password += ch + sys.stdout.write(f"{YELLOW}*{RESET}") + sys.stdout.flush() + else: + import tty + import termios + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(fd) + while True: + ch = sys.stdin.read(1) + if ch in ("\r", "\n"): + print() + break + elif ch in ("\x7f", "\x08"): + if password: + password = password[:-1] + sys.stdout.write("\b \b") + sys.stdout.flush() + elif ch == "\x03": + raise KeyboardInterrupt + else: + password += ch + sys.stdout.write(f"{YELLOW}*{RESET}") + sys.stdout.flush() + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + + return password diff --git a/evoting_refactored (1)/evoting_refactored/evoting/views/__init__.py b/evoting_refactored (1)/evoting_refactored/evoting/views/__init__.py new file mode 100644 index 0000000..18ad452 --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/views/__init__.py @@ -0,0 +1 @@ +# views package diff --git a/evoting_refactored (1)/evoting_refactored/evoting/views/__pycache__/__init__.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/views/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f7276ac25bdbac3af12c67d1d71fa4b8148a6644 GIT binary patch literal 195 zcmey&%ge<81nPS>X9@u6#~=<2FhUuhS%8eG4CxG-jD9N_ikN`B&mgH=Ny%0*p~b01 z#WAHoGR8kI)ukx2EH%a@wK%&ZzaS>HEWac(FFn2}H7zl@B)=#%MM1+*6N@lRD5fkk vwY)ecK0Y%qvm`!Vub}c4hfQvNN@-52T@fqLN|2L^L5z>gjEsy$%s>_ZQ?oXw literal 0 HcmV?d00001 diff --git a/evoting_refactored (1)/evoting_refactored/evoting/views/__pycache__/admin_view.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/views/__pycache__/admin_view.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ec0b3ef525e28f63008d4c77faed3e2605cd5e6d GIT binary patch literal 86061 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 QZ){ggod353Yc}!!0u!SMf&c&j literal 0 HcmV?d00001 diff --git a/evoting_refactored (1)/evoting_refactored/evoting/views/__pycache__/voter_view.cpython-313.pyc b/evoting_refactored (1)/evoting_refactored/evoting/views/__pycache__/voter_view.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..45d1fc97901302d8b0ccc5032ea3ae62fd94f940 GIT binary patch literal 20381 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 len(REQUIRED_EDUCATION_LEVELS): + error("Invalid choice.") + pause() + return + education = REQUIRED_EDUCATION_LEVELS[edu_choice - 1] + except ValueError: + error("Invalid input.") + pause() + return + + party = prompt("Political Party/Affiliation: ") + manifesto = prompt("Brief Manifesto/Bio: ") + address = prompt("Address: ") + phone = prompt("Phone: ") + email = prompt("Email: ") + criminal_record = prompt("Has Criminal Record? (yes/no): ") + years_exp_str = prompt("Years of Public Service/Political Experience: ") + try: + years_experience = int(years_exp_str) + except ValueError: + years_experience = 0 + + try: + candidate = self.candidate_svc.create( + data={ + "full_name": full_name, "national_id": national_id, + "date_of_birth": dob_str, "gender": gender, + "education": education, "party": party, + "manifesto": manifesto, "address": address, + "phone": phone, "email": email, + "criminal_record": criminal_record, + "years_experience": years_experience, + }, + created_by=self.session.current_user.username, + ) + print() + success(f"Candidate '{candidate.full_name}' created successfully! ID: {candidate.id}") + except ValueError as e: + error(str(e)) + pause() + + def view_all_candidates(self): + clear_screen() + header("ALL CANDIDATES", THEME_ADMIN) + if not self.store.candidates: + print() + info("No candidates found.") + pause() + return + print() + table_header(f"{'ID':<5} {'Name':<25} {'Party':<20} {'Age':<5} {'Education':<20} {'Status':<10}", THEME_ADMIN) + table_divider(85, THEME_ADMIN) + for c in self.store.candidates.values(): + badge = status_badge("Active", True) if c.is_active else status_badge("Inactive", False) + print(f" {c.id:<5} {c.full_name:<25} {c.party:<20} {c.age:<5} {c.education:<20} {badge}") + print(f"\n {DIM}Total Candidates: {len(self.store.candidates)}{RESET}") + pause() + + def update_candidate(self): + clear_screen() + header("UPDATE CANDIDATE", THEME_ADMIN) + if not self.store.candidates: + print() + info("No candidates found.") + pause() + return + print() + for c in self.store.candidates.values(): + print(f" {THEME_ADMIN}{c.id}.{RESET} {c.full_name} {DIM}({c.party}){RESET}") + try: + cid = int(prompt("\nEnter Candidate ID to update: ")) + except ValueError: + error("Invalid input.") + pause() + return + c = self.store.candidates.get(cid) + if not c: + error("Candidate not found.") + pause() + return + print(f"\n {BOLD}Updating: {c.full_name}{RESET}") + info("Press Enter to keep current value\n") + updates = { + "full_name": prompt(f"Full Name [{c.full_name}]: "), + "party": prompt(f"Party [{c.party}]: "), + "manifesto": prompt(f"Manifesto [{c.manifesto[:50]}...]: "), + "phone": prompt(f"Phone [{c.phone}]: "), + "email": prompt(f"Email [{c.email}]: "), + "address": prompt(f"Address [{c.address}]: "), + } + exp_input = prompt(f"Years Experience [{c.years_experience}]: ") + if exp_input: + try: + updates["years_experience"] = int(exp_input) + except ValueError: + warning("Invalid number, keeping old value.") + try: + self.candidate_svc.update(cid, updates, self.session.current_user.username) + print() + success(f"Candidate '{self.store.candidates[cid].full_name}' updated successfully!") + except ValueError as e: + error(str(e)) + pause() + + def delete_candidate(self): + clear_screen() + header("DELETE CANDIDATE", THEME_ADMIN) + if not self.store.candidates: + print() + info("No candidates found.") + pause() + return + print() + for c in self.store.candidates.values(): + badge = status_badge("Active", True) if c.is_active else status_badge("Inactive", False) + print(f" {THEME_ADMIN}{c.id}.{RESET} {c.full_name} {DIM}({c.party}){RESET} {badge}") + try: + cid = int(prompt("\nEnter Candidate ID to delete: ")) + except ValueError: + error("Invalid input.") + pause() + return + candidate = self.store.candidates.get(cid) + if not candidate: + error("Candidate not found.") + pause() + return + if prompt(f"Are you sure you want to delete '{candidate.full_name}'? (yes/no): ").lower() == "yes": + try: + self.candidate_svc.deactivate(cid, self.session.current_user.username) + print() + success(f"Candidate '{candidate.full_name}' has been deactivated.") + except ValueError as e: + error(str(e)) + else: + info("Deletion cancelled.") + pause() + + def search_candidates(self): + clear_screen() + header("SEARCH CANDIDATES", THEME_ADMIN) + subheader("Search by", THEME_ADMIN_ACCENT) + menu_item(1, "Name", THEME_ADMIN) + menu_item(2, "Party", THEME_ADMIN) + menu_item(3, "Education Level", THEME_ADMIN) + menu_item(4, "Age Range", THEME_ADMIN) + choice = prompt("\nChoice: ") + + try: + if choice == "1": + term = prompt("Enter name to search: ") + results = self.candidate_svc.search("name", term) + elif choice == "2": + term = prompt("Enter party name: ") + results = self.candidate_svc.search("party", term) + elif choice == "3": + subheader("Education Levels", THEME_ADMIN_ACCENT) + for i, level in enumerate(REQUIRED_EDUCATION_LEVELS, 1): + print(f" {THEME_ADMIN}{i}.{RESET} {level}") + edu_choice = int(prompt("Select: ")) + edu = REQUIRED_EDUCATION_LEVELS[edu_choice - 1] + results = self.candidate_svc.search("education", edu) + elif choice == "4": + min_age = int(prompt("Min age: ")) + max_age = int(prompt("Max age: ")) + results = self.candidate_svc.search("age_range", (min_age, max_age)) + else: + error("Invalid choice.") + pause() + return + except (ValueError, IndexError): + error("Invalid input.") + pause() + return + + if not results: + print() + info("No candidates found matching your criteria.") + else: + print(f"\n {BOLD}Found {len(results)} candidate(s):{RESET}") + table_header(f"{'ID':<5} {'Name':<25} {'Party':<20} {'Age':<5} {'Education':<20}", THEME_ADMIN) + table_divider(75, THEME_ADMIN) + for c in results: + print(f" {c.id:<5} {c.full_name:<25} {c.party:<20} {c.age:<5} {c.education:<20}") + pause() + + # ────────────────────────────────────────────── + # Station screens + # ────────────────────────────────────────────── + def create_station(self): + clear_screen() + header("CREATE VOTING STATION", THEME_ADMIN) + print() + name = prompt("Station Name: ") + location = prompt("Location/Address: ") + region = prompt("Region/District: ") + try: + capacity = int(prompt("Voter Capacity: ")) + except ValueError: + error("Invalid capacity.") + pause() + return + supervisor = prompt("Station Supervisor Name: ") + contact = prompt("Contact Phone: ") + opening_time = prompt("Opening Time (e.g. 08:00): ") + closing_time = prompt("Closing Time (e.g. 17:00): ") + try: + station = self.station_svc.create( + data={ + "name": name, "location": location, "region": region, + "capacity": capacity, "supervisor": supervisor, + "contact": contact, "opening_time": opening_time, + "closing_time": closing_time, + }, + created_by=self.session.current_user.username, + ) + print() + success(f"Voting Station '{station.name}' created! ID: {station.id}") + except ValueError as e: + error(str(e)) + pause() + + def view_all_stations(self): + clear_screen() + header("ALL VOTING STATIONS", THEME_ADMIN) + if not self.store.voting_stations: + print() + info("No voting stations found.") + pause() + return + print() + table_header( + f"{'ID':<5} {'Name':<25} {'Location':<25} {'Region':<15} {'Cap.':<8} {'Reg.':<8} {'Status':<10}", + THEME_ADMIN, + ) + table_divider(96, THEME_ADMIN) + for s in self.store.voting_stations.values(): + reg_count = self.station_svc.voter_count(s.id) + badge = status_badge("Active", True) if s.is_active else status_badge("Inactive", False) + print(f" {s.id:<5} {s.name:<25} {s.location:<25} {s.region:<15} {s.capacity:<8} {reg_count:<8} {badge}") + print(f"\n {DIM}Total Stations: {len(self.store.voting_stations)}{RESET}") + pause() + + def update_station(self): + clear_screen() + header("UPDATE VOTING STATION", THEME_ADMIN) + if not self.store.voting_stations: + print() + info("No stations found.") + pause() + return + print() + for s in self.store.voting_stations.values(): + print(f" {THEME_ADMIN}{s.id}.{RESET} {s.name} {DIM}- {s.location}{RESET}") + try: + sid = int(prompt("\nEnter Station ID to update: ")) + except ValueError: + error("Invalid input.") + pause() + return + s = self.store.voting_stations.get(sid) + if not s: + error("Station not found.") + pause() + return + print(f"\n {BOLD}Updating: {s.name}{RESET}") + info("Press Enter to keep current value\n") + cap_input = prompt(f"Capacity [{s.capacity}]: ") + capacity = None + if cap_input: + try: + capacity = int(cap_input) + except ValueError: + warning("Invalid number, keeping old value.") + updates = { + "name": prompt(f"Name [{s.name}]: "), + "location": prompt(f"Location [{s.location}]: "), + "region": prompt(f"Region [{s.region}]: "), + "supervisor": prompt(f"Supervisor [{s.supervisor}]: "), + "contact": prompt(f"Contact [{s.contact}]: "), + } + if capacity: + updates["capacity"] = capacity + try: + self.station_svc.update(sid, updates, self.session.current_user.username) + print() + success(f"Station '{self.store.voting_stations[sid].name}' updated successfully!") + except ValueError as e: + error(str(e)) + pause() + + def delete_station(self): + clear_screen() + header("DELETE VOTING STATION", THEME_ADMIN) + if not self.store.voting_stations: + print() + info("No stations found.") + pause() + return + print() + for s in self.store.voting_stations.values(): + badge = status_badge("Active", True) if s.is_active else status_badge("Inactive", False) + print(f" {THEME_ADMIN}{s.id}.{RESET} {s.name} {DIM}({s.location}){RESET} {badge}") + try: + sid = int(prompt("\nEnter Station ID to delete: ")) + except ValueError: + error("Invalid input.") + pause() + return + if sid not in self.store.voting_stations: + error("Station not found.") + pause() + return + voter_count = self.station_svc.voter_count(sid) + if voter_count > 0: + warning(f"{voter_count} voters are registered at this station.") + if prompt("Proceed with deactivation? (yes/no): ").lower() != "yes": + info("Cancelled.") + pause() + return + if prompt(f"Confirm deactivation of '{self.store.voting_stations[sid].name}'? (yes/no): ").lower() == "yes": + try: + self.station_svc.deactivate(sid, self.session.current_user.username) + print() + success(f"Station '{self.store.voting_stations[sid].name}' deactivated.") + except ValueError as e: + error(str(e)) + else: + info("Cancelled.") + pause() + + # ────────────────────────────────────────────── + # Position screens + # ────────────────────────────────────────────── + def create_position(self): + clear_screen() + header("CREATE POSITION", THEME_ADMIN) + print() + title = prompt("Position Title (e.g. President, Governor, Senator): ") + if not title: + error("Title cannot be empty.") + pause() + return + description = prompt("Description: ") + level = prompt("Level (National/Regional/Local): ") + try: + max_winners = int(prompt("Number of winners/seats: ")) + except ValueError: + error("Invalid number.") + pause() + return + min_age_str = prompt(f"Minimum candidate age [25]: ") + min_age = int(min_age_str) if min_age_str.isdigit() else 25 + try: + position = self.position_svc.create( + data={ + "title": title, "description": description, + "level": level, "max_winners": max_winners, + "min_candidate_age": min_age, + }, + created_by=self.session.current_user.username, + ) + print() + success(f"Position '{position.title}' created! ID: {position.id}") + except ValueError as e: + error(str(e)) + pause() + + def view_positions(self): + clear_screen() + header("ALL POSITIONS", THEME_ADMIN) + if not self.store.positions: + print() + info("No positions found.") + pause() + return + print() + table_header( + f"{'ID':<5} {'Title':<25} {'Level':<12} {'Seats':<8} {'Min Age':<10} {'Status':<10}", + THEME_ADMIN, + ) + table_divider(70, THEME_ADMIN) + for p in self.store.positions.values(): + badge = status_badge("Active", True) if p.is_active else status_badge("Inactive", False) + print(f" {p.id:<5} {p.title:<25} {p.level:<12} {p.max_winners:<8} {p.min_candidate_age:<10} {badge}") + print(f"\n {DIM}Total Positions: {len(self.store.positions)}{RESET}") + pause() + + def update_position(self): + clear_screen() + header("UPDATE POSITION", THEME_ADMIN) + if not self.store.positions: + print() + info("No positions found.") + pause() + return + print() + for p in self.store.positions.values(): + print(f" {THEME_ADMIN}{p.id}.{RESET} {p.title} {DIM}({p.level}){RESET}") + try: + pid = int(prompt("\nEnter Position ID to update: ")) + except ValueError: + error("Invalid input.") + pause() + return + p = self.store.positions.get(pid) + if not p: + error("Position not found.") + pause() + return + print(f"\n {BOLD}Updating: {p.title}{RESET}") + info("Press Enter to keep current value\n") + updates = { + "title": prompt(f"Title [{p.title}]: "), + "description": prompt(f"Description [{p.description[:50]}]: "), + } + new_level = prompt(f"Level [{p.level}]: ") + if new_level and new_level.lower() in POSITION_LEVELS: + updates["level"] = new_level.capitalize() + seats_str = prompt(f"Seats [{p.max_winners}]: ") + if seats_str: + try: + updates["max_winners"] = int(seats_str) + except ValueError: + warning("Keeping old value.") + try: + self.position_svc.update(pid, updates, self.session.current_user.username) + print() + success("Position updated!") + except ValueError as e: + error(str(e)) + pause() + + def delete_position(self): + clear_screen() + header("DELETE POSITION", THEME_ADMIN) + if not self.store.positions: + print() + info("No positions found.") + pause() + return + print() + for p in self.store.positions.values(): + print(f" {THEME_ADMIN}{p.id}.{RESET} {p.title} {DIM}({p.level}){RESET}") + try: + pid = int(prompt("\nEnter Position ID to delete: ")) + except ValueError: + error("Invalid input.") + pause() + return + if pid not in self.store.positions: + error("Position not found.") + pause() + return + if prompt(f"Confirm deactivation of '{self.store.positions[pid].title}'? (yes/no): ").lower() == "yes": + try: + self.position_svc.deactivate(pid, self.session.current_user.username) + print() + success("Position deactivated.") + except ValueError as e: + error(str(e)) + pause() + + # ────────────────────────────────────────────── + # Poll screens + # ────────────────────────────────────────────── + def create_poll(self): + clear_screen() + header("CREATE POLL / ELECTION", THEME_ADMIN) + print() + title = prompt("Poll/Election Title: ") + if not title: + error("Title cannot be empty.") + pause() + return + description = prompt("Description: ") + election_type = prompt("Election Type (General/Primary/By-election/Referendum): ") + start_date = prompt("Start Date (YYYY-MM-DD): ") + end_date = prompt("End Date (YYYY-MM-DD): ") + + active_positions = {pid: p for pid, p in self.store.positions.items() if p.is_active} + if not active_positions: + error("No active positions. Create positions first.") + pause() + return + + subheader("Available Positions", THEME_ADMIN_ACCENT) + for p in active_positions.values(): + print(f" {THEME_ADMIN}{p.id}.{RESET} {p.title} {DIM}({p.level}) - {p.max_winners} seat(s){RESET}") + try: + selected_ids = [int(x.strip()) for x in prompt("\nEnter Position IDs (comma-separated): ").split(",")] + except ValueError: + error("Invalid input.") + pause() + return + + active_stations = {sid: s for sid, s in self.store.voting_stations.items() if s.is_active} + if not active_stations: + error("No active voting stations.") + pause() + return + + subheader("Available Voting Stations", THEME_ADMIN_ACCENT) + for s in active_stations.values(): + print(f" {THEME_ADMIN}{s.id}.{RESET} {s.name} {DIM}({s.location}){RESET}") + + if prompt("\nUse all active stations? (yes/no): ").lower() == "yes": + selected_station_ids = list(active_stations.keys()) + else: + try: + selected_station_ids = [int(x.strip()) for x in prompt("Enter Station IDs (comma-separated): ").split(",")] + except ValueError: + error("Invalid input.") + pause() + return + + try: + poll = self.poll_svc.create( + data={ + "title": title, "description": description, + "election_type": election_type, "start_date": start_date, + "end_date": end_date, "position_ids": selected_ids, + "station_ids": selected_station_ids, + }, + created_by=self.session.current_user.username, + ) + print() + success(f"Poll '{poll.title}' created! ID: {poll.id}") + warning("Status: DRAFT — Assign candidates and then open the poll.") + except ValueError as e: + error(str(e)) + pause() + + def view_all_polls(self): + clear_screen() + header("ALL POLLS / ELECTIONS", THEME_ADMIN) + if not self.store.polls: + print() + info("No polls found.") + pause() + return + for poll in self.store.polls.values(): + sc = GREEN if poll.status == "open" else (YELLOW if poll.status == "draft" else RED) + print(f"\n {BOLD}{THEME_ADMIN}Poll #{poll.id}: {poll.title}{RESET}") + print(f" {DIM}Type:{RESET} {poll.election_type} {DIM}│ Status:{RESET} {sc}{BOLD}{poll.status.upper()}{RESET}") + print(f" {DIM}Period:{RESET} {poll.start_date} to {poll.end_date} {DIM}│ Votes:{RESET} {poll.total_votes_cast}") + for pos in poll.positions: + cand_names = [ + self.store.candidates[cid].full_name + for cid in pos.candidate_ids + if cid in self.store.candidates + ] + cand_display = ", ".join(cand_names) if cand_names else f"{DIM}None assigned{RESET}" + print(f" {THEME_ADMIN_ACCENT}▸{RESET} {pos.position_title}: {cand_display}") + print(f"\n {DIM}Total Polls: {len(self.store.polls)}{RESET}") + pause() + + def update_poll(self): + clear_screen() + header("UPDATE POLL", THEME_ADMIN) + if not self.store.polls: + print() + info("No polls found.") + pause() + return + print() + for poll in self.store.polls.values(): + sc = GREEN if poll.status == "open" else (YELLOW if poll.status == "draft" else RED) + print(f" {THEME_ADMIN}{poll.id}.{RESET} {poll.title} {sc}({poll.status}){RESET}") + try: + pid = int(prompt("\nEnter Poll ID to update: ")) + except ValueError: + error("Invalid input.") + pause() + return + poll = self.store.polls.get(pid) + if not poll: + error("Poll not found.") + pause() + return + print(f"\n {BOLD}Updating: {poll.title}{RESET}") + info("Press Enter to keep current value\n") + updates = { + "title": prompt(f"Title [{poll.title}]: "), + "description": prompt(f"Description [{poll.description[:50]}]: "), + "election_type": prompt(f"Election Type [{poll.election_type}]: "), + "start_date": prompt(f"Start Date [{poll.start_date}]: "), + "end_date": prompt(f"End Date [{poll.end_date}]: "), + } + try: + self.poll_svc.update(pid, updates, self.session.current_user.username) + print() + success("Poll updated!") + except ValueError as e: + error(str(e)) + pause() + + def delete_poll(self): + clear_screen() + header("DELETE POLL", THEME_ADMIN) + if not self.store.polls: + print() + info("No polls found.") + pause() + return + print() + for poll in self.store.polls.values(): + print(f" {THEME_ADMIN}{poll.id}.{RESET} {poll.title} {DIM}({poll.status}){RESET}") + try: + pid = int(prompt("\nEnter Poll ID to delete: ")) + except ValueError: + error("Invalid input.") + pause() + return + if pid not in self.store.polls: + error("Poll not found.") + pause() + return + if self.store.polls[pid].total_votes_cast > 0: + warning(f"This poll has {self.store.polls[pid].total_votes_cast} votes recorded.") + if prompt(f"Confirm deletion of '{self.store.polls[pid].title}'? (yes/no): ").lower() == "yes": + try: + title = self.store.polls[pid].title + self.poll_svc.delete(pid, self.session.current_user.username) + print() + success(f"Poll '{title}' deleted.") + except ValueError as e: + error(str(e)) + pause() + + def open_close_poll(self): + clear_screen() + header("OPEN / CLOSE POLL", THEME_ADMIN) + if not self.store.polls: + print() + info("No polls found.") + pause() + return + print() + for poll in self.store.polls.values(): + sc = GREEN if poll.status == "open" else (YELLOW if poll.status == "draft" else RED) + print(f" {THEME_ADMIN}{poll.id}.{RESET} {poll.title} {sc}{BOLD}{poll.status.upper()}{RESET}") + try: + pid = int(prompt("\nEnter Poll ID: ")) + except ValueError: + error("Invalid input.") + pause() + return + poll = self.store.polls.get(pid) + if not poll: + error("Poll not found.") + pause() + return + user = self.session.current_user.username + try: + if poll.status == "draft": + if prompt(f"Open poll '{poll.title}'? Voting will begin. (yes/no): ").lower() == "yes": + self.poll_svc.open(pid, user) + print() + success(f"Poll '{poll.title}' is now OPEN for voting!") + elif poll.status == "open": + if prompt(f"Close poll '{poll.title}'? No more votes accepted. (yes/no): ").lower() == "yes": + self.poll_svc.close(pid, user) + print() + success(f"Poll '{poll.title}' is now CLOSED.") + elif poll.status == "closed": + info("This poll is already closed.") + if prompt("Reopen it? (yes/no): ").lower() == "yes": + self.poll_svc.reopen(pid, user) + print() + success("Poll reopened!") + except ValueError as e: + error(str(e)) + pause() + + def assign_candidates_to_poll(self): + clear_screen() + header("ASSIGN CANDIDATES TO POLL", THEME_ADMIN) + if not self.store.polls or not self.store.candidates: + print() + info("No polls or candidates found.") + pause() + return + print() + for poll in self.store.polls.values(): + print(f" {THEME_ADMIN}{poll.id}.{RESET} {poll.title} {DIM}({poll.status}){RESET}") + try: + pid = int(prompt("\nEnter Poll ID: ")) + except ValueError: + error("Invalid input.") + pause() + return + poll = self.store.polls.get(pid) + if not poll: + error("Poll not found.") + pause() + return + if poll.status == "open": + error("Cannot modify candidates of an open poll.") + pause() + return + + for i, pos in enumerate(poll.positions): + subheader(f"Position: {pos.position_title}", THEME_ADMIN_ACCENT) + current_names = [ + f"{cid}: {self.store.candidates[cid].full_name}" + for cid in pos.candidate_ids + if cid in self.store.candidates + ] + if current_names: + print(f" {DIM}Current:{RESET} {', '.join(current_names)}") + else: + info("No candidates assigned yet.") + + pos_data = self.store.positions.get(pos.position_id, None) + min_age = pos_data.min_candidate_age if pos_data else 25 + eligible = { + cid: c for cid, c in self.store.candidates.items() + if c.is_active and c.is_approved and c.age >= min_age + } + + if not eligible: + info("No eligible candidates found.") + continue + + subheader("Available Candidates", THEME_ADMIN) + for c in eligible.values(): + marker = f" {GREEN}[ASSIGNED]{RESET}" if c.id in pos.candidate_ids else "" + print(f" {THEME_ADMIN}{c.id}.{RESET} {c.full_name} {DIM}({c.party}) - Age: {c.age}, Edu: {c.education}{RESET}{marker}") + + if prompt(f"\nModify candidates for {pos.position_title}? (yes/no): ").lower() == "yes": + try: + new_ids = [int(x.strip()) for x in prompt("Enter Candidate IDs (comma-separated): ").split(",")] + valid_ids = [nid for nid in new_ids if nid in eligible] + for nid in new_ids: + if nid not in eligible: + warning(f"Candidate {nid} not eligible. Skipping.") + self.poll_svc.assign_candidates(pid, i, valid_ids, self.session.current_user.username) + success(f"{len(valid_ids)} candidate(s) assigned.") + except ValueError: + error("Invalid input. Skipping this position.") + pause() + + # ────────────────────────────────────────────── + # Voter management screens + # ────────────────────────────────────────────── + def view_all_voters(self): + clear_screen() + header("ALL REGISTERED VOTERS", THEME_ADMIN) + if not self.store.voters: + print() + info("No voters registered.") + pause() + return + print() + table_header( + f"{'ID':<5} {'Name':<25} {'Card Number':<15} {'Stn':<6} {'Verified':<10} {'Active':<8}", + THEME_ADMIN, + ) + table_divider(70, THEME_ADMIN) + for v in self.store.voters.values(): + verified = status_badge("Yes", True) if v.is_verified else status_badge("No", False) + active = status_badge("Yes", True) if v.is_active else status_badge("No", False) + print(f" {v.id:<5} {v.full_name:<25} {v.voter_card_number:<15} {v.station_id:<6} {verified:<19} {active}") + verified_count = sum(1 for v in self.store.voters.values() if v.is_verified) + unverified_count = len(self.store.voters) - verified_count + print(f"\n {DIM}Total: {len(self.store.voters)} │ Verified: {verified_count} │ Unverified: {unverified_count}{RESET}") + pause() + + def verify_voter(self): + clear_screen() + header("VERIFY VOTER", THEME_ADMIN) + unverified = {vid: v for vid, v in self.store.voters.items() if not v.is_verified} + if not unverified: + print() + info("No unverified voters.") + pause() + return + subheader("Unverified Voters", THEME_ADMIN_ACCENT) + for v in unverified.values(): + print(f" {THEME_ADMIN}{v.id}.{RESET} {v.full_name} {DIM}│ NID: {v.national_id} │ Card: {v.voter_card_number}{RESET}") + print() + menu_item(1, "Verify a single voter", THEME_ADMIN) + menu_item(2, "Verify all pending voters", THEME_ADMIN) + choice = prompt("\nChoice: ") + if choice == "1": + try: + vid = int(prompt("Enter Voter ID: ")) + except ValueError: + error("Invalid input.") + pause() + return + try: + self.voter_svc.verify(vid, self.session.current_user.username) + print() + success(f"Voter '{self.store.voters[vid].full_name}' verified!") + except ValueError as e: + error(str(e)) + elif choice == "2": + count = self.voter_svc.verify_all_pending(self.session.current_user.username) + print() + success(f"{count} voters verified!") + pause() + + def deactivate_voter(self): + clear_screen() + header("DEACTIVATE VOTER", THEME_ADMIN) + if not self.store.voters: + print() + info("No voters found.") + pause() + return + print() + try: + vid = int(prompt("Enter Voter ID to deactivate: ")) + except ValueError: + error("Invalid input.") + pause() + return + voter = self.store.voters.get(vid) + if not voter: + error("Voter not found.") + pause() + return + if prompt(f"Deactivate '{voter.full_name}'? (yes/no): ").lower() == "yes": + try: + self.voter_svc.deactivate(vid, self.session.current_user.username) + print() + success("Voter deactivated.") + except ValueError as e: + error(str(e)) + pause() + + def search_voters(self): + clear_screen() + header("SEARCH VOTERS", THEME_ADMIN) + subheader("Search by", THEME_ADMIN_ACCENT) + menu_item(1, "Name", THEME_ADMIN) + menu_item(2, "Voter Card Number", THEME_ADMIN) + menu_item(3, "National ID", THEME_ADMIN) + menu_item(4, "Station", THEME_ADMIN) + choice = prompt("\nChoice: ") + try: + if choice == "1": + results = self.voter_svc.search("name", prompt("Name: ")) + elif choice == "2": + results = self.voter_svc.search("card", prompt("Card Number: ")) + elif choice == "3": + results = self.voter_svc.search("national_id", prompt("National ID: ")) + elif choice == "4": + sid = int(prompt("Station ID: ")) + results = self.voter_svc.search("station", sid) + else: + error("Invalid choice.") + pause() + return + except ValueError: + error("Invalid input.") + pause() + return + if not results: + print() + info("No voters found.") + else: + print(f"\n {BOLD}Found {len(results)} voter(s):{RESET}") + for v in results: + verified = status_badge("Verified", True) if v.is_verified else status_badge("Unverified", False) + print( + f" {THEME_ADMIN}ID:{RESET} {v.id} {DIM}│{RESET} {v.full_name}" + f" {DIM}│ Card:{RESET} {v.voter_card_number} {DIM}│{RESET} {verified}" + ) + pause() + + # ────────────────────────────────────────────── + # Admin management screens + # ────────────────────────────────────────────── + def create_admin(self): + clear_screen() + header("CREATE ADMIN ACCOUNT", THEME_ADMIN) + if not self.session.is_super_admin(): + print() + error("Only super admins can create admin accounts.") + pause() + return + print() + username = prompt("Username: ") + full_name = prompt("Full Name: ") + email = prompt("Email: ") + password = masked_input("Password: ").strip() + subheader("Available Roles", THEME_ADMIN_ACCENT) + for key, role in ADMIN_ROLES.items(): + label = ADMIN_ROLE_LABELS.get(role, "") + menu_item(int(key), f"{role} {DIM}─ {label}{RESET}", THEME_ADMIN) + role_choice = prompt("\nSelect role (1-4): ") + role = ADMIN_ROLES.get(role_choice) + if not role: + error("Invalid role.") + pause() + return + try: + admin = self.admin_svc.create( + data={"username": username, "full_name": full_name, "email": email, "password": password, "role": role}, + created_by=self.session.current_user.username, + ) + print() + success(f"Admin '{admin.username}' created with role: {admin.role}") + except ValueError as e: + error(str(e)) + pause() + + def view_admins(self): + clear_screen() + header("ALL ADMIN ACCOUNTS", THEME_ADMIN) + print() + table_header(f"{'ID':<5} {'Username':<20} {'Full Name':<25} {'Role':<20} {'Active':<8}", THEME_ADMIN) + table_divider(78, THEME_ADMIN) + for a in self.store.admins.values(): + active = status_badge("Yes", True) if a.is_active else status_badge("No", False) + print(f" {a.id:<5} {a.username:<20} {a.full_name:<25} {a.role:<20} {active}") + print(f"\n {DIM}Total Admins: {len(self.store.admins)}{RESET}") + pause() + + def deactivate_admin(self): + clear_screen() + header("DEACTIVATE ADMIN", THEME_ADMIN) + if not self.session.is_super_admin(): + print() + error("Only super admins can deactivate admins.") + pause() + return + print() + for a in self.store.admins.values(): + badge = status_badge("Active", True) if a.is_active else status_badge("Inactive", False) + print(f" {THEME_ADMIN}{a.id}.{RESET} {a.username} {DIM}({a.role}){RESET} {badge}") + try: + aid = int(prompt("\nEnter Admin ID to deactivate: ")) + except ValueError: + error("Invalid input.") + pause() + return + if prompt(f"Deactivate '{self.store.admins.get(aid, {}).username if aid in self.store.admins else '?'}'? (yes/no): ").lower() == "yes": + try: + self.admin_svc.deactivate(aid, self.session.current_user.id, self.session.current_user.username) + print() + success("Admin deactivated.") + except ValueError as e: + error(str(e)) + pause() + + # ────────────────────────────────────────────── + # Results & reports screens + # ────────────────────────────────────────────── + def view_poll_results(self): + clear_screen() + header("POLL RESULTS", THEME_ADMIN) + if not self.store.polls: + print() + info("No polls found.") + pause() + return + print() + for poll in self.store.polls.values(): + sc = GREEN if poll.status == "open" else (YELLOW if poll.status == "draft" else RED) + print(f" {THEME_ADMIN}{poll.id}.{RESET} {poll.title} {sc}({poll.status}){RESET}") + try: + pid = int(prompt("\nEnter Poll ID: ")) + except ValueError: + error("Invalid input.") + pause() + return + poll = self.store.polls.get(pid) + if not poll: + error("Poll not found.") + pause() + return + + print() + header(f"RESULTS: {poll.title}", THEME_ADMIN) + sc = GREEN if poll.status == "open" else RED + print(f" {DIM}Status:{RESET} {sc}{BOLD}{poll.status.upper()}{RESET} {DIM}│ Votes:{RESET} {BOLD}{poll.total_votes_cast}{RESET}") + + turnout = self.results_svc.poll_turnout(pid) + tc = GREEN if turnout["percentage"] > 50 else (YELLOW if turnout["percentage"] > 25 else RED) + print(f" {DIM}Eligible:{RESET} {turnout['eligible']} {DIM}│ Turnout:{RESET} {tc}{BOLD}{turnout['percentage']:.1f}%{RESET}") + + for pos in poll.positions: + subheader(f"{pos.position_title} (Seats: {pos.max_winners})", THEME_ADMIN_ACCENT) + tally = self.results_svc.tally_position(pid, pos.position_id) + self._render_tally(tally, pos.max_winners, THEME_ADMIN) + pause() + + def _render_tally(self, tally: dict, max_winners: int, bar_color): + vote_counts = tally["counts"] + abstain_count = tally["abstain"] + total = tally["total"] + if not vote_counts: + info(" No votes recorded for this position.") + return + for rank, (cid, count) in enumerate( + sorted(vote_counts.items(), key=lambda x: x[1], reverse=True), 1 + ): + cand = self.store.candidates.get(cid, None) + name = cand.full_name if cand else "?" + party = cand.party if cand else "?" + pct = (count / total * 100) if total > 0 else 0 + bl = int(pct / 2) + bar = f"{bar_color}{'█' * bl}{GRAY}{'░' * (50 - bl)}{RESET}" + winner = f" {BG_GREEN}{BLACK}{BOLD} ★ WINNER {RESET}" if rank <= max_winners else "" + print(f" {BOLD}{rank}. {name}{RESET} {DIM}({party}){RESET}") + print(f" {bar} {BOLD}{count}{RESET} ({pct:.1f}%){winner}") + if abstain_count > 0: + pct = (abstain_count / total * 100) if total > 0 else 0 + print(f" {GRAY}Abstained: {abstain_count} ({pct:.1f}%){RESET}") + + def view_detailed_statistics(self): + clear_screen() + header("DETAILED STATISTICS", THEME_ADMIN) + overview = self.results_svc.system_overview() + + subheader("SYSTEM OVERVIEW", THEME_ADMIN_ACCENT) + print(f" {THEME_ADMIN}Candidates:{RESET} {overview['total_candidates']} {DIM}(Active: {overview['active_candidates']}){RESET}") + print(f" {THEME_ADMIN}Voters:{RESET} {overview['total_voters']} {DIM}(Verified: {overview['verified_voters']}, Active: {overview['active_voters']}){RESET}") + print(f" {THEME_ADMIN}Stations:{RESET} {overview['total_stations']} {DIM}(Active: {overview['active_stations']}){RESET}") + print(f" {THEME_ADMIN}Polls:{RESET} {overview['total_polls']} " + f"{DIM}({GREEN}Open: {overview['open_polls']}{RESET}{DIM}, {RED}Closed: {overview['closed_polls']}{RESET}{DIM}, {YELLOW}Draft: {overview['draft_polls']}{RESET}{DIM}){RESET}") + print(f" {THEME_ADMIN}Total Votes:{RESET} {overview['total_votes']}") + + demo = self.results_svc.voter_demographics() + subheader("VOTER DEMOGRAPHICS", THEME_ADMIN_ACCENT) + tv = overview["total_voters"] + for g, count in demo["gender"].items(): + pct = (count / tv * 100) if tv > 0 else 0 + print(f" {g}: {count} ({pct:.1f}%)") + print(f" {BOLD}Age Distribution:{RESET}") + for group, count in demo["age_groups"].items(): + pct = (count / tv * 100) if tv > 0 else 0 + print(f" {group:>5}: {count:>3} ({pct:>5.1f}%) {THEME_ADMIN}{'█' * int(pct / 2)}{RESET}") + + subheader("STATION LOAD", THEME_ADMIN_ACCENT) + for item in self.results_svc.station_load(): + s = item["station"] + lp = item["load_pct"] + lc = RED if lp > 100 else (YELLOW if lp > 75 else GREEN) + st = f"{RED}{BOLD}OVERLOADED{RESET}" if item["overloaded"] else f"{GREEN}OK{RESET}" + print(f" {s.name}: {item['voter_count']}/{s.capacity} {lc}({lp:.0f}%){RESET} {st}") + + subheader("CANDIDATE PARTY DISTRIBUTION", THEME_ADMIN_ACCENT) + for party, count in sorted(self.results_svc.party_distribution().items(), key=lambda x: x[1], reverse=True): + print(f" {party}: {BOLD}{count}{RESET} candidate(s)") + + subheader("CANDIDATE EDUCATION LEVELS", THEME_ADMIN_ACCENT) + for edu, count in self.results_svc.education_distribution().items(): + print(f" {edu}: {BOLD}{count}{RESET}") + pause() + + def station_wise_results(self): + clear_screen() + header("STATION-WISE RESULTS", THEME_ADMIN) + if not self.store.polls: + print() + info("No polls found.") + pause() + return + print() + for poll in self.store.polls.values(): + sc = GREEN if poll.status == "open" else (YELLOW if poll.status == "draft" else RED) + print(f" {THEME_ADMIN}{poll.id}.{RESET} {poll.title} {sc}({poll.status}){RESET}") + try: + pid = int(prompt("\nEnter Poll ID: ")) + except ValueError: + error("Invalid input.") + pause() + return + poll = self.store.polls.get(pid) + if not poll: + error("Poll not found.") + pause() + return + print() + header(f"STATION RESULTS: {poll.title}", THEME_ADMIN) + for sid in poll.station_ids: + if sid not in self.store.voting_stations: + continue + station = self.store.voting_stations[sid] + subheader(f"{station.name} ({station.location})", BRIGHT_WHITE) + st_data = self.results_svc.station_turnout(pid, sid) + tc = GREEN if st_data["percentage"] > 50 else (YELLOW if st_data["percentage"] > 25 else RED) + print( + f" {DIM}Registered:{RESET} {st_data['registered']} " + f"{DIM}│ Voted:{RESET} {st_data['voted']} " + f"{DIM}│ Turnout:{RESET} {tc}{BOLD}{st_data['percentage']:.1f}%{RESET}" + ) + for pos in poll.positions: + print(f" {THEME_ADMIN_ACCENT}▸ {pos.position_title}:{RESET}") + pos_votes = [v for v in st_data["votes"] if v.position_id == pos.position_id] + vc = {} + ac = 0 + for v in pos_votes: + if v.abstained: + ac += 1 + else: + vc[v.candidate_id] = vc.get(v.candidate_id, 0) + 1 + total = sum(vc.values()) + ac + for cid, count in sorted(vc.items(), key=lambda x: x[1], reverse=True): + cand = self.store.candidates.get(cid) + name = cand.full_name if cand else "?" + party = cand.party if cand else "?" + pct = (count / total * 100) if total > 0 else 0 + print(f" {name} {DIM}({party}){RESET}: {BOLD}{count}{RESET} ({pct:.1f}%)") + if ac > 0: + pct = (ac / total * 100) if total > 0 else 0 + print(f" {GRAY}Abstained: {ac} ({pct:.1f}%){RESET}") + pause() + + def view_audit_log(self): + clear_screen() + header("AUDIT LOG", THEME_ADMIN) + if not self.store.audit_log: + print() + info("No audit records.") + pause() + return + print(f"\n {DIM}Total Records: {len(self.store.audit_log)}{RESET}") + subheader("Filter", THEME_ADMIN_ACCENT) + menu_item(1, "Last 20 entries", THEME_ADMIN) + menu_item(2, "All entries", THEME_ADMIN) + menu_item(3, "Filter by action type", THEME_ADMIN) + menu_item(4, "Filter by user", THEME_ADMIN) + choice = prompt("\nChoice: ") + entries = self.store.audit_log + + if choice == "1": + entries = self.store.audit_log[-20:] + elif choice == "3": + action_types = list(set(e["action"] for e in self.store.audit_log)) + for i, at in enumerate(action_types, 1): + print(f" {THEME_ADMIN}{i}.{RESET} {at}") + try: + at_choice = int(prompt("Select action type: ")) + entries = [e for e in self.store.audit_log if e["action"] == action_types[at_choice - 1]] + except (ValueError, IndexError): + error("Invalid choice.") + pause() + return + elif choice == "4": + uf = prompt("Enter username/card number: ") + entries = [e for e in self.store.audit_log if uf.lower() in e["user"].lower()] + + print() + table_header(f"{'Timestamp':<22} {'Action':<25} {'User':<20} {'Details'}", THEME_ADMIN) + table_divider(100, THEME_ADMIN) + for entry in entries: + ac = ( + GREEN if "CREATE" in entry["action"] or entry["action"] == "LOGIN" + else RED if "DELETE" in entry["action"] or "DEACTIVATE" in entry["action"] + else YELLOW if "UPDATE" in entry["action"] + else RESET + ) + print( + f" {DIM}{entry['timestamp'][:19]}{RESET} " + f"{ac}{entry['action']:<25}{RESET} " + f"{entry['user']:<20} " + f"{DIM}{entry['details'][:50]}{RESET}" + ) + pause() diff --git a/evoting_refactored (1)/evoting_refactored/evoting/views/auth_view.py b/evoting_refactored (1)/evoting_refactored/evoting/views/auth_view.py new file mode 100644 index 0000000..ec765de --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/views/auth_view.py @@ -0,0 +1,142 @@ +""" +Auth view — login screen and voter self-registration screen. +Handles only user input/output. Delegates logic to services. +""" + +from ui.colors import ( + RESET, BOLD, DIM, BRIGHT_YELLOW, BRIGHT_BLUE, + THEME_LOGIN, THEME_ADMIN, THEME_VOTER, +) +from ui.components import header, menu_item, error, success, warning, info, subheader +from ui.prompts import clear_screen, pause, prompt, masked_input + + +class AuthView: + def __init__(self, store, session, voter_svc): + self.store = store + self.session = session + self.voter_svc = voter_svc + + def show_login_menu(self) -> bool: + """ + Display the main login menu. Returns True when a user successfully + logs in, False otherwise (so the main loop retries). + """ + clear_screen() + header("E-VOTING SYSTEM", THEME_LOGIN) + print() + menu_item(1, "Login as Admin", THEME_LOGIN) + menu_item(2, "Login as Voter", THEME_LOGIN) + menu_item(3, "Register as Voter", THEME_LOGIN) + menu_item(4, "Exit", THEME_LOGIN) + print() + choice = prompt("Enter choice: ") + + if choice == "1": + return self._admin_login() + elif choice == "2": + return self._voter_login() + elif choice == "3": + self._register_voter() + return False + elif choice == "4": + print() + info("Goodbye!") + self.store.save() + exit() + else: + error("Invalid choice.") + pause() + return False + + def _admin_login(self) -> bool: + clear_screen() + header("ADMIN LOGIN", THEME_ADMIN) + print() + username = prompt("Username: ") + password = masked_input("Password: ").strip() + ok, msg, _ = self.session.login_admin(username, password, self.store.admins) + if ok: + from services.audit_service import log_action + log_action(self.store, "LOGIN", username, "Admin login successful") + print() + success(msg) + pause() + return True + else: + from services.audit_service import log_action + log_action(self.store, "LOGIN_FAILED", username, msg) + error(msg) + pause() + return False + + def _voter_login(self) -> bool: + clear_screen() + header("VOTER LOGIN", THEME_VOTER) + print() + voter_card = prompt("Voter Card Number: ") + password = masked_input("Password: ").strip() + ok, msg, _ = self.session.login_voter(voter_card, password, self.store.voters) + if ok: + from services.audit_service import log_action + log_action(self.store, "LOGIN", voter_card, "Voter login successful") + print() + success(msg) + pause() + return True + else: + from services.audit_service import log_action + log_action(self.store, "LOGIN_FAILED", voter_card, msg) + error(msg) + pause() + return False + + def _register_voter(self): + clear_screen() + header("VOTER REGISTRATION", THEME_VOTER) + print() + full_name = prompt("Full Name: ") + national_id = prompt("National ID Number: ") + dob_str = prompt("Date of Birth (YYYY-MM-DD): ") + gender = prompt("Gender (M/F/Other): ").upper() + address = prompt("Residential Address: ") + phone = prompt("Phone Number: ") + email = prompt("Email Address: ") + password = masked_input("Create Password: ").strip() + confirm_password = masked_input("Confirm Password: ").strip() + + if not self.store.voting_stations: + error("No voting stations available. Contact admin.") + pause() + return + + subheader("Available Voting Stations", THEME_VOTER) + for s in self.store.voting_stations.values(): + if s.is_active: + print(f" {BRIGHT_BLUE}{s.id}.{RESET} {s.name} {DIM}- {s.location}{RESET}") + + try: + station_choice = int(prompt("\nSelect your voting station ID: ")) + except ValueError: + error("Invalid input.") + pause() + return + + try: + voter = self.voter_svc.register( + data={ + "full_name": full_name, "national_id": national_id, + "date_of_birth": dob_str, "gender": gender, + "address": address, "phone": phone, "email": email, + "password": password, "confirm_password": confirm_password, + "station_id": station_choice, + } + ) + print() + success("Registration successful!") + print(f" {BOLD}Your Voter Card Number: {BRIGHT_YELLOW}{voter.voter_card_number}{RESET}") + warning("IMPORTANT: Save this number! You need it to login.") + info("Your registration is pending admin verification.") + except ValueError as e: + error(str(e)) + pause() diff --git a/evoting_refactored (1)/evoting_refactored/evoting/views/voter_view.py b/evoting_refactored (1)/evoting_refactored/evoting/views/voter_view.py new file mode 100644 index 0000000..0e80719 --- /dev/null +++ b/evoting_refactored (1)/evoting_refactored/evoting/views/voter_view.py @@ -0,0 +1,294 @@ +""" +Voter view — all voter-facing menus and screens. +Handles only user input and output. Delegates all logic to services. +""" + +from ui.colors import ( + RESET, BOLD, DIM, ITALIC, GRAY, + GREEN, RED, YELLOW, BLACK, + BRIGHT_GREEN, BRIGHT_YELLOW, BRIGHT_CYAN, BRIGHT_BLUE, + BG_GREEN, THEME_VOTER, THEME_VOTER_ACCENT, +) +from ui.components import ( + header, subheader, menu_item, error, success, warning, info, status_badge, +) +from ui.prompts import clear_screen, pause, prompt, masked_input + + +class VoterView: + def __init__(self, store, session, services): + self.store = store + self.session = session + self.vote_svc = services["vote"] + self.voter_svc = services["voter"] + self.results_svc = services["results"] + + def run(self): + while True: + clear_screen() + header("VOTER DASHBOARD", THEME_VOTER) + voter = self.session.current_user + station_name = self.store.voting_stations.get(voter.station_id, None) + station_label = station_name.name if station_name else "Unknown" + print(f" {THEME_VOTER} ● {RESET}{BOLD}{voter.full_name}{RESET}") + print(f" {DIM} Card: {voter.voter_card_number} │ Station: {station_label}{RESET}") + print() + menu_item(1, "View Open Polls", THEME_VOTER) + menu_item(2, "Cast Vote", THEME_VOTER) + menu_item(3, "View My Voting History", THEME_VOTER) + menu_item(4, "View Results (Closed Polls)", THEME_VOTER) + menu_item(5, "View My Profile", THEME_VOTER) + menu_item(6, "Change Password", THEME_VOTER) + menu_item(7, "Logout", THEME_VOTER) + print() + choice = prompt("Enter choice: ") + + if choice == "1": + self.view_open_polls() + elif choice == "2": + self.cast_vote() + elif choice == "3": + self.view_voting_history() + elif choice == "4": + self.view_closed_results() + elif choice == "5": + self.view_profile() + elif choice == "6": + self.change_password() + elif choice == "7": + from services.audit_service import log_action + log_action(self.store, "LOGOUT", voter.voter_card_number, "Voter logged out") + self.store.save() + break + else: + error("Invalid choice.") + pause() + + def view_open_polls(self): + clear_screen() + header("OPEN POLLS", THEME_VOTER) + voter = self.session.current_user + open_polls = {pid: p for pid, p in self.store.polls.items() if p.status == "open"} + if not open_polls: + print() + info("No open polls at this time.") + pause() + return + for pid, poll in open_polls.items(): + already_voted = pid in voter.has_voted_in + vs = f" {GREEN}[VOTED]{RESET}" if already_voted else f" {YELLOW}[NOT YET VOTED]{RESET}" + print(f"\n {BOLD}{THEME_VOTER}Poll #{poll.id}: {poll.title}{RESET}{vs}") + print(f" {DIM}Type:{RESET} {poll.election_type} {DIM}│ Period:{RESET} {poll.start_date} to {poll.end_date}") + for pos in poll.positions: + print(f" {THEME_VOTER_ACCENT}▸{RESET} {BOLD}{pos.position_title}{RESET}") + for cid in pos.candidate_ids: + if cid in self.store.candidates: + c = self.store.candidates[cid] + print(f" {DIM}•{RESET} {c.full_name} {DIM}({c.party}) │ Age: {c.age} │ Edu: {c.education}{RESET}") + pause() + + def cast_vote(self): + clear_screen() + header("CAST YOUR VOTE", THEME_VOTER) + voter = self.session.current_user + available_polls = self.vote_svc.get_available_polls(voter) + if not available_polls: + print() + info("No available polls to vote in.") + pause() + return + + subheader("Available Polls", THEME_VOTER_ACCENT) + for poll in available_polls.values(): + print(f" {THEME_VOTER}{poll.id}.{RESET} {poll.title} {DIM}({poll.election_type}){RESET}") + + try: + pid = int(prompt("\nSelect Poll ID to vote: ")) + except ValueError: + error("Invalid input.") + pause() + return + if pid not in available_polls: + error("Invalid poll selection.") + pause() + return + + poll = self.store.polls[pid] + print() + header(f"Voting: {poll.title}", THEME_VOTER) + info("Please select ONE candidate for each position.\n") + + selections = [] + for pos in poll.positions: + subheader(pos.position_title, THEME_VOTER_ACCENT) + if not pos.candidate_ids: + info("No candidates for this position.") + continue + + for idx, cid in enumerate(pos.candidate_ids, 1): + if cid in self.store.candidates: + c = self.store.candidates[cid] + print(f" {THEME_VOTER}{BOLD}{idx}.{RESET} {c.full_name} {DIM}({c.party}){RESET}") + print(f" {DIM}Age: {c.age} │ Edu: {c.education} │ Exp: {c.years_experience} yrs{RESET}") + if c.manifesto: + print(f" {ITALIC}{DIM}{c.manifesto[:80]}...{RESET}") + print(f" {GRAY}{BOLD}0.{RESET} {GRAY}Abstain / Skip{RESET}") + + try: + vote_choice = int(prompt(f"\nYour choice for {pos.position_title}: ")) + except ValueError: + warning("Invalid input. Skipping.") + vote_choice = 0 + + if vote_choice == 0 or not (1 <= vote_choice <= len(pos.candidate_ids)): + if vote_choice != 0: + warning("Invalid choice. Marking as abstain.") + selections.append({ + "position_id": pos.position_id, + "position_title": pos.position_title, + "candidate_id": None, + "abstained": True, + }) + else: + selected_cid = pos.candidate_ids[vote_choice - 1] + selections.append({ + "position_id": pos.position_id, + "position_title": pos.position_title, + "candidate_id": selected_cid, + "candidate_name": self.store.candidates[selected_cid].full_name, + "abstained": False, + }) + + subheader("VOTE SUMMARY", BRIGHT_CYAN) + for sel in selections: + if sel["abstained"]: + print(f" {sel['position_title']}: {GRAY}ABSTAINED{RESET}") + else: + print(f" {sel['position_title']}: {BRIGHT_GREEN}{BOLD}{sel['candidate_name']}{RESET}") + print() + + if prompt("Confirm your votes? This cannot be undone. (yes/no): ").lower() != "yes": + info("Vote cancelled.") + pause() + return + + try: + vote_hash = self.vote_svc.cast_ballot(voter, pid, selections) + print() + success("Your vote has been recorded successfully!") + print(f" {DIM}Vote Reference:{RESET} {BRIGHT_YELLOW}{vote_hash}{RESET}") + print(f" {BRIGHT_CYAN}Thank you for participating in the democratic process!{RESET}") + except ValueError as e: + error(str(e)) + pause() + + def view_voting_history(self): + clear_screen() + header("MY VOTING HISTORY", THEME_VOTER) + voter = self.session.current_user + voted_polls = voter.has_voted_in + if not voted_polls: + print() + info("You have not voted in any polls yet.") + pause() + return + print(f"\n {DIM}You have voted in {len(voted_polls)} poll(s):{RESET}\n") + for pid in voted_polls: + if pid not in self.store.polls: + continue + poll = self.store.polls[pid] + sc = GREEN if poll.status == "open" else RED + print(f" {BOLD}{THEME_VOTER}Poll #{pid}: {poll.title}{RESET}") + print(f" {DIM}Type:{RESET} {poll.election_type} {DIM}│ Status:{RESET} {sc}{poll.status.upper()}{RESET}") + my_votes = self.vote_svc.get_voter_votes(voter.id, pid) + for vr in my_votes: + pos_title = next( + (pos.position_title for pos in poll.positions if pos.position_id == vr.position_id), + "Unknown", + ) + if vr.abstained: + print(f" {THEME_VOTER_ACCENT}▸{RESET} {pos_title}: {GRAY}ABSTAINED{RESET}") + else: + cand = self.store.candidates.get(vr.candidate_id) + name = cand.full_name if cand else "Unknown" + print(f" {THEME_VOTER_ACCENT}▸{RESET} {pos_title}: {BRIGHT_GREEN}{name}{RESET}") + print() + pause() + + def view_closed_results(self): + clear_screen() + header("ELECTION RESULTS", THEME_VOTER) + closed_polls = {pid: p for pid, p in self.store.polls.items() if p.status == "closed"} + if not closed_polls: + print() + info("No closed polls with results.") + pause() + return + for pid, poll in closed_polls.items(): + print(f"\n {BOLD}{THEME_VOTER}{poll.title}{RESET}") + print(f" {DIM}Type:{RESET} {poll.election_type} {DIM}│ Votes:{RESET} {poll.total_votes_cast}") + for pos in poll.positions: + subheader(pos.position_title, THEME_VOTER_ACCENT) + tally = self.results_svc.tally_position(pid, pos.position_id) + vote_counts = tally["counts"] + abstain_count = tally["abstain"] + total = tally["total"] + for rank, (cid, count) in enumerate( + sorted(vote_counts.items(), key=lambda x: x[1], reverse=True), 1 + ): + cand = self.store.candidates.get(cid) + name = cand.full_name if cand else "?" + party = cand.party if cand else "?" + pct = (count / total * 100) if total > 0 else 0 + bar = f"{THEME_VOTER}{'█' * int(pct / 2)}{GRAY}{'░' * (50 - int(pct / 2))}{RESET}" + winner = f" {BG_GREEN}{BLACK}{BOLD} WINNER {RESET}" if rank <= pos.max_winners else "" + print(f" {BOLD}{rank}. {name}{RESET} {DIM}({party}){RESET}") + print(f" {bar} {BOLD}{count}{RESET} ({pct:.1f}%){winner}") + if abstain_count > 0: + pct = (abstain_count / total * 100) if total > 0 else 0 + print(f" {GRAY}Abstained: {abstain_count} ({pct:.1f}%){RESET}") + pause() + + def view_profile(self): + clear_screen() + header("MY PROFILE", THEME_VOTER) + v = self.session.current_user + station = self.store.voting_stations.get(v.station_id) + station_label = station.name if station else "Unknown" + print() + fields = [ + ("Name", v.full_name), + ("National ID", v.national_id), + ("Voter Card", f"{BRIGHT_YELLOW}{v.voter_card_number}{RESET}"), + ("Date of Birth", v.date_of_birth), + ("Age", v.age), + ("Gender", v.gender), + ("Address", v.address), + ("Phone", v.phone), + ("Email", v.email), + ("Station", station_label), + ("Verified", status_badge("Yes", True) if v.is_verified else status_badge("No", False)), + ("Registered", v.registered_at), + ("Polls Voted", len(v.has_voted_in)), + ] + for label, value in fields: + print(f" {THEME_VOTER}{label + ':':<16}{RESET} {value}") + pause() + + def change_password(self): + clear_screen() + header("CHANGE PASSWORD", THEME_VOTER) + voter = self.session.current_user + print() + old_pass = masked_input("Current Password: ").strip() + new_pass = masked_input("New Password: ").strip() + confirm_pass = masked_input("Confirm New Password: ").strip() + try: + self.voter_svc.change_password(voter.id, old_pass, new_pass, confirm_pass) + # Keep session in sync + self.session.current_user.password = self.store.voters[voter.id].password + print() + success("Password changed successfully!") + except ValueError as e: + error(str(e)) + pause()