はじめに
のつづきで、Web化したもの。
やりとりしてたのが2014年なので10年越しのWeb化である。あの当時のスキルレベルを振り返るとなつかしいな。
適用するのはおなじみ、ポートフォリオ(hospital)だ
要望
不在者投票の選挙事務業務効率化について
関わる人
※ここで「わたし」と書いてあるのは僕じゃなくて依頼主のこと
※太字が主な事務処理関係者
- 選管
- 病院
- 管理課(わたし)
- 病棟担当者(ケースワーカー)
- 患者
- 病棟看護師
選挙期間のタイムライン
- 選挙告示日
- 病院で行う不在者投票の日程を決めて周知と貼り出し
- 投票日前
- 病棟からわんさかやってくる請求者名簿の処理、各市区選管へ郵送、持ち込み
- 各市区選管から投票用紙を受取り、市区別のものを病棟ごとに仕分け(※受付期間は定めているが、受付期間終了後でも申し出があれば断れないため随時追加がでてきて、投票用紙の処理と同時並行で追加の人の請求処理もする。しんどい)
- 事務処理簿に”請求の方法”、”代理請求の依頼を受けた月日”、”請求月日”、”受領日”(投票用紙の)の項目を随時入力する
- 投票日~投票用紙提出
- 2日間にわけて投票場所を設置し、立会い、追加があれば請求事務
- 投票封筒に記載された立会人を事務処理簿に入力
- 投票日、投票場所も入力
- 選挙後
- 実際に投票した人数×727円を経費として請求できるので、退院や棄権を除いた人数を集計し、請求書作成。人数間違うと大変
1~2: 選挙告示日から投票日前まで
- 行政
- 選挙します! 不在者投票認可施設だから病院で選挙事務してね!
- 病院
- 選挙権のある入院患者さん、代理で手続きするから名乗り出てね!
- 病棟担当者
- 病棟別で意思表示のある患者さんの住所調べて、名前と生年月日を名簿にかくよ!管理課にファックスする!
- 管理課(病棟からのファックスを選管に提出できるように処理)
- 請求者名簿を病棟別→市区別にまとめなおし
- 投票済み用紙と一緒に送る事務処理簿にも随時項目を入力
3: 管理課にて投票日~投票用紙提出まで
アプリケーションのベースをつくる
このあたり見て
modelとseederをつくる
models.py
hospital/models.py
from django.contrib.auth.models import User
from django.db import models
class WardType(models.Model):
name = models.CharField(verbose_name="病棟種", max_length=100, unique=True)
def __str__(self):
return self.name
class Ward(models.Model):
abbreviation = models.CharField(verbose_name="略称", unique=True, max_length=10)
ward_type = models.ForeignKey(
WardType, verbose_name="病棟種", on_delete=models.CASCADE
)
name = models.CharField(
verbose_name="病棟名", max_length=100, null=False, blank=False
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True, null=True)
def __str__(self):
return self.name
class City(models.Model):
name = models.CharField(verbose_name="エリア", max_length=100, unique=True)
def __str__(self):
return self.name
class CitySector(models.Model):
name = models.CharField(verbose_name="市区", max_length=100)
city = models.ForeignKey(City, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True, null=True)
def __str__(self):
return self.name
class Election(models.Model):
name = models.CharField(verbose_name="選挙名", max_length=255, unique=True)
execution_date = models.DateField(verbose_name="執行日")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True, null=True)
def __str__(self):
return self.name
class VotePlace(models.Model):
name = models.CharField(max_length=255)
def __str__(self):
return self.name
class UserAttribute(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
address = models.TextField(verbose_name="住所")
date_of_birth = models.DateField(verbose_name="生年月日")
BILLING_METHOD_CHOICES = [
(1, "代理・直接"),
(2, "代理・郵便"),
]
class ElectionLedger(models.Model):
"""
選挙事務用の請求者名簿と事務処理簿の入力項目をまとめた、事務処理台帳
Note: モデルフィールドで choices 属性が設定されている場合、_get_FOO_display() メソッドが自動生成される。
これにより、レコード内部値に代替する表示名("代理・直接" など)を取得することができます。
>>> ledger = ElectionLedger.objects.get(id=1)
>>> print(ledger.get_billing_method_display())
Attributes:
- election (ForeignKey): 選挙名
- voter (ForeignKey): 投票者氏名
- vote_ward (ForeignKey): 病棟名
- vote_city_sector (ForeignKey): 投票区名
- remark (CharField): 備考
- billing_method (CharField): 投票用紙の請求方法
- proxy_billing_request_date (DateField): 代理請求依頼日
- proxy_billing_date (DateField): 代理請求日
- ballot_received_date (DateField): 投票用紙受領日
- vote_date (DateField): 投票日(投票済みか否かを判断できる)
- vote_place (ForeignKey): 投票場所
- voter_witness (ForeignKey): 投票立会人
- applied_for_proxy_voting (BooleanField): 代理投票申請の有無
- delivery_date (DateField): 投票用紙送付日
- created_at (DateTimeField): 取込日時
- updated_at (DateTimeField): 更新日時
"""
election = models.ForeignKey(
Election, verbose_name="選挙名", on_delete=models.CASCADE
)
voter = models.ForeignKey(
User,
verbose_name="選挙人氏名",
on_delete=models.CASCADE,
related_name="voter",
)
vote_ward = models.ForeignKey(Ward, verbose_name="病棟", on_delete=models.CASCADE)
vote_city_sector = models.ForeignKey(
CitySector, verbose_name="投票区", on_delete=models.CASCADE
)
remark = models.CharField(
verbose_name="備考", max_length=255, null=True, blank=True
)
billing_method = models.IntegerField(
verbose_name="投票用紙請求の方法",
choices=BILLING_METHOD_CHOICES,
null=True,
blank=True,
)
proxy_billing_request_date = models.DateField(
verbose_name="代理請求の依頼を受けた日", null=True, blank=True
)
proxy_billing_date = models.DateField(
verbose_name="代理請求日", null=True, blank=True
)
ballot_received_date = models.DateField(
verbose_name="投票用紙受領日", null=True, blank=True
)
vote_date = models.DateField(verbose_name="投票日", null=True, blank=True)
vote_place = models.ForeignKey(
VotePlace,
verbose_name="投票場所",
on_delete=models.CASCADE,
null=True,
blank=True,
)
vote_observer = models.ForeignKey(
User,
verbose_name="投票立会人",
on_delete=models.CASCADE,
related_name="voter_witness",
null=True,
blank=True,
)
applied_for_proxy_voting = models.BooleanField(
verbose_name="代理投票申請の有無",
default=False,
)
delivery_date = models.DateField(
verbose_name="投票用紙送付日", null=True, blank=True
)
created_at = models.DateTimeField(verbose_name="取込日", auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True, null=True)
def __str__(self):
return f"{self.election.name}, {self.voter}(病棟: {self.vote_ward.name})"
病棟名マスタ
hospital/fixtures/ward.json
[
{
"model": "hospital.WardType",
"pk": 1,
"fields": {
"name": "精神"
}
},
{
"model": "hospital.WardType",
"pk": 2,
"fields": {
"name": "一般"
}
},
{
"model": "hospital.ward",
"pk": 1,
"fields": {
"abbreviation": "A3",
"ward_type": 1,
"name": "A館3階",
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.ward",
"pk": 2,
"fields": {
"abbreviation": "A4",
"ward_type": 1,
"name": "A館4階",
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.ward",
"pk": 3,
"fields": {
"abbreviation": "A5",
"ward_type": 1,
"name": "A館5階",
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.ward",
"pk": 4,
"fields": {
"abbreviation": "A6",
"ward_type": 1,
"name": "A館6階",
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.ward",
"pk": 5,
"fields": {
"abbreviation": "A7",
"ward_type": 1,
"name": "A館7階",
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.ward",
"pk": 6,
"fields": {
"abbreviation": "A8",
"ward_type": 1,
"name": "A館8階",
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.ward",
"pk": 7,
"fields": {
"abbreviation": "B2",
"ward_type": 1,
"name": "B館2階",
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.ward",
"pk": 8,
"fields": {
"abbreviation": "B3",
"ward_type": 1,
"name": "B館3階",
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.ward",
"pk": 9,
"fields": {
"abbreviation": "B4",
"ward_type": 1,
"name": "B館4階",
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.ward",
"pk": 10,
"fields": {
"abbreviation": "B5",
"ward_type": 1,
"name": "B館5階",
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.ward",
"pk": 11,
"fields": {
"abbreviation": "B6",
"ward_type": 1,
"name": "B館6階",
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.ward",
"pk": 12,
"fields": {
"abbreviation": "C1",
"ward_type": 1,
"name": "C館1階",
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.ward",
"pk": 13,
"fields": {
"abbreviation": "西",
"ward_type": 1,
"name": "西館",
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.ward",
"pk": 14,
"fields": {
"abbreviation": "東3",
"ward_type": 2,
"name": "東館3階",
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.ward",
"pk": 15,
"fields": {
"abbreviation": "東4",
"ward_type": 2,
"name": "東館4階",
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.ward",
"pk": 16,
"fields": {
"abbreviation": "リハ",
"ward_type": 2,
"name": "リハビリテーション病棟",
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.ward",
"pk": 17,
"fields": {
"abbreviation": "包括",
"ward_type": 2,
"name": "地域包括ケア病棟",
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.ward",
"pk": 18,
"fields": {
"abbreviation": "緩和",
"ward_type": 2,
"name": "緩和ケア病棟",
"created_at": "2024-09-23T10:00:00Z"
}
}
]
市区名マスタ
hospital/fixtures/city.json
[
{
"model": "hospital.city",
"pk": 1,
"fields": {
"name": "堺市"
}
},
{
"model": "hospital.city",
"pk": 2,
"fields": {
"name": "大阪市"
}
},
{
"model": "hospital.city",
"pk": 3,
"fields": {
"name": "大阪府"
}
},
{
"model": "hospital.CitySector",
"pk": 1,
"fields": {
"name": "堺区",
"city": 1,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 2,
"fields": {
"name": "中区",
"city": 1,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 3,
"fields": {
"name": "東区",
"city": 1,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 4,
"fields": {
"name": "西区",
"city": 1,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 5,
"fields": {
"name": "南区",
"city": 1,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 6,
"fields": {
"name": "北区",
"city": 1,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 7,
"fields": {
"name": "美原区",
"city": 1,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 8,
"fields": {
"name": "阿倍野区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 9,
"fields": {
"name": "平野区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 10,
"fields": {
"name": "住吉区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 11,
"fields": {
"name": "住之江区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 12,
"fields": {
"name": "東住吉区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 13,
"fields": {
"name": "生野区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 14,
"fields": {
"name": "城東区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 15,
"fields": {
"name": "東成区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 16,
"fields": {
"name": "旭区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 17,
"fields": {
"name": "鶴見区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 18,
"fields": {
"name": "中央区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 19,
"fields": {
"name": "西成区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 20,
"fields": {
"name": "西区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 21,
"fields": {
"name": "天王寺区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 22,
"fields": {
"name": "浪速区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 23,
"fields": {
"name": "都島区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 24,
"fields": {
"name": "福島区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 25,
"fields": {
"name": "北区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 26,
"fields": {
"name": "東淀川区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 27,
"fields": {
"name": "西淀川区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 28,
"fields": {
"name": "港区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 29,
"fields": {
"name": "大正区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 30,
"fields": {
"name": "此花区",
"city": 2,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 31,
"fields": {
"name": "高石市",
"city": 3,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 32,
"fields": {
"name": "松原市",
"city": 3,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 33,
"fields": {
"name": "富田林市",
"city": 3,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 34,
"fields": {
"name": "大阪狭山市",
"city": 3,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 35,
"fields": {
"name": "岸和田市",
"city": 3,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 36,
"fields": {
"name": "和泉市",
"city": 3,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 37,
"fields": {
"name": "河内長野市",
"city": 3,
"created_at": "2024-09-23T10:00:00Z"
}
},
{
"model": "hospital.CitySector",
"pk": 37,
"fields": {
"name": "羽曳野市",
"city": 3,
"created_at": "2024-09-23T10:00:00Z"
}
}
]
看護師・患者マスタ
(※portfolioの既存のシーダーに追記している)
vietnam_research/fixtures/group.json
[
{
"model": "auth.group",
"pk": 1,
"fields": {
"name": "看護師"
}
},
{
"model": "auth.group",
"pk": 2,
"fields": {
"name": "患者"
}
}
]
vietnam_research/fixtures/user.json
[
:
{
"model": "auth.user",
"pk": 5,
"fields": {
"username": "A看護師",
"first_name": "A",
"last_name": "看護師",
"email": "kangoshiA@example.com",
"password": "XXX",
"groups": [
1
],
"is_staff": 1
}
},
{
"model": "auth.user",
"pk": 6,
"fields": {
"username": "B看護師",
"first_name": "B",
"last_name": "看護師",
"email": "kangoshiB@example.com",
"password": "XXX",
"groups": [
1
],
"is_staff": 1
}
},
{
"model": "auth.user",
"pk": 7,
"fields": {
"username": "C看護師",
"first_name": "C",
"last_name": "看護師",
"email": "kangoshiC@example.com",
"password": "XXX",
"groups": [
1
],
"is_staff": 1
}
},
{
"model": "auth.user",
"pk": 8,
"fields": {
"username": "D看護師",
"first_name": "D",
"last_name": "看護師",
"email": "kangoshiD@example.com",
"password": "XXX",
"groups": [
1
],
"is_staff": 1
}
},
{
"model": "auth.user",
"pk": 9,
"fields": {
"username": "E看護師",
"first_name": "E",
"last_name": "看護師",
"email": "kangoshiE@example.com",
"password": "XXX",
"groups": [
1
],
"is_staff": 1
}
},
{
"model": "auth.user",
"pk": 10,
"fields": {
"username": "A患者",
"first_name": "A",
"last_name": "患者",
"email": "kanjaA@example.com",
"password": "XXX",
"groups": [
2
]
}
},
{
"model": "auth.user",
"pk": 11,
"fields": {
"username": "B患者",
"first_name": "B",
"last_name": "患者",
"email": "kanjaB@example.com",
"password": "XXX",
"groups": [
2
]
}
},
{
"model": "auth.user",
"pk": 12,
"fields": {
"username": "C患者",
"first_name": "C",
"last_name": "患者",
"email": "kanjaC@example.com",
"password": "XXX",
"groups": [
2
]
}
},
{
"model": "auth.user",
"pk": 13,
"fields": {
"username": "D患者",
"first_name": "D",
"last_name": "患者",
"email": "kanjaD@example.com",
"password": "XXX",
"groups": [
2
]
}
},
{
"model": "auth.user",
"pk": 14,
"fields": {
"username": "E患者",
"first_name": "E",
"last_name": "患者",
"email": "kanjaE@example.com",
"password": "XXX",
"groups": [
2
]
}
},
{
"model": "auth.user",
"pk": 15,
"fields": {
"username": "F患者",
"first_name": "F",
"last_name": "患者",
"email": "kanjaF@example.com",
"password": "XXX",
"groups": [
2
]
}
},
{
"model": "auth.user",
"pk": 16,
"fields": {
"username": "G患者",
"first_name": "G",
"last_name": "患者",
"email": "kanjaG@example.com",
"password": "XXX",
"groups": [
2
]
}
},
{
"model": "auth.user",
"pk": 17,
"fields": {
"username": "H患者",
"first_name": "H",
"last_name": "患者",
"email": "kanjaH@example.com",
"password": "XXX",
"groups": [
2
]
}
},
{
"model": "auth.user",
"pk": 18,
"fields": {
"username": "I患者",
"first_name": "I",
"last_name": "患者",
"email": "kanjaI@example.com",
"password": "XXX",
"groups": [
2
]
}
},
{
"model": "auth.user",
"pk": 19,
"fields": {
"username": "J患者",
"first_name": "J",
"last_name": "患者",
"email": "kanjaJ@example.com",
"password": "XXX",
"groups": [
2
]
}
}
]
患者属性マスタ
hospital/fixtures/userattribute.json
[
{
"model": "hospital.UserAttribute",
"pk": 1,
"fields": {
"user": 10,
"address": "大阪府大阪市北区梅田1丁目1-1",
"date_of_birth": "1980-01-01"
}
},
{
"model": "hospital.UserAttribute",
"pk": 2,
"fields": {
"user": 11,
"address": "大阪府堺市堺区築港八町1丁",
"date_of_birth": "1982-02-02"
}
},
{
"model": "hospital.UserAttribute",
"pk": 3,
"fields": {
"user": 12,
"address": "大阪府大阪市中央区城見1丁目",
"date_of_birth": "1975-03-03"
}
},
{
"model": "hospital.UserAttribute",
"pk": 4,
"fields": {
"user": 13,
"address": "大阪府大阪市東淀川区東淡路5丁目",
"date_of_birth": "1985-04-04"
}
},
{
"model": "hospital.UserAttribute",
"pk": 5,
"fields": {
"user": 14,
"address": "大阪府堺市泉北区和泉野町",
"date_of_birth": "1978-05-05"
}
},
{
"model": "hospital.UserAttribute",
"pk": 6,
"fields": {
"user": 15,
"address": "大阪府大阪市此花区逸津川町",
"date_of_birth": "1972-06-06"
}
},
{
"model": "hospital.UserAttribute",
"pk": 7,
"fields": {
"user": 16,
"address": "大阪府大阪市西区江戸堀1丁目",
"date_of_birth": "1962-07-07"
}
},
{
"model": "hospital.UserAttribute",
"pk": 8,
"fields": {
"user": 17,
"address": "大阪府大阪市西淀川区姫里3丁目",
"date_of_birth": "1971-08-08"
}
},
{
"model": "hospital.UserAttribute",
"pk": 9,
"fields": {
"user": 18,
"address": "大阪府大阪市浪速区敷津東3丁目",
"date_of_birth": "1988-09-09"
}
},
{
"model": "hospital.UserAttribute",
"pk": 10,
"fields": {
"user": 19,
"address": "大阪府大阪市東成区星ヶ町",
"date_of_birth": "1987-10-10"
}
}
]
選挙名マスタ
hospital/fixtures/election.json
[
{
"model": "hospital.election",
"pk": 1,
"fields": {
"name": "大阪府議会・堺市議会議員選挙",
"execution_date": "2015-04-12",
"created_at": "2024-09-23T10:00:00Z"
}
}
]
選挙場所マスタ
hospital/fixtures/voteplace.json
[
{
"model": "hospital.VotePlace",
"pk": 1,
"fields": {
"name": "総務"
}
},
{
"model": "hospital.VotePlace",
"pk": 2,
"fields": {
"name": "食堂"
}
},
{
"model": "hospital.VotePlace",
"pk": 3,
"fields": {
"name": "院内ローソン"
}
}
]
CRUD
forms.py
hospital/forms.py
from django import forms
from django.contrib.auth.models import User
from hospital.models import (
ElectionLedger,
Election,
Ward,
CitySector,
BILLING_METHOD_CHOICES,
VotePlace,
)
class ElectionLedgerCreateForm(forms.ModelForm):
election = forms.ModelChoiceField(
queryset=Election.objects.all(),
label="選挙名*",
widget=forms.Select(attrs={"class": "form-control", "tabindex": "1"}),
)
patient_ids = User.objects.filter(groups__name__in=["患者"]).values_list(
"id", flat=True
)
voter = forms.ModelChoiceField(
queryset=User.objects.filter(id__in=patient_ids),
label="選挙人氏名*",
widget=forms.Select(attrs={"class": "form-control", "tabindex": "2"}),
)
vote_ward = forms.ModelChoiceField(
queryset=Ward.objects.all(),
label="病棟*",
widget=forms.Select(attrs={"class": "form-control", "tabindex": "3"}),
)
vote_city_sector = forms.ModelChoiceField(
queryset=CitySector.objects.all(),
label="投票区*",
widget=forms.Select(attrs={"class": "form-control", "tabindex": "4"}),
)
remark = forms.CharField(
label="備考",
required=False,
widget=forms.Textarea(
attrs={"class": "form-control", "rows": "3", "tabindex": "5"}
),
)
billing_method = forms.ChoiceField(
choices=BILLING_METHOD_CHOICES,
label="投票用紙請求の方法*",
initial=2,
widget=forms.Select(attrs={"class": "form-control", "tabindex": "6"}),
)
proxy_billing_request_date = forms.DateField(
label="代理請求の依頼を受けた日",
error_messages={
"invalid": "代理請求の依頼を受けた日を正しく入力してください。"
},
required=False,
widget=forms.DateInput(attrs={"class": "form-control", "tabindex": "7"}),
)
proxy_billing_date = forms.DateField(
label="代理請求日",
error_messages={"invalid": "代理請求日を正しく入力してください。"},
required=False,
widget=forms.DateInput(attrs={"class": "form-control", "tabindex": "8"}),
)
ballot_received_date = forms.DateField(
label="投票用紙受領日",
error_messages={"invalid": "投票用紙受領日を正しく入力してください。"},
required=False,
widget=forms.DateInput(attrs={"class": "form-control", "tabindex": "9"}),
)
vote_date = forms.DateField(
label="投票日",
error_messages={"invalid": "投票日を正しく入力してください。"},
required=False,
widget=forms.DateInput(attrs={"class": "form-control", "tabindex": "10"}),
)
vote_place = forms.ModelChoiceField(
queryset=VotePlace.objects.all(),
label="投票場所",
required=False,
widget=forms.Select(attrs={"class": "form-control", "tabindex": "11"}),
)
vote_observer_ids = User.objects.filter(groups__name__in=["看護師"]).values_list(
"id", flat=True
)
vote_observer = forms.ModelChoiceField(
queryset=User.objects.filter(id__in=vote_observer_ids),
label="投票立会人",
required=False,
widget=forms.Select(attrs={"class": "form-control", "tabindex": "12"}),
)
applied_for_proxy_voting = forms.BooleanField(
label="代理投票申請の有無",
required=False,
initial=False,
widget=forms.CheckboxInput(
attrs={
"class": "form-check-input",
"style": "margin-left: 20px;",
"tabindex": "13",
}
),
)
delivery_date = forms.DateField(
label="投票用紙送付日",
error_messages={"invalid": "投票用紙送付日を正しく入力してください。"},
required=False,
widget=forms.DateInput(attrs={"class": "form-control", "tabindex": "14"}),
)
class Meta:
model = ElectionLedger
fields = "__all__"
class ElectionLedgerUpdateForm(ElectionLedgerCreateForm):
pass
create.html
hospital/templates/hospital/election_ledger/create.html
{% extends 'hospital/base.html' %}
{% block content %}
<div class="container">
<h2>新規作成</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-outline-primary">送信</button>
</form>
</div>
{% endblock %}
index.html
hospital/templates/hospital/index.html
{% extends "hospital/base.html" %}
{% load static %}
{% load humanize %}
{% block content %}
<div class="jumbotron">
<h1 class="display-4">Let's work at the hospital!</h1>
<p class="lead">improving proxi voting operations.</p>
<hr class="my-4">
<p>You can do hospital administration too.</p>
<ul>
<li><a class="btn btn-secondary btn-sm" href="#" role="button">XXXX</a></li>
<li><a class="btn btn-secondary btn-sm" href="#" role="button">YYYY</a></li>
</ul>
</div>
<div class="container">
<div class="form-group">
<form method="get">
<select name="election" class="form-control" onchange="this.form.submit()">
<option value="">-- 選挙を選択 --</option>
{% for election in elections %}
<option value="{{ election.id }}"
{% if request.GET.election == election.id|stringformat:"s" %}selected{% endif %}>
{{ election.name }}
</option>
{% endfor %}
</select>
</form>
</div>
<a href="{% url 'hsp:election_ledger_create' %}" class="btn btn-outline-primary"
role="button">新規投票記録を追加</a>
<table class="table table-striped table-bordered">
<thead class="bg-primary text-white">
<tr>
<th scope="col">病棟名</th>
<th scope="col">投票区名</th>
<th scope="col">取込日</th>
<th scope="col">選挙人氏名</th>
<th scope="col">選挙人住所</th>
<th scope="col">選挙人生年月日</th>
<th scope="col">操作</th>
</tr>
</thead>
<tbody>
{% for ledger in electionledger_list %}
<tr>
<td>{{ ledger.vote_ward }}</td>
<td>{{ ledger.vote_city_sector }}</td>
<td>{{ ledger.created_at|date:"Y-m-d" }}</td>
<td>{{ ledger.voter }}</td>
<td>{{ ledger.voter.userattribute.address }}</td>
<td>{{ ledger.voter.userattribute.date_of_birth|date:"Y-m-d" }}</td>
<td>
<a href="{% url 'hsp:election_ledger_detail' ledger.id %}" class="btn btn-outline-info">詳細</a>
<a href="{% url 'hsp:election_ledger_update' ledger.id %}"
class="btn btn-outline-primary">更新</a>
<a href="{% url 'hsp:election_ledger_delete' ledger.id %}"
class="btn btn-outline-danger">削除</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="8">No ledger available.</td>
</tr>
{% endfor %}
</tbody>
</table>
<nav aria-label="Page navigation example">
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="page-item"><a class="page-link" href="?page=1">First</a></li>
<li class="page-item"><a class="page-link"
href="?page={{ page_obj.previous_page_number }}">Previous</a></li>
{% else %}
<li class="page-item disabled"><a class="page-link" href="#">First</a></li>
<li class="page-item disabled"><a class="page-link" href="#">Previous</a></li>
{% endif %}
<li class="page-item active"><a class="page-link" href="#">{{ page_obj.number }}</a></li>
{% if page_obj.has_next %}
<li class="page-item"><a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a>
</li>
<li class="page-item"><a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Last</a>
</li>
{% else %}
<li class="page-item disabled"><a class="page-link" href="#">Next</a></li>
<li class="page-item disabled"><a class="page-link" href="#">Last</a></li>
{% endif %}
</ul>
</nav>
</div>
{% endblock %}
detail.html
hospital/templates/hospital/election_ledger/detail.html
{% extends "hospital/base.html" %}
{% load humanize %}
{% block content %}
<div class="container">
<h2 class="my-2">投票記録詳細</h2>
<dl class="row">
<dt class="col-sm-3">選挙名</dt>
<dd class="col-sm-9">{{ object.election }}</dd>
<dt class="col-sm-3">選挙人氏名</dt>
<dd class="col-sm-9">{{ object.voter }}</dd>
<dt class="col-sm-3">病棟</dt>
<dd class="col-sm-9">{{ object.vote_ward }}</dd>
<dt class="col-sm-3">投票区</dt>
<dd class="col-sm-9">{{ object.vote_city_sector }}</dd>
<dt class="col-sm-3">備考</dt>
<dd class="col-sm-9">{{ object.remark|default:"-" }}</dd>
<dt class="col-sm-3">取込日</dt>
<dd class="col-sm-9">{{ object.created_at|date:"Y-m-d" }}</dd>
<dt class="col-sm-3">投票用紙請求の方法</dt>
<dd class="col-sm-9">{{ object.get_billing_method_display }}</dd>
<dt class="col-sm-3">代理請求の依頼を受けた日</dt>
<dd class="col-sm-9">{{ object.proxy_billing_request_date|date:"Y-m-d"|default:"-" }}</dd>
<dt class="col-sm-3">代理請求日</dt>
<dd class="col-sm-9">{{ object.proxy_billing_date|date:"Y-m-d"|default:"-" }}</dd>
<dt class="col-sm-3">投票用紙受領日</dt>
<dd class="col-sm-9">{{ object.ballot_received_date|date:"Y-m-d"|default:"-" }}</dd>
<dt class="col-sm-3">投票日</dt>
<dd class="col-sm-9">{{ object.vote_date|date:"Y-m-d"|default:"-" }}</dd>
<dt class="col-sm-3">投票場所</dt>
<dd class="col-sm-9">{{ object.vote_place|default:"-" }}</dd>
<dt class="col-sm-3">投票立会人</dt>
<dd class="col-sm-9">{{ object.vote_observer|default:"-" }}</dd>
<dt class="col-sm-3">代理投票申請の有無</dt>
<dd class="col-sm-9">{{ object.applied_for_proxy_voting|yesno:"有,無" }}</dd>
<dt class="col-sm-3">投票用紙送付日</dt>
<dd class="col-sm-9">{{ object.delivery_date|date:"Y-m-d"|default:"-" }}</dd>
<dt class="col-sm-3">更新日</dt>
<dd class="col-sm-9">{{ object.updated_at|date:"Y-m-d H:i"|default:"-" }}</dd>
</dl>
<a href="{% url 'hsp:election_ledger_update' object.id %}" class="btn btn-outline-primary mx-1">更新</a>
<a href="{% url 'hsp:election_ledger_delete' object.id %}" class="btn btn-outline-danger mx-1">削除</a>
<a href="{% url 'hsp:index' %}" role="button" class="btn btn-outline-secondary mx-1">戻る</a>
</div>
{% endblock content %}
update.html
hospital/templates/hospital/election_ledger/update.html
{% extends 'hospital/base.html' %}
{% block content %}
<div class="container">
<h2>更新</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-outline-primary">更新</button>
<a href="{% url 'hsp:index' %}" role="button" class="btn btn-outline-secondary mx-1">戻る</a>
</form>
</div>
{% endblock %}
delete.html
hospital/templates/hospital/election_ledger/delete.html
{% extends 'hospital/base.html' %}
{% block content %}
<h2>投票記録の削除</h2>
<p>本当に "{{ object }}" の投票記録を削除しますか?</p>
<form method="post">
{% csrf_token %}
<input type="submit" value="削除" class="btn btn-danger"/>
<a href="{% url 'hsp:index' %}" class="btn btn-outline-secondary" role="button">キャンセル</a>
</form>
{% endblock %}
urls.py
hospital/urls.py
from django.urls import path
from hospital.views import (
IndexView,
ElectionLedgerCreateView,
ElectionLedgerUpdateView,
ElectionLedgerDeleteView,
ElectionLedgerDetailView,
)
app_name = "hsp"
urlpatterns = [
path("", IndexView.as_view(), name="index"),
path(
"election_ledger/create/",
ElectionLedgerCreateView.as_view(),
name="election_ledger_create",
),
path(
"election_ledger/update/<int:pk>/",
ElectionLedgerUpdateView.as_view(),
name="election_ledger_update",
),
path(
"election_ledger/delete/<int:pk>/",
ElectionLedgerDeleteView.as_view(),
name="election_ledger_delete",
),
path(
"election_ledger/<int:pk>/detail/",
ElectionLedgerDetailView.as_view(),
name="election_ledger_detail",
),
]
views.py
hospital/views.py
from django.urls import reverse_lazy
from django.views.generic import (
CreateView,
ListView,
UpdateView,
DeleteView,
DetailView,
)
from hospital.forms import ElectionLedgerCreateForm, ElectionLedgerUpdateForm
from hospital.models import ElectionLedger, Election
class IndexView(ListView):
model = ElectionLedger
template_name = "hospital/index.html"
paginate_by = 5
ordering = ["-created_at"]
def get_queryset(self):
election = self.request.GET.get("election")
if election:
return ElectionLedger.objects.filter(election=election).order_by(
"-created_at"
)
return ElectionLedger.objects.all().order_by("-created_at")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["elections"] = Election.objects.all()
return context
class ElectionLedgerCreateView(CreateView):
form_class = ElectionLedgerCreateForm
template_name = "hospital/election_ledger/create.html"
success_url = reverse_lazy("hsp:index")
class ElectionLedgerUpdateView(UpdateView):
model = ElectionLedger
form_class = ElectionLedgerUpdateForm
template_name = "hospital/election_ledger/update.html"
success_url = reverse_lazy("hsp:index")
class ElectionLedgerDeleteView(DeleteView):
model = ElectionLedger
template_name = "hospital/election_ledger/delete.html"
success_url = reverse_lazy("hsp:index")
class ElectionLedgerDetailView(DetailView):
model = ElectionLedger
template_name = "hospital/election_ledger/detail.html"
帳票のExport
請求者名簿
export_report.py
hospital/domain/service/export_report.py
import os
import uuid
from abc import ABC, abstractmethod
from itertools import islice, groupby
from pathlib import Path
from urllib.parse import quote
from django.http import HttpResponse
from openpyxl.reader.excel import load_workbook
from hospital.domain.valueobject.export_report import BillingListRow
from hospital.models import ElectionLedger
class AbstractExport(ABC):
def __init__(self, temp_folder: Path):
self.temp_folder = temp_folder
os.makedirs(self.temp_folder, exist_ok=True)
def create_unique_filename(self):
return str(self.temp_folder / f"{str(uuid.uuid4())}.xlsx")
def get_excel_data(self, filename: str) -> bytes:
with open(self.temp_folder / filename, "rb") as f:
return f.read()
@abstractmethod
def export(self, *args, **kwargs):
pass
class ExportBillingService(AbstractExport):
def export(self, election_id) -> HttpResponse:
ledgers = (
ElectionLedger.objects.filter(election_id=election_id)
.select_related("vote_ward")
.order_by("vote_ward__name")
)
filepath = os.path.abspath("hospital/domain/service/xlsx/billing_list.xlsx")
wb = load_workbook(filepath)
filename = self.create_unique_filename()
chunk_size = 15
start_row = 5
sheet_counter = 1
for ward_name, group in groupby(ledgers, key=lambda x: x.vote_ward.name):
ledgers_iter = iter(group)
while True:
chunk = list(islice(ledgers_iter, chunk_size))
if not chunk:
break
new_worksheet = wb.copy_worksheet(wb["ひな形"])
new_worksheet.title = ward_name if sheet_counter == 1 else f"{ward_name} ({sheet_counter})"
sheet_counter += 1
for i, ledger in enumerate(chunk, start=0):
row = BillingListRow(ledger)
new_worksheet.cell(row=start_row + i, column=1, value=row.address) # A列
new_worksheet.cell(row=start_row + i, column=2, value=row.voter_name) # B列
new_worksheet.cell(row=start_row + i, column=3, value=row.date_of_birth) # C列
del wb["ひな形"]
wb.save(filename)
try:
excel_data = self.get_excel_data(filename)
response = HttpResponse(
excel_data,
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
)
header_content = f"attachment; filename*=UTF-8''{quote("請求者名簿.xlsx")}"
response["Content-Disposition"] = header_content
finally:
os.remove(self.temp_folder / filename)
return response
billing_list.xlsx
hospital/domain/service/xlsx/billing_list.xlsx
(excelのテンプレートをアップロード)
export_report.py
hospital/domain/valueobject/export_report.py
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import date
from hospital.models import ElectionLedger
def convert_to_japanese_era(birth_date: date) -> str:
year, month, day = birth_date.year, birth_date.month, birth_date.day
if year > 2019 or (year == 2019 and month >= 5 and day >= 1):
era_year = year - 2018
era_name = "令和"
elif year > 1989 or (year == 1989 and month >= 1 and day >= 8):
era_year = year - 1988
era_name = "平成"
else:
era_year = year - 1925
era_name = "昭和"
return f"{era_name} {era_year}.{month:02d}.{day:02d}"
class AbstractRow(ABC):
@staticmethod
@abstractmethod
def get_field_names() -> list[str]:
pass
@abstractmethod
def to_list(self) -> list[str]:
pass
@dataclass
class BillingListRow(AbstractRow):
ledger: ElectionLedger
@property
def address(self) -> str:
return self.ledger.voter.userattribute.address
@property
def voter_name(self) -> str:
return self.ledger.voter.username
@property
def date_of_birth(self) -> str:
return convert_to_japanese_era(self.ledger.voter.userattribute.date_of_birth)
@property
def ward_name(self) -> str:
return self.ledger.vote_ward.name
@staticmethod
def get_field_names() -> list[str]:
return ["選挙人住所", "選挙人氏名", "生年月日", "病棟"]
def to_list(self) -> list:
return [self.address, self.voter_name, self.date_of_birth, self.ward_name]
base.html
※portfolioのものだが必要な箇所だけ加筆した
hospital/templates/hospital/base.html
:
+ {% block extra_css %}{% endblock %}
:
+{% block extra_js %}{% endblock %}
:
index.html
hospital/templates/hospital/index.html
:
<ul>
- <li><a class="btn btn-secondary btn-sm" href="#" role="button">XXXX</a></li>
- <li><a class="btn btn-secondary btn-sm" href="#" role="button">YYYY</a></li>
+ <li>
+ <a class="btn btn-secondary btn-sm"
+ href="{% url 'hsp:export_billing_list' %}?election={{ request.GET.election }}"
+ role="button"
+ style="{% if not canExport %}opacity:0.65; cursor:not-allowed;{% endif %}">
+ 請求者名簿
+ </a>
+ </li>
+ <li>
+ <a class="btn btn-secondary btn-sm" href="#" role="button"
+ style="{% if not canExport %}opacity:0.65; cursor:not-allowed;{% endif %}">
+ 不在者投票事務処理簿
+ </a>
+ </li>
</ul>
:
- <select name="election" class="form-control" onchange="this.form.submit()">
+ <select name="election" class="form-control" onchange="onElectionChangeHandler(this)">
<option value="">-- 選挙を選択 --</option>
:
+{% block extra_js %}
+ <script>
+ function onElectionChangeHandler(selectElement) {
+ if (selectElement.value === '') {
+ location.href = location.pathname;
+ } else {
+ selectElement.form.submit();
+ }
+ }
+ </script>
+{% endblock %}
urls.py
hospital/urls.py
from django.urls import path
from hospital.views import (
:
+ ExportBillingListView,
)
app_name = "hsp"
:
+ path(
+ "export/billing-list/",
+ ExportBillingListView.as_view(),
+ name="export_billing_list",
+ ),
]
views.py
hospital/views.py
+from django.http import HttpResponse
from django.urls import reverse_lazy
+from django.views import View
from django.views.generic import (
CreateView,
ListView,
UpdateView,
DeleteView,
DetailView,
)
+from config.settings import MEDIA_ROOT
+from hospital.domain.service.export_report import ExportBillingService
from hospital.forms import ElectionLedgerCreateForm, ElectionLedgerUpdateForm
:
def get_context_data(self, **kwargs):
+ election = self.request.GET.get("election")
context = super().get_context_data(**kwargs)
context["elections"] = Election.objects.all()
+ context["canExport"] = True if election else False
return context
:
+class ExportBillingListView(View):
+ @staticmethod
+ def get(request, *args, **kwargs):
+ election_id = request.GET.get("election", None)
+ if not election_id:
+ return HttpResponse("Election ID not provided", status=400)
+ service = ExportBillingService(temp_folder=MEDIA_ROOT / "hospital/temp")
+ return service.export(election_id)
requirements.txt
requirements.txt
:
+openpyxl~=3.1.5
不在者投票事務処理簿
まぁテンプレートが違うだけなんで同じように作ってください
TODOではあるが、改善案ぐらいのテンションにしておこう
- 住所チェック(病院のオーダリング端末の患者情報マスタ、ネット検索)
- 後から追加の場合は、同じ区でも日付ごとで分けないといけない
- 提出済みかどうかをステータス管理すればよさそう