Compare commits

...

31 Commits

Author SHA1 Message Date
stupidcomputer 48a70fb637 fix: use the new bill template on index 2024-07-30 20:10:38 -05:00
stupidcomputer fad0b668fa fix: make deployment promptless 2024-07-30 17:03:03 -05:00
stupidcomputer 90e01d921f fix: forgot to add the country name to component 2024-07-30 17:02:50 -05:00
stupidcomputer 4d5d3f50a1 feature: all listing of all items in conferences 2024-07-30 16:58:19 -05:00
stupidcomputer 36c558821d feature: refactor bill display into components 2024-07-30 16:48:41 -05:00
stupidcomputer d713f4106d fix: change wording on stats page 2024-07-29 17:54:59 -05:00
stupidcomputer dce8c1b88c feature: add a search feature 2024-07-27 23:49:06 -05:00
stupidcomputer 25881d4cb3 feature: add a Category model
- add a Category model that represents the CCE
  defined categories for various legislation
2024-07-24 13:13:07 -05:00
stupidcomputer c92fbf0890 fix: remake migrations 2024-07-24 01:38:24 -05:00
stupidcomputer 61f1981925 fix: add __init__.py to migrations to make it work 2024-07-24 01:37:33 -05:00
stupidcomputer bccfa280e1 fix: sync the database after migrating 2024-07-24 01:32:23 -05:00
stupidcomputer c35c96e16b cleanup: refactor by_group view 2024-07-24 01:03:19 -05:00
stupidcomputer 2a6e42da9c to squash 2024-07-24 00:13:30 -05:00
stupidcomputer 25415a099f maint: add two sample books again 2024-07-23 23:51:47 -05:00
stupidcomputer 601ffd44c8 maint: squash migrations into one 2024-07-23 23:51:37 -05:00
stupidcomputer 4931c8ac94 fix: ValueError on index page when no legislation 2024-07-23 23:04:45 -05:00
stupidcomputer 281473262a make topics a part of the system we've made 2024-06-30 22:01:55 -05:00
stupidcomputer bfbea9e855 add automatic generation of country and school listing 2024-06-30 21:56:33 -05:00
stupidcomputer 0a6ed6ffe2 generalize the view generation into a factory 2024-06-30 21:05:34 -05:00
stupidcomputer 37e6a03bb3 final doc commit 2024-06-30 00:32:21 -05:00
stupidcomputer 08d9cb1b77 change 2 (to squash) 2024-06-30 00:32:21 -05:00
stupidcomputer 06539e332d some part of writing resolutions 2024-06-30 00:32:21 -05:00
stupidcomputer ff92a221f1 misc 2024-06-29 23:58:20 -05:00
stupidcomputer 39e81d5727 Add country model 2024-06-29 23:41:33 -05:00
stupidcomputer de4575a2f0 add a School model 2024-06-29 23:19:39 -05:00
stupidcomputer 761b00eb49 add country and catagory fields; simplify serialization into legislation objects 2024-06-29 22:27:05 -05:00
stupidcomputer a1c722295b add an admonition to contribute 2024-06-29 20:54:04 -05:00
stupidcomputer 2b026aaeee add contrib franklincce.nix NixOS module 2024-06-29 20:49:38 -05:00
stupidcomputer e8e83fc5d4 add a specific exemption for the admin panel 2024-06-29 18:33:30 -05:00
stupidcomputer f578e219c5 move /kb path to /knowledge
there's this weird thing that happens with nginx
with paths that end in /kb and I don't know why.
this at least fixes the problem.
2024-06-29 18:29:27 -05:00
stupidcomputer a965eb1aed add a homepage thing 2024-06-29 18:15:33 -05:00
35 changed files with 715 additions and 189 deletions

View File

@ -1,5 +1,5 @@
prod: # execute this target on the production server in the nix-shell
cd franklincce; python3 manage.py collectstatic
cd franklincce; yes yes | python3 manage.py collectstatic
sh gen_kb.sh
sed "s|change_me|$(shell dd if=/dev/urandom bs=1024 count=1|base64)|g" .env.prod.orig > .env.prod
docker-compose -f docker-compose.prod.yml up -d --build

BIN
archive/MUNB2023.pdf Normal file

Binary file not shown.

Binary file not shown.

15
contrib/franklincce.nix Normal file
View File

@ -0,0 +1,15 @@
{ lib, config, pkgs, ... }:
{
# nix expressions to configure the relevant things
# manual nix-shell -c "make clean && make" (etc) is still needed, sadly
virtualisation.docker.enable = true;
services.nginx.virtualHosts."franklincce.beepboop.systems" = {
forceSSL = true;
enableACME = true;
locations."/" = {
proxyPass = "http://localhost:1337";
};
};
}

View File

@ -1,6 +1,6 @@
#!/bin/sh
python3 manage.py makemigrations
python3 manage.py migrate
python3 manage.py migrate --run-syncdb
exec "$@"

