├── src ├── api │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ ├── admin.py │ ├── apps.py │ ├── pagination.py │ ├── serializers │ │ ├── __init__.py │ │ ├── detail_serializers.py │ │ └── articleSerializer.py │ ├── views │ │ ├── __init__.py │ │ ├── total_stats_view.py │ │ ├── article.py │ │ ├── interpellations.py │ │ ├── envoys.py │ │ ├── faq.py │ │ ├── committees.py │ │ └── home.py │ ├── decorators.py │ └── urls.py ├── eli_app │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0002_actsection.py │ ├── tests.py │ ├── tests │ │ ├── __init__.py │ │ └── test_pdf_section_split.py │ ├── urls.py │ ├── apps.py │ ├── db_update │ │ └── __init__.py │ ├── admin.py │ ├── libs │ │ └── clean_act_title.py │ └── templates │ │ └── act_list.html ├── community_app │ ├── forms.py │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0003_article_author.py │ │ └── 0002_alter_article_image.py │ ├── tests.py │ ├── db_update │ │ ├── __init__.py │ │ └── update_patrons.py │ ├── apps.py │ ├── admin.py │ ├── urls.py │ ├── views.py │ ├── templates │ │ ├── articles │ │ │ └── article_list.html │ │ └── privacy_policy.html │ ├── serializers.py │ └── models.py ├── sejm_app │ ├── __init__.py │ ├── views │ │ └── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_rename_id_committee_code.py │ │ ├── 0025_alter_printmodel_options.py │ │ ├── 0013_rename_interpolation_reply_interpellation.py │ │ ├── 0009_alter_committeesitting_video_id.py │ │ ├── 0005_process_pagescount.py │ │ ├── 0006_alter_process_pagescount.py │ │ ├── 0022_envoy_isfemale.py │ │ ├── 0012_remove_committeesitting_video_id_and_more.py │ │ ├── 0016_alter_votingoption_votes.py │ │ ├── 0010_alter_committeesitting_committee.py │ │ ├── 0027_alter_printmodel_id_alter_printmodel_number.py │ │ ├── 0028_alter_vote_id_alter_voting_id.py │ │ ├── 0024_alter_process_printmodel.py │ │ ├── 0017_alter_vote_vote.py │ │ ├── 0007_alter_process_comments_alter_process_description_and_more.py │ │ ├── 0026_alter_process_createdby.py │ │ ├── 0019_alter_voting_category.py │ │ ├── 0020_alter_voting_category.py │ │ ├── 0018_printmodel_processprint_voting_prints.py │ │ ├── 0015_alter_committeesitting_options_alter_envoy_options_and_more.py │ │ ├── 0011_alter_committeemember_options_and_more.py │ │ ├── 0023_alter_voting_options_alter_voting_description_and_more.py │ │ ├── 0008_committeesitting.py │ │ ├── 0021_alter_printmodel_options_alter_club_email_and_more.py │ │ ├── 0004_remove_printmodel_mps_remove_printmodel_club_and_more.py │ │ └── 0003_remove_process_mps_remove_process_club_and_more.py │ ├── urls.py │ ├── apps.py │ ├── models │ │ ├── aggregated_model.py │ │ ├── resolution.py │ │ ├── faq.py │ │ ├── __init__.py │ │ ├── club.py │ │ ├── vector.py │ │ ├── scandal.py │ │ ├── print_model.py │ │ ├── stage.py │ │ └── vote.py │ ├── tasks.py │ ├── libs │ │ ├── agenda_parser.py │ │ ├── resolution_parser.py │ │ └── wikipedia_searcher.py │ ├── db_updater │ │ ├── __init__.py │ │ ├── db_updater_task.py │ │ └── club_updater.py │ ├── signals.py │ ├── tests.py │ └── utils.py ├── core │ ├── __init__.py │ ├── asgi.py │ ├── wsgi.py │ ├── celery.py │ ├── urls.py │ └── generic │ │ └── mixins.py └── manage.py ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── others.yml │ ├── feature_request.yaml │ └── bug_report.yaml ├── dependabot.yml ├── workflows │ ├── check-pr-title.yml │ ├── delivery-production.yml │ └── python.yml └── pull_request_template.md ├── frontend ├── .eslintrc.json ├── app │ ├── favicon.ico │ ├── envoys │ │ ├── layout.tsx │ │ └── [id] │ │ │ ├── biography.tsx │ │ │ ├── page.tsx │ │ │ └── committees.tsx │ ├── votings │ │ └── layout.tsx │ ├── processes │ │ └── layout.tsx │ ├── interpellations │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── columns.tsx │ ├── committees │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── columns.tsx │ ├── api │ │ └── auth │ │ │ └── [...nextauth] │ │ │ └── route.ts │ ├── faq │ │ └── layout.tsx │ ├── home.module.css │ ├── layout.tsx │ ├── processes-results │ │ ├── page.tsx │ │ └── columns.tsx │ ├── clubs │ │ ├── page.tsx │ │ └── clubList.tsx │ ├── acts-results │ │ ├── page.tsx │ │ └── columns.tsx │ ├── articles │ │ └── create │ │ │ └── page.tsx │ ├── page.tsx │ └── votings-results │ │ ├── page.tsx │ │ └── columns.tsx ├── public │ ├── logo.png │ ├── home.webp │ ├── sejm.webp │ ├── no-picture.jpg │ ├── placeholder.jpg │ ├── mikrus-logo.svg │ ├── vercel.svg │ └── next.svg ├── postcss.config.mjs ├── utils │ └── text.tsx ├── components │ ├── ui │ │ ├── skeleton.tsx │ │ ├── collapsible.tsx │ │ ├── portal.tsx │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── progress.tsx │ │ ├── input.tsx │ │ ├── checkbox.tsx │ │ ├── confirm.tsx │ │ ├── switch.tsx │ │ ├── tooltip.tsx │ │ ├── badge.tsx │ │ ├── popover.tsx │ │ ├── avatar.tsx │ │ ├── spinner.tsx │ │ ├── skeleton-page.tsx │ │ ├── alert.tsx │ │ ├── scroll-area.tsx │ │ ├── stepper-footer.tsx │ │ ├── button.tsx │ │ ├── datePickerWithRange.tsx │ │ ├── loginButton.tsx │ │ ├── tabs.tsx │ │ ├── date-picker-with-range.tsx │ │ ├── card.tsx │ │ └── accordion.tsx │ ├── theme-provider.tsx │ ├── hooks │ │ └── useTransformedResults.tsx │ ├── loadableContainer.tsx │ ├── ModeToggle.tsx │ ├── chart-legend.tsx │ ├── newsCard.tsx │ ├── navbar.tsx │ ├── dataTable │ │ ├── columns.tsx │ │ └── toggle.tsx │ └── home │ │ ├── statCard.tsx │ │ └── votingCard.tsx ├── next.config.mjs ├── components.json ├── hooks │ └── use-mobile.tsx ├── Dockerfile.nextjs ├── tsconfig.json ├── lib │ └── utils.ts └── README.md ├── output.png ├── pyproject.toml ├── .dockerignore ├── dev-requirements.txt ├── .env.dist ├── requirements.txt ├── Dockerfile.postgres ├── Dockerfile.celery ├── promo ├── analisys.md └── devlog7.md ├── docker-compose-dev.yml ├── docker-entrypoint.sh ├── .devcontainer └── devcontainer.json └── Dockerfile /src/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/eli_app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/community_app/forms.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/sejm_app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/community_app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/sejm_app/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/eli_app/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/community_app/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskibin/sejm-stats/HEAD/output.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.djlint] 2 | indent = 2 3 | ignore = "H013,H006,H021,D018,T003" 4 | -------------------------------------------------------------------------------- /src/api/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /src/api/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /src/api/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /src/eli_app/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /src/community_app/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /src/core/__init__.py: -------------------------------------------------------------------------------- 1 | from .celery import app as celery_app 2 | 3 | __all__ = ("celery_app",) 4 | -------------------------------------------------------------------------------- /frontend/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskibin/sejm-stats/HEAD/frontend/app/favicon.ico -------------------------------------------------------------------------------- /frontend/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskibin/sejm-stats/HEAD/frontend/public/logo.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /research 2 | /promo 3 | /.devcontaier 4 | /readme 5 | /license 6 | /.github 7 | /.git 8 | -------------------------------------------------------------------------------- /frontend/public/home.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskibin/sejm-stats/HEAD/frontend/public/home.webp -------------------------------------------------------------------------------- /frontend/public/sejm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskibin/sejm-stats/HEAD/frontend/public/sejm.webp -------------------------------------------------------------------------------- /frontend/public/no-picture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskibin/sejm-stats/HEAD/frontend/public/no-picture.jpg -------------------------------------------------------------------------------- /frontend/public/placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskibin/sejm-stats/HEAD/frontend/public/placeholder.jpg -------------------------------------------------------------------------------- /src/community_app/db_update/__init__.py: -------------------------------------------------------------------------------- 1 | from .update_patrons import PatronsUpdaterTask 2 | 3 | tasks = (PatronsUpdaterTask(),) 4 | -------------------------------------------------------------------------------- /src/api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ApiConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "api" 7 | -------------------------------------------------------------------------------- /src/sejm_app/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from django.views.decorators.cache import cache_page 3 | 4 | 5 | from .views import * 6 | 7 | urlpatterns = [ 8 | ] 9 | -------------------------------------------------------------------------------- /frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | astroid==3.2.4 2 | dill==0.3.8 3 | isort==5.13.2 4 | mccabe==0.7.0 5 | platformdirs==4.2.1 6 | pylint==3.1.0 7 | setuptools==69.5.1 8 | tomlkit==0.12.5 9 | wheel==0.45.1 10 | -------------------------------------------------------------------------------- /src/community_app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ArticlesAppConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "community_app" 7 | -------------------------------------------------------------------------------- /src/community_app/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from community_app import models 4 | 5 | 6 | @admin.register(models.Article) 7 | class Article(admin.ModelAdmin): 8 | list_display = ("title",) -------------------------------------------------------------------------------- /src/api/pagination.py: -------------------------------------------------------------------------------- 1 | from rest_framework.pagination import PageNumberPagination 2 | 3 | 4 | class ApiViewPagination(PageNumberPagination): 5 | page_size = 1000 6 | page_size_query_param = "page_size" 7 | max_page_size = 4000 8 | -------------------------------------------------------------------------------- /src/eli_app/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # from test_pdf_section_split import ActSectionsExtractorTest 2 | import os 3 | import sys 4 | sys.path.append(os.path.join(os.path.dirname(__file__), '..')) 5 | 6 | from .test_article_extraction import ArticleExtractionTests 7 | -------------------------------------------------------------------------------- /frontend/utils/text.tsx: -------------------------------------------------------------------------------- 1 | export function truncateWords(sentence: string, maxWords: number) { 2 | const words = sentence.split(" "); 3 | if (words.length > maxWords) { 4 | return `${words.slice(0, maxWords).join(" ")}...`; 5 | } 6 | return sentence; 7 | } 8 | -------------------------------------------------------------------------------- /src/eli_app/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import * 4 | 5 | name = "eli" 6 | urlpatterns = [ 7 | path("acts/", ActHTMLListView.as_view(), name="acts"), 8 | path("api/acts/", ActJSONListView.as_view(), name="api_acts"), 9 | ] 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/others.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other Issue 3 | description: Other kind of the issue 4 | body: 5 | - type: textarea 6 | id: AC 7 | attributes: 8 | label: AC 9 | description: Any precise description of the issue 10 | validations: 11 | required: true 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "pip" 5 | directory: "/" 6 | schedule: 7 | interval: monthly 8 | time: "07:30" 9 | commit-message: 10 | prefix: "# - (python) " 11 | target-branch: main 12 | open-pull-requests-limit: 2 13 | -------------------------------------------------------------------------------- /frontend/public/mikrus-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/eli_app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class EliAppConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "eli_app" 7 | 8 | def ready(self) -> None: 9 | # from eli_app.libs.populate_db import init_db 10 | 11 | # init_db() 12 | return super().ready() 13 | -------------------------------------------------------------------------------- /frontend/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /frontend/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | experimental: { 5 | missingSuspenseWithCSRBailout: false, 6 | }, 7 | output: 'standalone', 8 | images: { 9 | domains: ['localhost', "sejm-stats.pl", "api.sejm.gov.pl"], 10 | }, 11 | }; 12 | 13 | export default nextConfig; -------------------------------------------------------------------------------- /.env.dist: -------------------------------------------------------------------------------- 1 | POSTGRES_DB=example 2 | POSTGRES_USER=postgres 3 | POSTGRES_PASSWORD=postgres 4 | POSTGRES_PORT=5432 5 | DATABASE_HOST=localhost 6 | DEBUG=false 7 | SECRET_KEY=does_not_matter 8 | EMAIL_HOST_PASSWORD=only_used_when_debug_is_false 9 | BUILD_TARGET=dev 10 | C_FORCE_ROOT=true 11 | PATRONITE_API_TOKEN="" 12 | PATRONITE_API_URL="https://patronite.pl/author-api/" -------------------------------------------------------------------------------- /frontend/app/envoys/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | 3 | export const metadata: Metadata = { 4 | title: 'Lista posłów - Sejm-Stats', 5 | description: 'Lista wszystkich posłów', 6 | }; 7 | 8 | interface LayoutProps { 9 | children: React.ReactNode; 10 | } 11 | 12 | export default function Layout({ children }: LayoutProps) { 13 | return <>{children}; 14 | } -------------------------------------------------------------------------------- /frontend/app/votings/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | 3 | export const metadata: Metadata = { 4 | title: 'Głosowania - Sejm-Stats', 5 | description: 'Lista głosowań w Sejmie', 6 | }; 7 | 8 | interface LayoutProps { 9 | children: React.ReactNode; 10 | } 11 | 12 | export default function Layout({ children }: LayoutProps) { 13 | return <>{children}; 14 | } -------------------------------------------------------------------------------- /frontend/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ThemeProvider as NextThemesProvider } from "next-themes" 5 | import { type ThemeProviderProps } from "next-themes/dist/types" 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children} 9 | } 10 | -------------------------------------------------------------------------------- /src/community_app/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import ArticleDetailView, ArticleListView, privacy_policy 4 | 5 | urlpatterns = [ 6 | path("privacy_policy/", privacy_policy, name="privacy_policy"), 7 | path("articles/", ArticleListView.as_view(), name="articles"), 8 | path("articles//", ArticleDetailView.as_view(), name="article_detail"), 9 | ] 10 | -------------------------------------------------------------------------------- /frontend/app/processes/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | 3 | export const metadata: Metadata = { 4 | title: 'Lista precesów - Sejm-Stats', 5 | description: 'Lista wszystkich procesów sejmowych', 6 | }; 7 | 8 | interface LayoutProps { 9 | children: React.ReactNode; 10 | } 11 | 12 | export default function Layout({ children }: LayoutProps) { 13 | return <>{children}; 14 | } -------------------------------------------------------------------------------- /frontend/app/interpellations/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | 3 | export const metadata: Metadata = { 4 | title: 'Lista interpelacji - Sejm-Stats', 5 | description: 'Lista wszystkich interpelacji', 6 | }; 7 | 8 | interface LayoutProps { 9 | children: React.ReactNode; 10 | } 11 | 12 | export default function Layout({ children }: LayoutProps) { 13 | return <>{children}; 14 | } -------------------------------------------------------------------------------- /frontend/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 4 | 5 | const Collapsible = CollapsiblePrimitive.Root 6 | 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 8 | 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 10 | 11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 12 | -------------------------------------------------------------------------------- /frontend/app/committees/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | 3 | export const metadata: Metadata = { 4 | title: 'Lista komisji sejmowych - Sejm-Stats', 5 | description: 'Lista wszystkich komisji sejmowych', 6 | }; 7 | 8 | interface LayoutProps { 9 | children: React.ReactNode; 10 | } 11 | 12 | export default function Layout({ children }: LayoutProps) { 13 | return <>{children}; 14 | } -------------------------------------------------------------------------------- /frontend/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import GoogleProvider from "next-auth/providers/google"; 3 | 4 | const handler = NextAuth({ 5 | providers: [ 6 | GoogleProvider({ 7 | clientId: process.env.GOOGLE_CLIENT_ID!, 8 | clientSecret: process.env.GOOGLE_CLIENT_SECRET!, 9 | }), 10 | ], 11 | 12 | }); 13 | 14 | export { handler as GET, handler as POST }; -------------------------------------------------------------------------------- /src/sejm_app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SejmAppConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "sejm_app" 7 | verbose_name = "Sejm" 8 | 9 | def ready(self): 10 | # import sejm_app.signals 11 | 12 | # from sejm_app import init_db 13 | 14 | # init_db.run() 15 | # pass 16 | super().ready() 17 | -------------------------------------------------------------------------------- /frontend/app/faq/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | 3 | export const metadata: Metadata = { 4 | title: 'Pytania i odpowiedzi - Sejm-Stats', 5 | description: 'Pytania i odpowiedzi oraz informacje o projekcie Sejm-Stats.', 6 | }; 7 | 8 | interface LayoutProps { 9 | children: React.ReactNode; 10 | } 11 | 12 | export default function Layout({ children }: LayoutProps) { 13 | return <>{children}; 14 | } -------------------------------------------------------------------------------- /frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "gray", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /src/api/serializers/__init__.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import Process 2 | 3 | from rest_framework import serializers 4 | 5 | from .articleSerializer import ArticleSerializer 6 | from .detail_serializers import * 7 | from .EnvoyDetailSerializer import EnvoyDetailSerializer 8 | from .list_serializers import * 9 | from .Total_stats_serializer import TotalStatsSerializer 10 | from .VotingDetailSerializer import VotingDetailSerializer 11 | -------------------------------------------------------------------------------- /src/sejm_app/models/aggregated_model.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class AggregatedModel(models.Model): 5 | max_total_activity = models.IntegerField(default=0) 6 | 7 | def save(self, *args, **kwargs): 8 | self.pk = 1 9 | super(AggregatedModel, self).save(*args, **kwargs) 10 | 11 | @classmethod 12 | def load(cls): 13 | obj, created = cls.objects.get_or_create(pk=1) 14 | return obj 15 | -------------------------------------------------------------------------------- /src/sejm_app/models/resolution.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from .voting import Voting 4 | 5 | 6 | class Resolution(models.Model): 7 | voting = models.ForeignKey(Voting, on_delete=models.CASCADE, primary_key=True) 8 | name = models.CharField(max_length=255) 9 | body = models.TextField(null=True, blank=True) 10 | summary = models.TextField(null=True, blank=True) 11 | 12 | def __str__(self): 13 | return f"{self.id}" 14 | -------------------------------------------------------------------------------- /src/core/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for core project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /src/core/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for core project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /src/sejm_app/models/faq.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | 5 | class FAQ(models.Model): 6 | question = models.CharField(max_length=255, null=True, blank=True) 7 | answer = models.TextField(null=True, blank=True) 8 | url1 = models.URLField(null=True, blank=True) 9 | url2 = models.URLField(null=True, blank=True) 10 | 11 | def __str__(self): 12 | return f"{self.question}" 13 | -------------------------------------------------------------------------------- /.github/workflows/check-pr-title.yml: -------------------------------------------------------------------------------- 1 | name: Check PR Title 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - edited 7 | - synchronize 8 | - ready_for_review 9 | - reopened 10 | 11 | jobs: 12 | check-pr-title: 13 | name: Check PR title 14 | timeout-minutes: 10 15 | if: github.event.pull_request.draft == false 16 | runs-on: ubuntu-22.04 17 | 18 | steps: 19 | - uses: blumilksoftware/action-pr-title@v1.2.0 -------------------------------------------------------------------------------- /src/sejm_app/migrations/0002_rename_id_committee_code.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-03-25 13:04 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("sejm_app", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RenameField( 13 | model_name="committee", 14 | old_name="id", 15 | new_name="code", 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /src/sejm_app/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .club import Club 2 | from .committee import Committee, CommitteeMember, CommitteeSitting, CommitteeType 3 | from .envoy import Envoy 4 | from .faq import FAQ 5 | from .interpellation import Interpellation, Reply 6 | from .print_model import AdditionalPrint, PrintModel 7 | from .process import Process 8 | from .scandal import Scandal 9 | from .stage import Stage 10 | from .vote import ClubVote, Vote, VoteOption 11 | from .voting import Voting 12 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0025_alter_printmodel_options.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-05-31 12:02 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("sejm_app", "0024_alter_process_printmodel"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="printmodel", 15 | options={"ordering": ["deliveryDate"]}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /frontend/components/ui/portal.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { createPortal } from 'react-dom' 3 | 4 | interface PortalProps { 5 | children: React.ReactNode 6 | } 7 | 8 | export const Portal: React.FC = ({ children }) => { 9 | const [mounted, setMounted] = useState(false) 10 | 11 | useEffect(() => { 12 | setMounted(true) 13 | return () => setMounted(false) 14 | }, []) 15 | 16 | return mounted 17 | ? createPortal(children, document.body) 18 | : null 19 | } -------------------------------------------------------------------------------- /src/api/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .acts import ActsMetaViewSet, ActViewSet 2 | from .clubs import ClubViewSet 3 | from .committees import CommitteeViewSet 4 | from .envoys import EnvoyViewSet 5 | from .faq import FAQViewSet 6 | from .home import HomeViewSet 7 | from .interpellations import InterpellationViewSet 8 | from .processes import ProcessesMetaViewSet, ProcessViewSet 9 | from .search import OptimizedSearchView 10 | from .total_stats_view import TotalStatsView 11 | from .votings import VotingsMetaViewSet, VotingViewSet 12 | -------------------------------------------------------------------------------- /frontend/app/home.module.css: -------------------------------------------------------------------------------- 1 | .bgBanner { 2 | position: relative; 3 | overflow: hidden; 4 | } 5 | 6 | 7 | .bgBanner::before { 8 | content: ""; 9 | position: absolute; 10 | top: 0; 11 | left: 3vw; 12 | width: 100%; 13 | height: 100%; 14 | background-image: linear-gradient(to right, var(--banner-image-start) 25%, var(--banner-image-end) 100%), 15 | url('/sejm.webp'); 16 | background-size: cover; 17 | background-position: 50% 25%; 18 | filter: blur(5px); 19 | z-index: 1; 20 | } -------------------------------------------------------------------------------- /src/community_app/migrations/0003_article_author.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-10-07 09:54 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("community_app", "0002_alter_article_image"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="article", 15 | name="author", 16 | field=models.CharField(default="Admin", max_length=200), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0013_rename_interpolation_reply_interpellation.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-03-28 10:02 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("sejm_app", "0012_remove_committeesitting_video_id_and_more"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RenameField( 13 | model_name="reply", 14 | old_name="interpolation", 15 | new_name="interpellation", 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /src/community_app/migrations/0002_alter_article_image.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-10-07 09:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("community_app", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="article", 15 | name="image", 16 | field=models.ImageField(blank=True, null=True, upload_to="article_images/"), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0009_alter_committeesitting_video_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-03-27 10:31 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("sejm_app", "0008_committeesitting"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="committeesitting", 14 | name="video_id", 15 | field=models.CharField(blank=True, max_length=60, null=True), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /src/eli_app/db_update/__init__.py: -------------------------------------------------------------------------------- 1 | from .update_acts import ActUpdaterTask 2 | from .update_helpers import ( 3 | ActStatusUpdaterTask, 4 | InstitutionUpdaterTask, 5 | KeywordUpdaterTask, 6 | PublisherUpdaterTask, 7 | ReferenceUpdaterTask, 8 | 9 | ) 10 | from .update_law import ActSectionUpdaterTask 11 | 12 | tasks = ( 13 | ActStatusUpdaterTask(), 14 | InstitutionUpdaterTask(), 15 | KeywordUpdaterTask(), 16 | PublisherUpdaterTask(), 17 | ReferenceUpdaterTask(), 18 | ActUpdaterTask(), 19 | ActSectionUpdaterTask(), 20 | ) 21 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0005_process_pagescount.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-03-26 18:01 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("sejm_app", "0004_remove_printmodel_mps_remove_printmodel_club_and_more"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="process", 14 | name="pagesCount", 15 | field=models.SmallIntegerField(default=0), 16 | preserve_default=False, 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /frontend/components/hooks/useTransformedResults.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | 3 | const useTransformedResults = (results: any[]) => { 4 | return useMemo(() => { 5 | return results.map((result) => ({ 6 | ...result, 7 | title: ( 8 |
9 | {result.title} 10 | 11 | {result.description ? result.description : result.topic} 12 | 13 |
14 | ), 15 | })); 16 | }, [results]); 17 | }; 18 | 19 | export default useTransformedResults; -------------------------------------------------------------------------------- /src/api/views/total_stats_view.py: -------------------------------------------------------------------------------- 1 | from django.utils.decorators import method_decorator 2 | from django.views.decorators.cache import cache_page 3 | from loguru import logger 4 | from rest_framework.response import Response 5 | from rest_framework.views import APIView 6 | 7 | from api.serializers import TotalStatsSerializer 8 | 9 | 10 | class TotalStatsView(APIView): 11 | @method_decorator(cache_page(60 * 60)) # Cache for 1 hour 12 | def get(self, request): 13 | serializer = TotalStatsSerializer(instance=None) 14 | return Response(serializer.to_representation(None)) 15 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0006_alter_process_pagescount.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-03-26 18:02 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("sejm_app", "0005_process_pagescount"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="process", 14 | name="pagesCount", 15 | field=models.SmallIntegerField( 16 | blank=True, default=0, help_text="Number of pages in the document" 17 | ), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0022_envoy_isfemale.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-04-22 09:11 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("sejm_app", "0021_alter_printmodel_options_alter_club_email_and_more"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="envoy", 14 | name="isFemale", 15 | field=models.BooleanField( 16 | blank=True, help_text="Whether the envoy is female", null=True 17 | ), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/sejm_app/tasks.py: -------------------------------------------------------------------------------- 1 | # for task in eli_tasks + sejm_tasks + community_tasks: 2 | # app.register_task(task) 3 | from celery import chain, shared_task 4 | 5 | from community_app.db_update import tasks as community_tasks 6 | from core.celery import app 7 | from eli_app.db_update import tasks as eli_tasks 8 | from sejm_app.db_updater import tasks as sejm_tasks 9 | 10 | 11 | @shared_task 12 | def run_update_tasks(): 13 | task_result = chain(t.s() for t in sejm_tasks + eli_tasks + community_tasks).delay() 14 | return task_result 15 | 16 | 17 | for task in eli_tasks + sejm_tasks + community_tasks: 18 | app.register_task(task) 19 | -------------------------------------------------------------------------------- /src/sejm_app/libs/agenda_parser.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from bs4 import BeautifulSoup 4 | 5 | 6 | def parse_agenda(agenda): 7 | soup = BeautifulSoup(agenda, "html.parser") 8 | divs = soup.find_all("div") 9 | text_lines = [div.get_text() for div in divs] 10 | readable_agenda = "\n".join(text_lines) 11 | 12 | return readable_agenda 13 | 14 | 15 | def get_prints_from_title(title) -> list[int]: 16 | titles = re.findall(r"\(druki? nr [\d,;\s\w-]+\)", title) 17 | print_ids = [] 18 | for title in titles: 19 | ids = re.findall(r"\b\d+-?\w*\b", title) 20 | print_ids.extend(ids) 21 | return print_ids 22 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | vertexai 2 | uvicorn==0.34.0 3 | gunicorn==23.0.0 4 | pgvector==0.3.6 5 | django==5.0 6 | loguru==0.7.2 7 | requests==2.32.3 8 | Pillow==10.4.0 9 | bs4==0.0.2 10 | django-select2==8.1.2 11 | psycopg==3.1.19 12 | django-filter==24.3 13 | django-cors-headers==4.6.0 14 | psycopg-binary==3.2.3 15 | wikipedia==1.4.0 16 | pdfplumber 17 | python-decouple==3.8 18 | pytesseract==0.3.13 19 | pdfminer.six 20 | pdf2image==1.17.0 21 | PyPDF2==3.0.1 22 | pydantic==2.9.2 23 | celery==5.4.0 24 | redis==5.2.1 25 | django-celery-beat==2.6.0 26 | django-celery-results==2.5.1 27 | django-redis==5.4.0 28 | django-debug-toolbar==4.4.2 29 | djangorestframework==3.15.2 -------------------------------------------------------------------------------- /frontend/components/loadableContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from "react"; 2 | import { SkeletonComponent } from "./ui/skeleton-page"; 3 | 4 | interface LoadableContainerProps { 5 | children: React.ReactNode; 6 | className?: string; 7 | } 8 | 9 | const LoadableContainer: React.FC = ({ children, className }) => { 10 | return ( 11 |
12 | }>{children} 13 |
14 | ); 15 | }; 16 | 17 | export default LoadableContainer; 18 | -------------------------------------------------------------------------------- /frontend/hooks/use-mobile.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener("change", onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener("change", onChange) 16 | }, []) 17 | 18 | return !!isMobile 19 | } 20 | -------------------------------------------------------------------------------- /src/api/serializers/detail_serializers.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db.models import IntegerField 3 | from django.utils.formats import date_format 4 | from rest_framework import serializers 5 | 6 | from sejm_app.models import Voting 7 | from sejm_app.models.committee import CommitteeMember 8 | from sejm_app.models.faq import FAQ 9 | from sejm_app.models.interpellation import Interpellation 10 | 11 | from .list_serializers import ClubListSerializer 12 | 13 | 14 | class InterpellationSerializer(serializers.ModelSerializer): 15 | class Meta: 16 | model = Interpellation 17 | fields = ["id", "title", "lastModified", "bodyLink"] 18 | -------------------------------------------------------------------------------- /src/api/decorators.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponseForbidden 2 | from django_ratelimit.decorators import ratelimit 3 | 4 | 5 | def referer_rate_limit(key, rate, method=["GET"], block=False): 6 | def decorator(view_func): 7 | @ratelimit(key=key, rate=rate, method=method, block=block) 8 | def _wrapped_view(request, *args, **kwargs): 9 | referer = request.META.get("HTTP_REFERER", "") 10 | if "sejm-stats.pl" not in referer: 11 | return HttpResponseForbidden("Forbidden: Invalid referer") 12 | return view_func(request, *args, **kwargs) 13 | 14 | return _wrapped_view 15 | 16 | return decorator 17 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0012_remove_committeesitting_video_id_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-03-27 17:37 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("sejm_app", "0011_alter_committeemember_options_and_more"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RemoveField( 13 | model_name="committeesitting", 14 | name="video_id", 15 | ), 16 | migrations.AddField( 17 | model_name="committeesitting", 18 | name="video_url", 19 | field=models.URLField(null=True), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0016_alter_votingoption_votes.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-04-19 15:43 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ( 9 | "sejm_app", 10 | "0015_alter_committeesitting_options_alter_envoy_options_and_more", 11 | ), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="votingoption", 17 | name="votes", 18 | field=models.IntegerField( 19 | blank=True, default=0, help_text="Number of votes", null=True 20 | ), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /src/sejm_app/db_updater/__init__.py: -------------------------------------------------------------------------------- 1 | from .club_updater import ClubUpdaterTask 2 | from .committees_updater import CommitteeUpdaterTask 3 | from .db_updater_task import DbUpdaterTask 4 | from .envoy_updater import EnvoyUpdaterTask 5 | from .interpellations_updater import InterpellationsUpdaterTask 6 | from .prints_updater import PrintsUpdaterTask 7 | from .processes_updater import ProcessesUpdaterTask 8 | from .votings_updater import VotingsUpdaterTask 9 | 10 | tasks = ( 11 | ClubUpdaterTask(), 12 | EnvoyUpdaterTask(), 13 | PrintsUpdaterTask(), 14 | VotingsUpdaterTask(), 15 | InterpellationsUpdaterTask(), 16 | ProcessesUpdaterTask(), 17 | CommitteeUpdaterTask(), 18 | ) 19 | -------------------------------------------------------------------------------- /src/sejm_app/models/club.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | 4 | 5 | class Club(models.Model): 6 | id = models.CharField(primary_key=True, max_length=255) 7 | name = models.CharField(max_length=255) 8 | phone = models.CharField(max_length=255, null=True, blank=True) 9 | fax = models.CharField(max_length=255) 10 | email = models.EmailField(max_length=255, null=True, blank=True) 11 | membersCount = models.IntegerField() 12 | photo = models.ImageField(upload_to="club_logos", null=True, blank=True) 13 | 14 | def __str__(self): 15 | return f"{self.id}" 16 | 17 | @property 18 | def api_url(self): 19 | return settings.CLUBS_URL 20 | -------------------------------------------------------------------------------- /frontend/Dockerfile.nextjs: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM node:20-alpine AS builder 3 | WORKDIR /app 4 | COPY package*.json ./ 5 | RUN npm install sharp 6 | RUN npm ci 7 | COPY . . 8 | RUN npm run build 9 | 10 | # Production stage 11 | FROM node:20-alpine AS runner 12 | WORKDIR /app 13 | 14 | ENV NODE_ENV production 15 | 16 | RUN addgroup --system --gid 1001 nodejs 17 | RUN adduser --system --uid 1001 nextjs 18 | 19 | COPY --from=builder /app/public ./public 20 | COPY --from=builder /app/.next/standalone ./ 21 | COPY --from=builder /app/.next/static ./.next/static 22 | RUN mkdir -p .next/cache && chown -R nextjs:nodejs .next 23 | 24 | USER nextjs 25 | 26 | EXPOSE 3000 27 | 28 | ENV PORT 3000 29 | 30 | CMD ["node", "server.js"] -------------------------------------------------------------------------------- /src/api/serializers/articleSerializer.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from community_app.models import Article 4 | 5 | 6 | class ArticleSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = Article 9 | fields = [ 10 | "id", 11 | "title", 12 | "content", 13 | "image", 14 | "created_at", 15 | "updated_at", 16 | "author", 17 | ] 18 | read_only_fields = ["created_at", "updated_at"] 19 | 20 | 21 | class ArticleListSerializer(serializers.ModelSerializer): 22 | class Meta: 23 | model = Article 24 | fields = ["id", "title", "image", "created_at", "author"] 25 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "target": "ES6", 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0010_alter_committeesitting_committee.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-03-27 10:35 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("sejm_app", "0009_alter_committeesitting_video_id"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="committeesitting", 15 | name="committee", 16 | field=models.ForeignKey( 17 | on_delete=django.db.models.deletion.CASCADE, 18 | related_name="sittings", 19 | to="sejm_app.committee", 20 | ), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an feature / idea for this project 3 | title: "[Feature Request / Suggestion]: " 4 | labels: ["enhancement"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | We appreciate your feedback on how to improve this project. Please be sure to include as much details & any resources if possible! 10 | - type: textarea 11 | id: Suggestion 12 | attributes: 13 | label: Suggestion / Feature Request 14 | description: Describe the feature(s) you would like to see added. 15 | placeholder: Tell us your suggestion 16 | value: "Your suggestion here" 17 | validations: 18 | required: true 19 | 20 | -------------------------------------------------------------------------------- /Dockerfile.postgres: -------------------------------------------------------------------------------- 1 | # Use the official PostgreSQL image as a base 2 | FROM pgvector/pgvector:pg16 3 | 4 | # Set the working directory 5 | WORKDIR /tmp 6 | # Copy the polish dictionary files into the container 7 | COPY tsearch_data/polish.affix /tmp/polish.affix 8 | COPY tsearch_data/polish.dict /tmp/polish.dict 9 | COPY tsearch_data/polish.stop /tmp/polish.stop 10 | 11 | # Run the commands to set up the full-text search dictionary 12 | RUN cp polish.affix $(pg_config --sharedir)/tsearch_data/ \ 13 | && cp polish.dict $(pg_config --sharedir)/tsearch_data/ \ 14 | && cp polish.stop $(pg_config --sharedir)/tsearch_data/ 15 | 16 | # Use the official PostgreSQL image as a base 17 | # Install necessary packages for building extensions and git -------------------------------------------------------------------------------- /src/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") 11 | try: 12 | from django.core.management import execute_from_command_line 13 | except ImportError as exc: 14 | raise ImportError( 15 | "Couldn't import Django. Are you sure it's installed and " 16 | "available on your PYTHONPATH environment variable? Did you " 17 | "forget to activate a virtual environment?" 18 | ) from exc 19 | execute_from_command_line(sys.argv) 20 | 21 | 22 | if __name__ == "__main__": 23 | main() 24 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0027_alter_printmodel_id_alter_printmodel_number.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-10-21 15:29 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('sejm_app', '0026_alter_process_createdby'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='printmodel', 15 | name='id', 16 | field=models.CharField(editable=False, max_length=18, primary_key=True, serialize=False), 17 | ), 18 | migrations.AlterField( 19 | model_name='printmodel', 20 | name='number', 21 | field=models.CharField(max_length=15), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0028_alter_vote_id_alter_voting_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2025-01-19 21:21 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('sejm_app', '0027_alter_printmodel_id_alter_printmodel_number'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='vote', 15 | name='id', 16 | field=models.BigIntegerField(primary_key=True, serialize=False), 17 | ), 18 | migrations.AlterField( 19 | model_name='voting', 20 | name='id', 21 | field=models.BigIntegerField(help_text='Voting ID', primary_key=True, serialize=False), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /frontend/components/ModeToggle.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { MoonIcon, SunIcon } from "@radix-ui/react-icons" 5 | import { useTheme } from "next-themes" 6 | 7 | import { Button } from "@/components/ui/button" 8 | 9 | export function ModeToggle() { 10 | const { theme, setTheme } = useTheme() 11 | 12 | const toggleTheme = () => { 13 | setTheme(theme === "light" ? "dark" : "light") 14 | } 15 | 16 | return ( 17 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0024_alter_process_printmodel.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-05-06 12:04 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("sejm_app", "0023_alter_voting_options_alter_voting_description_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="process", 15 | name="printModel", 16 | field=models.ForeignKey( 17 | blank=True, 18 | null=True, 19 | on_delete=django.db.models.deletion.CASCADE, 20 | related_name="process", 21 | to="sejm_app.printmodel", 22 | ), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["bug", "triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | - type: textarea 11 | id: what-happened 12 | attributes: 13 | label: What happened? 14 | description: Describe the issue here. 15 | placeholder: Tell us what you see! 16 | value: "A bug happened!" 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: logs 21 | attributes: 22 | label: Code to produce this issue. 23 | description: Please copy and paste any relevant code to re-produce this issue. And the version of the browser you are using. 24 | render: shell 25 | -------------------------------------------------------------------------------- /src/core/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from celery import Celery 4 | 5 | # Set the default Django settings module for the 'celery' program. 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") 7 | 8 | app = Celery("core") 9 | app.conf.broker_connection_retry_on_startup = True 10 | # Using a string here means the worker doesn't have to serialize 11 | # the configuration object to child processes. 12 | # - namespace='CELERY' means all celery-related configuration keys 13 | # should have a `CELERY_` prefix. 14 | app.config_from_object("django.conf:settings", namespace="CELERY") 15 | app.conf.update( 16 | worker_concurrency=2, # Reduce concurrency to 2 17 | worker_prefetch_multiplier=1, # Reduce prefetch multiplier 18 | ) 19 | 20 | # Load task modules from all registered Django apps. 21 | app.autodiscover_tasks() 22 | -------------------------------------------------------------------------------- /frontend/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0017_alter_vote_vote.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-04-19 15:51 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("sejm_app", "0016_alter_votingoption_votes"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="vote", 14 | name="vote", 15 | field=models.SmallIntegerField( 16 | blank=True, 17 | choices=[ 18 | (0, "No"), 19 | (1, "Yes"), 20 | (2, "ABSTAIN"), 21 | (3, "ABSENT"), 22 | (4, "VOTE_VALID"), 23 | ], 24 | help_text="Vote option", 25 | null=True, 26 | ), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /src/core/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.static import static 3 | from django.contrib import admin 4 | from django.urls import include, path 5 | 6 | # prefix 7 | 8 | 9 | urlpatterns = ( 10 | [ 11 | path("apiInt/admin/", admin.site.urls), 12 | path("apiInt/", include("api.urls")), 13 | path("", include("sejm_app.urls")), 14 | # path("", include("eli_app.urls")), 15 | # path("", include("community_app.urls")), 16 | # path("select2/", include("django_select2.urls")), 17 | ] 18 | + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 19 | + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 20 | ) 21 | 22 | if settings.DEBUG: 23 | import debug_toolbar 24 | 25 | urlpatterns = [ 26 | path("__debug__/", include(debug_toolbar.urls)), 27 | ] + urlpatterns 28 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0007_alter_process_comments_alter_process_description_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-03-27 10:04 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("sejm_app", "0006_alter_process_pagescount"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="process", 14 | name="comments", 15 | field=models.TextField(default="Brak"), 16 | ), 17 | migrations.AlterField( 18 | model_name="process", 19 | name="description", 20 | field=models.TextField(default="Brak"), 21 | ), 22 | migrations.AlterField( 23 | model_name="process", 24 | name="documentType", 25 | field=models.CharField(max_length=64), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /frontend/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ) 29 | Separator.displayName = SeparatorPrimitive.Root.displayName 30 | 31 | export { Separator } 32 | -------------------------------------------------------------------------------- /frontend/components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ProgressPrimitive from "@radix-ui/react-progress" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Progress = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, value, ...props }, ref) => ( 12 | 20 | 24 | 25 | )) 26 | Progress.displayName = ProgressPrimitive.Root.displayName 27 | 28 | export { Progress } 29 | -------------------------------------------------------------------------------- /src/sejm_app/models/vector.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from pgvector.django import VectorField 3 | 4 | 5 | class VectorizedModel(models.Model): 6 | embedding = VectorField( 7 | dimensions=768, null=True, blank=True 8 | ) # 1536-dimensional vector field 9 | summary = models.TextField( 10 | verbose_name="AI summary of the document", max_length=1024, null=True 11 | ) 12 | 13 | class Meta: 14 | abstract = True # This makes it a base class that won't create a database table 15 | 16 | def __str__(self): 17 | return f"{self.__class__.__name__} (ID: {self.id})" 18 | 19 | # You can add common methods here that all vectorized models might use 20 | def similarity(self, other): 21 | """ 22 | Calculate cosine similarity with another instance 23 | """ 24 | return 1 - (self.embedding @ other.embedding) 25 | 26 | # Add other common methods as needed 27 | -------------------------------------------------------------------------------- /src/sejm_app/signals.py: -------------------------------------------------------------------------------- 1 | # from django.db.models.signals import post_save 2 | # from django.dispatch import receiver 3 | # from sejm_app.models import AggregatedModel, Envoy, Process, Interpellation, Vote 4 | # from django.db import models 5 | 6 | 7 | # @receiver(post_save, sender=Envoy) 8 | # def update_max_total_activity(sender, instance, **kwargs): 9 | # max_total_activity = AggregatedModel.objects.first() 10 | # max_activity = Envoy.objects.aggregate(max_activity=models.Max("total_activity"))[ 11 | # "total_activity" 12 | # ] 13 | # if max_total_activity is None: 14 | # max_total_activity = AggregatedModel.objects.create( 15 | # max_total_activity=instance.total_activity 16 | # ) 17 | # else: 18 | # max_total_activity.max_total_activity = max( 19 | # max_total_activity.max_total_activity, instance.total_activity 20 | # ) 21 | # max_total_activity.save() 22 | -------------------------------------------------------------------------------- /Dockerfile.celery: -------------------------------------------------------------------------------- 1 | # Use the official Python image from the Docker Hub 2 | FROM python:3.12 3 | # Set environment variables 4 | ENV PYTHONDONTWRITEBYTECODE 1 5 | ENV PYTHONUNBUFFERED 1 6 | ENV TESSDATA_PREFIX /usr/share/tesseract-ocr/5/tessdata 7 | 8 | # Set the working directory in the container 9 | WORKDIR /code 10 | 11 | # Install system dependencies 12 | RUN apt-get update \ 13 | && apt-get install -y --no-install-recommends gcc libpq-dev postgresql-client poppler-utils tesseract-ocr wget \ 14 | && apt-get clean \ 15 | && rm -rf /var/lib/apt/lists/* \ 16 | && wget -O /tmp/pol.traineddata https://github.com/tesseract-ocr/tessdata/raw/main/pol.traineddata \ 17 | && mv /tmp/pol.traineddata /usr/share/tesseract-ocr/5/tessdata/ 18 | 19 | # Install Python dependencies 20 | COPY requirements.txt /code/ 21 | RUN pip install --upgrade pip && pip install -r requirements.txt 22 | 23 | # Copy application code 24 | COPY src/ /code/ 25 | 26 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0026_alter_process_createdby.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-10-07 09:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("sejm_app", "0025_alter_printmodel_options"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="process", 15 | name="createdBy", 16 | field=models.CharField( 17 | blank=True, 18 | choices=[ 19 | ("posłowie", "Envoys"), 20 | ("klub", "Club"), 21 | ("prezydium", "Presidium"), 22 | ("obywatele", "Citizens"), 23 | ("rząd", "Government"), 24 | ], 25 | default="Brak danych", 26 | max_length=20, 27 | null=True, 28 | ), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /frontend/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0019_alter_voting_category.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-04-20 15:57 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("sejm_app", "0018_printmodel_processprint_voting_prints"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="voting", 14 | name="category", 15 | field=models.CharField( 16 | blank=True, 17 | choices=[ 18 | ("APPLICATION", "Wniosek formalny"), 19 | ("PRINTS", "Głosowanie druków"), 20 | ("WHOLE_PROJECT", "Głosowanie nad całością projektu"), 21 | ("AMENDMENT", "Głosowanie nad poprawką"), 22 | ], 23 | help_text="Voting category", 24 | max_length=32, 25 | null=True, 26 | ), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /frontend/components/chart-legend.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface CustomLegendProps { 4 | dataKeys: string[]; 5 | colors: string[]; 6 | visibleKeys: string[]; 7 | toggleKey: (key: string) => void; 8 | customLabels?: { [key: string]: string }; 9 | } 10 | 11 | export const CustomLegend: React.FC = ({ 12 | dataKeys, 13 | colors, 14 | visibleKeys, 15 | toggleKey, 16 | customLabels, 17 | }) => { 18 | return ( 19 |
20 | {dataKeys.map((key, index) => ( 21 | 31 | ))} 32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/community_app/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.views.generic import DetailView, ListView 3 | 4 | from community_app.models import Article 5 | 6 | 7 | class ArticleListView(ListView): 8 | model = Article 9 | queryset = Article.objects.published().order_by("-published_at") 10 | template_name = "articles/article_list.html" 11 | context_object_name = "articles" 12 | paginate_by = 4 13 | 14 | 15 | class ArticleDetailView(DetailView): 16 | model = Article 17 | queryset = Article.objects.published() 18 | template_name = "articles/article_detail.html" 19 | context_object_name = "article" 20 | 21 | def get_context_data(self, **kwargs): 22 | context = super().get_context_data(**kwargs) 23 | context["latest_articles"] = Article.objects.published()[:5] 24 | context["meta"] = self.get_object().as_meta(self.request) 25 | return context 26 | 27 | 28 | def privacy_policy(request): 29 | return render(request, "privacy_policy.html") 30 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0020_alter_voting_category.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-04-20 15:59 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("sejm_app", "0019_alter_voting_category"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="voting", 14 | name="category", 15 | field=models.CharField( 16 | blank=True, 17 | choices=[ 18 | ("APPLICATION", "Wniosek formalny"), 19 | ("PRINTS", "Głosowanie druków"), 20 | ("WHOLE_PROJECT", "Głosowanie nad całością projektu"), 21 | ("AMENDMENT", "Głosowanie nad poprawką"), 22 | ("CANDIDATES", "Głosowanie na kandydatów"), 23 | ], 24 | help_text="Voting category", 25 | max_length=32, 26 | null=True, 27 | ), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /src/community_app/templates/articles/article_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/base.html' %} 2 | {% load static %} 3 | {% block content %} 4 |
5 |
6 | 16 |
17 |
18 |
19 |
20 | 25 |
26 | {% endblock %} 27 | {% block extra_js %} 28 | 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /src/eli_app/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from eli_app import models 4 | 5 | # Register your models here. 6 | 7 | 8 | @admin.register(models.Act) 9 | class act_admin(admin.ModelAdmin): 10 | list_display = ( 11 | "ELI", 12 | "title", 13 | ) 14 | 15 | 16 | @admin.register(models.ActSection) 17 | class ActSection(admin.ModelAdmin): 18 | list_display = ("chapters",) 19 | 20 | 21 | @admin.register(models.Publisher) 22 | class publisher_admin(admin.ModelAdmin): 23 | list_display = ("name",) 24 | 25 | 26 | @admin.register(models.ActStatus) 27 | class act_status_admin(admin.ModelAdmin): 28 | list_display = ("name",) 29 | 30 | 31 | @admin.register(models.DocumentType) 32 | class document_type_admin(admin.ModelAdmin): 33 | list_display = ("name",) 34 | 35 | 36 | @admin.register(models.Institution) 37 | class institution_admin(admin.ModelAdmin): 38 | list_display = ("name",) 39 | 40 | 41 | @admin.register(models.Keyword) 42 | class keyword_admin(admin.ModelAdmin): 43 | list_display = ("name",) 44 | -------------------------------------------------------------------------------- /frontend/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import ClientLayout from './clientLayout' 3 | 4 | export const metadata: Metadata = { 5 | title: 'Sejm Stats', 6 | description: 'Kompleksowy przegląd aktywności sejmowej', 7 | openGraph: { 8 | title: 'Sejm Stats', 9 | description: 'Kompleksowy przegląd aktywności sejmowej', 10 | url: 'https://sejm-stats.pl', 11 | siteName: 'Sejm Stats', 12 | images: [ 13 | { 14 | url: 'https://sejm-stats.pl/og-image.jpg', 15 | width: 1200, 16 | height: 630, 17 | }, 18 | ], 19 | locale: 'pl_PL', 20 | type: 'website', 21 | }, 22 | twitter: { 23 | card: 'summary_large_image', 24 | title: 'Sejm Stats', 25 | description: 'Kompleksowy przegląd aktywności sejmowej', 26 | images: ['https://sejm-stats.pl/twitter-image.jpg'], 27 | }, 28 | } 29 | 30 | export default function RootLayout({ 31 | children, 32 | }: { 33 | children: React.ReactNode 34 | }) { 35 | return ( 36 | 37 | {children} 38 | 39 | ) 40 | } -------------------------------------------------------------------------------- /src/community_app/serializers.py: -------------------------------------------------------------------------------- 1 | from django.template.defaultfilters import truncatechars 2 | from rest_framework import serializers 3 | from django.core.files.base import ContentFile 4 | import base64 5 | import uuid 6 | from community_app.models import Article 7 | from sejm_app.utils import format_human_friendly_date 8 | 9 | 10 | class ArticleSerializer(serializers.ModelSerializer): 11 | image = serializers.CharField(required=False, allow_null=True) 12 | 13 | class Meta: 14 | model = Article 15 | fields = ["title", "content", "image", "author", "created_at", "updated_at"] 16 | 17 | def create(self, validated_data): 18 | image_data = validated_data.pop("image", None) 19 | article = Article.objects.create(**validated_data) 20 | 21 | if image_data: 22 | format, imgstr = image_data.split(";base64,") 23 | ext = format.split("/")[-1] 24 | data = ContentFile(base64.b64decode(imgstr), name=f"{uuid.uuid4()}.{ext}") 25 | article.image.save(data.name, data, save=True) 26 | return article 27 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0018_printmodel_processprint_voting_prints.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-04-19 20:17 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("sejm_app", "0017_alter_vote_vote"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="printmodel", 15 | name="processPrint", 16 | field=models.ForeignKey( 17 | blank=True, 18 | null=True, 19 | on_delete=django.db.models.deletion.CASCADE, 20 | to="sejm_app.printmodel", 21 | ), 22 | ), 23 | migrations.AddField( 24 | model_name="voting", 25 | name="prints", 26 | field=models.ManyToManyField( 27 | blank=True, 28 | help_text="Prints related to the voting", 29 | related_name="votings", 30 | to="sejm_app.printmodel", 31 | ), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /frontend/app/envoys/[id]/biography.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Card, CardContent, CardHeader } from "@/components/ui/card"; 3 | import { Button } from "@/components/ui/button"; 4 | import { ExternalLink } from "lucide-react"; 5 | 6 | interface EnvoyBiographyProps { 7 | biography: string; 8 | biographySource: string; 9 | } 10 | 11 | const EnvoyBiography: React.FC = ({ biography, biographySource }) => { 12 | return ( 13 | 14 | 15 |

Biografia

16 |
17 | 18 |

19 | {biography.length > 1500 ? `${biography.slice(0, 1500)}...` : biography} 20 |

21 | 26 |
27 |
28 | ); 29 | }; 30 | 31 | export default EnvoyBiography; -------------------------------------------------------------------------------- /src/sejm_app/migrations/0015_alter_committeesitting_options_alter_envoy_options_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-04-19 15:37 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("sejm_app", "0014_alter_voting_kind_votingoption_listvote"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterModelOptions( 13 | name="committeesitting", 14 | options={"ordering": ["-date"]}, 15 | ), 16 | migrations.AlterModelOptions( 17 | name="envoy", 18 | options={"ordering": ["lastName"]}, 19 | ), 20 | migrations.AddField( 21 | model_name="voting", 22 | name="category", 23 | field=models.CharField( 24 | blank=True, 25 | choices=[ 26 | ("APPLICATION", "Wniosek formalny"), 27 | ("DRAFT", "Projekt ustawy"), 28 | ], 29 | help_text="Voting category", 30 | max_length=32, 31 | null=True, 32 | ), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /src/api/views/article.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | from rest_framework.viewsets import ReadOnlyModelViewSet 3 | from api.pagination import ApiViewPagination 4 | from api.serializers.articleSerializer import ArticleListSerializer 5 | from community_app.models import Article 6 | from community_app.serializers import ArticleSerializer 7 | from django.utils.decorators import method_decorator 8 | from django.views.decorators.cache import cache_page 9 | 10 | 11 | class ArticleViewSet(ReadOnlyModelViewSet): 12 | queryset = Article.objects.all() 13 | pagination_class = ApiViewPagination 14 | lookup_field = "id" 15 | 16 | def get_serializer_class(self): 17 | if self.action == "list": 18 | return ArticleListSerializer 19 | return ArticleSerializer 20 | 21 | @method_decorator(cache_page(60 * 15)) # Cache for 15 minutes 22 | def list(self, request, *args, **kwargs): 23 | return super().list(request, *args, **kwargs) 24 | 25 | @method_decorator(cache_page(60 * 15)) # Cache for 15 minutes 26 | def retrieve(self, request, *args, **kwargs): 27 | return super().retrieve(request, *args, **kwargs) 28 | -------------------------------------------------------------------------------- /promo/analisys.md: -------------------------------------------------------------------------------- 1 | 2 | Kto będzie rządził polską za 20 lat. 3 | Przeprowadziłem szczegółową analizę poszczególnych partii politycznych w Polsce. 4 | 5 | Partią, która ma najgorsze perspektywy rozwoju jest oczywiście PiS. Jak widzisz ponad połowa posłów ma powyżej 55 lat. 6 | za 3 kadencje, większość z obecnych posłów nie będzie w stanie startować w wyborach. 7 | Jeśli popatrzymy na największego rywala pisu czyli PO to mimo, że sam premier ma 67 lat to przeważają osoby mające około 45 lat. 8 | Takie osoby mają szansę rządzić jeszcze nawet 5 kadencji. 9 | Jeśli chodzi o PSL to partia ewidętnie wymiera, jako że jest tylko jedna osoba poniżej 40 lat. 10 | Po odejści korwina konfederacja to ewidentnie najmłodsza partia, i gdyby nie afery gaśnicowe i tym podobne pewnie była by najbardziej przyszłościową partią. 11 | I na końcu 12 | Statystyka twierdzi, że lewica oraz polska 2050 to partie boomerów, jako że średnia wieku to 45 lat. 13 | Jeśli chodzi o ilość kobiet w sejmie to nie istnieje partia w której przeważają kobiety, ale z kadencji na kadencję jest ich coraz więcej. 14 | Po więcej statystyk zapraszam na stronę sejm-stats.pl, która jest tworzona przez społeczność non-profit. 15 | 16 | -------------------------------------------------------------------------------- /frontend/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 5 | import { Check } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )) 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 29 | 30 | export { Checkbox } 31 | -------------------------------------------------------------------------------- /frontend/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | export function parsePLDate(dateString: string): Date { 9 | const months: { [key: string]: number } = { 10 | stycznia: 0, 11 | lutego: 1, 12 | marca: 2, 13 | kwietnia: 3, 14 | maja: 4, 15 | czerwca: 5, 16 | lipca: 6, 17 | sierpnia: 7, 18 | września: 8, 19 | października: 9, 20 | listopada: 10, 21 | grudnia: 11, 22 | }; 23 | const parts = dateString.replace(",", "").split(/\s+/); 24 | if (parts.length < 4) { 25 | throw new Error("Invalid date format"); 26 | } 27 | const day = parseInt(parts[0], 10); 28 | const month = months[parts[1].toLowerCase()]; 29 | const year = parseInt(parts[2], 10); 30 | const [hour, minute] = parts[3].split(":").map((num) => parseInt(num, 10)); 31 | 32 | if ( 33 | isNaN(day) || 34 | month === undefined || 35 | isNaN(year) || 36 | isNaN(hour) || 37 | isNaN(minute) 38 | ) { 39 | throw new Error("Invalid date components"); 40 | } 41 | 42 | return new Date(year, month, day, hour, minute); 43 | } 44 | -------------------------------------------------------------------------------- /src/api/views/interpellations.py: -------------------------------------------------------------------------------- 1 | from django.utils.decorators import method_decorator 2 | from django.views.decorators.cache import cache_page 3 | from django_filters.rest_framework import DjangoFilterBackend 4 | from rest_framework import filters, serializers 5 | from rest_framework.pagination import PageNumberPagination 6 | from rest_framework.viewsets import ReadOnlyModelViewSet 7 | 8 | from api.pagination import ApiViewPagination 9 | from api.serializers.list_serializers import InterpellationListSerializer 10 | from sejm_app.models.interpellation import Interpellation 11 | 12 | 13 | class InterpellationViewSet(ReadOnlyModelViewSet): 14 | queryset = Interpellation.objects.all() 15 | serializer_class = InterpellationListSerializer 16 | pagination_class = ApiViewPagination 17 | 18 | ordering = ["-receiptDate"] # Default to most recently received 19 | 20 | @method_decorator(cache_page(60 * 15)) # Cache for 15 minutes 21 | def list(self, request, *args, **kwargs): 22 | return super().list(request, *args, **kwargs) 23 | 24 | @method_decorator(cache_page(60 * 60)) # Cache for 1 hour 25 | def retrieve(self, request, *args, **kwargs): 26 | return super().retrieve(request, *args, **kwargs) 27 | -------------------------------------------------------------------------------- /src/sejm_app/libs/resolution_parser.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from bs4 import BeautifulSoup 3 | from loguru import logger 4 | 5 | 6 | def extract_text(html): 7 | try: 8 | soup = BeautifulSoup(html, "html.parser") 9 | 10 | title = soup.find( 11 | "p", {"class": "TYTUAKTUprzedmiotregulacjiustawylubrozporzdzenia"} 12 | ) 13 | content = soup.find( 14 | "p", {"class": "NIEARTTEKSTtekstnieartykuowanynppodstprawnarozplubpreambua"} 15 | ) 16 | 17 | if title and content: 18 | return ( 19 | title.text.replace("\n", " ").strip(), 20 | content.text.replace("\n", " ").strip(), 21 | ) 22 | else: 23 | logger.error("Could not find the required elements in the HTML.") 24 | return None, None 25 | 26 | except Exception as e: 27 | logger.error(f"An error occurred while parsing the HTML: {e}") 28 | return None, None 29 | 30 | 31 | url = "https://orka.sejm.gov.pl/proc10.nsf/uchwaly/1_u.htm" 32 | 33 | html = requests.get(url).text 34 | title, content = extract_text(html) 35 | 36 | 37 | if title and content: 38 | print(f"Title: {title}") 39 | print(f"Content: {content}") 40 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0011_alter_committeemember_options_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-03-27 16:29 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("sejm_app", "0010_alter_committeesitting_committee"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterModelOptions( 13 | name="committeemember", 14 | options={"ordering": ["function"]}, 15 | ), 16 | migrations.AddField( 17 | model_name="committeesitting", 18 | name="prints", 19 | field=models.ManyToManyField( 20 | related_name="committee_sittings", to="sejm_app.printmodel" 21 | ), 22 | ), 23 | migrations.AlterField( 24 | model_name="committeemember", 25 | name="function", 26 | field=models.CharField( 27 | blank=True, 28 | choices=[ 29 | ("CHAIR", "Przewodniczący"), 30 | ("VICE_CHAIR", "zastępca przewodniczącego"), 31 | ], 32 | max_length=100, 33 | null=True, 34 | ), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /frontend/app/processes-results/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useSearchParams } from "next/navigation"; 3 | import { DataTable } from "@/components/dataTable/dataTable"; 4 | import { useColumnsWithClickHandler } from "./columns"; 5 | import { useFetchData } from "@/lib/api"; 6 | import { SkeletonComponent } from "@/components/ui/skeleton-page"; 7 | 8 | export default function ProcessesResultsPage() { 9 | const searchParams = useSearchParams(); 10 | const { 11 | data: processes, 12 | isLoading, 13 | error, 14 | } = useFetchData(`/processes/?${searchParams?.toString()}`); 15 | 16 | const filters = [ 17 | { columnKey: "documentType", title: "Typ dokumentu" }, 18 | { columnKey: "createdBy", title: "Autorzy" }, 19 | { columnKey: "length_tag", title: "Długość" }, 20 | ]; 21 | 22 | const columnsWithClickHandler = useColumnsWithClickHandler(); 23 | 24 | if (isLoading) return ; 25 | if (error) return <>{error.message}; 26 | if (!processes) return null; 27 | 28 | return ( 29 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /frontend/components/ui/confirm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRouter } from "next/navigation"; 3 | import { Button } from "@/components/ui/button"; 4 | 5 | type ConfirmButtonProps = { 6 | url: string; 7 | selectedCategories?: string[]; 8 | selectedKinds?: string[]; 9 | selectedStart_date?: string; 10 | selectedEnd_date?: string; 11 | [key: string]: string[] | string | undefined; 12 | }; 13 | 14 | function ConfirmButton({ url, ...props }: ConfirmButtonProps) { 15 | const router = useRouter(); 16 | 17 | const handleConfirm = () => { 18 | const params = new URLSearchParams(); 19 | 20 | Object.entries(props).forEach(([key, value]) => { 21 | if (Array.isArray(value) && value.length) { 22 | const paramKey = key.replace("selected", "").toLowerCase(); 23 | params.append(paramKey, value.map((item) => item.split(" (")[0]).join(",")); 24 | } else if (typeof value === 'string' && value) { 25 | const paramKey = key.replace("selected", "").toLowerCase(); 26 | params.append(paramKey, value); 27 | } 28 | }); 29 | 30 | router.push(`/${url}?${params.toString()}`); 31 | }; 32 | 33 | return ; 34 | } 35 | 36 | export default ConfirmButton; -------------------------------------------------------------------------------- /frontend/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SwitchPrimitives from "@radix-ui/react-switch" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Switch = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 25 | 26 | )) 27 | Switch.displayName = SwitchPrimitives.Root.displayName 28 | 29 | export { Switch } 30 | -------------------------------------------------------------------------------- /docker-compose-dev.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | db: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile.postgres 8 | 9 | volumes: 10 | - postgres_data:/db/data 11 | env_file: 12 | - .env 13 | web: 14 | build: 15 | context: . 16 | dockerfile: Dockerfile 17 | args: 18 | SERVICE: web 19 | command: ["sh", "-c", "tail -f /dev/null"] 20 | volumes: 21 | - ./src:/code/src 22 | - ./src/manage.py:/code/manage.py 23 | - static_volume:/code/static 24 | - media_volume:/code/media 25 | depends_on: 26 | - db 27 | - redis 28 | env_file: 29 | - .env 30 | redis: 31 | image: redis:latest 32 | 33 | celery: 34 | build: 35 | context: . 36 | dockerfile: Dockerfile.celery 37 | command: ["celery", "-A", "core", "worker", "-l", "INFO"] 38 | volumes: 39 | - ./src:/code/src 40 | - ./src/manage.py:/code/manage.py 41 | - media_volume:/code/media 42 | depends_on: 43 | - db 44 | - redis 45 | - web 46 | environment: 47 | - C_FORCE_ROOT=true 48 | - CUDA=${CUDA:-true} 49 | env_file: 50 | - .env 51 | mem_limit: 2g 52 | 53 | volumes: 54 | postgres_data: 55 | static_volume: 56 | media_volume: 57 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0023_alter_voting_options_alter_voting_description_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-04-22 17:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("sejm_app", "0022_envoy_isfemale"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterModelOptions( 13 | name="voting", 14 | options={"ordering": ["-date"]}, 15 | ), 16 | migrations.AlterField( 17 | model_name="voting", 18 | name="description", 19 | field=models.CharField( 20 | blank=True, help_text="Voting description", max_length=512, null=True 21 | ), 22 | ), 23 | migrations.AlterField( 24 | model_name="voting", 25 | name="title", 26 | field=models.CharField( 27 | blank=True, help_text="Voting topic", max_length=512, null=True 28 | ), 29 | ), 30 | migrations.AlterField( 31 | model_name="voting", 32 | name="topic", 33 | field=models.CharField( 34 | blank=True, help_text="Short voting topic", max_length=512, null=True 35 | ), 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /frontend/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider 9 | 10 | const Tooltip = TooltipPrimitive.Root 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 27 | )) 28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName 29 | 30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 31 | -------------------------------------------------------------------------------- /.github/workflows/delivery-production.yml: -------------------------------------------------------------------------------- 1 | # name: Delivery Production 2 | 3 | # on: 4 | # push: 5 | # branches: 6 | # - main 7 | # tags: 8 | # - "v*" 9 | 10 | # jobs: 11 | # build-docker: 12 | # runs-on: ubuntu-22.04 13 | # environment: production 14 | 15 | # steps: 16 | # - name: Checkout code 17 | # uses: actions/checkout@v3 18 | 19 | # - name: Set up Docker 20 | # uses: docker/setup-buildx-action@v1 21 | 22 | # - name: Log in to GitHub Docker Registry 23 | # uses: docker/login-action@v1 24 | # with: 25 | # registry: ghcr.io 26 | # username: ${{ github.repository_owner }} 27 | # password: ${{ secrets.GHCR_TOKEN }} 28 | 29 | # - name: Build and push Docker image 30 | # uses: docker/build-push-action@v2 31 | # with: 32 | # context: . 33 | # file: .docker/python/Dockerfile 34 | # push: true 35 | # tags: ghcr.io/michalskibinski109/sejm-stats:latest 36 | # target: prod 37 | # labels: | 38 | # org.opencontainers.image.source=${{ github.event.repository.html_url }} 39 | # org.opencontainers.image.revision=${{ github.sha }} 40 | # org.opencontainers.image.created=${{ steps.date.outputs.datetime }} 41 | -------------------------------------------------------------------------------- /frontend/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/components/newsCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Card } from "@/components/ui/card"; 3 | import Image from "next/image"; 4 | import Link from "next/link"; 5 | 6 | interface NewsCardProps { 7 | id: number; 8 | title: string; 9 | image: string | null; 10 | } 11 | 12 | const NewsCard: React.FC = ({ id, title, image }) => { 13 | 14 | return ( 15 | 16 | 17 |
18 | {image ? ( 19 | {title} 20 | ) : ( 21 |
22 | No image available 23 |
24 | )} 25 |
26 |
27 |

28 | {title} 29 |

30 |
31 |
32 | 33 | 34 | ); 35 | }; 36 | 37 | export default NewsCard; 38 | -------------------------------------------------------------------------------- /frontend/app/clubs/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import ClubsList from "@/app/clubs/clubList"; 4 | import ClubsChart from "@/app/clubs/clubChart"; 5 | import { SkeletonComponent } from "@/components/ui/skeleton-page"; 6 | import LoadableContainer from "@/components/loadableContainer"; 7 | import { APIResponse, Club } from "@/lib/types"; 8 | import { useFetchData } from "@/lib/api"; 9 | 10 | export default function ClubsPage() { 11 | const { data, isLoading, error } = useFetchData>(`/clubs/`); 12 | 13 | if (isLoading) return ; 14 | if (error) return {error.message}; 15 | if (!data) return null; 16 | 17 | return ( 18 |
19 |

20 | Kluby Parlamentarne 21 |

22 |
23 |
24 |

Lista Klubów

25 | 26 |
27 |
28 |

Rozkład Mandatów

29 | 30 |
31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/core/generic/mixins.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | 3 | from django.http import JsonResponse 4 | from django.views.generic import ListView 5 | 6 | 7 | class SearchFormMixin: 8 | form_class = None # To be set in the subclass 9 | 10 | def get_queryset(self): 11 | queryset = super().get_queryset() 12 | form = self.form_class(self.request.GET) 13 | if form.is_valid(): 14 | for field, value in form.cleaned_data.items(): 15 | if value: 16 | queryset = queryset.filter(**{field: value}) 17 | return queryset 18 | 19 | 20 | class JsonResponseMixin: 21 | def render_to_json_response( 22 | self, context, add_data: dict = {}, **response_kwargs 23 | ) -> JsonResponse: 24 | fields = self.get_json_fields() 25 | queryset = self.get_queryset() 26 | urls = [self.get_item_url(i) for i in queryset] 27 | data = list(queryset.values(*fields)) 28 | data = [list(item.values()) for item in data] 29 | response_data = {"data": data, "urls": urls} 30 | response_data.update(add_data) 31 | return JsonResponse(response_data, safe=False) 32 | 33 | @abstractmethod 34 | def get_item_url(self, item): 35 | raise NotImplementedError() 36 | 37 | def get_json_fields(self): 38 | return ["title", "lastModified"] 39 | -------------------------------------------------------------------------------- /frontend/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | success: 18 | "border-transparent bg-green-500 text-white hover:bg-green-600", 19 | outline: "text-foreground", 20 | }, 21 | }, 22 | defaultVariants: { 23 | variant: "default", 24 | }, 25 | } 26 | ) 27 | 28 | export interface BadgeProps 29 | extends React.HTMLAttributes, 30 | VariantProps {} 31 | 32 | function Badge({ className, variant, ...props }: BadgeProps) { 33 | return ( 34 |
35 | ) 36 | } 37 | 38 | export { Badge, badgeVariants } -------------------------------------------------------------------------------- /frontend/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )) 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 30 | 31 | export { Popover, PopoverTrigger, PopoverContent } 32 | -------------------------------------------------------------------------------- /frontend/app/committees/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import { useSearchParams } from "next/navigation"; 5 | import { DataTable } from "@/components/dataTable/dataTable"; 6 | import LoadableContainer from "@/components/loadableContainer"; 7 | import { columns, useColumnsWithClickHandler } from "./columns"; 8 | import { useFetchData } from "@/lib/api"; 9 | import { APIResponse, Committee, Interpellation } from "@/lib/types"; 10 | import { SkeletonComponent } from "@/components/ui/skeleton-page"; 11 | 12 | export default function CommitteesPage() { 13 | const { data, isLoading, error } = 14 | useFetchData>(`/committees/`); 15 | const columnsWithClickHandler = useColumnsWithClickHandler(); 16 | 17 | if (isLoading) return ; 18 | if (error) return {error.message}; 19 | if (!data) return null; 20 | 21 | const filters = [ 22 | { columnKey: "type", title: "Typ" }, 23 | { columnKey: "compositionDate", title: "Data powołania" }, 24 | ]; 25 | 26 | return ( 27 |
28 | 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /frontend/components/navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import Link from "next/link"; 5 | import { usePathname } from "next/navigation"; 6 | import { FaUser, FaSignOutAlt } from "react-icons/fa"; 7 | import { ModeToggle } from "./ModeToggle"; // Adjust the import path as necessary 8 | import LoginButton from "./ui/loginButton"; 9 | 10 | interface NavbarProps { 11 | children?: React.ReactNode; 12 | } 13 | 14 | const Navbar: React.FC = ({ children }) => { 15 | const pathname = usePathname(); 16 | 17 | return ( 18 | 38 | ); 39 | }; 40 | 41 | export default Navbar; 42 | -------------------------------------------------------------------------------- /src/api/views/envoys.py: -------------------------------------------------------------------------------- 1 | from django.utils.decorators import method_decorator 2 | from django.views.decorators.cache import cache_page 3 | from rest_framework import serializers 4 | from rest_framework.pagination import PageNumberPagination 5 | from rest_framework.viewsets import ReadOnlyModelViewSet 6 | 7 | from api.pagination import ApiViewPagination 8 | from api.serializers import EnvoyDetailSerializer 9 | from api.serializers.list_serializers import EnvoyListSerializer 10 | from sejm_app.models.envoy import Envoy 11 | 12 | 13 | class EnvoyViewSet(ReadOnlyModelViewSet): 14 | pagination_class = ApiViewPagination 15 | queryset = Envoy.objects.all() 16 | lookup_field = "id" 17 | 18 | def get_serializer_class(self): 19 | if self.action == "list": 20 | return EnvoyListSerializer 21 | return EnvoyDetailSerializer 22 | 23 | def get_queryset(self): 24 | queryset = super().get_queryset() 25 | if self.action == "retrieve": 26 | return EnvoyDetailSerializer.setup_eager_loading(queryset) 27 | return queryset 28 | 29 | @method_decorator(cache_page(60 * 15)) 30 | def list(self, request, *args, **kwargs): 31 | return super().list(request, *args, **kwargs) 32 | 33 | @method_decorator(cache_page(60 * 15)) 34 | def retrieve(self, request, *args, **kwargs): 35 | return super().retrieve(request, *args, **kwargs) 36 | -------------------------------------------------------------------------------- /src/api/views/faq.py: -------------------------------------------------------------------------------- 1 | from django.utils.decorators import method_decorator 2 | from django.views.decorators.cache import cache_page 3 | from rest_framework import serializers 4 | from rest_framework.pagination import PageNumberPagination 5 | from rest_framework.response import Response 6 | from rest_framework.viewsets import ReadOnlyModelViewSet 7 | 8 | from api.pagination import ApiViewPagination 9 | from api.serializers import EnvoyDetailSerializer 10 | from api.serializers.list_serializers import ( 11 | EnvoyListSerializer, 12 | FAQSerializer, 13 | TeamMemberSerializer, 14 | ) 15 | from community_app.models import TeamMember 16 | from sejm_app.models.envoy import Envoy 17 | from sejm_app.models.faq import FAQ 18 | 19 | 20 | class FAQViewSet(ReadOnlyModelViewSet): 21 | queryset = FAQ.objects.all() 22 | serializer_class = FAQSerializer 23 | 24 | @method_decorator(cache_page(60 * 15)) 25 | def list(self, request, *args, **kwargs): 26 | faq_data = super().list(request, *args, **kwargs).data 27 | team_members = TeamMember.objects.all() 28 | team_members_data = TeamMemberSerializer(team_members, many=True).data 29 | 30 | return Response({"faqs": faq_data, "team_members": team_members_data}) 31 | 32 | @method_decorator(cache_page(60 * 15)) 33 | def retrieve(self, request, *args, **kwargs): 34 | return super().retrieve(request, *args, **kwargs) 35 | -------------------------------------------------------------------------------- /src/sejm_app/models/scandal.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | 4 | from .club import Club 5 | from .envoy import Envoy 6 | 7 | 8 | class ScandalEntryStatus(models.TextChoices): 9 | """Status of a scandal entry.""" 10 | 11 | PENDING = "PENDING", "Pending" 12 | APPROVED = "APPROVED", "Approved" 13 | REJECTED = "REJECTED", "Rejected" 14 | 15 | 16 | class Scandal(models.Model): 17 | title = models.CharField(max_length=255, null=True, blank=True) 18 | description = models.TextField(null=True, blank=True) 19 | date = models.DateField(null=True, blank=True) 20 | envoys = models.ManyToManyField(Envoy, related_name="scandals", blank=True) 21 | clubs = models.ManyToManyField(Club, related_name="scandals", blank=True) 22 | entry_status = models.CharField( 23 | max_length=20, 24 | choices=ScandalEntryStatus.choices, 25 | default=ScandalEntryStatus.PENDING, 26 | ) 27 | # get logged admin.user that created the scandal 28 | author = models.ForeignKey( 29 | settings.AUTH_USER_MODEL, 30 | on_delete=models.CASCADE, 31 | related_name="scandals", 32 | help_text="Author of the description", 33 | ) 34 | 35 | url1 = models.URLField(null=True, blank=True) 36 | url2 = models.URLField(null=True, blank=True) 37 | 38 | def __str__(self): 39 | return f"{self.title} ({self.date})" 40 | -------------------------------------------------------------------------------- /src/api/views/committees.py: -------------------------------------------------------------------------------- 1 | from django.utils.decorators import method_decorator 2 | from django.views.decorators.cache import cache_page 3 | from rest_framework import serializers 4 | from rest_framework.pagination import PageNumberPagination 5 | from rest_framework.viewsets import ReadOnlyModelViewSet 6 | 7 | from api.pagination import ApiViewPagination 8 | from api.serializers.CommitteeDetailSerialzer import CommitteeDetailSerializer 9 | from api.serializers.list_serializers import CommitteeListSerializer 10 | from sejm_app.models.committee import Committee 11 | 12 | 13 | class CommitteeViewSet(ReadOnlyModelViewSet): 14 | queryset = Committee.objects.all() 15 | pagination_class = ApiViewPagination 16 | lookup_field = "code" 17 | 18 | def get_serializer_class(self): 19 | if self.action == "list": 20 | return CommitteeListSerializer 21 | return CommitteeDetailSerializer 22 | 23 | def get_queryset(self): 24 | queryset = super().get_queryset() 25 | if self.action == "retrieve": 26 | return CommitteeDetailSerializer.setup_eager_loading(queryset) 27 | return queryset 28 | 29 | @method_decorator(cache_page(60 * 15)) 30 | def list(self, request, *args, **kwargs): 31 | return super().list(request, *args, **kwargs) 32 | 33 | @method_decorator(cache_page(60 * 15)) 34 | def retrieve(self, request, *args, **kwargs): 35 | return super().retrieve(request, *args, **kwargs) 36 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0008_committeesitting.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-03-27 10:19 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("sejm_app", "0007_alter_process_comments_alter_process_description_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="CommitteeSitting", 15 | fields=[ 16 | ( 17 | "id", 18 | models.BigAutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ("agenda", models.TextField()), 26 | ("closed", models.BooleanField()), 27 | ("date", models.DateField()), 28 | ("num", models.IntegerField()), 29 | ("remote", models.BooleanField()), 30 | ("video_id", models.CharField(blank=True, max_length=60)), 31 | ( 32 | "committee", 33 | models.ForeignKey( 34 | on_delete=django.db.models.deletion.CASCADE, 35 | to="sejm_app.committee", 36 | ), 37 | ), 38 | ], 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /src/sejm_app/tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from django.test import TestCase 4 | 5 | # Create your tests here. 6 | from sejm_app.libs.agenda_parser import get_prints_from_title 7 | 8 | 9 | class TestGetPrintsFromTitle(unittest.TestCase): 10 | def test_multiple_print_references(self): 11 | self.assertEqual( 12 | get_prints_from_title("Komisja o ustawie (druki nr 100, 200)"), 13 | ["100", "200"], 14 | ) 15 | 16 | def test_single_print_reference(self): 17 | self.assertEqual( 18 | get_prints_from_title("Komisja o ustawie (druk nr 300)"), ["300"] 19 | ) 20 | 21 | def test_no_print_references(self): 22 | self.assertEqual( 23 | get_prints_from_title("Komisja o ustawie bez numerów druków"), [] 24 | ) 25 | 26 | def test_complex_print_references(self): 27 | self.assertEqual( 28 | get_prints_from_title("Komisja o ustawie (druk nr 400-A, 401-B)"), 29 | ["400-A", "401-B"], 30 | ) 31 | 32 | def test_mixed_content(self): 33 | self.assertEqual( 34 | get_prints_from_title( 35 | "Dyskusja o ustawie (druki nr 500) i innym dokumencie" 36 | ), 37 | ["500"], 38 | ) 39 | 40 | def test_prints_with_varied_spacing_and_punctuation(self): 41 | self.assertEqual( 42 | get_prints_from_title("Dyskusja (druki nr 123,456-A; 789)"), 43 | ["123", "456-A", "789"], 44 | ) 45 | -------------------------------------------------------------------------------- /.github/workflows/python.yml: -------------------------------------------------------------------------------- 1 | name: Test&lint Python codebase 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | types: 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | jobs: 13 | test-and-lint-python: 14 | name: Test&lint Python codebase 15 | timeout-minutes: 10 16 | if: github.event.pull_request.draft == false 17 | runs-on: ubuntu-22.04 18 | services: 19 | pgsql: 20 | image: postgres:16 21 | env: 22 | POSTGRES_DB: example 23 | POSTGRES_USER: postgres 24 | POSTGRES_PASSWORD: postgres 25 | PGPASSWORD: postgres 26 | ports: 27 | - 5432:5432 28 | 29 | steps: 30 | - name: Checkout code 31 | uses: actions/checkout@v3 32 | 33 | - name: Set up Python 34 | uses: actions/setup-python@v4 35 | with: 36 | python-version: 3.12 37 | cache: "pip" 38 | 39 | - name: Install dependencies 40 | run: | 41 | cat requirements.txt dev-requirements.txt > combined-requirements.txt 42 | pip install -r combined-requirements.txt 43 | 44 | # - name: Run Pylint 45 | # working-directory: src 46 | # run: | 47 | # pylint sejm_app 48 | 49 | - name: Run tests 50 | working-directory: src 51 | run: | 52 | cp ../.env.dist ./.env 53 | python manage.py migrate 54 | python manage.py test 55 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | until pg_isready -h $DATABASE_HOST -p $POSTGRES_PORT -U $POSTGRES_USER; do 4 | echo "Waiting for the database to be ready..." 5 | echo $DATABASE_HOST 6 | echo $POSTGRES_PORT 7 | echo $POSTGRES_USER 8 | sleep 5 9 | done 10 | 11 | export PGPASSWORD=$POSTGRES_PASSWORD 12 | 13 | psql -h $DATABASE_HOST -p $POSTGRES_PORT -U $POSTGRES_USER -d $POSTGRES_DB -c " 14 | DO \$\$ 15 | BEGIN 16 | BEGIN 17 | CREATE TEXT SEARCH DICTIONARY pl_ispell ( 18 | Template = ispell, 19 | DictFile = polish, 20 | AffFile = polish, 21 | StopWords = polish 22 | ); 23 | EXCEPTION WHEN unique_violation THEN 24 | -- do nothing, and continue execution 25 | END; 26 | BEGIN 27 | CREATE TEXT SEARCH CONFIGURATION pl_ispell (PARSER = default); 28 | EXCEPTION WHEN unique_violation THEN 29 | -- do nothing, and continue execution 30 | END; 31 | ALTER TEXT SEARCH CONFIGURATION pl_ispell 32 | ALTER MAPPING FOR asciiword, asciihword, hword_asciipart, word, hword, hword_part 33 | WITH pl_ispell; 34 | END 35 | \$\$; 36 | " 37 | 38 | # Add the pgvector extension 39 | psql -h $DATABASE_HOST -p $POSTGRES_PORT -U $POSTGRES_USER -d $POSTGRES_DB -c "CREATE EXTENSION IF NOT EXISTS VECTOR;" 40 | 41 | python manage.py makemigrations 42 | python manage.py migrate 43 | 44 | python manage.py collectstatic --noinput 45 | python manage.py loaddata src/fixtures/faq.json 46 | 47 | exec "$@" -------------------------------------------------------------------------------- /frontend/app/acts-results/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useEffect, useState } from "react"; 3 | import { useSearchParams } from "next/navigation"; 4 | import { DataTable } from "@/components/dataTable/dataTable"; 5 | import LoadableContainer from "@/components/loadableContainer"; 6 | import { Act, APIResponse } from "@/lib/types"; 7 | import { getColumnsWithClickHandler } from "./columns"; 8 | import { useFetchData } from "@/lib/api"; 9 | import { SkeletonComponent } from "@/components/ui/skeleton-page"; 10 | 11 | export default function ActsResultsPage() { 12 | const searchParams = useSearchParams(); 13 | const { data, isLoading, error } = useFetchData>( 14 | `/acts/?${searchParams?.toString()}` 15 | ); 16 | if (isLoading) return ; 17 | if (error) return {error.message}; 18 | if (!data) return null; 19 | 20 | const filters = [ 21 | { columnKey: "publisher", title: "Wydawca" }, 22 | { columnKey: "status", title: "Status" }, 23 | { columnKey: "announcementDate", title: "Data ogłoszenia" }, 24 | { columnKey: "entryIntoForce", title: "Data wejścia w życie" }, 25 | ]; 26 | 27 | return ( 28 |
29 | 30 | 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /frontend/app/envoys/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { useParams } from "next/navigation"; 4 | import { useFetchData } from "@/lib/api"; 5 | import { SkeletonComponent } from "@/components/ui/skeleton-page"; 6 | import LoadableContainer from "@/components/loadableContainer"; 7 | import { EnvoyDetail } from "@/lib/types"; 8 | import EnvoyInfoCard from "./infoCard"; 9 | import EnvoyBiography from "./biography"; 10 | import EnvoyCommittees from "./committees"; 11 | import EnvoyTabs from "./envoyTabs"; 12 | 13 | const EnvoyDetailPage: React.FC = () => { 14 | const { id } = useParams<{ id: string }>() ?? {}; 15 | const { data: envoy, isLoading, error } = useFetchData(`/envoys/${id}/`); 16 | 17 | if (isLoading) return ; 18 | if (error) return {error.message}; 19 | if (!envoy) return null; 20 | 21 | return ( 22 |
23 |
24 |
25 | 26 |
27 |
28 | 29 | 30 | 31 |
32 |
33 |
34 | ); 35 | }; 36 | 37 | export default EnvoyDetailPage; -------------------------------------------------------------------------------- /frontend/components/dataTable/columns.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | ArrowDownIcon, 4 | ArrowUpIcon, 5 | CaretSortIcon, 6 | EyeNoneIcon, 7 | EyeOpenIcon, 8 | } from "@radix-ui/react-icons"; 9 | import { Column } from "@tanstack/react-table"; 10 | 11 | import { cn } from "@/lib/utils"; 12 | import { Button } from "@/components/ui/button"; 13 | 14 | interface DataTableColumnHeaderProps 15 | extends React.HTMLAttributes { 16 | column: Column; 17 | title: string; 18 | } 19 | 20 | export function DataTableColumnHeader({ 21 | column, 22 | title, 23 | className, 24 | }: DataTableColumnHeaderProps) { 25 | const handleSort = () => { 26 | column.toggleSorting(column.getIsSorted() === "asc"); 27 | }; 28 | 29 | return ( 30 |
31 | {title} 32 | {column.getCanSort() && ( 33 | 47 | )} 48 |
49 | ); 50 | } -------------------------------------------------------------------------------- /src/eli_app/tests/test_pdf_section_split.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from pathlib import Path 3 | import pdfplumber 4 | from eli_app.libs.act_sections_extractor import split_into_sections 5 | from eli_app.models import ActSection 6 | 7 | 8 | class ActSectionsExtractorTest(TestCase): 9 | def setUp(self): 10 | # Load sample text from a PDF file for testing 11 | path = Path(__file__).parent / "text.pdf" 12 | with pdfplumber.open(path) as pdf: 13 | self.sample_text = "".join(page.extract_text() or "" for page in pdf.pages) 14 | 15 | def test_section_content_length(self): 16 | sections = split_into_sections(self.sample_text) 17 | for section in sections: 18 | content_length = len(section.content) 19 | self.assertTrue( 20 | 4000 < content_length < 16000, 21 | f"Section content length {content_length} is not within the expected range (4000, 16000)", 22 | ) 23 | 24 | def test_total_sections(self): 25 | sections = split_into_sections(self.sample_text) 26 | total_sections = len(sections) 27 | self.assertGreater(total_sections, 0, "No sections were created") 28 | 29 | def test_total_characters(self): 30 | sections = split_into_sections(self.sample_text) 31 | total_characters = sum(len(section.content) for section in sections) 32 | self.assertGreater( 33 | total_characters, 0, "Total characters should be greater than 0" 34 | ) 35 | -------------------------------------------------------------------------------- /src/eli_app/migrations/0002_actsection.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-11-01 20:06 2 | 3 | import django.db.models.deletion 4 | import pgvector.django.vector 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('eli_app', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='ActSection', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('embedding', pgvector.django.vector.VectorField(blank=True, dimensions=768, null=True)), 20 | ('summary', models.TextField(max_length=1024, null=True, verbose_name='AI summary of the document')), 21 | ('start_char', models.IntegerField(help_text='The starting character position of the section within the act.', null=True)), 22 | ('end_char', models.IntegerField(help_text='The ending character position of the section within the act.', null=True)), 23 | ('content', models.TextField(help_text='The text content of the section.', null=True)), 24 | ('chapters', models.CharField(max_length=255, null=True)), 25 | ('act', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='eli_app.act')), 26 | ], 27 | options={ 28 | 'abstract': False, 29 | }, 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /frontend/app/interpellations/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { DataTable } from "@/components/dataTable/dataTable"; 3 | import { columns, getColumnsWithClickHandler } from "./columns"; 4 | import LoadableContainer from "@/components/loadableContainer"; 5 | import { useSearchParams } from "next/navigation"; 6 | import { Suspense, useEffect, useState } from "react"; 7 | import { APIResponse, Interpellation } from "@/lib/types"; 8 | import { useFetchData } from "@/lib/api"; 9 | import { SkeletonComponent } from "@/components/ui/skeleton-page"; 10 | 11 | async function InterpellationsTable() { 12 | const searchParams = useSearchParams(); 13 | const { data, isLoading, error } = useFetchData>( 14 | `/interpellations/?${searchParams?.toString()}` 15 | ); 16 | if (isLoading) return ; 17 | if (error) return <>{error.message}; 18 | if (!data) return null; 19 | 20 | const filters = [ 21 | { columnKey: "member", title: "Autor" }, 22 | { columnKey: "sentDate", title: "Data wysłania" }, 23 | { columnKey: "to", title: "Adresat" }, 24 | ]; 25 | return ( 26 | <> 27 | 32 | 33 | ); 34 | } 35 | 36 | export default function InterpellationsPage() { 37 | return ( 38 |
39 | }> 40 | 41 | 42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /frontend/app/articles/create/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useSession, signIn } from "next-auth/react"; 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | Card, 6 | CardContent, 7 | CardHeader, 8 | CardTitle, 9 | CardDescription, 10 | } from "@/components/ui/card"; 11 | import { Loader2 } from "lucide-react"; 12 | import RichTextEditor from "@/components/editor/text-editor"; 13 | 14 | function App() { 15 | const { data: session, status } = useSession(); 16 | 17 | if (status === "loading") { 18 | return ( 19 |
20 | 21 |
22 | ); 23 | } 24 | 25 | if (!session) { 26 | return ( 27 |
28 | 29 | 30 | Zaloguj się 31 | 32 | Musisz być zalogowany, aby utworzyć artykuł. 33 | 34 | 35 | 36 | 39 | 40 | 41 |
42 | ); 43 | } 44 | 45 | return ( 46 |
47 | 48 | 49 | 50 | 51 |
52 | ); 53 | } 54 | 55 | export default App; 56 | -------------------------------------------------------------------------------- /src/eli_app/libs/clean_act_title.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def clean_title(title): 5 | # Extract the authority and the rest of the title 6 | match = re.match( 7 | r"^(?:Rozporządzenie|Obwieszczenie)\s+(.*?)\s+z\s+dnia.*?(?:w sprawie|zmieniające)", 8 | title, 9 | re.IGNORECASE, 10 | ) 11 | authority = match.group(1) if match else "" 12 | 13 | # Remove the document type, date, and "w sprawie" phrases 14 | title = re.sub(r"^.*?(?:z dnia \d+\s+\w+\s+\d{4}\s*r\.\s*)", "", title) 15 | title = re.sub( 16 | r"(?:zmieniające\s+rozporządzenie\s+)?w\s+sprawie\s+", "dot. ", title 17 | ) 18 | title = title.replace( 19 | "Rzeczypospolitej Polskiej ogłoszenia jednolitego tekstu ustawy", "" 20 | ) 21 | 22 | # Combine authority with cleaned title 23 | cleaned_title = f"{authority} {title}".strip() 24 | 25 | # Remove code patterns like (PLH120079) 26 | cleaned_title = re.sub(r"\(\w+\d+\)", "", cleaned_title) 27 | 28 | # Remove extra whitespace 29 | cleaned_title = re.sub(r"\s+", " ", cleaned_title).strip() 30 | 31 | # Remove specific patterns - now excluding 'Ministra' and 'Marszałka' 32 | patterns_to_remove = [ 33 | r"Prezesa Rady Ministrów", 34 | r"Rady Ministrów", 35 | r"ogłoszenia jednolitego tekstu", 36 | r"zmieniające rozporządzenie", 37 | r"ustawy -", 38 | ] 39 | for pattern in patterns_to_remove: 40 | cleaned_title = re.sub(pattern, "", cleaned_title) 41 | 42 | # Final cleanup 43 | cleaned_title = re.sub(r"\s+", " ", cleaned_title).strip() 44 | 45 | return cleaned_title 46 | -------------------------------------------------------------------------------- /frontend/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0021_alter_printmodel_options_alter_club_email_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-04-21 21:14 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("sejm_app", "0020_alter_voting_category"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterModelOptions( 13 | name="printmodel", 14 | options={"ordering": ["documentDate"]}, 15 | ), 16 | migrations.AlterField( 17 | model_name="club", 18 | name="email", 19 | field=models.EmailField(blank=True, max_length=255, null=True), 20 | ), 21 | migrations.AlterField( 22 | model_name="club", 23 | name="phone", 24 | field=models.CharField(blank=True, max_length=255, null=True), 25 | ), 26 | migrations.AlterField( 27 | model_name="voting", 28 | name="category", 29 | field=models.CharField( 30 | blank=True, 31 | choices=[ 32 | ("APPLICATION", "Wniosek formalny"), 33 | ("PRINTS", "Głosowanie druków"), 34 | ("WHOLE_PROJECT", "Głosowanie nad całością projektu"), 35 | ("AMENDMENT", "Głosowanie nad poprawką"), 36 | ("CANDIDATES", "Głosowanie na kandydatów"), 37 | ("OTHER", "Inne"), 38 | ], 39 | help_text="Voting category", 40 | max_length=32, 41 | null=True, 42 | ), 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /frontend/components/home/statCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import { Card, CardContent, CardFooter } from "@/components/ui/card"; 4 | import { Badge } from "@/components/ui/badge"; 5 | import { ArrowRight } from "lucide-react"; 6 | 7 | interface StatCardProps { 8 | title: string; 9 | count: number; 10 | color: "default" | "secondary" | "destructive" | "outline"; 11 | url: string; 12 | } 13 | 14 | const StatCard: React.FC = ({ title, count, color, url }) => { 15 | return ( 16 | 17 | 18 |

