Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions evoting_refactored (1)/evoting_data.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
125 changes: 125 additions & 0 deletions evoting_refactored (1)/evoting_refactored/evoting/README.md
Original file line number Diff line number Diff line change
@@ -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
Binary file not shown.
37 changes: 37 additions & 0 deletions evoting_refactored (1)/evoting_refactored/evoting/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
Application-wide constants and configuration.
"""

MIN_CANDIDATE_AGE = 25
MAX_CANDIDATE_AGE = 75
MIN_VOTER_AGE = 18
MIN_PASSWORD_LENGTH = 6

REQUIRED_EDUCATION_LEVELS = [
"Bachelor's Degree",
"Master's Degree",
"PhD",
"Doctorate",
]

ADMIN_ROLES = {
"1": "super_admin",
"2": "election_officer",
"3": "station_manager",
"4": "auditor",
}

ADMIN_ROLE_LABELS = {
"super_admin": "Full access",
"election_officer": "Manage polls and candidates",
"station_manager": "Manage stations and verify voters",
"auditor": "Read-only access",
}

ELECTION_TYPES = ["General", "Primary", "By-election", "Referendum"]
POSITION_LEVELS = ["national", "regional", "local"]
DATA_FILE = "evoting_data.json"
DEFAULT_ADMIN_USERNAME = "admin"
DEFAULT_ADMIN_PASSWORD = "admin123"
DEFAULT_ADMIN_FULL_NAME = "System Administrator"
DEFAULT_ADMIN_EMAIL = "admin@evote.com"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# data package
Binary file not shown.
Binary file not shown.
138 changes: 138 additions & 0 deletions evoting_refactored (1)/evoting_refactored/evoting/data/storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""
Data storage layer — holds all application state and handles
JSON persistence. This is the single source of truth for all data.
"""

import datetime
import json
import os

from config import DATA_FILE, DEFAULT_ADMIN_USERNAME, DEFAULT_ADMIN_PASSWORD, DEFAULT_ADMIN_FULL_NAME, DEFAULT_ADMIN_EMAIL
from models.admin import Admin
from models.candidate import Candidate
from models.voter import Voter
from models.station import VotingStation
from models.poll import Poll, Position
from models.vote import Vote
from services.auth_service import hash_password


class Store:
"""Central data store for the entire application."""

def __init__(self):
self.candidates: dict[int, Candidate] = {}
self.candidate_id_counter: int = 1

self.voting_stations: dict[int, VotingStation] = {}
self.station_id_counter: int = 1

self.polls: dict[int, Poll] = {}
self.poll_id_counter: int = 1

self.positions: dict[int, Position] = {}
self.position_id_counter: int = 1

self.voters: dict[int, Voter] = {}
self.voter_id_counter: int = 1

self.admins: dict[int, Admin] = {}
self.admin_id_counter: int = 1

self.votes: list[Vote] = []
self.audit_log: list[dict] = []

self._seed_default_admin()

def _seed_default_admin(self):
"""Create the default super admin account on first run."""
self.admins[1] = Admin(
id=1,
username=DEFAULT_ADMIN_USERNAME,
password=hash_password(DEFAULT_ADMIN_PASSWORD),
full_name=DEFAULT_ADMIN_FULL_NAME,
email=DEFAULT_ADMIN_EMAIL,
role="super_admin",
created_at=str(datetime.datetime.now()),
is_active=True,
)
self.admin_id_counter = 2

def save(self):
"""Serialize all state to JSON and write to disk."""
data = {
"candidates": {k: v.to_dict() for k, v in self.candidates.items()},
"candidate_id_counter": self.candidate_id_counter,
"voting_stations": {k: v.to_dict() for k, v in self.voting_stations.items()},
"station_id_counter": self.station_id_counter,
"polls": {k: v.to_dict() for k, v in self.polls.items()},
"poll_id_counter": self.poll_id_counter,
"positions": {k: v.to_dict() for k, v in self.positions.items()},
"position_id_counter": self.position_id_counter,
"voters": {k: v.to_dict() for k, v in self.voters.items()},
"voter_id_counter": self.voter_id_counter,
"admins": {k: v.to_dict() for k, v in self.admins.items()},
"admin_id_counter": self.admin_id_counter,
"votes": [v.to_dict() for v in self.votes],
"audit_log": self.audit_log,
}
try:
with open(DATA_FILE, "w") as f:
json.dump(data, f, indent=2)
except Exception as e:
print(f" Error saving data: {e}")

def load(self):
"""Load and deserialize state from JSON file if it exists."""
if not os.path.exists(DATA_FILE):
return

try:
with open(DATA_FILE, "r") as f:
data = json.load(f)

self.candidates = {
int(k): Candidate.from_dict(v)
for k, v in data.get("candidates", {}).items()
}
self.candidate_id_counter = data.get("candidate_id_counter", 1)

self.voting_stations = {
int(k): VotingStation.from_dict(v)
for k, v in data.get("voting_stations", {}).items()
}
self.station_id_counter = data.get("station_id_counter", 1)

self.polls = {
int(k): Poll.from_dict(v)
for k, v in data.get("polls", {}).items()
}
self.poll_id_counter = data.get("poll_id_counter", 1)

self.positions = {
int(k): Position.from_dict(v)
for k, v in data.get("positions", {}).items()
}
self.position_id_counter = data.get("position_id_counter", 1)

self.voters = {
int(k): Voter.from_dict(v)
for k, v in data.get("voters", {}).items()
}
self.voter_id_counter = data.get("voter_id_counter", 1)

self.admins = {
int(k): Admin.from_dict(v)
for k, v in data.get("admins", {}).items()
}
self.admin_id_counter = data.get("admin_id_counter", 1)

self.votes = [Vote.from_dict(v) for v in data.get("votes", [])]
self.audit_log = data.get("audit_log", [])

except Exception as e:
print(f" Error loading data: {e}")


# Singleton store instance used across the application
store = Store()
Loading