View File

@ -11,7 +11,11 @@ class LegislationBookAdmin(admin.ModelAdmin):
to_register = [
[models.LegislativeText, LegislativeTextAdmin],
[models.LegislationBook, LegislationBookAdmin],
[models.LegislationClassification]
[models.LegislationClassification],
[models.School],
[models.Country],
[models.Sponsor],
[models.Category],
]
for i in to_register:
admin.site.register(*i)

View File

@ -163,45 +163,63 @@ class CCEParserBase():
for legislative_text in splitted:
# there are some blocks that contain just one value
# and are aligned to some x value on the pdf
# it's an easy way to extract stuff
try:
country = get_block_by_x_value(legislative_text, 139).text.rstrip()
country = country.replace("Sponsor: ", "").lstrip()
except AttributeError:
country = None # this is a yig bill
try:
category = get_block_by_x_value(legislative_text, 151).text.rstrip().lstrip()
except AttributeError:
try:
category = get_block_by_x_value(legislative_text, 153).text.rstrip().lstrip()
except AttributeError:
print([(i.text, i.x0) for i in legislative_text])
leg_code = get_block_by_x_value(legislative_text, 88).text.rstrip()
try:
school = get_block_by_x_value(legislative_text, 177).text.rstrip()
school = get_block_by_x_value(legislative_text, 177).text.rstrip().lstrip()
except AttributeError:
try:
school = get_block_by_x_value(legislative_text, 186).text.rstrip()
school = get_block_by_x_value(legislative_text, 186).text.rstrip().lstrip()
except AttributeError:
school = "you tell me, man"
try:
sponsors = get_block_by_x_value(legislative_text, 163).text.rstrip()
sponsors = get_block_by_x_value(legislative_text, 163).text.rstrip().lstrip()
except AttributeError:
try:
sponsors = get_block_by_x_value(legislative_text, 166).text.rstrip()
sponsors = get_block_by_x_value(legislative_text, 166).text.rstrip().lstrip()
except AttributeError:
sponsors = "you tell me, man"
try:
subcommittee = get_block_by_x_value(legislative_text, 151).text.rstrip()
except AttributeError:
try:
subcommittee = get_block_by_x_value(legislative_text, 153).text.rstrip()
except AttributeError:
subcommittee = "you tell me, man"
the_rest = ''.join([i.text for i in legislative_text[12:]])
print([i.text for i in legislative_text[12:]])
handled = self.handle_the_rest(the_rest)
title = handled["title"]
bill_text = handled["bill_text"]
codesplit = leg_code.split('/')
assembly = codesplit[0]
dashsplit = codesplit[1].split('-')
year = 2000 + int(dashsplit[0])
committee = int(dashsplit[1])
docket_order = int(dashsplit[2])
output.append({
"code": leg_code,
"assembly": assembly,
"year": year,
"committee": committee,
"docket_order": docket_order,
"category": category,
"country": country,
"school": school,
"sponsors": sponsors,
"subcommittee": subcommittee,
"title": title,
"bill_text": bill_text
"legislation_title": title,
"text": bill_text
})
self.output = output
@ -255,22 +273,33 @@ class HSYIG24(CCEParserBase):
# it's an easy way to extract stuff
legislative_text = remove_block_by_x_value(legislative_text, 565) # remove page numbers
category = get_block_by_x_value(legislative_text, 139).text.rstrip().lstrip()
leg_code = get_block_by_x_value(legislative_text, 88).text.rstrip()
school = get_block_by_x_value(legislative_text, 163).text.rstrip()
sponsors = get_block_by_x_value(legislative_text, 152).text.rstrip()
subcommittee = get_block_by_x_value(legislative_text, 139).text.rstrip()
school = get_block_by_x_value(legislative_text, 163).text.rstrip().lstrip()
sponsors = get_block_by_x_value(legislative_text, 152).text.rstrip().lstrip()
the_rest = ''.join([i.text for i in legislative_text[6:]])
handled = self.handle_the_rest(the_rest)
title = handled["title"]
bill_text = handled["bill_text"]
codesplit = leg_code.split('/')
assembly = codesplit[0]
dashsplit = codesplit[1].split('-')
year = 2000 + int(dashsplit[0])
committee = int(dashsplit[1])
docket_order = int(dashsplit[2])
output.append({
"code": leg_code,
"assembly": assembly,
"year": year,
"committee": committee,
"docket_order": docket_order,
"category": category,
"country": None, # this is a yig bill
"school": school,
"sponsors": sponsors,
"subcommittee": subcommittee,
"title": title,
"bill_text": bill_text
"legislation_title": title,
"text": bill_text
})
self.output = output
@ -287,8 +316,9 @@ def main():
return
for text in doc.output:
print("{} ---------------------------- {}".format(
text["title"], text["bill_text"]
print("{} {} {} ---------------------------- {}".format(
text["country"], text["category"],
text["legislation_title"], text["text"]
))
if __name__ == "__main__":

View File

@ -1,4 +1,4 @@
# Generated by Django 4.2.12 on 2024-06-19 06:53
# Generated by Django 4.2.14 on 2024-07-24 06:38
from django.db import migrations, models
import django.db.models.deletion
@ -12,6 +12,16 @@ class Migration(migrations.Migration):
]
operations = [
migrations.CreateModel(
name='Country',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=256)),
],
options={
'verbose_name_plural': 'Countries',
},
),
migrations.CreateModel(
name='LegislationBook',
fields=[
@ -19,7 +29,38 @@ class Migration(migrations.Migration):
('conference_type', models.CharField(choices=[('M', 'Middle School'), ('H', 'High School')], default='H', max_length=1)),
('pdf', models.FileField(upload_to='uploads/')),
('name', models.CharField(max_length=256)),
('import_strategy', models.CharField(max_length=128)),
('import_strategy', models.CharField(choices=[('HSYIGBookParser', 'High School YIG Book Parser 1'), ('HSMUNBookParser', 'High School MUN Book Parser 1')], default='HSYIGBookParser', max_length=128)),
('has_performed_export', models.BooleanField(default=False)),
],
options={
'verbose_name': 'Book',
'verbose_name_plural': 'Books',
},
),
migrations.CreateModel(
name='LegislationClassification',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Name of this classification.', max_length=256)),
('text_to_match', models.CharField(help_text='a comma seperated list of keywords to include in the classification. spaces and dashes are discluded.', max_length=256)),
],
options={
'verbose_name': 'Topic',
'verbose_name_plural': 'Topics',
},
),
migrations.CreateModel(
name='School',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=256)),
],
),
migrations.CreateModel(
name='Sponsor',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=256)),
],
),
migrations.CreateModel(
@ -30,10 +71,17 @@ class Migration(migrations.Migration):
('text', models.TextField()),
('year', models.IntegerField()),
('committee', models.IntegerField()),
('category', models.CharField(max_length=256)),
('docket_order', models.IntegerField()),
('school', models.CharField(max_length=256)),
('sponsors', models.CharField(max_length=256)),
('legislation_title', models.CharField(max_length=512)),
('country', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='explorer.country')),
('from_book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='explorer.legislationbook')),
('school', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='explorer.school')),
('sponsors', models.ManyToManyField(blank=True, to='explorer.sponsor')),
],
options={
'verbose_name': 'Legislation',
'verbose_name_plural': 'Legislation',
},
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 4.2.14 on 2024-07-24 17:58
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('explorer', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Category',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=256)),
],
),
migrations.AlterField(
model_name='legislativetext',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='explorer.category'),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 4.2.12 on 2024-06-19 07:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('explorer', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='legislativetext',
name='legislation_title',
field=models.CharField(default='Sample title', max_length=512),
preserve_default=False,
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 4.2.12 on 2024-06-19 17:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('explorer', '0002_legislativetext_legislation_title'),
]
operations = [
migrations.AddField(
model_name='legislationbook',
name='has_performed_export',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='legislationbook',
name='import_strategy',
field=models.CharField(choices=[('HSYIGBookParser', 'High School YIG Book Parser 1'), ('HSMUNBookParser', 'High School MUN Book Parser 1')], default='HSYIGBookParser', max_length=128),
),
]