{title}

19 |

32 | {count.toLocaleString()} 33 |

34 |
35 | 36 | 40 | Pokaż więcej 41 | 42 | 43 | 44 |
45 | ); 46 | }; 47 | 48 | export default StatCard; 49 | -------------------------------------------------------------------------------- /frontend/app/clubs/clubList.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Club } from "@/lib/types"; 4 | import Image from "next/image"; 5 | import Link from "next/link"; 6 | 7 | interface ClubsListProps { 8 | clubs: Club[]; 9 | } 10 | 11 | const ClubsList: React.FC = ({ clubs }) => { 12 | return ( 13 |
    14 | {clubs 15 | .sort((a: Club, b: Club) => b.membersCount - a.membersCount) 16 | .map((club: Club) => ( 17 |
  • 18 | 22 |
    23 | {club.id} 29 |
    30 |
    31 |
    {club.id}
    32 |
    33 |
    34 | 35 | {club.membersCount} 36 | 37 |

    mandatów

    38 |
    39 | 40 |
  • 41 | ))} 42 |
43 | ); 44 | }; 45 | 46 | export default ClubsList; 47 | -------------------------------------------------------------------------------- /src/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from api.views.vector_search import VectorSearchView 3 | from rest_framework.routers import DefaultRouter 4 | 5 | from api import views 6 | from api.views.article import ArticleViewSet 7 | from api.views.home import HomeViewSet 8 | from api.views.search import OptimizedSearchView 9 | 10 | router = DefaultRouter() 11 | router.register(r"envoys", views.EnvoyViewSet, basename="envoy") 12 | router.register( 13 | r"interpellations", views.InterpellationViewSet, basename="interpellation" 14 | ) 15 | router.register(r"faq", views.FAQViewSet, basename="faq") 16 | router.register(r"clubs", views.ClubViewSet, basename="club") 17 | router.register(r"acts", views.ActViewSet, basename="act") 18 | router.register(r"acts-meta", views.ActsMetaViewSet, basename="act-meta") 19 | router.register(r"committees", views.CommitteeViewSet, basename="committee") 20 | router.register(r"votings", views.VotingViewSet, basename="votings") 21 | router.register(r"votings-meta", views.VotingsMetaViewSet, basename="voting-meta") 22 | router.register(r"processes", views.ProcessViewSet, basename="processes") 23 | router.register(r"processes-meta", views.ProcessesMetaViewSet, basename="process-meta") 24 | router.register(r"home", views.HomeViewSet, basename="home") 25 | router.register(r"articles", ArticleViewSet) 26 | 27 | app_name = "api" 28 | 29 | urlpatterns = [ 30 | path("", include(router.urls)), 31 | path("search", OptimizedSearchView.as_view(), name="search"), 32 | path("vector-search/", VectorSearchView.as_view(), name="vector-search"), 33 | path("total-stats", views.TotalStatsView.as_view(), name="total-stats"), 34 | ] 35 | -------------------------------------------------------------------------------- /src/sejm_app/models/print_model.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import datetime 4 | from urllib.parse import urljoin 5 | 6 | from django.conf import settings 7 | from django.db import models 8 | from django.db.models import F 9 | from django.utils import timezone 10 | from django.utils.functional import cached_property 11 | 12 | 13 | class PrintModel(models.Model): 14 | id = models.CharField(max_length=18, primary_key=True, editable=False) 15 | processPrint = models.ForeignKey( 16 | "self", on_delete=models.CASCADE, null=True, blank=True 17 | ) 18 | number = models.CharField(max_length=15) 19 | term = models.SmallIntegerField() 20 | title = models.TextField() 21 | documentDate = models.DateField() 22 | deliveryDate = models.DateField() 23 | changeDate = models.DateTimeField() 24 | 25 | def save(self, *args, **kwargs): 26 | self.id = f"{self.term}{self.number}" 27 | super().save(*args, **kwargs) 28 | 29 | # id is term + number 30 | 31 | @cached_property 32 | def pdf_url(self) -> str: 33 | return f"{settings.SEJM_ROOT_URL}/prints/{self.number}/{self.number}.pdf" 34 | 35 | @cached_property 36 | def api_url(self) -> str: 37 | return f"settings.SEJM_ROOT_URL/prints/{self.number}" 38 | 39 | def __str__(self) -> str: 40 | return f"{self.number}. {self.title}" 41 | 42 | class Meta: 43 | ordering = ["deliveryDate"] 44 | 45 | 46 | class AdditionalPrint(PrintModel): 47 | main_print = models.ForeignKey( 48 | PrintModel, 49 | related_name="additional_prints", 50 | on_delete=models.CASCADE, 51 | ) 52 | -------------------------------------------------------------------------------- /frontend/app/envoys/[id]/committees.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import { Card, CardContent, CardHeader } from "@/components/ui/card"; 4 | import { Badge } from "@/components/ui/badge"; 5 | 6 | interface Committee { 7 | committee_name: string; 8 | committee_code: string; 9 | function?: string; 10 | } 11 | 12 | interface EnvoyCommitteesProps { 13 | committees: Committee[]; 14 | } 15 | 16 | const EnvoyCommittees: React.FC = ({ committees }) => { 17 | if (committees.length === 0) { 18 | return ( 19 | 20 | 21 |

