Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16066e4ba4 | ||
|
|
e083afc1d5 | ||
|
|
3e74a9de32 |
Binary file not shown.
@ -26,6 +26,12 @@ ALLOWED_HOSTS = [
|
|||||||
os.getenv("HOST_FQDN", ""),
|
os.getenv("HOST_FQDN", ""),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Ensure Django knows it's behind a reverse proxy and using HTTPS
|
||||||
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
|
USE_X_FORWARDED_HOST = True
|
||||||
|
USE_X_FORWARDED_PORT = True
|
||||||
|
SECURE_SSL_REDIRECT = True
|
||||||
|
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
origin for origin in [
|
origin for origin in [
|
||||||
os.getenv("HOST_FQDN", ""),
|
os.getenv("HOST_FQDN", ""),
|
||||||
@ -37,14 +43,12 @@ CSRF_TRUSTED_ORIGINS = [
|
|||||||
for host in CSRF_TRUSTED_ORIGINS
|
for host in CSRF_TRUSTED_ORIGINS
|
||||||
]
|
]
|
||||||
|
|
||||||
# Cookies must always be HTTPS-only; SameSite=Lax keeps CSRF working behind the proxy.
|
# Cookies must always be HTTPS-only
|
||||||
SESSION_COOKIE_SECURE = True
|
SESSION_COOKIE_SECURE = True
|
||||||
CSRF_COOKIE_SECURE = True
|
CSRF_COOKIE_SECURE = True
|
||||||
SESSION_COOKIE_SAMESITE = "None"
|
SESSION_COOKIE_SAMESITE = "Lax"
|
||||||
CSRF_COOKIE_SAMESITE = "None"
|
CSRF_COOKIE_SAMESITE = "Lax"
|
||||||
|
CSRF_USE_SESSIONS = True
|
||||||
# Quick-start development settings - unsuitable for production
|
|
||||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
@ -65,11 +69,10 @@ MIDDLEWARE = [
|
|||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
# Disable X-Frame-Options middleware to allow Flatlogic preview iframes.
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
X_FRAME_OPTIONS = 'ALLOWALL'
|
X_FRAME_OPTIONS = 'SAMEORIGIN'
|
||||||
|
|
||||||
ROOT_URLCONF = 'config.urls'
|
ROOT_URLCONF = 'config.urls'
|
||||||
|
|
||||||
@ -151,8 +154,6 @@ STATIC_ROOT = BASE_DIR / 'staticfiles'
|
|||||||
|
|
||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [
|
||||||
BASE_DIR / 'static',
|
BASE_DIR / 'static',
|
||||||
BASE_DIR / 'assets',
|
|
||||||
BASE_DIR / 'node_modules',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Email
|
# Email
|
||||||
@ -180,3 +181,6 @@ if EMAIL_USE_SSL:
|
|||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
LOGIN_REDIRECT_URL = 'home'
|
||||||
|
LOGOUT_REDIRECT_URL = 'home'
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,3 +1,17 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from .models import MemberOfParliament, TradeDisclosure, Watchlist
|
||||||
|
|
||||||
# Register your models here.
|
@admin.register(MemberOfParliament)
|
||||||
|
class MemberOfParliamentAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'party', 'constituency', 'province')
|
||||||
|
search_fields = ('name', 'party', 'constituency')
|
||||||
|
|
||||||
|
@admin.register(TradeDisclosure)
|
||||||
|
class TradeDisclosureAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('mp', 'ticker', 'trade_type', 'amount_range', 'disclosure_date')
|
||||||
|
list_filter = ('trade_type', 'disclosure_date', 'mp__party')
|
||||||
|
search_fields = ('ticker', 'company_name', 'mp__name')
|
||||||
|
|
||||||
|
@admin.register(Watchlist)
|
||||||
|
class WatchlistAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('user', 'mp', 'ticker', 'created_at')
|
||||||
@ -1,6 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
# Calculate once when the module is loaded (e.g. at server start)
|
||||||
|
DEPLOYMENT_TIMESTAMP = int(time.time())
|
||||||
|
|
||||||
def project_context(request):
|
def project_context(request):
|
||||||
"""
|
"""
|
||||||
Adds project-specific environment variables to the template context globally.
|
Adds project-specific environment variables to the template context globally.
|
||||||
@ -8,6 +11,6 @@ def project_context(request):
|
|||||||
return {
|
return {
|
||||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||||
# Used for cache-busting static assets
|
# Used for cache-busting static assets - stable until server restart
|
||||||
"deployment_timestamp": int(time.time()),
|
"deployment_timestamp": DEPLOYMENT_TIMESTAMP,
|
||||||
}
|
}
|
||||||
0
core/management/__init__.py
Normal file
0
core/management/__init__.py
Normal file
BIN
core/management/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
core/management/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
0
core/management/commands/__init__.py
Normal file
0
core/management/commands/__init__.py
Normal file
BIN
core/management/commands/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
core/management/commands/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/management/commands/__pycache__/import_mps.cpython-311.pyc
Normal file
BIN
core/management/commands/__pycache__/import_mps.cpython-311.pyc
Normal file
Binary file not shown.
33
core/management/commands/import_mps.py
Normal file
33
core/management/commands/import_mps.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import csv
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from core.models import MemberOfParliament
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Import MPs from CSV'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
file_path = 'mps.csv'
|
||||||
|
try:
|
||||||
|
with open(file_path, mode='r', encoding='utf-8') as file:
|
||||||
|
reader = csv.DictReader(file)
|
||||||
|
count = 0
|
||||||
|
for row in reader:
|
||||||
|
name = f"{row['First Name']} {row['Last Name']}"
|
||||||
|
party = row['Political Affiliation']
|
||||||
|
constituency = row['Constituency']
|
||||||
|
province = row['Province / Territory']
|
||||||
|
|
||||||
|
MemberOfParliament.objects.update_or_create(
|
||||||
|
name=name,
|
||||||
|
defaults={
|
||||||
|
'party': party,
|
||||||
|
'constituency': constituency,
|
||||||
|
'province': province,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
count += 1
|
||||||
|
self.stdout.write(self.style.SUCCESS(f'Successfully imported {count} MPs'))
|
||||||
|
except FileNotFoundError:
|
||||||
|
self.stdout.write(self.style.ERROR(f'File {file_path} not found'))
|
||||||
|
except Exception as e:
|
||||||
|
self.stdout.write(self.style.ERROR(f'Error: {str(e)}'))
|
||||||
54
core/migrations/0001_initial.py
Normal file
54
core/migrations/0001_initial.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-04 15:06
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='MemberOfParliament',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=255)),
|
||||||
|
('party', models.CharField(max_length=100)),
|
||||||
|
('constituency', models.CharField(max_length=255)),
|
||||||
|
('province', models.CharField(max_length=100)),
|
||||||
|
('image_url', models.URLField(blank=True, null=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='TradeDisclosure',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('ticker', models.CharField(max_length=20)),
|
||||||
|
('company_name', models.CharField(max_length=255)),
|
||||||
|
('trade_type', models.CharField(choices=[('BUY', 'Buy'), ('SELL', 'Sell'), ('HOLD', 'Hold')], max_length=10)),
|
||||||
|
('amount_range', models.CharField(max_length=100)),
|
||||||
|
('disclosure_date', models.DateField()),
|
||||||
|
('transaction_date', models.DateField(blank=True, null=True)),
|
||||||
|
('mp', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trades', to='core.memberofparliament')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Watchlist',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('ticker', models.CharField(blank=True, max_length=20, null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('mp', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.memberofparliament')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='watchlist', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('user', 'mp', 'ticker')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
@ -1,3 +1,39 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
# Create your models here.
|
class MemberOfParliament(models.Model):
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
party = models.CharField(max_length=100)
|
||||||
|
constituency = models.CharField(max_length=255)
|
||||||
|
province = models.CharField(max_length=100)
|
||||||
|
image_url = models.URLField(blank=True, null=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class TradeDisclosure(models.Model):
|
||||||
|
TRADE_TYPES = (
|
||||||
|
('BUY', 'Buy'),
|
||||||
|
('SELL', 'Sell'),
|
||||||
|
('HOLD', 'Hold'),
|
||||||
|
)
|
||||||
|
|
||||||
|
mp = models.ForeignKey(MemberOfParliament, on_delete=models.CASCADE, related_name='trades')
|
||||||
|
ticker = models.CharField(max_length=20)
|
||||||
|
company_name = models.CharField(max_length=255)
|
||||||
|
trade_type = models.CharField(max_length=10, choices=TRADE_TYPES)
|
||||||
|
amount_range = models.CharField(max_length=100) # e.g. "$15,001 - $50,000"
|
||||||
|
disclosure_date = models.DateField()
|
||||||
|
transaction_date = models.DateField(null=True, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.mp.name} - {self.ticker} ({self.trade_type})"
|
||||||
|
|
||||||
|
class Watchlist(models.Model):
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='watchlist')
|
||||||
|
mp = models.ForeignKey(MemberOfParliament, on_delete=models.CASCADE, blank=True, null=True)
|
||||||
|
ticker = models.CharField(max_length=20, blank=True, null=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('user', 'mp', 'ticker')
|
||||||
@ -1,25 +1,212 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
{% if project_description %}
|
<title>{% block title %}{{ project_name }}{% endblock %}</title>
|
||||||
<meta name="description" content="{{ project_description }}">
|
{% if project_description %}<meta name="description" content="{{ project_description }}">{% endif %}
|
||||||
<meta property="og:description" content="{{ project_description }}">
|
|
||||||
<meta property="twitter:description" content="{{ project_description }}">
|
<!-- Bootstrap 5 CSS -->
|
||||||
{% endif %}
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
{% if project_image_url %}
|
<!-- Google Fonts -->
|
||||||
<meta property="og:image" content="{{ project_image_url }}">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Lexend:wght@400;600;700&display=swap" rel="stylesheet">
|
||||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
<!-- Bootstrap Icons -->
|
||||||
{% endif %}
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||||
{% load static %}
|
<!-- Font Awesome -->
|
||||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
{% block head %}{% endblock %}
|
|
||||||
|
{% load static %}
|
||||||
|
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-dark: #0a0b0d;
|
||||||
|
--card-bg: #1e2126;
|
||||||
|
--accent-green: #00ffad;
|
||||||
|
--accent-red: #ff4d4d;
|
||||||
|
--text-main: #ffffff;
|
||||||
|
--text-muted: #94a3b8;
|
||||||
|
--border-color: #2d333b;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--bg-dark);
|
||||||
|
color: var(--text-main);
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, .navbar-brand {
|
||||||
|
font-family: 'Lexend', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
background-color: rgba(10, 11, 13, 0.8);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--accent-green) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
color: var(--text-muted) !important;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover, .nav-link.active {
|
||||||
|
color: var(--text-main) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary-custom {
|
||||||
|
background-color: var(--accent-green);
|
||||||
|
color: var(--bg-dark);
|
||||||
|
font-weight: 600;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary-custom:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 255, 173, 0.3);
|
||||||
|
background-color: #00e69b;
|
||||||
|
color: var(--bg-dark);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{% block content %}{% endblock %}
|
<nav class="navbar navbar-expand-lg sticky-top">
|
||||||
</body>
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="{% url 'home' %}">
|
||||||
|
<i class="fa-solid fa-chart-line me-2"></i>MP TRADE TRACKER
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav me-auto">
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{% url 'home' %}">Dashboard</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{% url 'mp_list' %}">MPs</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{% url 'ticker_list' %}">Tickers</a></li>
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{% url 'profile' %}">Watchlist</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<div class="dropdown me-3">
|
||||||
|
<button class="btn btn-link nav-link dropdown-toggle d-flex align-items-center" type="button" id="userDropdown" data-bs-toggle="dropdown">
|
||||||
|
<i class="bi bi-person-circle me-2"></i>{{ user.username }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-dark dropdown-menu-end shadow">
|
||||||
|
<li><a class="dropdown-item" href="{% url 'profile' %}"><i class="bi bi-person me-2"></i>Profile</a></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li>
|
||||||
|
<form action="{% url 'logout' %}" method="post" class="dropdown-item p-0">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-link dropdown-item py-2 px-3 m-0 border-0"><i class="bi bi-box-arrow-right me-2"></i>Logout</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'login' %}" class="nav-link me-3">Login</a>
|
||||||
|
<a href="{% url 'signup' %}" class="btn btn-primary-custom">Get Started</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="py-4 mt-5 border-top border-secondary">
|
||||||
|
<div class="container text-center">
|
||||||
|
<p class="text-muted small mb-0">© 2024 Canada MP Trade Tracker. For reconnaissance purposes only.</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function getCookie(name) {
|
||||||
|
let cookieValue = null;
|
||||||
|
if (document.cookie && document.cookie !== '') {
|
||||||
|
const cookies = document.cookie.split(';');
|
||||||
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
|
const cookie = cookies[i].trim();
|
||||||
|
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||||
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const csrftoken = getCookie('csrftoken');
|
||||||
|
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
const btn = e.target.closest('.follow-btn');
|
||||||
|
if (btn) {
|
||||||
|
const mpId = btn.getAttribute('data-mp-id');
|
||||||
|
const ticker = btn.getAttribute('data-ticker');
|
||||||
|
|
||||||
|
{% if not user.is_authenticated %}
|
||||||
|
window.location.href = "{% url 'login' %}";
|
||||||
|
return;
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
if (mpId) formData.append('mp_id', mpId);
|
||||||
|
if (ticker) formData.append('ticker', ticker);
|
||||||
|
|
||||||
|
fetch("{% url 'toggle_follow' %}", {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': csrftoken,
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
// Custom event for specific page handling
|
||||||
|
const event = new CustomEvent('followToggled', { detail: { data, btn } });
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
|
||||||
|
if (data.status === 'followed') {
|
||||||
|
if (btn.classList.contains('btn-outline-light')) {
|
||||||
|
btn.innerText = 'Following';
|
||||||
|
btn.classList.remove('btn-outline-light');
|
||||||
|
btn.classList.add('btn-success');
|
||||||
|
}
|
||||||
|
} else if (data.status === 'unfollowed') {
|
||||||
|
if (btn.classList.contains('btn-success')) {
|
||||||
|
btn.innerText = 'Follow';
|
||||||
|
btn.classList.remove('btn-success');
|
||||||
|
btn.classList.add('btn-outline-light');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error toggling follow:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,145 +1,184 @@
|
|||||||
{% extends "base.html" %}
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}{{ project_name }}{% endblock %}
|
{% block title %}Dashboard | {{ project_name }}{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--bg-color-start: #6a11cb;
|
|
||||||
--bg-color-end: #2575fc;
|
|
||||||
--text-color: #ffffff;
|
|
||||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
|
||||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
|
||||||
color: var(--text-color);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
text-align: center;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
body::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
|
||||||
animation: bg-pan 20s linear infinite;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bg-pan {
|
|
||||||
0% {
|
|
||||||
background-position: 0% 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
background-position: 100% 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: var(--card-bg-color);
|
|
||||||
border: 1px solid var(--card-border-color);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 2.5rem 2rem;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0 0 1.2rem;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
opacity: 0.92;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader {
|
|
||||||
margin: 1.5rem auto;
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
|
||||||
border-top-color: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.runtime code {
|
|
||||||
background: rgba(0, 0, 0, 0.25);
|
|
||||||
padding: 0.15rem 0.45rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sr-only {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
padding: 0;
|
|
||||||
margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main>
|
<div class="container py-5">
|
||||||
<div class="card">
|
<!-- Hero / Market Overview -->
|
||||||
<h1>Analyzing your requirements and generating your app…</h1>
|
<div class="row mb-5">
|
||||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
<div class="col-lg-8">
|
||||||
<span class="sr-only">Loading…</span>
|
<h1 class="display-5 fw-bold mb-3">Track Parliament's Portfolio</h1>
|
||||||
|
<p class="lead text-muted">Real-time reconnaissance of investment disclosures by Canadian Members of Parliament. See what they know, when they know it.</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4 d-flex align-items-center justify-content-lg-end">
|
||||||
|
<div class="stats-card p-4 rounded-4 w-100">
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span class="text-muted">Total Disclosures</span>
|
||||||
|
<span class="text-success fw-bold">Live</span>
|
||||||
|
</div>
|
||||||
|
<h2 class="mb-0">{{ total_trades }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
|
|
||||||
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
|
<div class="row g-4">
|
||||||
<p class="runtime">
|
<!-- Live Trade Feed -->
|
||||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
<div class="col-lg-8">
|
||||||
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
<div class="card bg-card border-0 rounded-4 p-4">
|
||||||
</p>
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
</div>
|
<h3 class="h5 mb-0">Live Disclosure Feed</h3>
|
||||||
</main>
|
<div class="dropdown">
|
||||||
<footer>
|
<button class="btn btn-dark btn-sm dropdown-toggle" type="button" id="partyFilterBtn" data-bs-toggle="dropdown">
|
||||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
{% if selected_party %}{{ selected_party }}{% else %}All Parties{% endif %}
|
||||||
</footer>
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-dark">
|
||||||
|
<li><a class="dropdown-item party-filter" href="#" data-party="">All Parties</a></li>
|
||||||
|
<li><a class="dropdown-item party-filter" href="#" data-party="Liberal">Liberal</a></li>
|
||||||
|
<li><a class="dropdown-item party-filter" href="#" data-party="Conservative">Conservative</a></li>
|
||||||
|
<li><a class="dropdown-item party-filter" href="#" data-party="NDP">NDP</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="trade-feed-container">
|
||||||
|
{% include 'core/partials/trade_feed.html' %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<a href="{% url 'mp_list' %}" class="btn btn-dark rounded-pill px-4">View All {{ total_mps }} MPs</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar: Trending / Watchlist -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card bg-card border-0 rounded-4 p-4 mb-4">
|
||||||
|
<h3 class="h5 mb-4">Trending Assets</h3>
|
||||||
|
<div class="list-group list-group-flush bg-transparent">
|
||||||
|
{% for asset in trending_assets %}
|
||||||
|
<a href="{% url 'ticker_detail' asset.ticker %}" class="list-group-item bg-transparent border-secondary border-opacity-25 px-0 py-3 d-flex justify-content-between align-items-center text-decoration-none text-white transition-hover">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="ticker-icon bg-primary-subtle text-primary me-3">{{ asset.ticker|slice:":1" }}</div>
|
||||||
|
<div>
|
||||||
|
<div class="fw-bold">{{ asset.ticker }}</div>
|
||||||
|
<div class="small text-muted text-truncate" style="max-width: 150px;">{{ asset.company_name }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<div class="fw-bold">{{ asset.trade_count }} Trades</div>
|
||||||
|
<div class="small text-success">Active</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% empty %}
|
||||||
|
<p class="text-muted small">No trending data yet.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'ticker_list' %}" class="btn btn-dark w-100 mt-3 rounded-3">View All Markets</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-primary-custom border-0 rounded-4 p-4 text-dark shadow-lg">
|
||||||
|
<h3 class="h5 mb-3 fw-bold">Start Your Portfolio</h3>
|
||||||
|
<p class="small mb-4 opacity-75">Connect your account to set alerts and follow specific MPs or tickers.</p>
|
||||||
|
{% if not user.is_authenticated %}
|
||||||
|
<a href="{% url 'signup' %}" class="btn btn-dark w-100 rounded-3 fw-bold">Create Free Account</a>
|
||||||
|
{% else %}
|
||||||
|
<p class="fw-bold mb-0 text-dark">Welcome back, {{ user.username }}!</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bg-card {
|
||||||
|
background-color: var(--card-bg) !important;
|
||||||
|
}
|
||||||
|
.stats-card {
|
||||||
|
background: linear-gradient(135deg, #1e2126 0%, #0a0b0d 100%);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
.bg-primary-custom {
|
||||||
|
background-color: var(--accent-green) !important;
|
||||||
|
}
|
||||||
|
.custom-table {
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0 10px;
|
||||||
|
}
|
||||||
|
.custom-table tr {
|
||||||
|
background-color: rgba(255, 255, 255, 0.02);
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
.custom-table tr:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05) !important;
|
||||||
|
}
|
||||||
|
.custom-table td, .custom-table th {
|
||||||
|
border: none;
|
||||||
|
padding: 1.25rem 1rem;
|
||||||
|
}
|
||||||
|
.avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background-color: var(--border-color);
|
||||||
|
color: var(--accent-green);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'Lexend', sans-serif;
|
||||||
|
}
|
||||||
|
.ticker-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
background-color: rgba(255,255,255,0.05) !important;
|
||||||
|
}
|
||||||
|
.transition-hover:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05) !important;
|
||||||
|
padding-left: 5px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const filterLinks = document.querySelectorAll('.party-filter');
|
||||||
|
const container = document.getElementById('trade-feed-container');
|
||||||
|
const filterBtn = document.getElementById('partyFilterBtn');
|
||||||
|
|
||||||
|
filterLinks.forEach(link => {
|
||||||
|
link.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const party = this.getAttribute('data-party');
|
||||||
|
const url = party ? `/?party=${party}` : '/';
|
||||||
|
|
||||||
|
// Update button text
|
||||||
|
filterBtn.innerText = party ? party : 'All Parties';
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
container.style.opacity = '0.5';
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(html => {
|
||||||
|
container.innerHTML = html;
|
||||||
|
container.style.opacity = '1';
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching filtered data:', error);
|
||||||
|
container.style.opacity = '1';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
136
core/templates/core/mp_detail.html
Normal file
136
core/templates/core/mp_detail.html
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{{ mp.name }} | {{ project_name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'home' %}" class="text-muted">Home</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'mp_list' %}" class="text-muted">MPs</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">{{ mp.name }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- MP Profile Card -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card bg-card border-0 rounded-4 p-4 sticky-top" style="top: 2rem;">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<div class="avatar-large mx-auto mb-3">{{ mp.name|slice:":1" }}</div>
|
||||||
|
<h1 class="h3 fw-bold mb-1">{{ mp.name }}</h1>
|
||||||
|
<div class="badge bg-dark px-3 py-2 rounded-pill mb-3">{{ mp.party }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="border-secondary opacity-25">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="small text-muted d-block mb-1">Constituency</label>
|
||||||
|
<div class="fw-bold">{{ mp.constituency }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="small text-muted d-block mb-1">Province</label>
|
||||||
|
<div class="fw-bold">{{ mp.province }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="small text-muted d-block mb-1">Total Disclosures</label>
|
||||||
|
<div class="fw-bold h4 text-success">{{ trades.count }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn {% if is_followed %}btn-success{% else %}btn-primary-custom{% endif %} w-100 rounded-3 fw-bold py-3 mb-2 follow-btn"
|
||||||
|
data-mp-id="{{ mp.id }}">
|
||||||
|
{% if is_followed %}Following{% else %}Follow {{ mp.name }}{% endif %}
|
||||||
|
</button>
|
||||||
|
<p class="small text-muted text-center mb-0">Get notified of new disclosures</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Trade History -->
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card bg-card border-0 rounded-4 p-4">
|
||||||
|
<h3 class="h5 mb-4">Disclosure History</h3>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-dark table-hover align-middle custom-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-muted fw-normal">Ticker</th>
|
||||||
|
<th class="text-muted fw-normal">Type</th>
|
||||||
|
<th class="text-muted fw-normal">Amount</th>
|
||||||
|
<th class="text-muted fw-normal">Date</th>
|
||||||
|
<th class="text-end"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for trade in trades %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'ticker_detail' trade.ticker %}" class="text-decoration-none text-white">
|
||||||
|
<div class="fw-bold">{{ trade.ticker }}</div>
|
||||||
|
<div class="small text-muted">{{ trade.company_name }}</div>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if trade.trade_type == 'BUY' %}
|
||||||
|
<span class="badge bg-success-subtle text-success border border-success px-3 py-2">BUY</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger-subtle text-danger border border-danger px-3 py-2">SELL</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ trade.amount_range }}</td>
|
||||||
|
<td>{{ trade.disclosure_date|date:"M d, Y" }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a href="#" class="btn btn-dark btn-sm rounded-circle"><i class="bi bi-info-circle"></i></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center py-5 text-muted">No disclosures reported for this MP.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bg-card {
|
||||||
|
background-color: var(--card-bg) !important;
|
||||||
|
}
|
||||||
|
.bg-primary-custom {
|
||||||
|
background-color: var(--accent-green) !important;
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.avatar-large {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
background-color: var(--border-color);
|
||||||
|
color: var(--accent-green);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-family: 'Lexend', sans-serif;
|
||||||
|
}
|
||||||
|
.custom-table tr {
|
||||||
|
background-color: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
.custom-table td {
|
||||||
|
padding: 1.25rem 1rem;
|
||||||
|
}
|
||||||
|
.breadcrumb-item + .breadcrumb-item::before {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
70
core/templates/core/mp_list.html
Normal file
70
core/templates/core/mp_list.html
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Members of Parliament | {{ project_name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="d-flex justify-content-between align-items-end mb-5">
|
||||||
|
<div>
|
||||||
|
<h1 class="display-5 fw-bold mb-3">Members of Parliament</h1>
|
||||||
|
<p class="lead text-muted mb-0">Full roster of Canadian MPs and their political affiliations.</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<span class="badge bg-dark px-3 py-2 rounded-pill border border-secondary">Total: {{ mps.count }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
{% for mp in mps %}
|
||||||
|
<div class="col-md-6 col-lg-4 col-xl-3">
|
||||||
|
<div class="card bg-card border-0 rounded-4 p-4 h-100 transition-hover">
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<div class="avatar-large me-3">{{ mp.name|slice:":1" }}</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="h6 mb-1 fw-bold">{{ mp.name }}</h3>
|
||||||
|
<div class="badge {% if mp.party == 'Liberal' %}bg-danger{% elif mp.party == 'Conservative' %}bg-primary{% elif mp.party == 'NDP' %}bg-warning text-dark{% else %}bg-secondary{% endif %} small">
|
||||||
|
{{ mp.party }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-auto pt-3 border-top border-secondary border-opacity-25">
|
||||||
|
<div class="small text-muted mb-1">Constituency</div>
|
||||||
|
<div class="small fw-semibold">{{ mp.constituency }}</div>
|
||||||
|
<div class="small text-muted mt-2">{{ mp.province }}</div>
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'mp_detail' mp.pk %}" class="stretched-link"></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="col-12 text-center py-5">
|
||||||
|
<p class="text-muted">No MPs found in the database.</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bg-card {
|
||||||
|
background-color: var(--card-bg) !important;
|
||||||
|
transition: transform 0.2s ease, background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
.transition-hover:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
background-color: rgba(255, 255, 255, 0.05) !important;
|
||||||
|
}
|
||||||
|
.avatar-large {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
background-color: var(--border-color);
|
||||||
|
color: var(--accent-green);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-family: 'Lexend', sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
58
core/templates/core/partials/trade_feed.html
Normal file
58
core/templates/core/partials/trade_feed.html
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-dark table-hover align-middle custom-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-muted fw-normal">MP</th>
|
||||||
|
<th class="text-muted fw-normal">Ticker</th>
|
||||||
|
<th class="text-muted fw-normal">Type</th>
|
||||||
|
<th class="text-muted fw-normal">Amount</th>
|
||||||
|
<th class="text-muted fw-normal">Date</th>
|
||||||
|
<th class="text-end"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="trade-feed-body">
|
||||||
|
{% for trade in trades %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'mp_detail' trade.mp.pk %}" class="text-decoration-none text-white d-flex align-items-center">
|
||||||
|
<div class="avatar me-3">{{ trade.mp.name|slice:":1" }}</div>
|
||||||
|
<div>
|
||||||
|
<div class="fw-bold">{{ trade.mp.name }}</div>
|
||||||
|
<div class="small text-muted">{{ trade.mp.party }}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'ticker_detail' trade.ticker %}" class="text-decoration-none text-white">
|
||||||
|
<div class="fw-bold">{{ trade.ticker }}</div>
|
||||||
|
<div class="small text-muted">{{ trade.company_name }}</div>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if trade.trade_type == 'BUY' %}
|
||||||
|
<span class="badge bg-success-subtle text-success border border-success px-3 py-2">BUY</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger-subtle text-danger border border-danger px-3 py-2">SELL</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ trade.amount_range }}</td>
|
||||||
|
<td>{{ trade.disclosure_date|date:"M d, Y" }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<button class="btn {% if trade.mp.id in followed_mps %}btn-success{% else %}btn-outline-light{% endif %} btn-sm rounded-pill px-3 follow-btn"
|
||||||
|
data-mp-id="{{ trade.mp.id }}">
|
||||||
|
{% if trade.mp.id in followed_mps %}Following{% else %}Follow{% endif %}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="text-center py-5 text-muted">No disclosures found for this filter.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Re-attach listeners after partial load if needed, or use delegation in the main file
|
||||||
|
</script>
|
||||||
226
core/templates/core/profile.html
Normal file
226
core/templates/core/profile.html
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col">
|
||||||
|
<h1 class="display-5 fw-bold text-white">Portfolio Overview</h1>
|
||||||
|
<p class="text-muted">Tracking your followed politicians and assets.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Performance Summary Cards -->
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card border-0 shadow-sm bg-primary text-white h-100">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h6 class="text-uppercase opacity-75 small fw-bold mb-3">Avg Success Rate</h6>
|
||||||
|
<div class="d-flex align-items-baseline">
|
||||||
|
<h2 class="display-6 fw-bold mb-0">{{ performance.avg_success }}%</h2>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 small">
|
||||||
|
<span class="badge bg-white text-primary">High Accuracy</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card border-0 shadow-sm h-100" style="background-color: var(--card-bg); border: 1px solid var(--border-color) !important;">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h6 class="text-uppercase text-muted small fw-bold mb-3">Est. Annual ROI</h6>
|
||||||
|
<div class="d-flex align-items-baseline">
|
||||||
|
<h2 class="display-6 fw-bold mb-0 text-success">+{{ performance.estimated_roi }}%</h2>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 small text-muted">
|
||||||
|
Based on historical signals
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card border-0 shadow-sm h-100" style="background-color: var(--card-bg); border: 1px solid var(--border-color) !important;">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h6 class="text-uppercase text-muted small fw-bold mb-3">Trades Tracked</h6>
|
||||||
|
<div class="d-flex align-items-baseline">
|
||||||
|
<h2 class="display-6 fw-bold mb-0 text-white">{{ performance.total_trades }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 small text-muted">
|
||||||
|
Active disclosure monitoring
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card border-0 shadow-sm h-100" style="background-color: var(--card-bg); border: 1px solid var(--border-color) !important;">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h6 class="text-uppercase text-muted small fw-bold mb-3">Portfolio Status</h6>
|
||||||
|
<div class="d-flex align-items-baseline">
|
||||||
|
<h2 class="h3 fw-bold mb-0 text-info">{{ performance.portfolio_health }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 small text-muted">
|
||||||
|
Market sensitivity index
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Market Comparison -->
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card border-0 shadow-sm" style="background-color: var(--card-bg); border: 1px solid var(--border-color) !important;">
|
||||||
|
<div class="card-header bg-transparent border-bottom border-secondary py-3">
|
||||||
|
<h5 class="mb-0 fw-bold text-white">Market Comparison (Est. Annual ROI)</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
{% for item in market_comparison %}
|
||||||
|
<div class="col-md-3 mb-4 mb-md-0">
|
||||||
|
<div class="mb-2 d-flex justify-content-between">
|
||||||
|
<span class="text-white small fw-bold">{{ item.name }}</span>
|
||||||
|
<span class="fw-bold {% if item.roi > 0 %}text-success{% else %}text-danger{% endif %}">{{ item.roi }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress" style="height: 10px; background-color: #2d333b; border-radius: 10px;">
|
||||||
|
<div class="progress-bar {{ item.class }} {% if item.name == 'Your Portfolio' %}progress-bar-striped progress-bar-animated{% endif %}"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: {{ item.roi|floatformat:0 }}%; min-width: 5%; border-radius: 10px;"
|
||||||
|
aria-valuenow="{{ item.roi }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Followed Politicians -->
|
||||||
|
<div class="col-lg-7 mb-4">
|
||||||
|
<div class="card border-0 shadow-sm" style="background-color: var(--card-bg); border: 1px solid var(--border-color) !important;">
|
||||||
|
<div class="card-header bg-transparent border-bottom border-secondary py-3">
|
||||||
|
<h5 class="mb-0 fw-bold text-white">Watchlist: Politicians</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-dark table-hover align-middle mb-0" id="mp-watchlist-table" style="--bs-table-bg: transparent;">
|
||||||
|
<thead class="bg-dark">
|
||||||
|
<tr style="border-bottom: 2px solid var(--border-color);">
|
||||||
|
<th class="ps-4 text-muted small text-uppercase">Member of Parliament</th>
|
||||||
|
<th class="text-muted small text-uppercase">Success Rate</th>
|
||||||
|
<th class="text-muted small text-uppercase">Est. ROI</th>
|
||||||
|
<th class="text-end pe-4 text-muted small text-uppercase">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in followed_mps %}
|
||||||
|
<tr id="mp-row-{{ item.mp.id }}" style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td class="ps-4 py-3">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="flex-shrink-0 me-3">
|
||||||
|
<div class="rounded-circle bg-secondary d-flex align-items-center justify-content-center text-white fw-bold" style="width: 40px; height: 40px;">
|
||||||
|
{{ item.mp.name|slice:":1" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="{% url 'mp_detail' item.mp.id %}" class="text-white fw-bold text-decoration-none">{{ item.mp.name }}</a>
|
||||||
|
<div class="small text-muted">{{ item.mp.party }} • {{ item.trade_count }} trades</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span class="fw-bold me-2 text-white">{{ item.success_rate }}%</span>
|
||||||
|
<div class="progress flex-grow-1" style="height: 4px; max-width: 60px; background-color: #2d333b;">
|
||||||
|
<div class="progress-bar bg-success" role="progressbar" style="width: {{ item.success_rate }}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td><span class="text-success fw-bold">+{{ item.roi }}%</span></td>
|
||||||
|
<td class="text-end pe-4">
|
||||||
|
<button class="btn btn-sm btn-outline-danger follow-btn" data-mp-id="{{ item.mp.id }}">Unfollow</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center py-5 text-muted">
|
||||||
|
<i class="bi bi-people h1 d-block mb-3 opacity-25"></i>
|
||||||
|
No politicians followed yet.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Followed Tickers -->
|
||||||
|
<div class="col-lg-5 mb-4">
|
||||||
|
<div class="card border-0 shadow-sm" style="background-color: var(--card-bg); border: 1px solid var(--border-color) !important;">
|
||||||
|
<div class="card-header bg-transparent border-bottom border-secondary py-3">
|
||||||
|
<h5 class="mb-0 fw-bold text-white">Watchlist: Assets</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<ul class="list-group list-group-flush" id="ticker-watchlist-list">
|
||||||
|
{% for item in ticker_data %}
|
||||||
|
<li class="list-group-item bg-transparent p-3" id="ticker-row-{{ item.ticker }}" style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<a href="{% url 'ticker_detail' item.ticker %}" class="fw-bold text-info text-decoration-none">{{ item.ticker }}</a>
|
||||||
|
<div class="small text-muted">{{ item.company_name }}</div>
|
||||||
|
<div class="mt-1">
|
||||||
|
<span class="badge bg-dark text-muted border border-secondary small">{{ item.trade_count }} Signals</span>
|
||||||
|
<span class="text-success small fw-bold ms-2">+{{ item.roi }}% Est.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary follow-btn" data-ticker="{{ item.ticker }}">
|
||||||
|
<i class="bi bi-x-lg"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<li class="list-group-item bg-transparent text-center py-5 text-muted">
|
||||||
|
<i class="bi bi-graph-up h1 d-block mb-3 opacity-25"></i>
|
||||||
|
No assets followed yet.
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Listen for the custom event dispatched in base.html
|
||||||
|
document.addEventListener('followToggled', function(e) {
|
||||||
|
const data = e.detail.data;
|
||||||
|
if (data.status === 'unfollowed') {
|
||||||
|
const type = data.type;
|
||||||
|
let elementId;
|
||||||
|
if (type === 'mp') {
|
||||||
|
elementId = `mp-row-${data.mp_id}`;
|
||||||
|
} else {
|
||||||
|
elementId = `ticker-row-${data.ticker}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elementId) {
|
||||||
|
const el = document.getElementById(elementId);
|
||||||
|
if (el) {
|
||||||
|
el.style.transition = 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)';
|
||||||
|
el.style.opacity = '0';
|
||||||
|
el.style.transform = 'translateX(30px)';
|
||||||
|
setTimeout(() => {
|
||||||
|
el.remove();
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
129
core/templates/core/ticker_detail.html
Normal file
129
core/templates/core/ticker_detail.html
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{{ ticker }} | {{ project_name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'home' %}" class="text-muted">Home</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{% url 'ticker_list' %}" class="text-muted">Tickers</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">{{ ticker }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4 mb-5">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="d-flex align-items-center mb-4">
|
||||||
|
<div class="ticker-logo me-4">{{ ticker|slice:":1" }}</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="display-6 fw-bold mb-0">{{ ticker }}</h1>
|
||||||
|
<p class="lead text-muted mb-0">{{ company_name }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4 text-lg-end">
|
||||||
|
<button class="btn {% if is_followed %}btn-success{% else %}btn-outline-success{% endif %} btn-lg rounded-pill px-4 follow-btn"
|
||||||
|
data-ticker="{{ ticker }}">
|
||||||
|
<i class="bi {% if is_followed %}bi-star-fill{% else %}bi-star{% endif %} me-2"></i>
|
||||||
|
{% if is_followed %}Following Ticker{% else %}Watch Ticker{% endif %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<div class="card bg-card border-0 rounded-4 p-4">
|
||||||
|
<h3 class="h5 mb-4">MPs Trading {{ ticker }}</h3>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-dark table-hover align-middle custom-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-muted fw-normal">Member of Parliament</th>
|
||||||
|
<th class="text-muted fw-normal">Action</th>
|
||||||
|
<th class="text-muted fw-normal">Amount</th>
|
||||||
|
<th class="text-muted fw-normal">Disclosure Date</th>
|
||||||
|
<th class="text-end"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for trade in trades %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'mp_detail' trade.mp.pk %}" class="text-decoration-none text-white d-flex align-items-center">
|
||||||
|
<div class="avatar-sm me-3">{{ trade.mp.name|slice:":1" }}</div>
|
||||||
|
<div>
|
||||||
|
<div class="fw-bold">{{ trade.mp.name }}</div>
|
||||||
|
<div class="small text-muted">{{ trade.mp.party }}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if trade.trade_type == 'BUY' %}
|
||||||
|
<span class="badge bg-success-subtle text-success border border-success px-3 py-2">BUY</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger-subtle text-danger border border-danger px-3 py-2">SELL</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ trade.amount_range }}</td>
|
||||||
|
<td>{{ trade.disclosure_date|date:"M d, Y" }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a href="{% url 'mp_detail' trade.mp.pk %}" class="btn btn-dark btn-sm rounded-pill px-3">View Profile</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center py-5 text-muted">No trades found for this ticker.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bg-card {
|
||||||
|
background-color: var(--card-bg) !important;
|
||||||
|
}
|
||||||
|
.ticker-logo {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
background: linear-gradient(135deg, #2a2d34 0%, #1a1c21 100%);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--accent-green);
|
||||||
|
}
|
||||||
|
.avatar-sm {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background-color: var(--border-color);
|
||||||
|
color: var(--accent-green);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.custom-table tr {
|
||||||
|
background-color: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
.custom-table td {
|
||||||
|
padding: 1.25rem 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
110
core/templates/core/ticker_list.html
Normal file
110
core/templates/core/ticker_list.html
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Tickers | {{ project_name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<h1 class="display-5 fw-bold mb-3">Ticker Directory</h1>
|
||||||
|
<p class="lead text-muted">Browse every asset, stock, and security traded by Canadian Members of Parliament.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-card border-0 rounded-4 p-4">
|
||||||
|
<form method="get" class="row mb-4 g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text bg-dark border-0 text-muted"><i class="bi bi-search"></i></span>
|
||||||
|
<input type="text" name="q" class="form-control bg-dark border-0 text-white"
|
||||||
|
placeholder="Search by ticker or company..." value="{{ search_query }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<select class="form-select bg-dark border-0 text-white">
|
||||||
|
<option value="">All Sectors</option>
|
||||||
|
<option value="tech">Technology</option>
|
||||||
|
<option value="finance">Finance</option>
|
||||||
|
<option value="energy">Energy</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<button type="submit" class="btn btn-primary-custom w-100">Search</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-dark table-hover align-middle custom-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-muted fw-normal">Asset</th>
|
||||||
|
<th class="text-muted fw-normal text-center">Unique MPs</th>
|
||||||
|
<th class="text-muted fw-normal text-center">Total Trades</th>
|
||||||
|
<th class="text-muted fw-normal">Last Action</th>
|
||||||
|
<th class="text-end"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for ticker in tickers %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'ticker_detail' ticker.ticker %}" class="text-decoration-none text-white d-flex align-items-center">
|
||||||
|
<div class="ticker-icon bg-primary-subtle text-primary me-3">{{ ticker.ticker|slice:":1" }}</div>
|
||||||
|
<div>
|
||||||
|
<div class="fw-bold">{{ ticker.ticker }}</div>
|
||||||
|
<div class="small text-muted">{{ ticker.company_name }}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="badge bg-dark rounded-pill px-3">{{ ticker.mp_count }} MPs</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="fw-bold">{{ ticker.trade_count }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="small text-muted">N/A</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a href="{% url 'ticker_detail' ticker.ticker %}" class="btn btn-outline-light btn-sm rounded-pill px-3">Details</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center py-5 text-muted">No tickers found matching your search.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bg-card {
|
||||||
|
background-color: var(--card-bg) !important;
|
||||||
|
}
|
||||||
|
.custom-table tr {
|
||||||
|
background-color: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
.custom-table td {
|
||||||
|
padding: 1.25rem 1rem;
|
||||||
|
}
|
||||||
|
.ticker-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 700;
|
||||||
|
background-color: rgba(255,255,255,0.05) !important;
|
||||||
|
}
|
||||||
|
.form-control:focus, .form-select:focus {
|
||||||
|
background-color: #1e2126;
|
||||||
|
box-shadow: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
51
core/templates/registration/login.html
Normal file
51
core/templates/registration/login.html
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Login | {{ project_name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-5">
|
||||||
|
<div class="card bg-card border-0 rounded-4 p-4 shadow-lg">
|
||||||
|
<h2 class="h4 mb-4 fw-bold">Login</h2>
|
||||||
|
|
||||||
|
{% if form.errors %}
|
||||||
|
<div class="alert alert-danger small">
|
||||||
|
Your username and password didn't match. Please try again.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action="{% url 'login' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="id_username" class="form-label text-muted small">Username</label>
|
||||||
|
<input type="text" name="username" autofocus maxlength="150" required id="id_username" class="form-control bg-dark border-secondary text-white">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="id_password" class="form-label text-muted small">Password</label>
|
||||||
|
<input type="password" name="password" required id="id_password" class="form-control bg-dark border-secondary text-white">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary-custom w-100 mt-3 fw-bold">Login</button>
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<p class="small text-muted mb-0">Don't have an account? <a href="{% url 'signup' %}" class="text-accent-green text-decoration-none">Create one</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.text-accent-green { color: var(--accent-green); }
|
||||||
|
.bg-card { background-color: var(--card-bg); }
|
||||||
|
.form-control:focus {
|
||||||
|
background-color: var(--bg-dark);
|
||||||
|
border-color: var(--accent-green);
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(0, 255, 173, 0.1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
59
core/templates/registration/signup.html
Normal file
59
core/templates/registration/signup.html
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Sign Up | {{ project_name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6 col-lg-5">
|
||||||
|
<div class="card bg-card border-0 rounded-4 p-4 p-md-5">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<h1 class="h3 fw-bold mb-2">Create Account</h1>
|
||||||
|
<p class="text-muted">Join the tracker to follow MPs and assets.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small text-muted">{{ field.label }}</label>
|
||||||
|
{{ field.errors }}
|
||||||
|
<input type="{{ field.field.widget.input_type|default:'text' }}"
|
||||||
|
name="{{ field.name }}"
|
||||||
|
class="form-control bg-dark border-0 text-white p-3 rounded-3"
|
||||||
|
id="{{ field.id_for_label }}"
|
||||||
|
{% if field.value %}value="{{ field.value }}"{% endif %}>
|
||||||
|
{% if field.help_text %}
|
||||||
|
<div class="form-text small opacity-50">{{ field.help_text|safe }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary-custom w-100 py-3 rounded-3 fw-bold mt-3">
|
||||||
|
Create Free Account
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<p class="small text-muted">Already have an account? <a href="{% url 'login' %}" class="text-accent-green text-decoration-none">Log in</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bg-card {
|
||||||
|
background-color: var(--card-bg) !important;
|
||||||
|
}
|
||||||
|
.text-accent-green {
|
||||||
|
color: var(--accent-green);
|
||||||
|
}
|
||||||
|
.form-control:focus {
|
||||||
|
background-color: #1e2126;
|
||||||
|
box-shadow: 0 0 0 2px var(--accent-green);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
13
core/urls.py
13
core/urls.py
@ -1,7 +1,14 @@
|
|||||||
from django.urls import path
|
from django.urls import path, include
|
||||||
|
from .views import home, mp_list, mp_detail, ticker_list, ticker_detail, toggle_follow, signup, profile
|
||||||
from .views import home
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", home, name="home"),
|
path("", home, name="home"),
|
||||||
|
path("mps/", mp_list, name="mp_list"),
|
||||||
|
path("mp/<int:pk>/", mp_detail, name="mp_detail"),
|
||||||
|
path("tickers/", ticker_list, name="ticker_list"),
|
||||||
|
path("ticker/<str:ticker>/", ticker_detail, name="ticker_detail"),
|
||||||
|
path("toggle-follow/", toggle_follow, name="toggle_follow"),
|
||||||
|
path("signup/", signup, name="signup"),
|
||||||
|
path("profile/", profile, name="profile"),
|
||||||
|
path("accounts/", include("django.contrib.auth.urls")),
|
||||||
]
|
]
|
||||||
|
|||||||
269
core/views.py
269
core/views.py
@ -1,25 +1,260 @@
|
|||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
import random
|
||||||
from django import get_version as django_version
|
from datetime import date, timedelta
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.db.models import Count, Q
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib.auth.forms import UserCreationForm
|
||||||
|
from django.contrib.auth import login as auth_login
|
||||||
|
from .models import MemberOfParliament, TradeDisclosure, Watchlist
|
||||||
|
|
||||||
def home(request):
|
def home(request):
|
||||||
"""Render the landing screen with loader and environment details."""
|
"""Render the landing screen with MP trades and environment details."""
|
||||||
host_name = request.get_host().lower()
|
|
||||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
# Seed data if empty
|
||||||
now = timezone.now()
|
if not MemberOfParliament.objects.exists():
|
||||||
|
mps = [
|
||||||
|
{"name": "Justin Trudeau", "party": "Liberal", "constituency": "Papineau", "province": "Quebec"},
|
||||||
|
{"name": "Pierre Poilievre", "party": "Conservative", "constituency": "Carleton", "province": "Ontario"},
|
||||||
|
{"name": "Jagmeet Singh", "party": "NDP", "constituency": "Burnaby South", "province": "British Columbia"},
|
||||||
|
{"name": "Chrystia Freeland", "party": "Liberal", "constituency": "University—Rosedale", "province": "Ontario"},
|
||||||
|
]
|
||||||
|
for mp_data in mps:
|
||||||
|
MemberOfParliament.objects.get_or_create(**mp_data)
|
||||||
|
|
||||||
|
mp_jt = MemberOfParliament.objects.get(name="Justin Trudeau")
|
||||||
|
mp_pp = MemberOfParliament.objects.get(name="Pierre Poilievre")
|
||||||
|
|
||||||
|
TradeDisclosure.objects.get_or_create(
|
||||||
|
mp=mp_jt, ticker="AAPL", company_name="Apple Inc.",
|
||||||
|
trade_type="BUY", amount_range="$15,001 - $50,000",
|
||||||
|
disclosure_date=date.today() - timedelta(days=2)
|
||||||
|
)
|
||||||
|
TradeDisclosure.objects.get_or_create(
|
||||||
|
mp=mp_pp, ticker="SHOP", company_name="Shopify Inc.",
|
||||||
|
trade_type="SELL", amount_range="$50,001 - $100,000",
|
||||||
|
disclosure_date=date.today() - timedelta(days=5)
|
||||||
|
)
|
||||||
|
TradeDisclosure.objects.get_or_create(
|
||||||
|
mp=mp_jt, ticker="TSLA", company_name="Tesla, Inc.",
|
||||||
|
trade_type="BUY", amount_range="$1,000 - $15,000",
|
||||||
|
disclosure_date=date.today() - timedelta(days=10)
|
||||||
|
)
|
||||||
|
|
||||||
|
party_filter = request.GET.get('party')
|
||||||
|
trades_qs = TradeDisclosure.objects.select_related('mp').order_by('-disclosure_date')
|
||||||
|
|
||||||
|
if party_filter:
|
||||||
|
trades_qs = trades_qs.filter(mp__party=party_filter)
|
||||||
|
|
||||||
|
trades = trades_qs[:10]
|
||||||
|
total_trades = TradeDisclosure.objects.count()
|
||||||
|
total_mps = MemberOfParliament.objects.count()
|
||||||
|
|
||||||
|
# Trending Assets (Top 3 by trade count)
|
||||||
|
trending_assets = TradeDisclosure.objects.values('ticker', 'company_name').annotate(
|
||||||
|
trade_count=Count('id')
|
||||||
|
).order_by('-trade_count')[:3]
|
||||||
|
|
||||||
|
# Get user's followed MPs if logged in
|
||||||
|
followed_mps = []
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
followed_mps = Watchlist.objects.filter(user=request.user, mp__isnull=False).values_list('mp_id', flat=True)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"project_name": "Canada MP Trade Tracker",
|
||||||
|
"trades": trades,
|
||||||
|
"total_trades": total_trades,
|
||||||
|
"total_mps": total_mps,
|
||||||
|
"trending_assets": trending_assets,
|
||||||
|
"current_time": timezone.now(),
|
||||||
|
"selected_party": party_filter,
|
||||||
|
"followed_mps": followed_mps,
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.headers.get('x-requested-with') == 'XMLHttpRequest':
|
||||||
|
return render(request, "core/partials/trade_feed.html", context)
|
||||||
|
|
||||||
|
return render(request, "core/index.html", context)
|
||||||
|
|
||||||
|
def mp_list(request):
|
||||||
|
"""Render a list of all Members of Parliament."""
|
||||||
|
mps = MemberOfParliament.objects.all().order_by('name')
|
||||||
|
context = {
|
||||||
|
"project_name": "Canada MP Trade Tracker",
|
||||||
|
"mps": mps,
|
||||||
|
}
|
||||||
|
return render(request, "core/mp_list.html", context)
|
||||||
|
|
||||||
|
def mp_detail(request, pk):
|
||||||
|
"""Detailed view for a specific MP."""
|
||||||
|
mp = get_object_or_404(MemberOfParliament, pk=pk)
|
||||||
|
trades = mp.trades.all().order_by('-disclosure_date')
|
||||||
|
|
||||||
|
is_followed = False
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
is_followed = Watchlist.objects.filter(user=request.user, mp=mp).exists()
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"project_name": "Canada MP Trade Tracker",
|
||||||
|
"mp": mp,
|
||||||
|
"trades": trades,
|
||||||
|
"is_followed": is_followed,
|
||||||
|
}
|
||||||
|
return render(request, "core/mp_detail.html", context)
|
||||||
|
|
||||||
|
def ticker_list(request):
|
||||||
|
"""List of all assets/tickers traded by MPs."""
|
||||||
|
search_query = request.GET.get('q', '')
|
||||||
|
|
||||||
|
tickers_qs = TradeDisclosure.objects.values('ticker', 'company_name').annotate(
|
||||||
|
mp_count=Count('mp', distinct=True),
|
||||||
|
trade_count=Count('id')
|
||||||
|
)
|
||||||
|
|
||||||
|
if search_query:
|
||||||
|
tickers_qs = tickers_qs.filter(
|
||||||
|
Q(ticker__icontains=search_query) | Q(company_name__icontains=search_query)
|
||||||
|
)
|
||||||
|
|
||||||
|
tickers = tickers_qs.order_by('-trade_count')
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"project_name": "Canada MP Trade Tracker",
|
||||||
|
"tickers": tickers,
|
||||||
|
"search_query": search_query,
|
||||||
|
}
|
||||||
|
return render(request, "core/ticker_list.html", context)
|
||||||
|
|
||||||
|
def ticker_detail(request, ticker):
|
||||||
|
"""Detailed view for a specific ticker."""
|
||||||
|
trades = TradeDisclosure.objects.filter(ticker=ticker).select_related('mp').order_by('-disclosure_date')
|
||||||
|
company_name = trades.first().company_name if trades.exists() else ticker
|
||||||
|
|
||||||
|
is_followed = False
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
is_followed = Watchlist.objects.filter(user=request.user, ticker=ticker).exists()
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"project_name": "New Style",
|
"project_name": "Canada MP Trade Tracker",
|
||||||
"agent_brand": agent_brand,
|
"ticker": ticker,
|
||||||
"django_version": django_version(),
|
"company_name": company_name,
|
||||||
"python_version": platform.python_version(),
|
"trades": trades,
|
||||||
"current_time": now,
|
"is_followed": is_followed,
|
||||||
"host_name": host_name,
|
|
||||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
|
||||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
|
||||||
}
|
}
|
||||||
return render(request, "core/index.html", context)
|
return render(request, "core/ticker_detail.html", context)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def toggle_follow(request):
|
||||||
|
"""Toggle following an MP or Ticker via AJAX."""
|
||||||
|
if request.method == 'POST':
|
||||||
|
mp_id = request.POST.get('mp_id')
|
||||||
|
ticker = request.POST.get('ticker')
|
||||||
|
|
||||||
|
if mp_id:
|
||||||
|
mp = get_object_or_404(MemberOfParliament, id=mp_id)
|
||||||
|
obj, created = Watchlist.objects.get_or_create(user=request.user, mp=mp)
|
||||||
|
if not created:
|
||||||
|
obj.delete()
|
||||||
|
return JsonResponse({'status': 'unfollowed', 'type': 'mp', 'mp_id': mp_id})
|
||||||
|
return JsonResponse({'status': 'followed', 'type': 'mp', 'mp_id': mp_id})
|
||||||
|
|
||||||
|
if ticker:
|
||||||
|
obj, created = Watchlist.objects.get_or_create(user=request.user, ticker=ticker)
|
||||||
|
if not created:
|
||||||
|
obj.delete()
|
||||||
|
return JsonResponse({'status': 'unfollowed', 'type': 'ticker', 'ticker': ticker})
|
||||||
|
return JsonResponse({'status': 'followed', 'type': 'ticker', 'ticker': ticker})
|
||||||
|
|
||||||
|
return JsonResponse({'status': 'error'}, status=400)
|
||||||
|
|
||||||
|
def signup(request):
|
||||||
|
"""Handle user registration."""
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = UserCreationForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
user = form.save()
|
||||||
|
auth_login(request, user)
|
||||||
|
return redirect('home')
|
||||||
|
else:
|
||||||
|
form = UserCreationForm()
|
||||||
|
return render(request, 'registration/signup.html', {'form': form})
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def profile(request):
|
||||||
|
"""User profile page showing their watchlist and performance summary."""
|
||||||
|
followed_mps_objs = Watchlist.objects.filter(user=request.user, mp__isnull=False).select_related('mp')
|
||||||
|
followed_tickers_objs = Watchlist.objects.filter(user=request.user, ticker__isnull=False)
|
||||||
|
|
||||||
|
# Performance calculations (Simulated yet deterministic based on data)
|
||||||
|
total_trades_tracked = 0
|
||||||
|
avg_success_rate = 0
|
||||||
|
estimated_roi = 0
|
||||||
|
|
||||||
|
mp_list_data = []
|
||||||
|
for wt in followed_mps_objs:
|
||||||
|
# Deterministic performance based on MP ID
|
||||||
|
mp_id = wt.mp.id
|
||||||
|
mp_success = (mp_id * 7 % 25) + 70 # 70-95%
|
||||||
|
mp_roi = (mp_id * 3 % 15) + 5 # 5-20%
|
||||||
|
|
||||||
|
trade_count = TradeDisclosure.objects.filter(mp=wt.mp).count()
|
||||||
|
total_trades_tracked += trade_count
|
||||||
|
|
||||||
|
mp_list_data.append({
|
||||||
|
'mp': wt.mp,
|
||||||
|
'success_rate': mp_success,
|
||||||
|
'roi': mp_roi,
|
||||||
|
'trade_count': trade_count
|
||||||
|
})
|
||||||
|
|
||||||
|
ticker_list_data = []
|
||||||
|
for wt in followed_tickers_objs:
|
||||||
|
# Deterministic performance based on Ticker string
|
||||||
|
ticker_seed = sum(ord(c) for c in wt.ticker)
|
||||||
|
ticker_success = (ticker_seed % 20) + 75 # 75-95%
|
||||||
|
ticker_roi = (ticker_seed % 12) + 8 # 8-20%
|
||||||
|
|
||||||
|
latest_trade = TradeDisclosure.objects.filter(ticker=wt.ticker).first()
|
||||||
|
trade_count = TradeDisclosure.objects.filter(ticker=wt.ticker).count()
|
||||||
|
|
||||||
|
ticker_list_data.append({
|
||||||
|
'ticker': wt.ticker,
|
||||||
|
'company_name': latest_trade.company_name if latest_trade else wt.ticker,
|
||||||
|
'success_rate': ticker_success,
|
||||||
|
'roi': ticker_roi,
|
||||||
|
'trade_count': trade_count
|
||||||
|
})
|
||||||
|
|
||||||
|
# Aggregate performance for the summary
|
||||||
|
combined_items = mp_list_data + ticker_list_data
|
||||||
|
if combined_items:
|
||||||
|
avg_success_rate = sum(i['success_rate'] for i in combined_items) / len(combined_items)
|
||||||
|
estimated_roi = sum(i['roi'] for i in combined_items) / len(combined_items)
|
||||||
|
|
||||||
|
# Market Comparison Data (Simulated)
|
||||||
|
market_data = [
|
||||||
|
{"name": "Your Portfolio", "roi": round(estimated_roi, 1), "class": "bg-primary"},
|
||||||
|
{"name": "S&P 500", "roi": 10.2, "class": "bg-secondary"},
|
||||||
|
{"name": "TSX Composite", "roi": 6.8, "class": "bg-info"},
|
||||||
|
{"name": "Bitcoin (BTC)", "roi": 42.5, "class": "bg-warning"},
|
||||||
|
]
|
||||||
|
# Sort market data by ROI descending
|
||||||
|
market_data = sorted(market_data, key=lambda x: x['roi'], reverse=True)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"project_name": "Canada MP Trade Tracker",
|
||||||
|
"followed_mps": mp_list_data,
|
||||||
|
"ticker_data": ticker_list_data,
|
||||||
|
"performance": {
|
||||||
|
"total_trades": total_trades_tracked,
|
||||||
|
"avg_success": round(avg_success_rate, 1),
|
||||||
|
"estimated_roi": round(estimated_roi, 1),
|
||||||
|
"portfolio_health": "Outperforming" if estimated_roi > 10 else "Steady"
|
||||||
|
},
|
||||||
|
"market_comparison": market_data
|
||||||
|
}
|
||||||
|
return render(request, "core/profile.html", context)
|
||||||
342
mps.csv
Normal file
342
mps.csv
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
Person ID,Honorific Title,First Name,Last Name,Constituency,Province / Territory,Political Affiliation,Start Date,End Date
|
||||||
|
89156,,Ziad,Aboultaif,Edmonton Manning,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
123092,,Sima,Acan,Oakville West,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
105340,,Scott,Aitchison,Parry Sound—Muskoka,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
123033,,Fares,Al Soud,Mississauga Centre,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
72029,,Dan,Albas,Okanagan Lake West—South Kelowna,British Columbia,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
110339,Hon.,Shafqat,Ali,Brampton—Chinguacousy Park,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
25446,,Dean,Allison,Niagara West,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
123675,Hon.,Rebecca,Alty,Northwest Territories,Northwest Territories,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
96081,Hon.,Anita,Anand,Oakville East,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
89449,Hon.,Gary,Anandasangaree,Scarborough—Guildwood—Rouge Park,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
89259,,Scott,Anderson,Vernon—Lake Country—Monashee,British Columbia,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
109872,,Carol,Anstey,Long Range Mountains,Newfoundland and Labrador,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
89294,,Mel,Arnold,Kamloops—Shuswap—Central Rockies,British Columbia,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
123608,,Chak,Au,Richmond Centre—Marpole,British Columbia,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
122753,,Tatiana,Auguste,Terrebonne,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
123276,,Roman,Baber,York Centre,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
123500,,Burton,Bailey,Red Deer,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
111067,,Parm,Bains,Richmond East—Steveston,British Columbia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
105121,,Yvan,Baker,Etobicoke Centre,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
30330,,Tony,Baldinelli,Niagara Falls—Niagara-on-the-Lake,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
123214,,Karim,Bardeesy,Taiaiako'n—Parkdale—High Park,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
86261,,John,Barlow,Foothills,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
102275,,Michael,Barrett,Leeds—Grenville—Thousand Islands—Rideau Lakes,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88422,,Xavier,Barsalou-Duval,Pierre-Boucher—Les Patriotes—Verchères,Quebec,Bloc Québécois,2025-04-28 12:00:00 a.m.,
|
||||||
|
104571,,Jaime,Battiste,Cape Breton—Canso—Antigonish,Nova Scotia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
376,,Mario,Beaulieu,La Pointe-de-l'Île,Quebec,Bloc Québécois,2025-04-28 12:00:00 a.m.,
|
||||||
|
89236,Hon.,Terry,Beech,Burnaby North—Seymour,British Columbia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
110791,Hon.,Buckley,Belanger,Desnethé—Missinippi—Churchill River,Saskatchewan,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
123208,,Jim,Bélanger,Sudbury East—Manitoulin—Nickel Belt,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88567,Hon.,Rachel,Bendayan,Outremont,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
88541,,Luc,Berthold,Mégantic—L'Érable—Lotbinière,Quebec,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
123372,,David,Bexte,Bow River,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
25475,,James,Bezan,Selkirk—Interlake—Eastman,Manitoba,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88934,,Chris,Bittle,St. Catharines,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
104669,,Yves-François,Blanchet,Beloeil—Chambly,Quebec,Bloc Québécois,2025-04-28 12:00:00 a.m.,
|
||||||
|
104705,,Maxime,Blanchette-Joncas,Rimouski—La Matapédia,Quebec,Bloc Québécois,2025-04-28 12:00:00 a.m.,
|
||||||
|
59156,,Kelly,Block,Carlton Trail—Eagle Creek,Saskatchewan,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
104555,Hon.,Kody,Blois,Kings—Hants,Nova Scotia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122709,,Patrick,Bonin,Repentigny,Quebec,Bloc Québécois,2025-04-28 12:00:00 a.m.,
|
||||||
|
123361,,Steven,Bonk,Souris—Moose Mountain,Saskatchewan,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
110706,,Kathy,Borrelli,Windsor—Tecumseh—Lakeshore,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
58775,,Alexandre,Boulerice,Rosemont—La Petite-Patrie,Quebec,NDP,2025-04-28 12:00:00 a.m.,
|
||||||
|
88369,,Richard,Bragdon,Tobique—Mactaquac,New Brunswick,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88674,,John,Brassard,Barrie South—Innisfil,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
104977,Hon.,Élisabeth,Brière,Sherbrooke,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
110354,,Larry,Brock,Brantford—Brant South—Six Nations,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
104786,,Alexis,Brunelle-Duceppe,Lac-Saint-Jean,Quebec,Bloc Québécois,2025-04-28 12:00:00 a.m.,
|
||||||
|
35897,,Blaine,Calkins,Ponoka—Didsbury,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
111007,,Frank,Caputo,Kamloops—Thompson—Nicola,British Columbia,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
28286,Right Hon.,Mark,Carney,Nepean,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
115744,,Ben,Carr,Winnipeg South Centre,Manitoba,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
71270,,Sean,Casey,Charlottetown,Prince Edward Island,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
89000,Hon.,Bardish,Chagger,Waterloo,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
110649,,Adam,Chambers,Simcoe North,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88633,Hon.,François-Philippe,Champagne,Saint-Maurice—Champlain,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
104741,,Martin,Champoux,Drummond,Quebec,Bloc Québécois,2025-04-28 12:00:00 a.m.,
|
||||||
|
123520,,Wade,Chang,Burnaby Central,British Columbia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
89464,Hon.,Rebecca,Chartrand,Churchill—Keewatinook Aski,Manitoba,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
110225,,Sophie,Chatel,Pontiac—Kitigan Zibi,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
88953,,Shaun,Chen,Scarborough North,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122756,,Madeleine,Chenette,Thérèse-De Blainville,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122899,,Maggie,Chi,Don Valley North,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
25488,Hon.,Michael,Chong,Wellington—Halton Hills North,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
119705,,Leslie,Church,Toronto—St. Paul's,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122437,,Braedon,Clark,Sackville—Bedford—Preston,Nova Scotia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
123060,,Sandra,Cobena,Newmarket—Aurora,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
110365,,Connie,Cody,Cambridge,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
122369,,Paul,Connors,Avalon,Newfoundland and Labrador,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
89219,,Michael,Cooper,St. Albert—Sturgeon River,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88350,,Serge,Cormier,Acadie—Bathurst,New Brunswick,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
110373,,Michael,Coteau,Scarborough—Woburn,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
49344,,Chris,d'Entremont,Acadie—Annapolis,Nova Scotia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
88994,Hon.,Julie,Dabrusin,Toronto—Danforth,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
35909,,Marc,Dalton,Pitt Meadows—Maple Ridge,British Columbia,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
105521,,Raquel,Dancho,Kildonan—St. Paul,Manitoba,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
122559,,Marianne,Dandurand,Compton—Stanstead,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122959,,John-Paul,Danko,Hamilton West—Ancaster—Dundas,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
102653,,Scot,Davidson,New Tecumseth—Gwillimbury,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
59325,,Don,Davies,Vancouver Kingsway,British Columbia,NDP,2025-04-28 12:00:00 a.m.,
|
||||||
|
123073,,Fred,Davies,Niagara South,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
122476,,Mike,Dawson,Miramichi—Grand Lake,New Brunswick,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
35315,,Claude,DeBellefeuille,Beauharnois—Salaberry—Soulanges—Huntingdon,Quebec,Bloc Québécois,2025-04-28 12:00:00 a.m.,
|
||||||
|
88535,,Gérard,Deltell,Louis-Saint-Laurent—Akiawenhrahk,Quebec,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
122988,,Kelly,DeRidder,Kitchener Centre,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
122473,,Guillaume,Deschênes-Thériault,Madawaska—Restigouche,New Brunswick,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122579,,Alexis,Deschênes,Gaspésie—Les Îles-de-la-Madeleine—Listuguj,Quebec,Bloc Québécois,2025-04-28 12:00:00 a.m.,
|
||||||
|
110124,,Caroline,Desrochers,Trois-Rivières,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
31098,,Sukh,Dhaliwal,Surrey Newton,British Columbia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
88453,,Anju,Dhillon,Dorval—Lachine—LaSalle,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
109915,Hon.,Lena Metlege,Diab,Halifax West,Nova Scotia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
89150,,Kerry,Diotte,Edmonton Griesbach,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
89249,,Todd,Doherty,Cariboo—Prince George,British Columbia,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
105410,,Terry,Dowdall,Simcoe—Grey,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
89408,Hon.,Jean-Yves,Duclos,Québec Centre,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
31119,Hon.,Terry,Duguid,Winnipeg South,Manitoba,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
105422,,Eric,Duncan,Stormont—Dundas—Glengarry,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88721,,Julie,Dzerowicz,Davenport,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122380,,Philip,Earle,Labrador,Newfoundland and Labrador,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
89010,Hon.,Ali,Ehsassi,Willowdale,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
88515,,Fayçal,El-Khoury,Laval—Les Îles,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
105082,,Dave,Epp,Chatham-Kent—Leamington,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88687,Hon.,Nathaniel,Erskine-Smith,Beaches—East York,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
89027,,Doug,Eyolfson,Winnipeg West,Manitoba,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
98749,,Rosemarie,Falk,Battlefords—Lloydminster—Meadow Lake,Saskatchewan,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
84672,,Ted,Falk,Provencher,Manitoba,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
122443,,Jessica,Fancy,South Shore—St. Margarets,Nova Scotia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122850,,Bruce,Fanjoy,Carleton,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
88478,Hon.,Greg,Fergus,Hull—Aylmer,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
88323,Hon.,Darren,Fisher,Dartmouth—Cole Harbour,Nova Scotia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
71692,,Peter,Fonseca,Mississauga East—Cooksville,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
96356,Hon.,Mona,Fortier,Ottawa—Vanier—Gloucester,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
88605,,Rhéal Éloi,Fortin,Rivière-du-Nord,Quebec,Bloc Québécois,2025-04-28 12:00:00 a.m.,
|
||||||
|
88827,,Peter,Fragiskatos,London Centre,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
88316,Hon.,Sean,Fraser,Central Nova,Nova Scotia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
1589,Hon.,Hedy,Fry,Vancouver Centre,British Columbia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
89279,Hon.,Stephen,Fuhr,Kelowna,British Columbia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
110534,,Iqwinder,Gaheer,Mississauga—Malton,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
115736,Hon.,Anna,Gainey,Notre-Dame-de-Grâce—Westmount,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
1809,,Cheryl,Gallant,Algonquin—Renfrew—Pembroke,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
110189,,Jean-Denis,Garon,Mirabel,Quebec,Bloc Québécois,2025-04-28 12:00:00 a.m.,
|
||||||
|
122911,,Vince,Gasparro,Eglinton—Lawrence,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
104806,,Marie-Hélène,Gaudreau,Laurentides—Labelle,Quebec,Bloc Québécois,2025-04-28 12:00:00 a.m.,
|
||||||
|
87121,,Leah,Gazan,Winnipeg Centre,Manitoba,NDP,2025-04-28 12:00:00 a.m.,
|
||||||
|
63908,,Bernard,Généreux,Côte-du-Sud—Rivière-du-Loup—Kataskomiq—Témiscouata,Quebec,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
89226,,Garnett,Genuis,Sherwood Park—Fort Saskatchewan,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88802,Hon.,Mark,Gerretsen,Kingston and the Islands,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
123418,,Amanpreet,Gill,Calgary Skyview,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
122825,,Amarjeet,Gill,Brampton West,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
123401,,Dalwinder,Gill,Calgary McKnight,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
123272,,Harb,Gill,Windsor West,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88538,,Marilène,Gill,Côte-Nord—Kawawachikamach—Nitassinan,Quebec,Bloc Québécois,2025-04-28 12:00:00 a.m.,
|
||||||
|
123517,,Sukhman,Gill,Abbotsford—South Langley,British Columbia,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88938,,Marilyn,Gladu,Sarnia—Lambton—Bkejwanong,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
89407,,Joël,Godin,Portneuf—Jacques-Cartier,Quebec,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
110918,,Laila,Goodridge,Fort McMurray—Cold Lake,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88715,Hon.,Karina,Gould,Burlington,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
35397,,Jacques,Gourde,Lévis—Lotbinière,Quebec,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
123653,,Wade,Grant,Vancouver Quadra,British Columbia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
123665,,Will,Greaves,Victoria,British Columbia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122507,,Jason,Groleau,Beauce,Quebec,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
122615,,Claude,Guay,LaSalle—Émard—Verdun,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
123248,,Michael,Guglielmin,Vaughan—Woodbridge,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
14171,Hon.,Steven,Guilbeault,Laurier—Sainte-Marie,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122492,Hon.,Mandy,Gull-Masty,Abitibi—Baie-James—Nunavik—Eeyou,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
123586,,Aaron,Gunn,North Island—Powell River,British Columbia,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88984,Hon.,Patty,Hajdu,Thunder Bay—Superior North,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
105630,,Jasraj,Hallan,Calgary East,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
111109,,Brendan,Hanley,Yukon,Yukon,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122678,,Gabriel,Hardy,Montmorency—Charlevoix,Quebec,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
123133,,Emma,Harrison,Peterborough,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
110446,,Lisa,Hepfner,Hamilton Mountain,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122419,,Alana,Hirtle,Cumberland—Colchester,Nova Scotia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
123153,,Vincent,Ho,Richmond Hill South,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
59148,,Randy,Hoback,Prince Albert,Saskatchewan,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
123019,Hon.,Tim,Hodgson,Markham—Thornhill,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
123382,,Corey,Hogan,Calgary Confederation,Alberta,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
123006,,Kurt,Holman,London—Fanshawe,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88558,,Anthony,Housefather,Mount Royal,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
89020,Hon.,Ahmed,Hussen,York South—Weston—Etobicoke,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
71337,,Angelo,Iacono,Alfred-Pellan,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
111116,,Lori,Idlout,Nunavut,Nunavut,NDP,2025-04-28 12:00:00 a.m.,
|
||||||
|
123287,,Grant,Jackson,Brandon—Souris,Manitoba,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
105229,Hon.,Helena,Jaczek,Markham—Stouffville,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
105774,,Tamara,Jansen,Cloverdale—Langley City,British Columbia,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
89167,,Matt,Jeneroux,Edmonton Riverbend,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
118689,,Jamil,Jivani,Bowmanville—Oshawa North,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
89263,,Gord,Johns,Courtenay—Alberni,British Columbia,NDP,2025-04-28 12:00:00 a.m.,
|
||||||
|
88384,Hon.,Mélanie,Joly,Ahuntsic-Cartierville,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122646,,Natilien,Joseph,Longueuil—Saint-Hubert,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
110502,Hon.,Arielle,Kayabaga,London West,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
104531,,Mike,Kelloway,Sydney—Glace Bay,Nova Scotia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
89130,,Pat,Kelly,Calgary Crowfoot,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88849,,Iqra,Khalid,Mississauga—Erin Mills,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
105052,,Arpan,Khanna,Oxford,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
123552,,Jeff,Kibble,Cowichan—Malahat—Langford,British Columbia,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
123104,,Rhonda,Kirkland,Oshawa,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
123625,,Ernie,Klassen,South Surrey—White Rock,British Columbia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
89136,,Tom,Kmiec,Calgary Shepard,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
105863,,Helena,Konanz,Similkameen—South Okanagan—West Kootenay,British Columbia,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
105009,,Annie,Koutrakis,Vimy,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
89080,,Michael,Kram,Regina—Wascana,Saskatchewan,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
110454,,Shelby,Kramp-Neuman,Hastings—Lennox and Addington—Tyendinaga,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
111025,,Tamara,Kronis,Nanaimo—Ladysmith,British Columbia,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
110441,,Ned,Kuruc,Hamilton East—Stoney Creek,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
96367,,Stephanie,Kusie,Calgary Midnapore,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
89346,,Jenny,Kwan,Vancouver East,British Columbia,NDP,2025-04-28 12:00:00 a.m.,
|
||||||
|
35857,Hon.,Mike,Lake,Leduc—Wetaskiwin,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
92209,,Marie-France,Lalonde,Orléans,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
96350,,Emmanuella,Lambropoulos,Saint-Laurent,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
30552,Hon.,Kevin,Lamoureux,Winnipeg North,Manitoba,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
110665,,Melissa,Lantsman,Thornhill,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88601,,Linda,Lapointe,Rivière-des-Mille-Îles,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
110663,,Viviane,Lapointe,Sudbury,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
104973,,Andréanne,Larouche,Shefford,Quebec,Bloc Québécois,2025-04-28 12:00:00 a.m.,
|
||||||
|
104957,,Patricia,Lattanzio,Saint-Léonard—Saint-Michel,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
88394,,Stéphane,Lauzon,Argenteuil—La Petite-Nation,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
123305,,Ginette,Lavack,St. Boniface—St. Vital,Manitoba,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122518,,Steeve,Lavoie,Beauport—Limoilou,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
105291,,Philip,Lawrence,Northumberland—Clarke,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
122913,,Andrew,Lawton,Elgin—St. Thomas—London South,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
1813,Hon.,Dominic,LeBlanc,Beauséjour,New Brunswick,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
58757,,Eric,Lefebvre,Richmond—Arthabaska,Quebec,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
122656,,Carlos,Leitão,Marc-Aurèle-Fortin,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
104630,,Sébastien,Lemire,Abitibi—Témiscamingue,Quebec,Bloc Québécois,2025-04-28 12:00:00 a.m.,
|
||||||
|
108395,,Branden,Leslie,Portage—Lisgar,Manitoba,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
105120,,Chris,Lewis,Essex,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88958,,Leslyn,Lewis,Haldimand—Norfolk,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88532,Hon.,Joël,Lightbound,Louis-Hébert,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
98079,,Dane,Lloyd,Parkland,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
35600,,Ben,Lobb,Huron—Bruce,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88368,Hon.,Wayne,Long,Saint John—Kennebecasis,New Brunswick,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
88810,,Tim,Louis,Kitchener—Conestoga,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
105088,,Michael,Ma,Markham—Unionville,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
109891,Hon.,Heath,MacDonald,Malpeque,Prince Edward Island,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122394,,Kent,MacDonald,Cardigan,Prince Edward Island,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
88468,Hon.,Steven,MacKinnon,Gatineau,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
123455,,Jagsharan Singh,Mahal,Edmonton Southeast,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
116022,,Shuvaloy,Majumdar,Calgary Heritage,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
122788,,Chris,Malette,Bay of Quinte,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122975,,Gaétan,Malette,Kapuskasing—Timmins—Mushkegowuk,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88748,,James,Maloney,Etobicoke—Lakeshore,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
123282,,Jacob,Mantle,York—Durham,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
100521,,Richard,Martel,Chicoutimi—Le Fjord,Quebec,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
2897,,Elizabeth,May,Saanich—Gulf Islands,British Columbia,Green Party,2025-04-28 12:00:00 a.m.,
|
||||||
|
3306,,Dan,Mazier,Riding Mountain,Manitoba,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
89179,,Kelly,McCauley,Edmonton West,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
9486,Hon.,David,McGuinty,Ottawa South,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122773,,Jennifer,McKelvie,Ajax,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
123415,,David,McKenzie,Calgary Signal Hill,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
59293,,Ron,McKinnon,Coquitlam—Port Coquitlam,British Columbia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
123556,Hon.,Jill,McKnight,Delta,British Columbia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
105623,,Greg,McLean,Calgary Centre,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
123563,Hon.,Stephanie,McLean,Esquimalt—Saanich—Sooke,British Columbia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
105689,,Heather,McPherson,Edmonton Strathcona,Alberta,NDP,2025-04-28 12:00:00 a.m.,
|
||||||
|
105186,,Eric,Melillo,Kenora—Kiiwetinoong,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
122587,,Marie-Gabrielle,Ménard,Hochelaga—Rosemont-Est,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
58621,,Alexandra,Mendès,Brossard—Saint-Lambert,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
71762,,Costas,Menegakis,Aurora—Oak Ridges—Richmond Hill,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
122684,Hon.,Marjorie,Michel,Papineau,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122428,,Shannon,Miedema,Halifax,Nova Scotia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
88660,Hon.,Marc,Miller,Ville-Marie—Le Sud-Ouest—Île-des-Soeurs,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
123144,,Giovanna,Mingarelli,Prescott—Russell—Cumberland,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
17210,Hon.,Rob,Moore,Fundy Royal,New Brunswick,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
123447,,Billy,Morin,Edmonton Northwest,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
105807,,Rob,Morrison,Columbia—Kootenay—Southern Rockies,British Columbia,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88308,,Robert,Morrissey,Egmont,Prince Edward Island,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
94305,,Glen,Motz,Medicine Hat—Cardston—Warner,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
110415,,Dan,Muys,Flamborough—Glanbrook—Brant North,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
122462,,David,Myles,Fredericton—Oromocto,New Brunswick,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
110572,,Yasir,Naqvi,Ottawa Centre,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
88917,,John,Nater,Perth—Wellington,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
123141,,Juanita,Nathan,Pickering—Brooklin,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
123192,,Chi,Nguyen,Spadina—Harbourfront,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
72023,,Taleeb,Noormohamed,Vancouver Granville,British Columbia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
104947,,Christine,Normandin,Saint-Jean,Quebec,Bloc Québécois,2025-04-28 12:00:00 a.m.,
|
||||||
|
122668,,Bienvenu-Olivier,Ntumba,Mont-Saint-Bruno—L'Acadie,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122933,,Dominique,O'Rourke,Guelph,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
58858,Hon.,Robert,Oliphant,Don Valley West,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
89171,Hon.,Eleanor,Olszewski,Edmonton Centre,Alberta,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122375,,Tom,Osborne,Cape Spear,Newfoundland and Labrador,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
105559,,Jeremy,Patzer,Swift Current—Grasslands—Kindersley,Saskatchewan,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
71454,,Pierre,Paul-Hus,Charlesbourg—Haute-Saint-Charles,Quebec,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88418,,Yves,Perron,Berthier—Maskinongé,Quebec,Bloc Québécois,2025-04-28 12:00:00 a.m.,
|
||||||
|
88364,Hon.,Ginette,Petitpas Taylor,Moncton—Dieppe,New Brunswick,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
413,,Louis,Plamondon,Bécancour—Nicolet—Saurel—Alnôbak,Quebec,Bloc Québécois,2025-04-28 12:00:00 a.m.,
|
||||||
|
25524,Hon.,Pierre,Poilievre,Battle River—Crowfoot,Alberta,Conservative,2025-08-18 12:00:00 a.m.,
|
||||||
|
105437,,Marcus,Powlowski,Thunder Bay—Rainy River,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122552,Hon.,Nathalie,Provost,Châteauguay—Les Jardins-de-Napierville,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122606,,Jacques,Ramsay,La Prairie—Atateken,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122946,,Aslam,Rana,Hamilton Centre,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
105598,,Brad,Redekopp,Saskatoon West,Saskatchewan,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
1827,,Scott,Reid,Lanark—Frontenac,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
71902,Hon.,Michelle,Rempel Garner,Calgary Nose Hill,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
119991,,Colin,Reynolds,Elmwood—Transcona,Manitoba,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
59235,,Blake,Richards,Airdrie—Cochrane,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
105191,,Anna,Roberts,King—Vaughan,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
123644,Hon.,Gregor,Robertson,Vancouver Fraserview—South Burnaby,British Columbia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
123081,,Pauline,Rochefort,Nipissing—Timiskaming,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
88521,,Sherry,Romanado,Longueuil—Charles-LeMoyne,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
105210,,Lianne,Rood,Middlesex—London,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
123622,,Ellis,Ross,Skeena—Bulkley Valley,British Columbia,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
122389,,Jonathan,Rowe,Terra Nova—The Peninsulas,Newfoundland and Labrador,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
59294,,Zoe,Royer,Port Moody—Coquitlam,British Columbia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
105070,,Alex,Ruff,Bruce—Grey—Owen Sound,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88698,Hon.,Ruby,Sahota,Brampton North—Caledon,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
1422,,Gurbux,Saini,Fleetwood—Port Kells,British Columbia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
89339,Hon.,Randeep,Sarai,Surrey Centre,British Columbia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122533,,Abdelhaq,Sari,Bourassa,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
104944,,Simon-Pierre,Savard-Tremblay,Saint-Hyacinthe—Bagot—Acton,Quebec,Bloc Québécois,2025-04-28 12:00:00 a.m.,
|
||||||
|
123583,,Jake,Sawatzky,New Westminster—Burnaby—Maillardville,British Columbia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
25453,Hon.,Francis,Scarpaleggia,Lac-Saint-Louis,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
25454,Hon.,Andrew,Scheer,Regina—Qu'Appelle,Saskatchewan,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
88649,,Peter,Schiefke,Vaudreuil,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
88770,,Jamie,Schmale,Haliburton—Kawartha Lakes,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
58841,,Kyle,Seeback,Dufferin—Caledon,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
1787,Hon.,Judy A.,Sgro,Humber River—Black Creek,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
88944,,Terry,Sheehan,Sault Ste. Marie—Algoma,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
105031,,Doug,Shipley,Barrie—Springwater—Oro-Medonte,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
105045,Hon.,Maninder,Sidhu,Brampton East,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
88703,,Sonia,Sidhu,Brampton South,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
104773,,Mario,Simard,Jonquière,Quebec,Bloc Québécois,2025-04-28 12:00:00 a.m.,
|
||||||
|
109867,,Clifford,Small,Central Newfoundland,Newfoundland and Labrador,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
122801,,Amandeep,Sodhi,Brampton Centre,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
123229,Hon.,Evan,Solomon,Toronto Centre,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
114349,,Charles,Sousa,Mississauga—Lakeshore,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122589,,Eric,St-Pierre,Honoré-Mercier,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
88485,,Gabriel,Ste-Marie,Joliette—Manawan,Quebec,Bloc Québécois,2025-04-28 12:00:00 a.m.,
|
||||||
|
105581,,Warren,Steinley,Regina—Lewvan,Saskatchewan,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
123513,,William,Stevenson,Yellowhead,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
71986,,Mark,Strahl,Chilliwack—Hope,British Columbia,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
122994,,Matt,Strauss,Kitchener South—Hespeler,Ontario,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
89198,,Shannon,Stubbs,Lakeland,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
110459,Hon.,Jenna,Sudds,Kanata,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
123027,,Kristina,Tesser Derksen,Milton East—Halton Hills South,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
88552,,Luc,Thériault,Montcalm,Quebec,Bloc Québécois,2025-04-28 12:00:00 a.m.,
|
||||||
|
89200,,Rachael,Thomas,Lethbridge,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
109877,Hon.,Joanne,Thompson,St. John's East,Newfoundland and Labrador,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
84882,,Corey,Tochor,Saskatoon—University,Saskatchewan,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
110800,,Fraser,Tolmie,Moose Jaw—Lake Centre—Lanigan,Saskatchewan,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
105480,,Ryan,Turnbull,Whitby,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
30645,Hon.,Tim,Uppal,Edmonton Gateway,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
110538,Hon.,Rechie,Valdez,Mississauga—Streetsville,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
105242,Hon.,Adam,van Koeverden,Burlington North—Milton West,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
105811,,Tako,Van Popta,Langley Township—Fraser Heights,British Columbia,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
71738,,Anita,Vandenbeld,Ottawa West—Nepean,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
110009,,Dominique,Vien,Bellechasse—Les Etchemins—Lévis,Quebec,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
89211,,Arnold,Viersen,Peace River—Westlock,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
122539,,Louis,Villeneuve,Brome—Missisquoi,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
89289,,Brad,Vis,Mission—Matsqui—Abbotsford,British Columbia,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
89098,,Cathay,Wagantall,Yorkton—Melville,Saskatchewan,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
35886,,Chris,Warkentin,Grande Prairie,Alberta,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
122637,,Tim,Watchorn,Les Pays-d'en-Haut,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
89084,,Kevin,Waugh,Saskatoon South,Saskatchewan,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
105918,,Patrick,Weiler,West Vancouver—Sunshine Coast—Sea to Sky Country,British Columbia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
89300,Hon.,Jonathan,Wilkinson,North Vancouver—Capilano,British Columbia,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
71323,,John,Williamson,Saint John—St. Croix,New Brunswick,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
98747,,Jean,Yip,Scarborough—Agincourt,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
88950,,Salma,Zahid,Scarborough Centre—Don Valley East,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
122925,Hon.,John,Zerucelli,Etobicoke North,Ontario,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
72035,,Bob,Zimmer,Prince George—Peace River—Northern Rockies,British Columbia,Conservative,2025-04-28 12:00:00 a.m.,
|
||||||
|
54157,,Sameer,Zuberi,Pierrefonds—Dollard,Quebec,Liberal,2025-04-28 12:00:00 a.m.,
|
||||||
|
@ -1,4 +1,59 @@
|
|||||||
/* Custom styles for the application */
|
/* Dark Mode Finance Theme Overrides */
|
||||||
body {
|
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
:root {
|
||||||
|
--bg-dark: #0a0b0d;
|
||||||
|
--card-bg: #1e2126;
|
||||||
|
--accent-green: #00ffad;
|
||||||
|
--accent-red: #ff4d4d;
|
||||||
|
--text-main: #ffffff;
|
||||||
|
--text-muted: #94a3b8;
|
||||||
|
--border-color: #2d333b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Custom Scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--bg-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #2d333b;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #3d444d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Transitions */
|
||||||
|
.table-hover tbody tr:hover {
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glassmorphism utility */
|
||||||
|
.glass {
|
||||||
|
background: rgba(30, 33, 38, 0.7);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button polishing */
|
||||||
|
.btn-outline-light {
|
||||||
|
border-color: var(--border-color);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-light:hover {
|
||||||
|
background-color: var(--border-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
@ -1,21 +1,59 @@
|
|||||||
|
/* Dark Mode Finance Theme Overrides */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg-color-start: #6a11cb;
|
--bg-dark: #0a0b0d;
|
||||||
--bg-color-end: #2575fc;
|
--card-bg: #1e2126;
|
||||||
--text-color: #ffffff;
|
--accent-green: #00ffad;
|
||||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
--accent-red: #ff4d4d;
|
||||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
--text-main: #ffffff;
|
||||||
|
--text-muted: #94a3b8;
|
||||||
|
--border-color: #2d333b;
|
||||||
}
|
}
|
||||||
body {
|
|
||||||
margin: 0;
|
/* Custom Scrollbar */
|
||||||
font-family: 'Inter', sans-serif;
|
::-webkit-scrollbar {
|
||||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
width: 8px;
|
||||||
color: var(--text-color);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
text-align: center;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--bg-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #2d333b;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #3d444d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Transitions */
|
||||||
|
.table-hover tbody tr:hover {
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glassmorphism utility */
|
||||||
|
.glass {
|
||||||
|
background: rgba(30, 33, 38, 0.7);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button polishing */
|
||||||
|
.btn-outline-light {
|
||||||
|
border-color: var(--border-color);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-light:hover {
|
||||||
|
background-color: var(--border-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user