View File

@ -1,21 +0,0 @@
# Generated by Django 4.2.12 on 2024-06-28 20:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('explorer', '0003_legislationbook_has_performed_export_and_more'),
]
operations = [
migrations.CreateModel(
name='LegislationClassification',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Name of this classification.', max_length=256)),
('text_to_match', models.CharField(help_text='a comma seperated list of keywords to include in the classification. spaces and dashes are discluded.', max_length=256)),
],
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 4.2.12 on 2024-06-28 21:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('explorer', '0004_legislationclassification'),
]
operations = [
migrations.AddField(
model_name='legislationclassification',
name='obvious_change',
field=models.CharField(default='test', help_text='Name of this classification.', max_length=256),
preserve_default=False,
),
]

View File

@ -1,17 +0,0 @@
# Generated by Django 4.2.12 on 2024-06-28 21:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('explorer', '0005_legislationclassification_obvious_change'),
]
operations = [
migrations.RemoveField(
model_name='legislationclassification',
name='obvious_change',
),
]

View File

@ -1,5 +1,6 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.urls import reverse
from .leglib import HSYIG24, HSMUN23
import io
@ -7,7 +8,70 @@ import fitz
from collections import namedtuple
def InstantiateIfNone(model, name):
"""
Search the model for instances by name.
If there's none, then create one.
"""
filtered = model.objects.filter(name__exact=name)
try:
return filtered[0]
except IndexError:
obj = model(name=name)
obj.save()
return obj
class School(models.Model):
name = models.CharField(max_length=256)
def __str__(self):
return self.name
def get_absolute_url(self):
our_name = __class__.__name__
return reverse("{}.detail".format(our_name), kwargs={"model_id": self.id})
class Country(models.Model):
name = models.CharField(max_length=256)
class Meta:
verbose_name_plural = "Countries"
def __str__(self):
return self.name
def get_absolute_url(self):
our_name = __class__.__name__
return reverse("{}.detail".format(our_name), kwargs={"model_id": self.id})
class Sponsor(models.Model):
name = models.CharField(max_length=256)
def __str__(self):
return self.name
def get_absolute_url(self):
our_name = __class__.__name__
return reverse("{}.detail".format(our_name), kwargs={"model_id": self.id})
class Category(models.Model):
name = models.CharField(max_length=256)
class Meta:
verbose_name_plural = "Categories"
def __str__(self):
return "{}".format(self.name)
def get_absolute_url(self):
our_name = __class__.__name__
return reverse("{}.detail".format(our_name), kwargs={"model_id": self.id})
class LegislationBook(models.Model):
class Meta:
verbose_name = "Book"
verbose_name_plural = "Books"
class ConferenceType(models.TextChoices):
MIDDLE = "M", _("Middle School")
HIGH = "H", _("High School")
@ -47,30 +111,40 @@ class LegislationBook(models.Model):
return
for text in parsed.output:
print(text["code"])
codesplit = text["code"].split('/')
assembly = codesplit[0]
dashsplit = codesplit[1].split('-')
year = 2000 + int(dashsplit[0])
committee = int(dashsplit[1])
docket_order = int(dashsplit[2])
text = LegislativeText(
assembly=assembly,
year=year,
committee=committee,
docket_order=docket_order,
school=text["school"],
sponsors=text["sponsors"],
legislation_title=text["title"],
text=text["bill_text"],
from_book=self
)
text["school"] = InstantiateIfNone(School, text["school"])
if text["country"]:
# there's sometimes "Dominican Republic" and "Dominican Republic 2"
# handle that gracefully
text["country"] = text["country"].replace(" 2", "")
text["country"] = InstantiateIfNone(Country, text["country"])
if not text["category"] or text["category"] == "Select One--":
text["category"] = "No category"
text["category"] = InstantiateIfNone(Category, text["category"])
sponsors = text["sponsors"].split(', ')
sponsors = [InstantiateIfNone(Sponsor, sponsor) for sponsor in sponsors]
del text["sponsors"]
text = LegislativeText(**text, from_book=self)
text.save()
for sponsor in sponsors:
text.sponsors.add(sponsor)
def __str__(self):
return "{}".format(self.name)
def get_absolute_url(self):
our_name = __class__.__name__
return reverse("{}.detail".format(our_name), kwargs={"model_id": self.id})
class LegislativeText(models.Model):
class Meta:
verbose_name = "Legislation"
verbose_name_plural = "Legislation"
class Assemblies(models.TextChoices):
RGA = "RGA", _("Red General Assembly")
BGA = "BGA", _("Blue General Assembly")
@ -93,16 +167,13 @@ class LegislativeText(models.Model):
text = models.TextField()
year = models.IntegerField()
committee = models.IntegerField()
category = models.ForeignKey(Category, on_delete=models.CASCADE)
docket_order = models.IntegerField()
school = models.CharField(max_length=256)
sponsors = models.CharField(max_length=256)
school = models.ForeignKey(School, on_delete=models.CASCADE)
sponsors = models.ManyToManyField(Sponsor, blank=True)
from_book = models.ForeignKey(LegislationBook, on_delete=models.CASCADE)
legislation_title = models.CharField(max_length=512)
country = models.CharField(
max_length=512,
null=True,
blank=True
)
country = models.ForeignKey(Country, on_delete=models.CASCADE, null=True)
def __str__(self):
return "{}/{}-{}-{}".format(
@ -136,6 +207,10 @@ class LegislativeText(models.Model):
return False
class LegislationClassification(models.Model):
class Meta:
verbose_name = "Topic"
verbose_name_plural = "Topics"
name = models.CharField(max_length=256, help_text="Name of this classification.")
text_to_match = models.CharField(
max_length=256,
@ -144,3 +219,16 @@ class LegislationClassification(models.Model):
def __str__(self):
return "{}".format(self.name)
def get_absolute_url(self):
our_name = __class__.__name__
return reverse("{}.detail".format(our_name), kwargs={"model_id": self.id})
models_in_index = [
LegislationClassification,
School,
Country,
Sponsor,
Category,
LegislationBook
]

View File

@ -14,11 +14,11 @@
<nav id="navbar" class="boxed">
<div id="leftnav">
<a href="/explorer">explorer</a>
<a href="/kb">knowledge</a>
<a href="/knowledge">knowledge</a>
</div>
<div id="rightnav">
<a href="/explorer/all">all</a>
<a href="/explorer/topics">by topic</a>
<a href="/explorer/groups">by group</a>
<a href="/explorer/search">search</a>
<a href="/explorer/stats">stats</a>
</div>

View File

@ -0,0 +1,15 @@
{% extends "explorer/base.html" %}
{% block content %}
<link rel="stylesheet" type="text/css" href="/static/tn.css" />
<div class="boxed">
<h1>View legislation</h1>
<ul>
{% for name, url in listing.items %}
<li><a href="{{ url }}">By {{ name }}</a></li>
{% endfor %}
</ul>
</div>
{% endblock content %}

View File

@ -0,0 +1,38 @@
{% comment %}
This is a component -- I'm only using this because:
1. I'm too lazy to install django-components, and
2. This is the only time a component is necessary, so it doesn't make
any sense to take on an extra dependancy.
{% endcomment %}
<div class="legcomponent">
<a href="/explorer/legislation/{{ legislation.id }}"><h2 class="legtitle">{{ legislation.legislation_title }}</h2></a>
<div class="legmetadata">
<p>
<i>{{ legislation.assembly }}/{{ legislation.committee }}/{{ legislation.docket_order }}</i>
&middot;
{% for sponsor in legislation.sponsors.all %}
<a href="/explorer/sponsors/{{ sponsor.id }}">{{ sponsor.name }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
&middot;
<a href="/explorer/schools/{{ legislation.school.id }}">{{ legislation.school }}</a>
&middot;
{% if legislation.country %}
<a href="/explorer/countries/{{ legislation.country.id }}">{{ legislation.country }}</a>
&middot;
{% endif %}
<a href="/explorer/categories/{{ legislation.category.id }}">{{ legislation.category }}</a>
&middot;
<a href="/explorer/conference/{{ legislation.from_book.id }}">{{ legislation.from_book.name }}</a>
</p>
</div>
</div>

View File

@ -9,14 +9,16 @@
<h2>Some randomly selected legislation</h2>
{% if legislative_texts %}
{% if legislation %}
{% autoescape off %}
<ul>
{% for text in legislative_texts %}
<li><a href="{% url 'viewleg' text.id %}">{{ text.legislation_title }}</a></li>
{% for text in legislation %}
{{ text }}
{% endfor %}
</ul>
{% endautoescape %}
{% else %}
<p>No texts available</p>
<p>No texts available. If you're the admin, you need to add some texts with the admin interface.</p>
{% endif %}
<h2>About this instance</h2>

View File

@ -13,7 +13,18 @@
<p><i>{{ legislation.assembly }}/{{ legislation.committee }}/{{ legislation.docket_order }}</i></p>
<p>Sponsored by {{ legislation.sponsors }} of {{ legislation.school }}</p>
<p>Sponsored by
{% for sponsor in legislation.sponsors.all %}
<a href="/explorer/sponsors/{{ sponsor.id }}">{{ sponsor.name }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
of <a href="/explorer/schools/{{ legislation.school.id }}">{{ legislation.school }}</a></p>
{% if legislation.country %}
<p>The delegates above represented the <a href="/explorer/countries/{{ legislation.country.id }}">Delegation of {{ legislation.country }}</a>.</p>
{% endif %}
<p>This legislation was filed in the <a href="/explorer/categories/{{ legislation.category.id }}">{{ legislation.category }}</a> category</p>
<p>Presented as part of the <a href="/explorer/conference/{{ legislation.from_book.id }}">{{ legislation.from_book.name }}</a> conference</p>
</div>

View File

@ -0,0 +1,15 @@
{% extends "explorer/base.html" %}
{% block content %}
<link rel="stylesheet" type="text/css" href="/static/tn.css" />
<div class="boxed">
<h1>{{ result_name }}</h1>
<ul>
{% for instance in instances %}
<li><a href="{{ instance.get_absolute_url }}">{{ instance.name }}</a></li>
{% endfor %}
</ul>
</div>
{% endblock content %}

View File

@ -6,10 +6,12 @@
<div class="boxed">
<h1>{{ result_name }}</h1>
{% autoescape off %}
<ul>
{% for text in legislation %}
<li><a href="/explorer/legislation/{{ text.id }}">{{ text.legislation_title }}</a></li>
{{ text }}
{% endfor %}
</ul>
{% endautoescape %}
</div>
{% endblock content %}

View File

@ -0,0 +1,15 @@
{% extends "explorer/base.html" %}
{% block content %}
<link rel="stylesheet" type="text/css" href="/static/tn.css" />
<div class="boxed">
<h1>Search legislation by keyword</h1>
<form action="/explorer/search" method="get">
<input type="text" id="search_term" name="search_term" />
<button type="submit">Execute search</button>
<p>
You can use this page to search through legislation by keyword. The search is case sensitive, so be careful when pressing your shift key.
</p>
</form>
</div>
{% endblock %}

View File

@ -5,7 +5,7 @@
<div class="boxed">
<h1>Explorer statistics</h1>
<p>Total bills: {{ all }}</p>
<p>Total pieces of legislation: {{ all }}</p>
<p>Red Senate Bills: {{ red_senate }}</p>
@ -19,10 +19,10 @@
<p>Blue House Bills: {{ blue_house }}</p>
<p>Red General Assembly Bills: {{ red_ga }}</p>
<p>Red General Assembly Resolutions: {{ red_ga }}</p>
<p>White General Assembly Bills: {{ white_ga }}</p>
<p>White General Assembly Resolutions: {{ white_ga }}</p>
<p>Blue General Assembly Bills: {{ blue_ga }}</p>
<p>Blue General Assembly Resolutions: {{ blue_ga }}</p>
</div>
{% endblock content %}

View File

@ -7,7 +7,20 @@ urlpatterns = [
path("all/", views.all, name="all"),
path("stats/", views.stats, name="stats"),
path("legislation/<int:legislation_id>/", views.view_legislation, name="viewleg"),
path("conference/<int:conference_id>/", views.view_conference, name="viewconf"),
path("topics/<int:classification_id>/", views.get_all_classified_by_id, name="classificationview"),
path("topics/", views.get_all_classifications, name="classificationview"),
path("topics/<int:model_id>/", views.get_all_classified_by_id, name="LegislationClassification.detail"),
path("topics/", views.get_all_classifications, name="LegislationClassification"),
path("search/", views.handle_search, name="search_legislation"),
# these are named weirdly -- see models.py School and Country definitions
path("schools/<int:model_id>/", views.get_all_by_school, name="School.detail"),
path("countries/<int:model_id>/", views.get_all_by_country, name="Country.detail"),
path("sponsors/<int:model_id>/", views.get_all_by_sponsor, name="Sponsor.detail"),
path("categories/<int:model_id>/", views.get_all_by_category, name="Category.detail"),
path("conference/<int:model_id>/", views.get_all_by_conference, name="LegislationBook.detail"),
path("schools/", views.get_all_schools, name="School"),
path("countries/", views.get_all_countries, name="Country"),
path("groups/", views.return_groups, name="Groups"),
path("sponsors/", views.get_all_sponsors, name="Sponsor"),
path("categories/", views.get_all_categories, name="Category"),
path("conference/", views.get_all_conferences, name="LegislationBook"),
]

View File

@ -1,15 +1,40 @@
from django.shortcuts import get_object_or_404, render
from django.template.loader import render_to_string
from django.urls import reverse
from django.http import HttpResponse
from .models import LegislativeText, LegislationBook, LegislationClassification
from .models import (
LegislativeText,
LegislationBook,
LegislationClassification,
School,
Country,
Sponsor,
Category,
models_in_index,
)
from random import sample
def legislation_to_html(legislation):
return render_to_string("explorer/comp_legislation.html", {
"legislation": legislation,
})
def index(request):
legislative_texts = list(LegislativeText.objects.all())
legislative_texts = sample(legislative_texts, 5)
try:
legislative_texts = sample(legislative_texts, 5)
except ValueError:
# there's not enough texts, so just return nothing
legislative_texts = []
legislative_texts = [
legislation_to_html(text) for text in legislative_texts
]
context = {
"legislative_texts": legislative_texts,
"legislation": legislative_texts,
}
return render(request, "explorer/index.html", context)
@ -54,14 +79,8 @@ def stats(request):
}
return render(request, "explorer/stats.html", context)
def get_all_classifications(request):
classifications = LegislationClassification.objects.all()
return render(request, "explorer/classifications.html", {
"classifications": classifications,
})
def get_all_classified_by_id(request, classification_id):
classification = get_object_or_404(LegislationClassification, pk=classification_id)
def get_all_classified_by_id(request, model_id):
classification = get_object_or_404(LegislationClassification, pk=model_id)
# this is very expensive; make a way for this to be cached please?
all_texts = LegislativeText.objects.all()
@ -80,3 +99,86 @@ def get_all_classified_by_id(request, classification_id):
"legislation": matches,
"result_name": "All legislation in topic {}".format(classification.name)
})
def get_all_by_x(model):
def wrapped(request, model_id):
instance = get_object_or_404(model, pk=model_id)
legislation = instance.legislativetext_set.all()
legislation = [legislation_to_html(i) for i in legislation]
return render(request, "explorer/results.html", {
"result_name": "All legislation by {}".format(instance.name),
"legislation": legislation
})
return wrapped
def get_all_xs(model):
def wrapper(request):
instances = model.objects.all()
try:
# what the heck, django?????
plural = model._meta.verbose_name_plural
except:
plural = model.__name__ + "s"
plural = plural.lower()
return render(request, "explorer/listing.html", {
"result_name": "All {}".format(plural),
"instances": instances,
})
return wrapper
def return_groups(request):
listing = {}
for model in models_in_index:
try:
name = model._meta.verbose_name.lower()
except:
name = model.__name__.lower()
listing[name] = reverse(model.__name__)
print(listing)
return render(request, "explorer/by_group.html", { "listing": listing })
def handle_search(request):
try:
query = request.GET['search_term']
except KeyError:
return render(request, "explorer/search.html", {})
f = LegislativeText.objects.filter
text_results = f(text__icontains=query)
title_results = f(legislation_title__icontains=query)
school_results = f(school__name__icontains=query)
sponsor_results = f(sponsors__name__icontains=query)
country_results = f(country__name__icontains=query)
results = text_results.union(
title_results,
school_results,
sponsor_results,
country_results
)
results = [legislation_to_html(i) for i in results]
return render(request, "explorer/results.html", {
"result_name": "Results for search term '{}'".format(query),
"legislation": results
})
get_all_by_school = get_all_by_x(School)
get_all_by_country = get_all_by_x(Country)
get_all_by_sponsor = get_all_by_x(Sponsor)
get_all_by_category = get_all_by_x(Category)
get_all_by_conference = get_all_by_x(LegislationBook)
get_all_schools = get_all_xs(School)
get_all_countries = get_all_xs(Country)
get_all_sponsors = get_all_xs(Sponsor)
get_all_categories = get_all_xs(Category)
get_all_classifications = get_all_xs(LegislationClassification)
get_all_conferences = get_all_xs(LegislationBook)

View File

@ -2,9 +2,12 @@ files=$(find kb -type f | awk -F'/' '{print $NF}')
IFS='
'
mkdir -p franklincce/staticfiles/kb
mkdir -p franklincce/staticfiles/root
for file in $files; do
without_extension=${file%.*}
echo $file, $without_extension
pandoc -s --template=./template.html -f markdown -t html -o "franklincce/staticfiles/kb/$without_extension.html" "kb/$without_extension.md" --lua-filter=links-to-html.lua
done
done
cp franklincce/staticfiles/kb/web_root.html franklincce/staticfiles/root/index.html

View File

@ -0,0 +1,33 @@
---
title: "Choosing a resolution topic"
---
This is a tricky thing.
You've already picked a country, and so you have to choose a topic to write your resolution on.
Here are some things to keep in mind as you're choosing a topic.
1. Choose a *worldwide* topic.
Unless you're requesting immediate aid in the wake of a disaster, personalized solutions (ie. solutions only applicable to one country) are not the point of the UN.
Make the solutions enumerated in your resolution applicable to other countries, too.
2. Choose a *memorable* topic.
Don't do a resolution about drilling wells in Africa for water, because that's been done to death.
Try to come up with a unique way to solve the problems at hand.
One of the components which your resolutions will be scored on is originality, so keep that in mind.
3. Choose a topic with an asymmetrical *information advantage*.
What is a topic with an "asymmetrical information advantage"?
It's a topic that is common and prevalant, but not a lot of people know the intricate details of.
Like the American healthcare system, or the ICAO.
This is less important than the other ones, but is still something to consider -- choose a topic with an information advantage and it makes it easier during debate.
4. Choose an *important* topic.
This is the most important one.
Choose a relevant topic.
If you're country's in the news recently, look there for inspiration.
Got a good idea? Great. Now it's time to [write your resolution](./writing-resolution.md).

View File

@ -4,4 +4,7 @@ title: "Franklin CCE Knowledgebase"
## Model UN
- [Writing a resolution](./writing-resolution.md)
- [Writing a resolution](./writing-resolution.md)
Want to see more articles in the knowledgebase?
[Learn how to contribute](./CONTRIBUTING.md).

38
kb/operative-verbs.md Normal file
View File

@ -0,0 +1,38 @@
---
title: "Operative verbs reference"
---
- Accepts
- Adopts
- Agrees
- Appeals
- Approves
- Authorizes
- Calls upon
- Commends
- Considers
- Decides
- Declares
- Determines
- Directs
- Emphasizes
- Encourages
- Endorses
- Expresses appreciation
- Expresses hope
- Invites
- Notes
- Notes with approval
- Notes with concern
- Notes with satisfaction
- Proclaims
- Reaffirms
- Recommends
- Reminds
- Repeals
- Requests
- Resolves
- Suggests
- Supports
- Takes note
- Urges

40
kb/preambular-verbs.md Normal file
View File

@ -0,0 +1,40 @@
---
title: "Preambular verbs reference"
---
- Acknowledging
- Affirming
- Appreciating
- Approving
- Aware
- Bearing in mind
- Believing
- Commending
- Concerned
- Conscious
- Considering
- Convinced
- Desiring
- Emphasizing
- Expecting
- Expressing
- Fully aware
- Guided by
- Having adopted
- Having considered
- Having noted
- Having reviewed
- Mindful
- Noting
- Noting with approval
- Noting with concern
- Noting with satisfaction
- Observing
- Realising
- Recalling
- Recognizing
- Seeking
- Taking into consideration
- Underlining
- Welcoming
- Whereas

6
kb/web_root.md Normal file
View File

@ -0,0 +1,6 @@
---
title: "Franklin CCE Delegation website"
---
This website contains some materials for the Franklin High School YMCA Center for Civic Engagement delegation.
Specifically, a [bill database](/explorer) and a [collection of helpful articles](/knowledge) on various things are available here.

View File

@ -2,4 +2,66 @@
title: "Writing a Resolution"
---
This is a test
*Note: This guide assumes you've already choosen a topic; if not, see [Choosing a resolution topic](./choosing-topic-resolution.md).*
The General Assembly, the largest law-making body of the UN, expresses its intentions and actions as a *resolution*.
In this article, we'll learn how to write one.
A resolution is composed of two parts: the *preambulatory* (or preambular) and *operative* clauses.
Preambulatory clauses provide background on why your resolution is needed, the problem it addresses, and sets the tone for the operative clauses.
Operative clauses are the items of action; they are directives that countries agree to.
(Only the Security Council has the power of passing *mandatory* directives.)
As an example, let's assume we're writing a resolution that condemns kicking puppies. (I would hope that you'd agree with the premise; if not -- that's an interesting opinion.) There was a recent puppy kicking event in the country of `Madeupia`, so let's note that:
> Noting with concern the International Puppy Kicking Event held in `Madeupia` on 22 September 2024, where various kinds of dogs were subject to unconsionable acts of depravity and abuse;
This is a good example of a preambulatory clause -- it states background for the operative clauses. If someone asked you if people actually kick dogs, this is your response. It demonstrates the necessity of the resolution.
There also might be past resulutions about animal abuse or dogs, so it might be prudent to mention those, too:
> Affirming resolution UNEP/5/1, which specifically emphasizes the value of animals and the contributions their welfare makes to the UN Sustainable Development Goals,
You essentially want to create many of these preambulatory clauses to paint a vivid picture of the need of your resolution. If you want to see more verbs to go in front of your preambulatory clauses, check out the [preambulatory verbs reference](./preambular-verbs.md).
Once you're done writing your preambulatory clauses, you write:
> The General Assembly hereby:
You do **NOT** write:
> The Delegation of (your country here) hereby:
because the resolution is adopted by the Assembly. That's why there's a vote and stuff -- if a country wants to express a feeling it feels by itself, it can submit a Communique to the UN instead.
Now it's time to talk about operative clauses.
These clauses are where the actual works gets done.
I think a good first move is the condemn the country of `Madeupia` for their puppy kicking event:
> Condemns, in the strongest terms, the affront to animal welfare perpetuated by the State of Madeupia;
Here are some other forms of clauses you might want to consider:
> Takes note of the report of the Secretary-General on "Animal Welfare as it pertains to the advancement of the SDGs";
>
> Expresses support of the Totally Real Animal Anti-Violence Directorate of the United Nations and their actions in response to this issue;
>
> Recognizes the positive contribution of various Member States towards the resolution of this issue, including the Republic of Dog Lovers;
Do note -- when you're ending your resolution, your final clause ends with a period.
> Requests the Secretary-General to submit a report on the implementation of the present resolution including recommendations for future action at the next session of the General Assembly.
For a full list of operative verbs, see the [operative verbs reference](./operative-verbs.md).
One final note: if you use a comma in any of your clauses, all of the clauses (except the last one) have to end in a semicolon.
This is because of the rules of Standard English.
## Some other resources for resolution writing
- [The UN's Model UN resources](https://www.un.org/en/model-united-nations/drafting-resolutions)
The UN does say some different things in this article -- it talks about L-documents and agreement annotations and soliciting agreement from other delegations beforehand.
That's not relevant to our conference, but it is still a good resource.
For an example of a finished resolution that is somewhat up to format, see the heading "Compilation Text as of 21 October 2016 (Rev. 3) The General Assembly".

View File

@ -4,8 +4,20 @@ upstream franklincce {
server {
listen 80;
server_name franklincce.beepboop.systems;
port_in_redirect off;
absolute_redirect off;
server_name_in_redirect off;
location / {
location /explorer/ {
proxy_pass http://franklincce;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
client_max_body_size 100M;
}
location /admin/ {
proxy_pass http://franklincce;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
@ -17,7 +29,11 @@ server {
alias /home/app/web/staticfiles/;
}
location /kb {
location /knowledge {
alias /home/app/web/staticfiles/kb;
}
location / {
alias /home/app/web/staticfiles/root/;
}
}

View File

@ -14,7 +14,7 @@
<nav id="navbar" class="boxed">
<div id="leftnav">
<a href="/explorer">explorer</a>
<a href="/kb">knowledge</a>
<a href="/knowledge">knowledge</a>
</div>
</nav>