Członkostwo w komisjach

22 |
23 | 24 |

Brak członkostwa w komisjach.

25 |
26 |
27 | ); 28 | } 29 | 30 | return ( 31 | 32 | 33 |

Członkostwo w komisjach

34 |
35 | 36 |
    37 | {committees.map((membership, index) => ( 38 |
  • 39 | 40 | {membership.committee_name} 41 | 42 | {membership.function && {membership.function}} 43 |
  • 44 | ))} 45 |
46 |
47 |
48 | ); 49 | }; 50 | 51 | export default EnvoyCommittees; -------------------------------------------------------------------------------- /frontend/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Home from "@/components/home/home"; 3 | import { Suspense } from "react"; 4 | import { SkeletonComponent } from "@/components/ui/skeleton-page"; 5 | import type { Metadata } from "next"; 6 | import { useFetchData } from "@/lib/api"; 7 | import LoadableContainer from "@/components/loadableContainer"; 8 | import LoginButton from "@/components/ui/loginButton"; 9 | import { HomeHeader } from "@/components/home/homeHeader"; 10 | import { APIResponse, ArticleListItem } from "@/lib/types"; 11 | 12 | function HomeContainer() { 13 | const { data, isLoading, error } = useFetchData(`/home/`); 14 | const { 15 | data: lastArticlesData, 16 | isLoading: isLastArticlesLoading, 17 | error: lastArticlesError, 18 | } = useFetchData>(`/articles/`); 19 | 20 | if (isLoading || isLastArticlesLoading) return ; 21 | if (error || lastArticlesError) 22 | return ( 23 | 24 | {error?.message || lastArticlesError?.message} 25 | 26 | ); 27 | if (!data || !lastArticlesData) return null; 28 | 29 | return ( 30 | 36 | ); 37 | } 38 | 39 | export default function Page() { 40 | return ( 41 |
42 | 43 | }> 44 | 45 | 46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/sejm_app/libs/wikipedia_searcher.py: -------------------------------------------------------------------------------- 1 | import wikipedia 2 | from loguru import logger 3 | 4 | 5 | def get_wikipedia_biography(envoy: str, with_source: bool = False) -> str: 6 | wikipedia.set_lang("pl") 7 | logger.debug(f"Searching for: {envoy}") 8 | search_results = wikipedia.search(envoy) 9 | first, last = envoy.split(" ")[1], envoy.split(" ")[-1] 10 | page_name = search_results[0] 11 | for result in search_results: 12 | if ( 13 | first.lower() in result.lower() 14 | and last.lower() in result.lower() 15 | and ( 16 | "polityk" in result.lower() 17 | or "poseł" in result.lower() 18 | or "posłanka" in result.lower() 19 | ) 20 | ): 21 | page_name = result 22 | break 23 | logger.debug(f"Found page: {page_name}") 24 | try: 25 | if ( 26 | not first.lower() in page_name.lower() 27 | or not last.lower() in page_name.lower() 28 | ): 29 | raise ValueError(f"Found invalid page") 30 | res = wikipedia.page(page_name) 31 | except (wikipedia.exceptions.DisambiguationError, ValueError) as e: 32 | logger.error(f"DisambiguationError: {e} ") 33 | return "Zbyt wiele wyników, nie udało się znaleźć biografii", "" 34 | biography = res.section("Życiorys") 35 | 36 | if not biography: 37 | biography = res.summary if res.summary else "-" 38 | if with_source: 39 | return biography, res.url 40 | logger.debug(f"biography: {biography[:10]}") 41 | return biography 42 | 43 | 44 | # wikipedia.set_lang("pl") 45 | # res = wikipedia.search("Poseł Krystian Łuczak") 46 | # print(res) 47 | -------------------------------------------------------------------------------- /frontend/components/ui/spinner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { cn } from "@/lib/utils"; 3 | 4 | type SpinnerSize = 'small' | 'default' | 'large'; 5 | type SpinnerVariant = 'primary' | 'secondary' | 'muted' | 'accent' | 'destructive'; 6 | 7 | interface SpinnerProps { 8 | size?: SpinnerSize; 9 | className?: string; 10 | variant?: SpinnerVariant; 11 | } 12 | 13 | const sizeClasses: Record = { 14 | small: 'w-8 h-8', 15 | default: 'w-12 h-12', 16 | large: 'w-16 h-16', 17 | }; 18 | 19 | const variantClasses: Record = { 20 | primary: 'text-primary', 21 | secondary: 'text-secondary', 22 | muted: 'text-muted', 23 | accent: 'text-accent', 24 | destructive: 'text-destructive', 25 | }; 26 | 27 | const Spinner: React.FC = ({ 28 | size = 'default', 29 | className, 30 | variant = 'primary' 31 | }) => { 32 | return ( 33 |
34 |
35 | {[0, 1, 2].map((index) => ( 36 |
52 | ))} 53 |
54 |
55 | ); 56 | }; 57 | 58 | export default Spinner; -------------------------------------------------------------------------------- /src/sejm_app/db_updater/db_updater_task.py: -------------------------------------------------------------------------------- 1 | from celery import Task 2 | from django.db import OperationalError, ProgrammingError 3 | from django.db.models import Model 4 | from django.utils import timezone 5 | from loguru import logger 6 | 7 | from sejm_app import models 8 | 9 | 10 | class DbUpdaterTask(Task): 11 | MODEL: Model = None 12 | DATE_FIELD_NAME = None 13 | SKIP_BY_DEFAULT = False 14 | time_limit = 24 * 60 * 60 15 | 16 | def __init__(self) -> None: 17 | self.name = self.MODEL.__name__ 18 | super().__init__() 19 | 20 | def check_if_should_update(self): 21 | try: 22 | self.MODEL.objects.first() 23 | except (OperationalError, ProgrammingError) as e: 24 | logger.warning(f"Database not initialized yet {e}") 25 | return False 26 | return self._should_be_updated() 27 | 28 | def _should_be_updated(self) -> bool: 29 | if not self.DATE_FIELD_NAME: 30 | logger.warning(f"DATE_FIELD_NAME not set for {self.name}") 31 | if self.MODEL.objects.count() == 0: 32 | return True 33 | return not self.SKIP_BY_DEFAULT 34 | return ( 35 | not self.MODEL.objects.order_by("-" + self.DATE_FIELD_NAME).first() 36 | == timezone.now().date() 37 | ) 38 | 39 | def on_success(self, retval, task_id, args, kwargs): 40 | logger.info(f"Finished {self.name} update") 41 | return super().on_success(retval, task_id, args, kwargs) 42 | 43 | def __call__(self, *args, **kwargs): 44 | if not self.check_if_should_update(): 45 | logger.info(f"Skipping {self.name} update") 46 | return 47 | res = super().__call__(*args, **kwargs) 48 | return res 49 | -------------------------------------------------------------------------------- /src/sejm_app/db_updater/club_updater.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import requests 4 | from django.conf import settings 5 | from django.core.files.base import ContentFile 6 | from django.db.models import Model 7 | from loguru import logger 8 | 9 | from sejm_app import models 10 | 11 | from .db_updater_task import DbUpdaterTask 12 | 13 | 14 | class ClubUpdaterTask(DbUpdaterTask): 15 | MODEL: Model = models.Club 16 | SKIP_BY_DEFAULT = False 17 | 18 | def run(self, *args, **kwargs): 19 | logger.info("Updating clubs") 20 | self._download_clubs() 21 | 22 | def _download_photo(self, club: models.Club): 23 | if club.photo and Path(club.photo.path).exists(): 24 | return 25 | photo_url = f"{settings.CLUBS_URL}/{club.id}/logo" 26 | photo = requests.get(photo_url) 27 | logger.info(f"Downloading photo for {club.id}") 28 | if photo.status_code == 200: 29 | photo_file = ContentFile(photo.content) 30 | club.photo.save(f"{club.id}.jpg", photo_file) 31 | club.save() 32 | else: 33 | logger.warning(f"Photo for {club.id} not found") 34 | 35 | def _download_clubs(self): 36 | logger.info(f"Calling {settings.CLUBS_URL}") 37 | clubs = requests.get(settings.CLUBS_URL).json() 38 | for club in clubs: 39 | if club_model := self.MODEL.objects.filter(id=club["id"]).first(): 40 | for key, value in club.items(): 41 | setattr(club_model, key, value) 42 | club_model.save() 43 | 44 | else: 45 | club_model, _ = self.MODEL.objects.update_or_create(**club) 46 | logger.info(f"Club {club_model.id} created") 47 | self._download_photo(club_model) 48 | -------------------------------------------------------------------------------- /frontend/app/committees/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { DataTableColumnHeader } from "@/components/dataTable/columns"; 4 | import { ColumnDefE, Committee } from "@/lib/types"; 5 | import { ColumnDef } from "@tanstack/react-table"; 6 | import { useRouter } from "next/navigation"; 7 | 8 | export const columns: ColumnDefE[] = [ 9 | { 10 | accessorKey: "code", 11 | header: ({ column }) => ( 12 | 13 | ), 14 | }, 15 | { 16 | accessorKey: "name", 17 | header: ({ column }) => ( 18 | 19 | ), 20 | }, 21 | { 22 | accessorKey: "type", 23 | header: ({ column }) => ( 24 | 25 | ), 26 | filterFn: (row, id, value) => { 27 | return value.includes(row.getValue(id)); 28 | }, 29 | }, 30 | 31 | { 32 | accessorKey: "compositionDate", 33 | header: ({ column }) => ( 34 | 35 | ), 36 | filterFn: (row, id, value) => { 37 | return value.includes(row.getValue(id)); 38 | }, 39 | }, 40 | ]; 41 | 42 | type RowType = { 43 | original: { 44 | code: string; 45 | }; 46 | getValue: (accessorKey: string) => string; 47 | }; 48 | 49 | export const useColumnsWithClickHandler = () => { 50 | const router = useRouter(); 51 | 52 | return columns.map((column) => ({ 53 | ...column, 54 | cell: ({ row }: { row: RowType }) => ( 55 |
router.push(`/committees/${row.original.code}`)} 57 | className="cursor-pointer" 58 | > 59 | {row.getValue(column.accessorKey as string)} 60 |
61 | ), 62 | })); 63 | }; 64 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ## Checklist before requesting a review 6 | 7 | - [ ] A self-review of the code has been performed 8 | - [ ] Commit messages and code follows our `guidelines and standards` 9 | - [ ] Tests have been added or updated where appropriate 10 | - [ ] Changes generate no new errors or warnings 11 | - [ ] There are no open [Pull Requests](../../../pulls) for the same update/change 12 | - [ ] Any dependent changes have been merged and published 13 | - [ ] This change is covered by a project issue ticket 14 | 15 | 16 | 17 | ## Change Description 18 | 19 | 20 | 21 | 22 | 23 | 24 | ### How Has This Been Tested? 25 | 26 | 27 | 28 | 29 | Does this PR introduce a breaking change? 30 | 31 | 32 | 33 | ## Related Issue 34 | 35 | Does this change resolve any [open issues](../../../issues)? 36 | 37 | Fixes #(issue number) 38 | 39 | 40 | -------------------------------------------------------------------------------- /frontend/components/ui/skeleton-page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Skeleton } from "@/components/ui/skeleton" 3 | import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card" 4 | 5 | const SkeletonLoader: React.FC = () => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export const SkeletonComponent: React.FC = () => { 25 | return ( 26 |
27 |
28 |
29 | 30 | 31 |
32 | 33 |
34 | 35 | 36 | 37 |
38 | 39 |
40 | 41 | 42 | 43 |
44 | 45 |
46 | 47 | 48 |
49 |
50 |
51 | ); 52 | }; -------------------------------------------------------------------------------- /frontend/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )) 33 | Alert.displayName = "Alert" 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )) 45 | AlertTitle.displayName = "AlertTitle" 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | AlertDescription.displayName = "AlertDescription" 58 | 59 | export { Alert, AlertTitle, AlertDescription } 60 | -------------------------------------------------------------------------------- /frontend/app/votings-results/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useSearchParams } from "next/navigation"; 3 | import { DataTable } from "@/components/dataTable/dataTable"; 4 | import { columns, useColumnsWithClickHandler } from "./columns"; 5 | import { useFetchData } from "@/lib/api"; 6 | import { APIResponse, Voting } from "@/lib/types"; 7 | import { SkeletonComponent } from "@/components/ui/skeleton-page"; 8 | import { useMemo } from "react"; 9 | 10 | function VotingResultsTable() { 11 | const searchParams = useSearchParams(); 12 | const columnsWithClickHandler = useColumnsWithClickHandler(); 13 | const { data, isLoading, error } = useFetchData>( 14 | `/votings/?${searchParams?.toString()}` 15 | ); 16 | 17 | const filters = useMemo( 18 | () => [ 19 | { columnKey: "category", title: "Kategoria" }, 20 | { columnKey: "sitting", title: "Posiedzenie" }, 21 | ], 22 | [] 23 | ); 24 | 25 | const processedData = useMemo(() => { 26 | if (!data) return []; 27 | return data.results.map((result) => ({ 28 | ...result, 29 | title: ( 30 | <> 31 | {result.title} 32 |

33 | {result.description ? result.description : result.topic} 34 |

35 | 36 | ), 37 | })); 38 | }, [data]); 39 | 40 | if (isLoading) return ; 41 | if (error) return
Error: {error.message}
; 42 | if (!data) return null; 43 | 44 | return ( 45 | 51 | ); 52 | } 53 | 54 | export default function VotingResultsPage() { 55 | return ( 56 |
57 | 58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /frontend/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )) 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 43 | 44 | 45 | )) 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 47 | 48 | export { ScrollArea, ScrollBar } 49 | -------------------------------------------------------------------------------- /frontend/components/ui/stepper-footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from "@/components/ui/button"; 3 | import { useStepper } from "@/components/ui/stepper"; 4 | import { 5 | ArrowLeft, 6 | ArrowRight, 7 | ListChecks, 8 | } from "lucide-react"; 9 | 10 | const Footer = () => { 11 | const { 12 | nextStep, 13 | prevStep, 14 | isDisabledStep, 15 | hasCompletedAllSteps, 16 | isLastStep, 17 | setStep, 18 | steps, 19 | } = useStepper(); 20 | 21 | const goToSummary = () => { 22 | setStep(steps.length - 1); // Assuming the last step is the summary 23 | }; 24 | 25 | return ( 26 |
27 |
28 |
29 | {!hasCompletedAllSteps && ( 30 | 40 | )} 41 |
42 |
43 | {!isLastStep && !hasCompletedAllSteps && ( 44 | 48 | )} 49 | {!hasCompletedAllSteps && ( 50 | 54 | )} 55 |
56 |
57 |
58 | ); 59 | }; 60 | export default Footer; -------------------------------------------------------------------------------- /src/community_app/templates/privacy_policy.html: -------------------------------------------------------------------------------- 1 | {% extends 'generic/base.html' %} 2 | {% block content %} 3 |
4 |

Polityka Prywatności

5 |

Data ostatniej aktualizacji: 04.18.2024

6 |

Użycie Google Analytics

7 |

8 | Na naszej stronie stosujemy Google Analytics, narzędzie analizy webowej firmy Google Inc. ("Google"). Google Analytics używa tzw. "cookies", czyli plików tekstowych zapisywanych na Twoim komputerze, które umożliwiają analizę sposobu korzystania ze strony internetowej. Informacje generowane przez cookie na temat Twojego korzystania ze strony internetowej (łącznie z Twoim adresem IP) są przekazywane do Google i przechowywane przez firmę na serwerach w Stanach Zjednoczonych. 9 |

10 |

11 | Google wykorzystuje te informacje w celu oceny Twojego korzystania ze strony internetowej, tworzenia raportów dotyczących aktywności na stronie dla operatorów stron internetowych oraz świadczenia innych usług związanych z aktywnością na stronie i korzystaniem z Internetu. Google może również przekazywać te informacje osobom trzecim, jeżeli wymaga tego prawo, lub gdy takie osoby trzecie przetwarzają informacje w imieniu Google. 12 |

13 |

14 | Możesz odmówić korzystania z cookies, wybierając odpowiednie ustawienia w swojej przeglądarce, jednak należy pamiętać, że w takim przypadku nie będziesz mógł korzystać z pełnej funkcjonalności tej strony. Korzystając z tej strony, wyrażasz zgodę na przetwarzanie danych o Tobie przez Google w sposób i w celach opisanych powyżej. 15 |

16 |

Jak wyłączyć cookies?

17 |

18 | Jeśli nie chcesz, aby cookies były zapisywane na Twoim urządzeniu, możesz zmienić ustawienia swojej przeglądarki. Informacje o tym, jak to zrobić, znajdziesz na stronach pomocniczych swojej przeglądarki. 19 |

20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /src/community_app/models.py: -------------------------------------------------------------------------------- 1 | import html 2 | from datetime import datetime 3 | 4 | from django.conf import settings 5 | from django.db import models 6 | from django.template.defaultfilters import safe, slugify 7 | from django.urls import reverse 8 | from django.utils.functional import cached_property 9 | from django.utils.html import strip_tags 10 | from django.utils.translation import gettext_lazy as _ 11 | from rest_framework import serializers 12 | 13 | 14 | class Article(models.Model): 15 | title = models.CharField(max_length=200) 16 | content = models.JSONField() # Store Slate's content as JSON 17 | image = models.ImageField(upload_to="article_images/", null=True, blank=True) 18 | created_at = models.DateTimeField(auto_now_add=True) 19 | updated_at = models.DateTimeField(auto_now=True) 20 | author = models.CharField(max_length=200, default="Admin") 21 | 22 | def __str__(self): 23 | return self.title 24 | 25 | 26 | class TeamMember(models.Model): 27 | class Role(models.IntegerChoices): 28 | CREATOR = 0, _("Twórca aplikacji") 29 | DEVELOPER = 1, _("Programista") 30 | SUPPORTER = 2, _("Wyjątkowo chojny wspierający") 31 | SUPPORTER_SMALL = 3, _("Wspierający") 32 | 33 | # badge = models.CharField(max_length=10, null=True, blank=True) 34 | name = models.CharField(max_length=64) 35 | role = models.IntegerField(choices=Role.choices, default=Role.SUPPORTER_SMALL) 36 | since = models.CharField(max_length=7, default="YYYY-MM") 37 | facebook_url = models.URLField(null=True, blank=True) 38 | # linkedin_url = models.URLField(null=True, blank=True) 39 | about = models.TextField(null=True, blank=True) 40 | photo = models.ImageField(upload_to="photos", null=True, blank=True) 41 | 42 | def __str__(self) -> str: 43 | return f"{self.name} - {self.role}" 44 | 45 | class Meta: 46 | ordering = ["-role"] 47 | -------------------------------------------------------------------------------- /src/api/views/home.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from django.utils.decorators import method_decorator 3 | from django.views.decorators.cache import cache_page 4 | from rest_framework import status 5 | from rest_framework.response import Response 6 | from rest_framework.viewsets import ViewSet 7 | 8 | from sejm_app.models import Club, Committee, Process, Voting 9 | 10 | 11 | class HomeViewSet(ViewSet): 12 | @method_decorator(cache_page(60 * 15)) # Cache for 15 minutes 13 | def list(self, request): 14 | latest_votings = Voting.objects.order_by("-date")[:4].values( 15 | "id", "title", "success", "category", "topic" 16 | ) 17 | 18 | cards = [ 19 | { 20 | "title": "Wszystkich głosowań", 21 | "count": Voting.objects.count(), 22 | "color": "destructive", 23 | "url": "/votings/", 24 | }, 25 | { 26 | "title": "Komisji parlamentarnych", 27 | "count": Committee.objects.count(), 28 | "color": "destructive", 29 | "url": "/committees/", 30 | }, 31 | { 32 | "title": "Wszystkich projektów", 33 | "count": Process.objects.count(), 34 | "color": "destructive", 35 | "url": "/processes/", 36 | }, 37 | { 38 | "title": "Oczekujących projektów", 39 | "count": sum( 40 | not process.is_finished for process in Process.objects.all() 41 | ), 42 | "color": "default", 43 | "url": "/processes/" + "?state=on", 44 | }, 45 | ] 46 | 47 | data = { 48 | "latest_votings": latest_votings, 49 | "cards": cards, 50 | } 51 | 52 | return Response(data, status=status.HTTP_200_OK) 53 | -------------------------------------------------------------------------------- /frontend/app/processes-results/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { DataTableColumnHeader } from "@/components/dataTable/columns"; 4 | import { ColumnDefE } from "@/lib/types"; 5 | import { ColumnDef } from "@tanstack/react-table"; 6 | import { useRouter } from "next/navigation"; 7 | 8 | export const columns: ColumnDefE[] = [ 9 | { 10 | accessorKey: "title", 11 | header: ({ column }) => ( 12 | 13 | ), 14 | }, 15 | { 16 | accessorKey: "createdBy", 17 | header: ({ column }) => ( 18 | 19 | ), 20 | cell: ({ row }) => row.original.createdBy || "Nieznany", 21 | 22 | filterFn: (row, id, value) => { 23 | return value.includes(row.getValue(id)); 24 | }, 25 | }, 26 | { 27 | accessorKey: "documentDate", 28 | header: ({ column }) => ( 29 | 30 | ), 31 | filterFn: (row, id, value) => { 32 | return value.includes(row.getValue(id)); 33 | }, 34 | }, 35 | { 36 | accessorKey: "length_tag", 37 | header: ({ column }) => ( 38 | 39 | ), 40 | filterFn: (row, id, value) => { 41 | return value.includes(row.getValue(id)); 42 | }, 43 | }, 44 | ]; 45 | 46 | export const useColumnsWithClickHandler = () => { 47 | const router = useRouter(); 48 | 49 | return columns.map((column) => ({ 50 | ...column, 51 | cell: ({ 52 | row, 53 | }: { 54 | row: { 55 | original: any; 56 | getValue: (accessorKey: string) => string; 57 | }; 58 | }) => ( 59 |
router.push(`/processes/${row.original.id}`)} 61 | className="cursor-pointer" 62 | > 63 | {row.getValue(column.accessorKey as string)} 64 |
65 | ), 66 | })); 67 | }; 68 | -------------------------------------------------------------------------------- /promo/devlog7.md: -------------------------------------------------------------------------------- 1 | Rozumiem. Oto rozszerzona wersja devloga, uwzględniająca Twoje sugestie: 2 | 3 | # Devlog 7: Nowy rozdział Sejm-Stats - wyzwania, zmiany i zaproszenie do współpracy 4 | 5 | Cześć wszystkim! Witam Was po dłuższej przerwie w kolejnym devlogu Sejm-Stats. Ostatnie tygodnie były intensywne i pełne wyzwań, 6 | Musiałem w końcu zmierzyć z moimi nemezis. Ale od początku. 7 | 8 | 9 | Korzystając z rady klasyka: 10 | > Zadaj sobie pytanie, co chcesz w życiu zrobić, a następnie to zrób. 11 | 12 | Odpowiedziałem sobie na to pytanie. 13 | Po pierwsze, chcę stworzyć aplikację, która jest lepsza od strony sejmu, 14 | a więc działa też od niej szybciej. - Niestety dotychczas nie działała. 15 | Po drugie zaś, chcę by każdy mógł zajrzeć do kodu, źródłowego by upewnić się, że nie próbuję manipulować danymi. Lub też poprostu, żeby pomóc. 16 | 17 | Druga kwestia jest już zrealizowana, link do repozytorium znajdziecie poniżej. 18 | Optymalizacja strony to sprawa znaczenie trudniejsza, gdy brakowało mi już pomysłów, na przeciw wyszła mi praca zawodowa, która 19 | zmusiła mnie do wykorzystania najnowyszych technologii. Jako, że zakochałem się w nich, znaczną część aplikacji przepisałem na nowo. Myślałem, że to troche pomoże. Myliłem się. Wrażenia z użytkowania zmieniły się nie do poznania, ładowanie się strony praktycznie nie istnieje. 20 | . Codziennie przepisuję kolejne części aplikacji, jednak dopiero gdy uda mi się zmigrować całość ukaże się ona na serwerze. Postępy możecie śledzić na moim profilu na githubie. 21 | 22 | Niestety, z powodu że udzielałem się troche mniej wasze wsparcie znacznie zmalało, jest to zrozumiałe, jednak mam nadzieję, że jak zobaczycie efekty, znów uwierzycie w projekt. 23 | 24 | 25 | 26 | Każdego kto zna się na technologiach, z których korzystam, zapraszam do włączenia się w projekt. Teraz jest to jeszcze łatwiejsze niż kiedykolwiek wcześniej. 27 | 28 | Jako, że nowe funkcjonalności same w sobie się nie pojawił na ten tydzień to już wszystko. Do usłyszenia następnym razem! -------------------------------------------------------------------------------- /src/sejm_app/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from django.contrib.humanize.templatetags.humanize import naturalday 4 | from django.utils import timezone 5 | from django.utils.dateparse import parse_date, parse_datetime 6 | from django.utils.timesince import timesince 7 | from django.utils.timezone import localtime 8 | 9 | 10 | def camel_to_snake(name: str) -> str: 11 | if len(name) < 4 and all([i.isupper() for i in name]): 12 | return name 13 | return "".join(["_" + i.lower() if i.isupper() else i for i in name]).lstrip("_") 14 | 15 | 16 | def format_human_friendly_date(date): 17 | date = localtime(date) 18 | # Using 'naturalday' for 'today', 'yesterday', or 'tomorrow' - or the date 19 | natural_date = naturalday(date) 20 | if natural_date == "today": 21 | natural_date = "dzisiaj" 22 | elif natural_date == "yesterday": 23 | natural_date = "wczoraj" 24 | time_str = date.strftime("%H:%M") 25 | if natural_date in ["dzisiaj", "wczoraj"]: 26 | return f"{natural_date} o godz {time_str}" 27 | return f"{natural_date} o godz {time_str}" 28 | 29 | 30 | def parse_all_dates(response: dict, date_only=False) -> dict: 31 | if not response: 32 | return response 33 | for key, value in response.items(): 34 | if ( 35 | (isinstance(value, str) or isinstance(value, datetime)) 36 | and (("date" in key.lower()) or ("modified" in key.lower())) 37 | or ("modified" in key.lower()) 38 | ): 39 | if not isinstance(value, datetime): 40 | value = parse_datetime(value) if "T" in value else parse_date(value) 41 | if isinstance(value, datetime): 42 | value = timezone.make_aware(value) 43 | if date_only: 44 | try: 45 | value = value.date() 46 | except AttributeError: 47 | pass 48 | response[key] = value 49 | 50 | return response 51 | -------------------------------------------------------------------------------- /frontend/components/home/votingCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import { CheckCircle, XCircle, AlertTriangle } from "lucide-react"; 4 | import { truncateWords } from "@/utils/text"; 5 | import { Card } from "../ui/card"; 6 | 7 | interface VotingCardProps { 8 | voting: { 9 | id: number; 10 | success: boolean; 11 | title: string; 12 | category: string; 13 | topic: string; 14 | }; 15 | } 16 | 17 | const VotingCard: React.FC = ({ voting }) => { 18 | const isImportant = voting.category === "WHOLE_PROJECT"; 19 | 20 | return ( 21 | 22 | 29 |
33 | 34 |
35 |
36 |

{truncateWords(voting.title, 22)}

37 |
38 | {isImportant && ( 39 | 40 | )} 41 | {voting.success ? ( 42 | 43 | ) : ( 44 | 45 | )} 46 |
47 |
48 | 49 |

{voting.topic}

50 | 51 |
52 |
53 | 54 | ); 55 | }; 56 | 57 | export default VotingCard; -------------------------------------------------------------------------------- /frontend/app/interpellations/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { DataTableColumnHeader } from "@/components/dataTable/columns"; 4 | import { ColumnDefE, Interpellation } from "@/lib/types"; 5 | import { ColumnDef } from "@tanstack/react-table"; 6 | 7 | export const columns: ColumnDefE[] = [ 8 | { 9 | accessorKey: "title", 10 | header: ({ column }) => ( 11 | 12 | ), 13 | }, 14 | { 15 | accessorKey: "to", 16 | header: ({ column }) => ( 17 | 18 | ), 19 | filterFn: (row, id, value) => { 20 | return value.includes(row.getValue(id)); 21 | }, 22 | }, 23 | { 24 | accessorKey: "member", 25 | header: ({ column }) => ( 26 | 27 | ), 28 | filterFn: (row, id, value) => { 29 | return value.includes(row.getValue(id)); 30 | }, 31 | }, 32 | { 33 | accessorKey: "sentDate", 34 | header: ({ column }) => ( 35 | 36 | ), 37 | filterFn: (row, id, value) => { 38 | return value.includes(row.getValue(id)); 39 | }, 40 | }, 41 | ]; 42 | 43 | export const getColumnsWithClickHandler = () => { 44 | return columns.map((column) => ({ 45 | ...column, 46 | cell: ({ 47 | row, 48 | }: { 49 | row: { 50 | original: Interpellation; 51 | getValue: (accessorKey: string) => string; 52 | }; 53 | }) => ( 54 | { 60 | e.preventDefault(); 61 | window.open(row.original.bodyLink, "_blank", "noopener,noreferrer"); 62 | }} 63 | > 64 | {row.getValue(column.accessorKey as string)} 65 | 66 | ), 67 | })); 68 | }; 69 | -------------------------------------------------------------------------------- /src/sejm_app/models/stage.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import datetime 4 | 5 | from django.db import models 6 | from django.db.models import Max 7 | from django.utils.dateparse import parse_date, parse_datetime 8 | from loguru import logger 9 | 10 | from sejm_app.utils import camel_to_snake, parse_all_dates 11 | 12 | from .voting import Voting 13 | 14 | 15 | class Stage(models.Model): 16 | process = models.ForeignKey( 17 | "Process", on_delete=models.CASCADE, null=True, related_name="stages" 18 | ) 19 | stageNumber = models.IntegerField(null=True, blank=True) 20 | date = models.DateField(null=True, blank=True) 21 | stageName = models.CharField(max_length=255) 22 | sittingNum = models.IntegerField(null=True, blank=True) 23 | comment = models.TextField(null=True, blank=True) 24 | decision = models.CharField(max_length=255, null=True, blank=True) 25 | textAfter3 = models.URLField(null=True, blank=True) 26 | voting = models.ForeignKey(Voting, on_delete=models.CASCADE, null=True, blank=True) 27 | 28 | def save(self, *args, **kwargs): 29 | if self.pk is None: # only for new objects 30 | max_stage_number = ( 31 | self.process.stages.aggregate(Max("stageNumber"))["stageNumber__max"] 32 | or 0 33 | ) 34 | self.stageNumber = max_stage_number + 1 35 | super().save(*args, **kwargs) 36 | 37 | @property 38 | def result(self): 39 | pass_phrases = ("uchwalono", "przyjęto", "uchwałę") 40 | fail_phrases = ("nie przyjęto", "nie przyjeto", "odrzucono") 41 | if self.decision: 42 | if any(phrase in self.decision.lower() for phrase in pass_phrases): 43 | return "PASS" 44 | if any(phrase in self.decision.lower() for phrase in fail_phrases): 45 | return "FAIL" 46 | return "" 47 | 48 | def __str__(self) -> str: 49 | return f"{self.process.id} stage {self.stageNumber}: {self.stageName}" 50 | -------------------------------------------------------------------------------- /src/eli_app/templates/act_list.html: -------------------------------------------------------------------------------- 1 | {% extends "generic/base.html" %} 2 | {% load static %} 3 | {% block extra_head %} 4 | 5 | 8 | {{ form.media.js }} 9 | {{ form.media.css }} 10 | {% endblock %} 11 | {% block content %} 12 |
13 | 21 |
22 |
23 |
24 | {% load crispy_forms_tags %} 25 |
26 |

Filtruj

27 |
28 | {{ form|crispy }} 29 | 30 | 31 |
32 |
33 | 34 |
35 |
36 | 37 | 38 |
39 |
44 |
45 |
46 |
47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transparlament", 3 | "workspaceFolder": "/code/src", 4 | "dockerComposeFile": ["../docker-compose-dev.yml"], 5 | "service": "web", 6 | "shutdownAction": "stopCompose", 7 | "runServices": ["db", "web", "redis"], 8 | "postCreateCommand": "pip install -r /code/requirements.txt && pip install djlint isort black mypy pylint", 9 | "postStartCommand": "black .", 10 | "forwardPorts": [8000], 11 | "customizations": { 12 | "vscode": { 13 | "extensions": [ 14 | "ms-python.python", 15 | "ms-azuretools.vscode-docker", 16 | "monosans.djlint", 17 | "ms-python.black-formatter", 18 | "streetsidesoftware.code-spell-checker", 19 | "mcright.auto-save", 20 | "ChakrounAnas.turbo-console-log", 21 | "ms-python.vscode-pylance", 22 | "eamodio.gitlens", 23 | "github.copilot", 24 | "esbenp.prettier-vscode" 25 | ], 26 | "settings": { 27 | "terminal.integrated.defaultProfile.linux": "bash", 28 | "python.defaultInterpreterPath": "/usr/local/bin/python", 29 | "python.linting.enabled": true, 30 | "python.linting.pylintEnabled": true, 31 | "python.linting.mypyEnabled": true, 32 | "python.formatting.provider": "black", 33 | "editor.formatOnSave": true, 34 | "[python]": { 35 | "editor.defaultFormatter": "ms-python.black-formatter" 36 | }, 37 | "[javascript]": { 38 | "editor.defaultFormatter": "esbenp.prettier-vscode" 39 | }, 40 | "[typescript]": { 41 | "editor.defaultFormatter": "esbenp.prettier-vscode" 42 | }, 43 | "[json]": { 44 | "editor.defaultFormatter": "esbenp.prettier-vscode" 45 | } 46 | } 47 | } 48 | }, 49 | "features": { 50 | "ghcr.io/devcontainers/features/node:1": { 51 | "version": "lts" 52 | }, 53 | "ghcr.io/devcontainers/features/git:1": {}, 54 | "ghcr.io/devcontainers/features/github-cli:1": {} 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /frontend/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /frontend/components/ui/datePickerWithRange.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { addDays, format } from "date-fns"; 5 | import { Calendar as CalendarIcon } from "lucide-react"; 6 | import { DateRange } from "react-day-picker"; 7 | 8 | import { cn } from "@/lib/utils"; 9 | import { Button } from "@/components/ui/button"; 10 | import { Calendar } from "@/components/ui/calendar"; 11 | import { 12 | Popover, 13 | PopoverContent, 14 | PopoverTrigger, 15 | } from "@/components/ui/popover"; 16 | 17 | export function DatePickerWithRange({ 18 | className, 19 | }: React.HTMLAttributes) { 20 | const [date, setDate] = React.useState({ 21 | from: new Date(2022, 0, 20), 22 | to: addDays(new Date(2022, 0, 20), 20), 23 | }); 24 | 25 | return ( 26 |
27 | 28 | 29 | 51 | 52 | 53 | 61 | 62 | 63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /frontend/components/ui/loginButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { signIn, signOut, useSession } from "next-auth/react"; 3 | import { Button } from "@/components/ui/button"; 4 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 5 | import { FaGoogle } from "react-icons/fa"; 6 | import { ChevronDown } from "lucide-react"; 7 | import { 8 | DropdownMenuItem, 9 | DropdownMenuSeparator, 10 | DropdownMenu, 11 | DropdownMenuContent, 12 | DropdownMenuLabel, 13 | DropdownMenuTrigger, 14 | } from "./dropdown-menu"; 15 | 16 | export default function LoginButton() { 17 | const { data: session } = useSession(); 18 | if (session && session.user) { 19 | return ( 20 | 21 | 22 | 30 | 31 | 32 | Moje konto 33 | 34 | Profil 35 | Ustawienia 36 | 37 | signOut()}>Wyloguj 38 | 39 | 40 | ); 41 | } 42 | return ( 43 | 52 | ); 53 | } -------------------------------------------------------------------------------- /frontend/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TabsPrimitive from "@radix-ui/react-tabs" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Tabs = TabsPrimitive.Root 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )) 23 | TabsList.displayName = TabsPrimitive.List.displayName 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )) 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )) 53 | TabsContent.displayName = TabsPrimitive.Content.displayName 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent } 56 | -------------------------------------------------------------------------------- /frontend/components/ui/date-picker-with-range.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import * as React from "react" 3 | import { format } from "date-fns" 4 | import { DateRange } from "react-day-picker" 5 | import { cn } from "@/lib/utils" 6 | import { Calendar } from "@/components/ui/calendar" 7 | import { CalendarIcon, CheckIcon } from "lucide-react" 8 | 9 | interface DatePickerWithRangeProps extends React.HTMLAttributes { 10 | date: DateRange | undefined; 11 | setDate: (date: DateRange | undefined) => void; 12 | } 13 | 14 | export function DatePickerWithRange({ 15 | className, 16 | date, 17 | setDate, 18 | }: DatePickerWithRangeProps) { 19 | return ( 20 |
21 |
22 | 30 |
31 | {date?.from ? ( 32 | date.to ? ( 33 |
34 | 35 |

36 | {format(date.from, "dd.MM.yyyy")} - {format(date.to, "dd.MM.yyyy")} 37 |

38 |
39 | ) : ( 40 |
41 | 42 |

43 | Start: {format(date.from, "dd.MM.yyyy")} 44 |

45 |
46 | ) 47 | ) : ( 48 |
49 | 50 |

Wybierz zakres dat

51 |
52 | )} 53 |
54 |
55 |
56 | ) 57 | } -------------------------------------------------------------------------------- /src/community_app/db_update/update_patrons.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import requests 4 | from django.conf import settings 5 | from django.db.models import Model 6 | from loguru import logger 7 | 8 | from community_app.models import TeamMember 9 | from sejm_app.db_updater.db_updater_task import DbUpdaterTask 10 | 11 | ILLEGAL_WORDS = ("śmierć",) 12 | 13 | 14 | class PatronsUpdaterTask(DbUpdaterTask): 15 | MODEL: Model = TeamMember 16 | 17 | def run(self, *args, **kwargs): 18 | logger.info("Updating patrons") 19 | self._download_patrons() 20 | 21 | def clean_nickname(self, text): 22 | """ 23 | This function replaces profane words in the text with asterisks. 24 | """ 25 | regex_pattern = r"\b(" + "|".join(map(re.escape, ILLEGAL_WORDS)) + r")\b" 26 | clean_text = re.sub( 27 | regex_pattern, lambda x: "*" * len(x.group()), text, flags=re.IGNORECASE 28 | ) 29 | return clean_text 30 | 31 | def _download_patrons(self): 32 | url = settings.PATRONITE_API_URL + "patrons/active" 33 | logger.debug(f"Downloading patrons from {url}") 34 | headers = { 35 | "Authorization": f"token {settings.PATRONITE_API_TOKEN}", 36 | "Content-Type": "application/json", 37 | } 38 | logger.debug(f"Headers: {headers}") 39 | response = requests.get(url, headers=headers).json() 40 | self.MODEL.objects.filter( 41 | role__in=[TeamMember.Role.SUPPORTER, TeamMember.Role.SUPPORTER_SMALL] 42 | ).delete() 43 | for patron in response["results"]: 44 | if patron["isAnonymous"] or patron["amount"] == 5: 45 | continue 46 | role = ( 47 | TeamMember.Role.SUPPORTER_SMALL 48 | if patron["amount"] > 10 49 | else TeamMember.Role.SUPPORTER 50 | ) 51 | name = self.clean_nickname(patron["name"]) 52 | self.MODEL.objects.create( 53 | name=name, 54 | role=role, 55 | since=patron["firstPaymentAt"][:7], # 2024-05-13 12:27:17 56 | ) 57 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0004_remove_printmodel_mps_remove_printmodel_club_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-03-26 18:00 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("sejm_app", "0003_remove_process_mps_remove_process_club_and_more"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="printmodel", 15 | name="MPs", 16 | ), 17 | migrations.RemoveField( 18 | model_name="printmodel", 19 | name="club", 20 | ), 21 | migrations.RemoveField( 22 | model_name="printmodel", 23 | name="createdBy", 24 | ), 25 | migrations.RemoveField( 26 | model_name="printmodel", 27 | name="pages_count", 28 | ), 29 | migrations.AddField( 30 | model_name="process", 31 | name="MPs", 32 | field=models.ManyToManyField( 33 | blank=True, related_name="processes", to="sejm_app.envoy" 34 | ), 35 | ), 36 | migrations.AddField( 37 | model_name="process", 38 | name="club", 39 | field=models.ForeignKey( 40 | blank=True, 41 | null=True, 42 | on_delete=django.db.models.deletion.CASCADE, 43 | related_name="processes", 44 | to="sejm_app.club", 45 | ), 46 | ), 47 | migrations.AddField( 48 | model_name="process", 49 | name="createdBy", 50 | field=models.CharField( 51 | blank=True, 52 | choices=[ 53 | ("posłowie", "Envoys"), 54 | ("klub", "Club"), 55 | ("prezydium", "Presidium"), 56 | ("obywatele", "Citizens"), 57 | ("rząd", "Government"), 58 | ], 59 | default=None, 60 | max_length=20, 61 | null=True, 62 | ), 63 | ), 64 | ] 65 | -------------------------------------------------------------------------------- /src/sejm_app/migrations/0003_remove_process_mps_remove_process_club_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2024-03-26 17:53 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("sejm_app", "0002_rename_id_committee_code"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="process", 15 | name="MPs", 16 | ), 17 | migrations.RemoveField( 18 | model_name="process", 19 | name="club", 20 | ), 21 | migrations.RemoveField( 22 | model_name="process", 23 | name="createdBy", 24 | ), 25 | migrations.AddField( 26 | model_name="printmodel", 27 | name="MPs", 28 | field=models.ManyToManyField( 29 | blank=True, related_name="prints", to="sejm_app.envoy" 30 | ), 31 | ), 32 | migrations.AddField( 33 | model_name="printmodel", 34 | name="club", 35 | field=models.ForeignKey( 36 | blank=True, 37 | null=True, 38 | on_delete=django.db.models.deletion.CASCADE, 39 | related_name="prints", 40 | to="sejm_app.club", 41 | ), 42 | ), 43 | migrations.AddField( 44 | model_name="printmodel", 45 | name="createdBy", 46 | field=models.CharField( 47 | blank=True, 48 | choices=[ 49 | ("posłowie", "Envoys"), 50 | ("klub", "Club"), 51 | ("prezydium", "Presidium"), 52 | ("obywatele", "Citizens"), 53 | ("rząd", "Government"), 54 | ], 55 | default=None, 56 | max_length=20, 57 | null=True, 58 | ), 59 | ), 60 | migrations.AddField( 61 | model_name="printmodel", 62 | name="pages_count", 63 | field=models.SmallIntegerField(blank=True, null=True), 64 | ), 65 | ] 66 | -------------------------------------------------------------------------------- /frontend/components/dataTable/toggle.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; 3 | import { MixerHorizontalIcon } from "@radix-ui/react-icons"; 4 | import { Table } from "@tanstack/react-table"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | import { 8 | DropdownMenu, 9 | DropdownMenuCheckboxItem, 10 | DropdownMenuContent, 11 | DropdownMenuLabel, 12 | DropdownMenuSeparator, 13 | } from "@/components/ui/dropdown-menu"; 14 | 15 | interface DataTableViewOptionsProps { 16 | table: Table; 17 | defaultVisibleColumns?: string[]; 18 | } 19 | 20 | export function DataTableViewOptions({ 21 | table, 22 | defaultVisibleColumns, 23 | }: DataTableViewOptionsProps) { 24 | return ( 25 | 26 | 27 | 35 | 36 | 37 | Zmień 38 | 39 | {table 40 | .getAllColumns() 41 | .filter( 42 | (column) => 43 | typeof column.accessorFn !== "undefined" && column.getCanHide() 44 | ) 45 | .map((column) => { 46 | const isDefaultVisible = defaultVisibleColumns 47 | ? defaultVisibleColumns.includes(column.id) 48 | : true; 49 | return ( 50 | column.toggleVisibility(!!value)} 55 | defaultChecked={isDefaultVisible} 56 | > 57 | {column.id} 58 | 59 | ); 60 | })} 61 | 62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /frontend/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM python:3.12 AS builder 3 | 4 | # Set environment variables 5 | ENV PYTHONDONTWRITEBYTECODE 1 6 | ENV PYTHONUNBUFFERED 1 7 | 8 | WORKDIR /code 9 | 10 | # Copy requirements file 11 | COPY requirements.txt . 12 | 13 | # Install build dependencies and Python packages 14 | RUN apt-get update && apt-get install -y --no-install-recommends \ 15 | gcc \ 16 | libpq-dev \ 17 | && pip install --upgrade pip \ 18 | && pip install --user -r requirements.txt \ 19 | && apt-get clean \ 20 | && rm -rf /var/lib/apt/lists/* 21 | 22 | # Final stage 23 | FROM python:3.12-slim 24 | 25 | # Set environment variables 26 | ENV PYTHONDONTWRITEBYTECODE 1 27 | ENV PYTHONUNBUFFERED 1 28 | ENV TESSDATA_PREFIX /usr/share/tesseract-ocr/5/tessdata 29 | ENV PATH=/root/.local/bin:$PATH 30 | 31 | WORKDIR /code 32 | 33 | # Install runtime dependencies 34 | RUN apt-get update && apt-get install -y --no-install-recommends \ 35 | libpq-dev \ 36 | postgresql-client \ 37 | poppler-utils \ 38 | tesseract-ocr \ 39 | wget \ 40 | && apt-get clean \ 41 | && rm -rf /var/lib/apt/lists/* \ 42 | && wget -O /tmp/pol.traineddata https://github.com/tesseract-ocr/tessdata/raw/main/pol.traineddata \ 43 | && mkdir -p /usr/share/tesseract-ocr/5/tessdata \ 44 | && mv /tmp/pol.traineddata /usr/share/tesseract-ocr/5/tessdata/ 45 | 46 | # Copy installed packages from builder stage 47 | COPY --from=builder /root/.local /root/.local 48 | 49 | # Copy application code 50 | COPY src/ /code/ 51 | COPY /sejm-stats-439117-39efc9d2f8b8.json /code/ 52 | COPY docker-entrypoint.sh /code/ 53 | RUN chmod +x /code/docker-entrypoint.sh 54 | 55 | EXPOSE 8000 56 | 57 | ENTRYPOINT ["/code/docker-entrypoint.sh"] 58 | 59 | # Use build argument to determine which service to run 60 | ARG SERVICE=web 61 | ENV SERVICE=${SERVICE} 62 | 63 | CMD if [ "$SERVICE" = "celery" ]; then \ 64 | celery -A core worker --beat --scheduler django -l INFO; \ 65 | else \ 66 | gunicorn --bind 0.0.0.0:8000 core.wsgi:application; \ 67 | fi 68 | 69 | # Build commands: 70 | # For web: docker build --build-arg SERVICE=web -t transparlament:web . 71 | # For celery: docker build --build-arg SERVICE=celery -t transparlament:celery . -------------------------------------------------------------------------------- /frontend/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 5 | import { ChevronDown } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Accordion = AccordionPrimitive.Root 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )) 21 | AccordionItem.displayName = "AccordionItem" 22 | 23 | const AccordionTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | svg]:rotate-180", 32 | className 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 38 | 39 | 40 | )) 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 52 |
{children}
53 |
54 | )) 55 | 56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName 57 | 58 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 59 | -------------------------------------------------------------------------------- /src/sejm_app/models/vote.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | from sejm_app.models.envoy import Envoy 5 | from sejm_app.utils import camel_to_snake, parse_all_dates 6 | 7 | 8 | class VoteOption(models.IntegerChoices): 9 | NO = 0, _("No") 10 | YES = 1, _("Yes") 11 | ABSTAIN = 2, _("ABSTAIN") 12 | ABSENT = 3, _("ABSENT") 13 | VOTE_VALID = 4, _("VOTE_VALID") 14 | 15 | 16 | class Vote(models.Model): 17 | id = models.BigIntegerField(primary_key=True) # Changed to BigIntegerField 18 | voting = models.ForeignKey("Voting", on_delete=models.CASCADE, related_name="votes") 19 | MP = models.ForeignKey("Envoy", on_delete=models.CASCADE, related_name="votes") 20 | vote = models.SmallIntegerField( 21 | choices=VoteOption.choices, 22 | help_text=_("Vote option"), 23 | null=True, 24 | blank=True, # for list votes :( 25 | ) 26 | 27 | def save(self, *args, **kwargs): 28 | self.id = self.voting.pk * 1000 + self.MP.pk 29 | super().save(*args, **kwargs) 30 | 31 | LABELS = { 32 | VoteOption.NO: "Przeciw", 33 | VoteOption.YES: "Za", 34 | VoteOption.ABSTAIN: "Wstzymanie się", 35 | VoteOption.ABSENT: "Nieobecność", 36 | VoteOption.VOTE_VALID: "Głos ważny", 37 | } 38 | 39 | @property 40 | def vote_label(self): 41 | return self.LABELS[self.vote] 42 | 43 | 44 | class ListVote(models.Model): 45 | voteOption = models.SmallIntegerField( 46 | choices=VoteOption.choices, 47 | help_text=_("Vote option"), 48 | ) 49 | vote = models.ForeignKey("Vote", on_delete=models.CASCADE, related_name="listVotes") 50 | optionIndex = models.ForeignKey("VotingOption", on_delete=models.CASCADE) 51 | 52 | 53 | class ClubVote(models.Model): 54 | club = models.ForeignKey( 55 | "Club", on_delete=models.CASCADE, null=True, blank=True, related_name="votes" 56 | ) 57 | yes = models.IntegerField(default=0) 58 | no = models.IntegerField(default=0) 59 | abstain = models.IntegerField(default=0) 60 | voting = models.ForeignKey( 61 | "Voting", 62 | on_delete=models.CASCADE, 63 | related_name="club_votes", 64 | ) 65 | -------------------------------------------------------------------------------- /frontend/app/acts-results/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { DataTableColumnHeader } from "@/components/dataTable/columns"; 4 | import { Act, ColumnDefE } from "@/lib/types"; 5 | import { ColumnDef } from "@tanstack/react-table"; 6 | 7 | export const columns: ColumnDefE[] = [ 8 | { 9 | accessorKey: "title", 10 | header: ({ column }) => ( 11 | 12 | ), 13 | }, 14 | { 15 | accessorKey: "publisher", 16 | header: ({ column }) => ( 17 | 18 | ), 19 | filterFn: (row, id, value) => { 20 | return value.includes(row.getValue(id)); 21 | }, 22 | }, 23 | { 24 | accessorKey: "status", 25 | header: ({ column }) => ( 26 | 27 | ), 28 | filterFn: (row, id, value) => { 29 | return value.includes(row.getValue(id)); 30 | }, 31 | }, 32 | { 33 | accessorKey: "announcementDate", 34 | header: ({ column }) => ( 35 | 36 | ), 37 | filterFn: (row, id, value) => { 38 | return value.includes(row.getValue(id)); 39 | }, 40 | }, 41 | { 42 | accessorKey: "entryIntoForce", 43 | header: ({ column }) => ( 44 | 45 | ), 46 | filterFn: (row, id, value) => { 47 | return value.includes(row.getValue(id)); 48 | }, 49 | }, 50 | ]; 51 | 52 | type RowType = { 53 | original: { 54 | url: string; 55 | }; 56 | getValue: (accessorKey: string) => string; 57 | }; 58 | 59 | export const getColumnsWithClickHandler = () => { 60 | return columns.map((column) => ({ 61 | ...column, 62 | cell: ({ row }: { row: RowType }) => ( 63 | { 69 | e.preventDefault(); 70 | window.open(row.original.url, "_blank", "noopener,noreferrer"); 71 | }} 72 | > 73 | {row.getValue(column.accessorKey as string)} 74 | 75 | ), 76 | })); 77 | }; 78 | -------------------------------------------------------------------------------- /frontend/app/votings-results/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { DataTableColumnHeader } from "@/components/dataTable/columns"; 4 | import { ColumnDefE } from "@/lib/types"; 5 | import { ColumnDef } from "@tanstack/react-table"; 6 | import { useRouter } from "next/navigation"; 7 | 8 | export const columns: ColumnDefE[] = [ 9 | { 10 | accessorKey: "title", 11 | header: ({ column }) => ( 12 | 13 | ), 14 | }, 15 | { 16 | accessorKey: "category", 17 | header: ({ column }) => ( 18 | 19 | ), 20 | filterFn: (row, id, value) => { 21 | return value.includes(row.getValue(id)); 22 | }, 23 | }, 24 | { 25 | accessorKey: "date", 26 | header: ({ column }) => ( 27 | 28 | ), 29 | filterFn: (row, id, value) => { 30 | return value.includes(row.getValue(id)); 31 | }, 32 | }, 33 | { 34 | accessorKey: "description", 35 | header: ({ column }) => ( 36 | 37 | ), 38 | }, 39 | 40 | { 41 | accessorKey: "kind", 42 | header: ({ column }) => ( 43 | 44 | ), 45 | }, 46 | 47 | { 48 | accessorKey: "sitting", 49 | header: ({ column }) => ( 50 | 51 | ), 52 | filterFn: (row, id, value) => { 53 | const rowValue = row.getValue(id); 54 | return value.some((v: any) => Number(v) === rowValue); 55 | }, 56 | }, 57 | ]; 58 | 59 | export const useColumnsWithClickHandler = () => { 60 | const router = useRouter(); 61 | 62 | return columns.map((column) => ({ 63 | ...column, 64 | cell: ({ 65 | row, 66 | }: { 67 | row: { 68 | original: any; 69 | getValue: (accessorKey: string) => string; 70 | }; 71 | }) => ( 72 |
router.push(`/votings/${row.original.id}`)} 74 | className="cursor-pointer" 75 | > 76 | {row.getValue(column.accessorKey as string)} 77 |
78 | ), 79 | })); 80 | }; 81 | --------------------------------------------------------------------------------