diff --git a/.circleci/config.yml b/.circleci/config.yml
index 3e34e87..5d09ab8 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -50,7 +50,7 @@ jobs:
- run:
name: Run linting
command: |
- flake8 shift/ shift_table/ --max-line-length=120 --ignore=E501,W503 || echo "Linting completed"
+ flake8 shift/ shift_table/ || echo "Linting completed"
black --check shift/ shift_table/ || echo "Black check completed"
security:
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..5cf2093
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,9 @@
+[flake8]
+max-line-length = 120
+ignore = W503
+per-file-ignores =
+ shift_table/settings_test.py:F403,F405
+ shift/test_files/test_direct_password_change.py:E402
+ shift/test_files/test_models.py:E402
+ shift/test_files/test_e2e.py:E402
+ shift/test_files/test_views.py:E402
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..2c77a35
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,30 @@
+name: CI
+on:
+ push:
+ branches: [main, develop]
+ pull_request:
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+ - run: pip install -r requirements.txt -r requirements-dev.txt
+ - run: flake8 shift/ shift_table/
+ - run: black --check shift/ shift_table/
+
+ test:
+ runs-on: ubuntu-latest
+ needs: lint
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+ - run: pip install -r requirements.txt -r requirements-dev.txt
+ - run: pytest shift/test_files/ --cov=shift --cov=shift_table --cov-report=term-missing
+ env:
+ DJANGO_SETTINGS_MODULE: shift_table.settings_test
diff --git a/Makefile b/Makefile
index 418b6d9..7a3ae89 100644
--- a/Makefile
+++ b/Makefile
@@ -27,7 +27,7 @@ test-e2e: ## Run end-to-end tests
$(EXEC_CMD) python manage.py test shift.test_files.test_e2e --settings=shift_table.settings_test
lint: ## Run linting
- $(EXEC_CMD) flake8 shift/ shift_table/ --max-line-length=120 --ignore=E501,W503
+ $(EXEC_CMD) flake8 shift/ shift_table/
$(EXEC_CMD) black --check shift/ shift_table/
format: ## Format code
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 35165bf..2f24ace 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -5,6 +5,7 @@
pytest>=7.0.0
pytest-django>=4.5.0
pytest-cov>=4.0.0
+pytest-xdist>=3.0.0
factory-boy>=3.3.0
selenium>=4.15.0
webdriver-manager>=4.0.0
diff --git a/shift/admin.py b/shift/admin.py
index bddacd1..9dd4d0e 100644
--- a/shift/admin.py
+++ b/shift/admin.py
@@ -1,130 +1,151 @@
+import re
+from datetime import timedelta
+
from django.contrib import admin
from django.contrib import messages
-from .models import Employee, ShiftType, Shift, CompanyHoliday, LaborLawSettings, Role, ShiftTypeRoleMinWorker
-from .forms import ShiftForm, AutoShiftForm, CompanyHolidayBulkAddForm
-from django.urls import reverse
+from django.shortcuts import redirect, render
+from django.urls import path, reverse
from django.utils.html import format_html
-from django.http import HttpResponseRedirect
-from django.urls import path
-from django.shortcuts import render, redirect
-from django import forms
-from datetime import date, timedelta
+
+from .forms import AutoShiftForm, CompanyHolidayBulkAddForm, ShiftForm
+from .models import (
+ CompanyHoliday,
+ Employee,
+ LaborLawSettings,
+ Role,
+ Shift,
+ ShiftType,
+ ShiftTypeRoleMinWorker,
+)
+
jpholiday = None
try:
import jpholiday
except ImportError:
jpholiday = None
-from calendar import monthrange
-import re
-from django.http import HttpResponse
-from django.template.loader import render_to_string
@admin.register(CompanyHoliday)
class CompanyHolidayAdmin(admin.ModelAdmin):
- list_display = ['date', 'name', 'description']
- list_filter = ['date']
- search_fields = ['name', 'description']
- date_hierarchy = 'date'
- ordering = ['date']
-
+ list_display = ["date", "name", "description"]
+ list_filter = ["date"]
+ search_fields = ["name", "description"]
+ date_hierarchy = "date"
+ ordering = ["date"]
+
def changelist_view(self, request, extra_context=None):
extra_context = extra_context or {}
- extra_context['show_bulk_add_button'] = True
+ extra_context["show_bulk_add_button"] = True
return super().changelist_view(request, extra_context)
-
+
def get_urls(self):
urls = super().get_urls()
custom_urls = [
- path('bulk-add/', self.admin_site.admin_view(self.bulk_add_view), name='shift_companyholiday_bulk_add'),
+ path(
+ "bulk-add/",
+ self.admin_site.admin_view(self.bulk_add_view),
+ name="shift_companyholiday_bulk_add",
+ ),
]
return custom_urls + urls
-
+
def bulk_add_view(self, request):
"""一括追加ビュー"""
- global jpholiday
- if request.method == 'POST':
+ if request.method == "POST":
form = CompanyHolidayBulkAddForm(request.POST)
if form.is_valid():
# フォーム処理(既存のコード)
- holiday_type = form.cleaned_data['holiday_type']
- start_date = form.cleaned_data['start_date']
- end_date = form.cleaned_data['end_date']
- weekday = form.cleaned_data['weekday']
- holiday_name = form.cleaned_data['name']
-
+ holiday_type = form.cleaned_data["holiday_type"]
+ start_date = form.cleaned_data["start_date"]
+ end_date = form.cleaned_data["end_date"]
+ weekday = form.cleaned_data["weekday"]
+ holiday_name = form.cleaned_data["name"]
+
if holiday_type and start_date and end_date:
created_count = 0
-
- if holiday_type == 'custom_weekday':
+
+ if holiday_type == "custom_weekday":
if weekday is not None:
current = start_date
while current <= end_date:
if current.weekday() == weekday:
- holiday, created = CompanyHoliday.objects.get_or_create(
- date=current,
- defaults={'name': holiday_name or f'指定曜日休日', 'description': ''}
+ holiday, created = (
+ CompanyHoliday.objects.get_or_create(
+ date=current,
+ defaults={
+ "name": holiday_name or "指定曜日休日",
+ "description": "",
+ },
+ )
)
if created:
created_count += 1
current += timedelta(days=1)
-
- elif holiday_type == 'holidays':
+
+ elif holiday_type == "holidays":
current = start_date
while current <= end_date:
# 祝日かどうかをチェック
if jpholiday and jpholiday.is_holiday(current):
holiday, created = CompanyHoliday.objects.get_or_create(
date=current,
- defaults={'name': holiday_name or f'祝日', 'description': ''}
+ defaults={
+ "name": holiday_name or "祝日",
+ "description": "",
+ },
)
if created:
created_count += 1
current += timedelta(days=1)
-
- elif holiday_type == 'date_range':
+
+ elif holiday_type == "date_range":
current = start_date
while current <= end_date:
holiday, created = CompanyHoliday.objects.get_or_create(
date=current,
- defaults={'name': holiday_name or f'期間休日', 'description': ''}
+ defaults={
+ "name": holiday_name or "期間休日",
+ "description": "",
+ },
)
if created:
created_count += 1
current += timedelta(days=1)
-
+
if created_count > 0:
- messages.success(request, f'{created_count}件の会社休日を追加しました。')
+ messages.success(
+ request, f"{created_count}件の会社休日を追加しました。"
+ )
else:
- messages.warning(request, '追加された会社休日はありません。')
-
- return redirect('admin:shift_companyholiday_changelist')
+ messages.warning(request, "追加された会社休日はありません。")
+
+ return redirect("admin:shift_companyholiday_changelist")
else:
- messages.error(request, '必要な情報を入力してください。')
+ messages.error(request, "必要な情報を入力してください。")
else:
form = CompanyHolidayBulkAddForm()
-
+
context = {
- 'title': '会社休日一括追加',
- 'opts': self.model._meta,
- 'form': form,
+ "title": "会社休日一括追加",
+ "opts": self.model._meta,
+ "form": form,
}
- return render(request, 'admin/shift/companyholiday/bulk_add.html', context)
+ return render(request, "admin/shift/companyholiday/bulk_add.html", context)
class ShiftTypeRoleMinWorkerInline(admin.TabularInline):
model = ShiftTypeRoleMinWorker
extra = 0
- verbose_name = '役割別最低人数'
- verbose_name_plural = '役割別最低人数'
- fields = ('role', 'min_workers')
- verbose_name_display = {'role': '役割', 'min_workers': '最低人数'}
-
+ verbose_name = "役割別最低人数"
+ verbose_name_plural = "役割別最低人数"
+ fields = ("role", "min_workers")
+ verbose_name_display = {"role": "役割", "min_workers": "最低人数"}
+
def get_formset(self, request, obj=None, **kwargs):
formset = super().get_formset(request, obj, **kwargs)
form = formset.form
- form.base_fields['role'].label = '役割'
- form.base_fields['min_workers'].label = '最低人数'
+ form.base_fields["role"].label = "役割"
+ form.base_fields["min_workers"].label = "最低人数"
return formset
@@ -139,138 +160,163 @@ def color_display(self, obj):
if obj.color:
return format_html(
'{}',
- obj.color, obj.color
+ obj.color,
+ obj.color,
)
return "-"
+
color_display.short_description = "色"
@admin.register(Shift)
class ShiftAdmin(admin.ModelAdmin):
- list_display = ['employee', 'date', 'shift_type']
- list_filter = ['date', 'shift_type', 'employee']
- search_fields = ['employee__name', 'shift_type__name']
- date_hierarchy = 'date'
+ list_display = ["employee", "date", "shift_type"]
+ list_filter = ["date", "shift_type", "employee"]
+ search_fields = ["employee__name", "shift_type__name"]
+ date_hierarchy = "date"
form = ShiftForm
def changelist_view(self, request, extra_context=None):
"""一覧画面に自動シフト作成リンクを追加"""
extra_context = extra_context or {}
- extra_context['auto_create_url'] = 'admin:shift_auto_create'
- extra_context['title'] = 'シフト管理'
- extra_context['subtitle'] = None # サブタイトルを削除
-
+ extra_context["auto_create_url"] = "admin:shift_auto_create"
+ extra_context["title"] = "シフト管理"
+ extra_context["subtitle"] = None # サブタイトルを削除
+
# モデルのverbose_nameを一時的に変更
original_verbose_name = self.model._meta.verbose_name
original_verbose_name_plural = self.model._meta.verbose_name_plural
- self.model._meta.verbose_name = 'シフト管理'
- self.model._meta.verbose_name_plural = 'シフト管理'
-
+ self.model._meta.verbose_name = "シフト管理"
+ self.model._meta.verbose_name_plural = "シフト管理"
+
response = super().changelist_view(request, extra_context)
-
+
# 元に戻す
self.model._meta.verbose_name = original_verbose_name
self.model._meta.verbose_name_plural = original_verbose_name_plural
-
+
return response
def get_urls(self):
urls = super().get_urls()
custom_urls = [
- path('auto-create/', self.admin_site.admin_view(self.auto_create_view), name='shift_auto_create'),
+ path(
+ "auto-create/",
+ self.admin_site.admin_view(self.auto_create_view),
+ name="shift_auto_create",
+ ),
]
return custom_urls + urls
def auto_create_view(self, request):
"""自動シフト作成ビュー"""
- if request.method == 'POST':
+ if request.method == "POST":
form = AutoShiftForm(request.POST)
if form.is_valid():
- year = form.cleaned_data['year']
- month = form.cleaned_data['month']
- creation_mode = form.cleaned_data['creation_mode']
-
+ year = form.cleaned_data["year"]
+ month = form.cleaned_data["month"]
+ creation_mode = form.cleaned_data["creation_mode"]
+
# 自動シフト作成を実行
result = Shift.create_auto_shifts(year, month, creation_mode)
-
- if result['success']:
- messages.success(request, result['message'])
- return redirect('admin:shift_shift_changelist')
+
+ if result["success"]:
+ messages.success(request, result["message"])
+ return redirect("admin:shift_shift_changelist")
else:
- messages.error(request, result['error'])
+ messages.error(request, result["error"])
else:
form = AutoShiftForm()
-
+
context = {
- 'title': '自動シフト作成',
- 'form': form,
- 'opts': self.model._meta,
- 'site_title': '自動シフト作成',
- 'site_header': '自動シフト作成',
- 'subtitle': None, # サブタイトルを削除
+ "title": "自動シフト作成",
+ "form": form,
+ "opts": self.model._meta,
+ "site_title": "自動シフト作成",
+ "site_header": "自動シフト作成",
+ "subtitle": None, # サブタイトルを削除
}
-
- response = render(request, 'admin/shift/auto_create.html', context)
-
+
+ response = render(request, "admin/shift/auto_create.html", context)
+
# レスポンスの内容を直接修正
- if hasattr(response, 'content'):
- content = response.content.decode('utf-8')
+ if hasattr(response, "content"):
+ content = response.content.decode("utf-8")
# h1タグの内容を修正
- content = re.sub(r'
]*>.*?自動シフト作成.*?自動シフト作成.*?
', '自動シフト作成
', content, flags=re.DOTALL)
- content = re.sub(r']*>.*?自動シフト作成.*?
', '自動シフト作成
', content, flags=re.DOTALL)
+ content = re.sub(
+ r"]*>.*?自動シフト作成.*?自動シフト作成.*?
",
+ "自動シフト作成
",
+ content,
+ flags=re.DOTALL,
+ )
+ content = re.sub(
+ r"]*>.*?自動シフト作成.*?
",
+ "自動シフト作成
",
+ content,
+ flags=re.DOTALL,
+ )
# タイトルタグも修正
- content = re.sub(r'.*?自動シフト作成.*?自動シフト作成.*?', '自動シフト作成', content, flags=re.DOTALL)
- response.content = content.encode('utf-8')
-
+ content = re.sub(
+ r".*?自動シフト作成.*?自動シフト作成.*?",
+ "自動シフト作成",
+ content,
+ flags=re.DOTALL,
+ )
+ response.content = content.encode("utf-8")
+
return response
@admin.register(LaborLawSettings)
class LaborLawSettingsAdmin(admin.ModelAdmin):
- list_display = ['max_consecutive_work_days', 'min_workers', 'updated_at']
- readonly_fields = ['created_at', 'updated_at']
-
+ list_display = ["max_consecutive_work_days", "min_workers", "updated_at"]
+ readonly_fields = ["created_at", "updated_at"]
+
def has_add_permission(self, request):
# 設定は1つだけ作成可能
return not LaborLawSettings.objects.exists()
-
+
def has_delete_permission(self, request, obj=None):
# 削除を許可
return True
-
+
def response_add(self, request, obj, post_url_continue=None):
# 追加後は変更画面にリダイレクト(「保存してもう1つ追加」ボタンを非表示にするため)
return self.response_post_save_add(request, obj)
-
+
def get_readonly_fields(self, request, obj=None):
# 作成日時と更新日時のみ読み取り専用
- return ['created_at', 'updated_at']
-
- def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
+ return ["created_at", "updated_at"]
+
+ def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
# 追加画面で「保存してもう一つ追加」ボタンを非表示にする
extra_context = extra_context or {}
if object_id is None: # 追加画面の場合
- extra_context['show_save_and_add_another'] = False
+ extra_context["show_save_and_add_another"] = False
return super().changeform_view(request, object_id, form_url, extra_context)
@admin.register(Role)
class RoleAdmin(admin.ModelAdmin):
- list_display = ['name', 'description']
- search_fields = ['name', 'description']
+ list_display = ["name", "description"]
+ search_fields = ["name", "description"]
@admin.register(Employee)
class EmployeeAdmin(admin.ModelAdmin):
- list_display = ['name', 'get_roles']
- search_fields = ['name']
- filter_horizontal = ['roles']
-
+ list_display = ["name", "get_roles"]
+ search_fields = ["name"]
+ filter_horizontal = ["roles"]
+
def get_roles(self, obj):
return ", ".join([role.name for role in obj.roles.all()])
+
get_roles.short_description = "役割"
+
# 管理画面の上部に「シフト表」リンクを追加
-admin.site.site_header = 'シフト管理'
-admin.site.site_title = 'シフト管理'
-admin.site.index_title = format_html('シフト表を見る', reverse('shift_table'))
+admin.site.site_header = "シフト管理"
+admin.site.site_title = "シフト管理"
+admin.site.index_title = format_html(
+ 'シフト表を見る', reverse("shift_table")
+)
diff --git a/shift/factories.py b/shift/factories.py
index 2e694b7..87a13be 100644
--- a/shift/factories.py
+++ b/shift/factories.py
@@ -4,7 +4,15 @@
import factory
from django.contrib.auth.models import User
-from .models import Employee, ShiftType, CompanyHoliday, LaborLawSettings, Shift, Role, ShiftTypeRoleMinWorker
+from .models import (
+ Employee,
+ ShiftType,
+ CompanyHoliday,
+ LaborLawSettings,
+ Shift,
+ Role,
+ ShiftTypeRoleMinWorker,
+)
from datetime import date, timedelta
import random
@@ -13,9 +21,9 @@ class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = User
- username = factory.Sequence(lambda n: f'user{n}')
- email = factory.LazyAttribute(lambda obj: f'{obj.username}@example.com')
- password = factory.PostGenerationMethodCall('set_password', 'password123')
+ username = factory.Sequence(lambda n: f"user{n}")
+ email = factory.LazyAttribute(lambda obj: f"{obj.username}@example.com")
+ password = factory.PostGenerationMethodCall("set_password", "password123")
is_staff = True
is_superuser = True
@@ -24,15 +32,15 @@ class RoleFactory(factory.django.DjangoModelFactory):
class Meta:
model = Role
- name = factory.Sequence(lambda n: f'役割{n}')
+ name = factory.Sequence(lambda n: f"役割{n}")
class EmployeeFactory(factory.django.DjangoModelFactory):
class Meta:
model = Employee
- name = factory.Sequence(lambda n: f'従業員{n}')
-
+ name = factory.Sequence(lambda n: f"従業員{n}")
+
@factory.post_generation
def roles(self, create, extracted, **kwargs):
if not create:
@@ -45,6 +53,7 @@ def roles(self, create, extracted, **kwargs):
class ShiftTypeFactory(factory.django.DjangoModelFactory):
class Meta:
model = ShiftType
+
name = factory.Sequence(lambda n: f"シフト{n}")
is_work = True
color = "#ffffff"
@@ -65,7 +74,7 @@ class RestShiftTypeFactory(ShiftTypeFactory):
class ShiftTypeRoleMinWorkerFactory(factory.django.DjangoModelFactory):
class Meta:
model = ShiftTypeRoleMinWorker
-
+
shift_type = factory.SubFactory(ShiftTypeFactory)
role = factory.SubFactory(RoleFactory)
min_workers = 1
@@ -75,9 +84,11 @@ class CompanyHolidayFactory(factory.django.DjangoModelFactory):
class Meta:
model = CompanyHoliday
- date = factory.LazyFunction(lambda: date.today() + timedelta(days=random.randint(1, 30)))
- name = factory.Sequence(lambda n: f'会社休日{n}')
- description = factory.Faker('text', max_nb_chars=200)
+ date = factory.LazyFunction(
+ lambda: date.today() + timedelta(days=random.randint(1, 30))
+ )
+ name = factory.Sequence(lambda n: f"会社休日{n}")
+ description = factory.Faker("text", max_nb_chars=200)
class LaborLawSettingsFactory(factory.django.DjangoModelFactory):
@@ -93,9 +104,11 @@ class Meta:
model = Shift
employee = factory.SubFactory(EmployeeFactory)
- date = factory.LazyFunction(lambda: date.today() + timedelta(days=random.randint(0, 30)))
+ date = factory.LazyFunction(
+ lambda: date.today() + timedelta(days=random.randint(0, 30))
+ )
shift_type = factory.SubFactory(WorkShiftTypeFactory)
class RestShiftFactory(ShiftFactory):
- shift_type = factory.SubFactory(RestShiftTypeFactory)
\ No newline at end of file
+ shift_type = factory.SubFactory(RestShiftTypeFactory)
diff --git a/shift/forms.py b/shift/forms.py
index a8bb8f9..d75bdb0 100644
--- a/shift/forms.py
+++ b/shift/forms.py
@@ -1,5 +1,5 @@
from django import forms
-from .models import Shift, ShiftType, Employee, CompanyHoliday
+from .models import Shift
from datetime import date
COLOR_CHOICES = [
@@ -15,24 +15,26 @@
("#343a40", "黒"),
]
+
class ColorSelectWidget(forms.Widget):
- template_name = 'admin/widgets/color_select.html'
+ template_name = "admin/widgets/color_select.html"
def __init__(self, attrs=None):
super().__init__(attrs)
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
- context['widget']['value'] = value
- context['widget']['name'] = name
- context['widget']['color_choices'] = COLOR_CHOICES
- context['widget']['preset_codes'] = [c[0] for c in COLOR_CHOICES]
+ context["widget"]["value"] = value
+ context["widget"]["name"] = name
+ context["widget"]["color_choices"] = COLOR_CHOICES
+ context["widget"]["preset_codes"] = [c[0] for c in COLOR_CHOICES]
return context
class Media:
- css = {'all': ('admin/css/forms.css',)}
+ css = {"all": ("admin/css/forms.css",)}
js = ()
+
class ShiftForm(forms.ModelForm):
class Meta:
model = Shift
@@ -66,7 +68,7 @@ def clean(self):
if warning:
# 警告をフォームに追加(登録は可能)
self.add_warning("shift_type", warning["message"])
-
+
# シフト種別ごとの人数制限をチェック
worker_limit_warning = temp_shift.check_shift_type_worker_limits()
if worker_limit_warning:
@@ -87,54 +89,56 @@ def get_warnings(self):
"""警告メッセージを取得"""
return getattr(self, "_warnings", {})
+
class AutoShiftForm(forms.Form):
CREATION_MODE_CHOICES = [
- ('overwrite', '既存のシフトを上書きして1から作成'),
- ('fill_gaps', '既存のシフトを保持して空欄の日付に登録'),
+ ("overwrite", "既存のシフトを上書きして1から作成"),
+ ("fill_gaps", "既存のシフトを保持して空欄の日付に登録"),
]
-
+
year = forms.IntegerField(
- label='年',
+ label="年",
min_value=1900,
max_value=2099,
- widget=forms.NumberInput(attrs={'class': 'form-control'})
+ widget=forms.NumberInput(attrs={"class": "form-control"}),
)
-
+
month = forms.IntegerField(
- label='月',
+ label="月",
min_value=1,
max_value=12,
- widget=forms.NumberInput(attrs={'class': 'form-control'})
+ widget=forms.NumberInput(attrs={"class": "form-control"}),
)
-
+
creation_mode = forms.ChoiceField(
- label='作成モード',
+ label="作成モード",
choices=CREATION_MODE_CHOICES,
- widget=forms.RadioSelect(attrs={'class': 'form-control'}),
- initial='fill_gaps'
+ widget=forms.RadioSelect(attrs={"class": "form-control"}),
+ initial="fill_gaps",
)
-
+
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# デフォルト値を現在の年月に設定
today = date.today()
if not self.initial:
self.initial = {
- 'year': today.year,
- 'month': today.month,
+ "year": today.year,
+ "month": today.month,
}
+
# class ShiftTypeForm(forms.ModelForm):
# color = forms.CharField(
# label='色',
# widget=ColorSelectWidget,
# required=True
# )
-#
+#
# class Meta:
# model = ShiftType
# fields = ['name', 'is_work', 'color', 'min_workers', 'max_workers'] # role_min_workersを除外
-#
+#
# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)
# # 役割一覧を取得
@@ -152,7 +156,7 @@ def __init__(self, *args, **kwargs):
# initial=0,
# help_text=f'{role.name}役割の最低必要人数を入力してください'
# )
-#
+#
# # 既存データがある場合は初期値を設定
# if self.instance and self.instance.pk:
# for role in roles:
@@ -163,11 +167,11 @@ def __init__(self, *args, **kwargs):
# except:
# # マイグレーション前などでRoleモデルが存在しない場合
# pass
-#
+#
# def clean(self):
# """個別フィールドをJSONに変換"""
# cleaned_data = super().clean()
-#
+#
# # 役割別最低人数をJSONに変換
# role_min_workers = {}
# try:
@@ -181,54 +185,61 @@ def __init__(self, *args, **kwargs):
# role_min_workers[role.name] = value
# except:
# pass
-#
+#
# cleaned_data['role_min_workers'] = role_min_workers
# return cleaned_data
-#
+#
# def save(self, commit=True):
# """フォームデータを保存"""
# instance = super().save(commit=False)
-#
+#
# # 役割別最低人数を設定
# if 'role_min_workers' in self.cleaned_data:
# instance.role_min_workers = self.cleaned_data['role_min_workers']
-#
+#
# if commit:
# instance.save()
# return instance
+
class CompanyHolidayBulkAddForm(forms.Form):
HOLIDAY_TYPE_CHOICES = [
- ('holidays', '祝日'),
- ('custom_weekday', '指定曜日'),
- ('date_range', '期間'),
+ ("holidays", "祝日"),
+ ("custom_weekday", "指定曜日"),
+ ("date_range", "期間"),
]
holiday_type = forms.ChoiceField(
- label='休日タイプ',
+ label="休日タイプ",
choices=HOLIDAY_TYPE_CHOICES,
- widget=forms.Select(attrs={'class': 'form-control'})
+ widget=forms.Select(attrs={"class": "form-control"}),
)
start_date = forms.DateField(
- label='開始日',
- widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
- required=False
+ label="開始日",
+ widget=forms.DateInput(attrs={"type": "date", "class": "form-control"}),
+ required=False,
)
end_date = forms.DateField(
- label='終了日',
- widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
- required=False
+ label="終了日",
+ widget=forms.DateInput(attrs={"type": "date", "class": "form-control"}),
+ required=False,
)
weekday = forms.ChoiceField(
- label='曜日',
+ label="曜日",
choices=[
- (0, '月曜日'), (1, '火曜日'), (2, '水曜日'), (3, '木曜日'), (4, '金曜日'), (5, '土曜日'), (6, '日曜日'),
+ (0, "月曜日"),
+ (1, "火曜日"),
+ (2, "水曜日"),
+ (3, "木曜日"),
+ (4, "金曜日"),
+ (5, "土曜日"),
+ (6, "日曜日"),
],
required=False,
- widget=forms.Select(attrs={'class': 'form-control'})
+ widget=forms.Select(attrs={"class": "form-control"}),
)
name = forms.CharField(
- label='休日名',
+ label="休日名",
max_length=100,
- widget=forms.TextInput(attrs={'class': 'form-control'}),
- required=False
+ widget=forms.TextInput(attrs={"class": "form-control"}),
+ required=False,
)
diff --git a/shift/migrations/0008_add_color_to_shifttype.py b/shift/migrations/0008_add_color_to_shifttype.py
index 2ed2ce0..83fa915 100644
--- a/shift/migrations/0008_add_color_to_shifttype.py
+++ b/shift/migrations/0008_add_color_to_shifttype.py
@@ -6,18 +6,18 @@
class Migration(migrations.Migration):
dependencies = [
- ('shift', '0007_alter_shift_options'),
+ ("shift", "0007_alter_shift_options"),
]
operations = [
migrations.AddField(
- model_name='shifttype',
- name='color',
+ model_name="shifttype",
+ name="color",
field=models.CharField(
- default='#79aec8',
- help_text='シフト表での表示色(例: #79aec8)',
+ default="#79aec8",
+ help_text="シフト表での表示色(例: #79aec8)",
max_length=7,
- verbose_name='色'
+ verbose_name="色",
),
),
- ]
\ No newline at end of file
+ ]
diff --git a/shift/migrations/0009_set_default_colors.py b/shift/migrations/0009_set_default_colors.py
index 66f39f3..3463d14 100644
--- a/shift/migrations/0009_set_default_colors.py
+++ b/shift/migrations/0009_set_default_colors.py
@@ -4,26 +4,26 @@
def set_default_colors(apps, schema_editor):
- ShiftType = apps.get_model('shift', 'ShiftType')
-
+ ShiftType = apps.get_model("shift", "ShiftType")
+
# 既存のシフト種別にデフォルトの色を設定
default_colors = {
- '勤務': '#79aec8', # 青
- '休み': '#dc3545', # 赤
- '早番': '#28a745', # 緑
- '遅番': '#ffc107', # 黄
- '夜勤': '#6f42c1', # 紫
+ "勤務": "#79aec8", # 青
+ "休み": "#dc3545", # 赤
+ "早番": "#28a745", # 緑
+ "遅番": "#ffc107", # 黄
+ "夜勤": "#6f42c1", # 紫
}
-
+
for shift_type in ShiftType.objects.all():
if shift_type.name in default_colors:
shift_type.color = default_colors[shift_type.name]
else:
# 勤務日かどうかで色を分ける
if shift_type.is_work:
- shift_type.color = '#79aec8' # 青(勤務)
+ shift_type.color = "#79aec8" # 青(勤務)
else:
- shift_type.color = '#dc3545' # 赤(休み)
+ shift_type.color = "#dc3545" # 赤(休み)
shift_type.save()
@@ -35,9 +35,9 @@ def reverse_default_colors(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
- ('shift', '0008_add_color_to_shifttype'),
+ ("shift", "0008_add_color_to_shifttype"),
]
operations = [
migrations.RunPython(set_default_colors, reverse_default_colors),
- ]
\ No newline at end of file
+ ]
diff --git a/shift/models.py b/shift/models.py
index 38b3873..129b428 100644
--- a/shift/models.py
+++ b/shift/models.py
@@ -1,7 +1,6 @@
from django.db import models
-from datetime import datetime, timedelta, date
from calendar import monthrange
-import random
+from datetime import date, timedelta
from django.core.exceptions import ValidationError
# Create your models here.
@@ -9,12 +8,13 @@
class Role(models.Model):
"""従業員の役割(ホール、キッチンなど)"""
+
name = models.CharField("役割名", max_length=50, unique=True)
description = models.TextField("説明", blank=True)
-
+
def __str__(self):
return self.name
-
+
class Meta:
verbose_name = "役割"
verbose_name_plural = "役割"
@@ -38,15 +38,19 @@ class ShiftType(models.Model):
"勤務日", default=True, help_text="休み以外の勤務日かどうか"
)
color = models.CharField(
- "色", max_length=7, default="#79aec8",
- help_text="シフト表での表示色(例: #79aec8)"
+ "色",
+ max_length=7,
+ default="#79aec8",
+ help_text="シフト表での表示色(例: #79aec8)",
)
min_workers = models.PositiveIntegerField(
"最低人数", default=1, help_text="このシフト種別に必要な最低人数"
)
max_workers = models.PositiveIntegerField(
- "最大人数", null=True, blank=True,
- help_text="このシフト種別の最大人数(設定しない場合は制限なし)"
+ "最大人数",
+ null=True,
+ blank=True,
+ help_text="このシフト種別の最大人数(設定しない場合は制限なし)",
)
def __str__(self):
@@ -98,23 +102,24 @@ class Meta:
def get_current_settings(cls):
"""現在の設定を取得(設定がない場合はデフォルト値で作成)"""
settings, created = cls.objects.get_or_create(
- defaults={
- "max_consecutive_work_days": 6,
- "min_workers": 1
- }
+ defaults={"max_consecutive_work_days": 6, "min_workers": 1}
)
return settings
class Shift(models.Model):
- employee = models.ForeignKey(Employee, verbose_name='従業員', on_delete=models.CASCADE)
- date = models.DateField('日付')
- shift_type = models.ForeignKey(ShiftType, verbose_name='シフト種別', on_delete=models.PROTECT)
+ employee = models.ForeignKey(
+ Employee, verbose_name="従業員", on_delete=models.CASCADE
+ )
+ date = models.DateField("日付")
+ shift_type = models.ForeignKey(
+ ShiftType, verbose_name="シフト種別", on_delete=models.PROTECT
+ )
class Meta:
- unique_together = ('employee', 'date')
- verbose_name = 'シフト'
- verbose_name_plural = 'シフト'
+ unique_together = ("employee", "date")
+ verbose_name = "シフト"
+ verbose_name_plural = "シフト"
def __str__(self):
return f"{self.date} {self.employee.name} {self.shift_type.name}"
@@ -123,51 +128,53 @@ def check_consecutive_work_days(self):
"""連続勤務日数制限をチェック"""
if not self.shift_type.is_work:
return None # 休みの場合はチェック不要
-
+
settings = LaborLawSettings.get_current_settings()
max_days = settings.max_consecutive_work_days
-
+
# 指定日から前後「最大連続勤務日数」分のシフトを取得
start_date = self.date - timedelta(days=max_days)
end_date = self.date + timedelta(days=max_days)
-
+
# 勤務日のみのシフトを取得(休みは除外)
shifts = Shift.objects.filter(
employee=self.employee,
date__range=[start_date, end_date],
- shift_type__is_work=True
- ).exclude(id=self.id) # 現在のシフトを除外(更新時)
-
+ shift_type__is_work=True,
+ ).exclude(
+ id=self.id
+ ) # 現在のシフトを除外(更新時)
+
# 勤務日の日付リストを作成
- work_dates = set(shifts.values_list('date', flat=True))
+ work_dates = set(shifts.values_list("date", flat=True))
if self.shift_type.is_work: # 現在のシフトが勤務日の場合のみ追加
work_dates.add(self.date)
-
+
# 連続勤務日数を計算(勤務日のみ)
max_consecutive = 0
current_consecutive = 0
sorted_dates = sorted(work_dates)
-
+
for i, work_date in enumerate(sorted_dates):
if i == 0:
current_consecutive = 1
else:
- prev_date = sorted_dates[i-1]
+ prev_date = sorted_dates[i - 1]
if (work_date - prev_date).days == 1:
current_consecutive += 1
else:
current_consecutive = 1
-
+
max_consecutive = max(max_consecutive, current_consecutive)
-
+
if max_consecutive > max_days:
return {
- 'warning': True,
- 'message': f'連続勤務日数が{max_consecutive}日となり、設定された上限({max_days}日)を超えています。',
- 'consecutive_days': max_consecutive,
- 'max_days': max_days
+ "warning": True,
+ "message": f"連続勤務日数が{max_consecutive}日となり、設定された上限({max_days}日)を超えています。",
+ "consecutive_days": max_consecutive,
+ "max_days": max_days,
}
-
+
return None
def check_min_workers(self):
@@ -176,116 +183,127 @@ def check_min_workers(self):
company_holiday = CompanyHoliday.objects.filter(date=self.date).first()
if company_holiday:
return None
-
+
settings = LaborLawSettings.get_current_settings()
min_workers = settings.min_workers
-
+
# 指定日の勤務者数を取得(現在のシフトを除外)
work_shifts = Shift.objects.filter(
- date=self.date,
- shift_type__is_work=True
+ date=self.date, shift_type__is_work=True
).exclude(id=self.id)
-
+
work_count = work_shifts.count()
-
+
# 新しいシフト種別が勤務日の場合、カウントに追加
if self.shift_type.is_work:
work_count += 1
-
+
if work_count < min_workers:
return {
- 'warning': True,
- 'message': f'{self.date}の勤務者数が{work_count}人となり、設定された最低労働者数({min_workers}人)を下回っています。',
- 'current_workers': work_count,
- 'min_workers': min_workers
+ "warning": True,
+ "message": f"{self.date}の勤務者数が{work_count}人となり、設定された最低労働者数({min_workers}人)を下回っています。",
+ "current_workers": work_count,
+ "min_workers": min_workers,
}
-
+
return None
def check_shift_type_worker_limits(self):
"""シフト種別ごとの人数制限をチェック"""
if not self.shift_type.is_work:
return None # 休みの場合はチェック不要
-
+
# 指定日の同じシフト種別の人数を取得(現在のシフトを除外)
- same_shift_count = Shift.objects.filter(
- date=self.date,
- shift_type=self.shift_type
- ).exclude(id=self.id).count()
-
+ same_shift_count = (
+ Shift.objects.filter(date=self.date, shift_type=self.shift_type)
+ .exclude(id=self.id)
+ .count()
+ )
+
# 新しいシフトを追加した場合の人数
total_count = same_shift_count + 1
-
+
# 最低人数チェック
if total_count < self.shift_type.min_workers:
return {
- 'warning': True,
- 'message': f'{self.date}の{self.shift_type.name}の人数が{total_count}人となり、設定された最低人数({self.shift_type.min_workers}人)を下回っています。',
- 'current_workers': total_count,
- 'min_workers': self.shift_type.min_workers
+ "warning": True,
+ "message": (
+ f"{self.date}の{self.shift_type.name}の人数が{total_count}人となり、"
+ f"設定された最低人数({self.shift_type.min_workers}人)を下回っています。"
+ ),
+ "current_workers": total_count,
+ "min_workers": self.shift_type.min_workers,
}
-
+
# 最大人数チェック
if self.shift_type.max_workers and total_count > self.shift_type.max_workers:
return {
- 'warning': True,
- 'message': f'{self.date}の{self.shift_type.name}の人数が{total_count}人となり、設定された最大人数({self.shift_type.max_workers}人)を超えています。',
- 'current_workers': total_count,
- 'max_workers': self.shift_type.max_workers
+ "warning": True,
+ "message": (
+ f"{self.date}の{self.shift_type.name}の人数が{total_count}人となり、"
+ f"設定された最大人数({self.shift_type.max_workers}人)を超えています。"
+ ),
+ "current_workers": total_count,
+ "max_workers": self.shift_type.max_workers,
}
-
+
return None
@classmethod
- def create_auto_shifts(cls, year, month, creation_mode='fill_gaps'):
+ def create_auto_shifts(cls, year, month, creation_mode="fill_gaps"):
"""自動シフト作成(シフト種別ごとのmin/max人数+全体min_workers+役割別最低人数を考慮)"""
settings = LaborLawSettings.get_current_settings()
employees = list(Employee.objects.all())
work_shift_types = list(ShiftType.objects.filter(is_work=True))
rest_shift_type = ShiftType.objects.filter(is_work=False).first()
roles = list(Role.objects.all())
-
+
if not employees:
- return {'success': False, 'error': '従業員が登録されていません。'}
+ return {"success": False, "error": "従業員が登録されていません。"}
if not work_shift_types:
- return {'success': False, 'error': '勤務シフト種別が登録されていません。'}
+ return {"success": False, "error": "勤務シフト種別が登録されていません。"}
if not rest_shift_type:
- return {'success': False, 'error': '休みシフト種別が登録されていません。'}
-
+ return {"success": False, "error": "休みシフト種別が登録されていません。"}
+
num_days = monthrange(year, month)[1]
target_dates = [date(year, month, d) for d in range(1, num_days + 1)]
- company_holidays = set(CompanyHoliday.objects.filter(
- date__year=year, date__month=month
- ).values_list('date', flat=True))
-
+ company_holidays = set(
+ CompanyHoliday.objects.filter(
+ date__year=year, date__month=month
+ ).values_list("date", flat=True)
+ )
+
existing_shifts = {}
- if creation_mode == 'fill_gaps':
+ if creation_mode == "fill_gaps":
existing_shifts = {
- f"{s.employee_id}_{s.date.isoformat()}": s
+ f"{s.employee_id}_{s.date.isoformat()}": s
for s in cls.objects.filter(date__year=year, date__month=month)
}
- elif creation_mode == 'overwrite':
+ elif creation_mode == "overwrite":
cls.objects.filter(date__year=year, date__month=month).delete()
-
+
work_days_count = {emp.id: 0 for emp in employees}
for shift in cls.objects.filter(date__year=year, date__month=month):
if shift.shift_type.is_work:
work_days_count[shift.employee.id] += 1
-
+
created_count = 0
updated_count = 0
-
+
for target_date in target_dates:
shift_key = f"{target_date.isoformat()}"
if target_date in company_holidays:
for employee in employees:
employee_shift_key = f"{employee.id}_{shift_key}"
- if creation_mode == 'fill_gaps' and employee_shift_key in existing_shifts:
+ if (
+ creation_mode == "fill_gaps"
+ and employee_shift_key in existing_shifts
+ ):
continue
shift, created = cls.objects.update_or_create(
employee=employee,
date=target_date,
- defaults={'shift_type': rest_shift_type}
+ defaults={"shift_type": rest_shift_type},
)
if created:
created_count += 1
@@ -295,27 +313,38 @@ def create_auto_shifts(cls, year, month, creation_mode='fill_gaps'):
available_employees = []
for employee in employees:
employee_shift_key = f"{employee.id}_{shift_key}"
- if creation_mode == 'fill_gaps' and employee_shift_key in existing_shifts:
+ if (
+ creation_mode == "fill_gaps"
+ and employee_shift_key in existing_shifts
+ ):
continue
available_employees.append(employee)
if not available_employees:
continue
assigned_employees = set()
assigned_work_employees = set()
-
+
# 1. 各シフト種別のmin_workersをまず割り当て
for shift_type in work_shift_types:
- already_assigned = cls.objects.filter(date=target_date, shift_type=shift_type).count()
+ already_assigned = cls.objects.filter(
+ date=target_date, shift_type=shift_type
+ ).count()
min_needed = max(0, shift_type.min_workers - already_assigned)
- max_allowed = (shift_type.max_workers or len(employees)) - already_assigned
- candidates = [e for e in available_employees if e not in assigned_employees]
- candidates_sorted = sorted(candidates, key=lambda e: work_days_count[e.id])
+ max_allowed = (
+ shift_type.max_workers or len(employees)
+ ) - already_assigned
+ candidates = [
+ e for e in available_employees if e not in assigned_employees
+ ]
+ candidates_sorted = sorted(
+ candidates, key=lambda e: work_days_count[e.id]
+ )
selected = candidates_sorted[:max_allowed]
for employee in selected[:min_needed]:
shift, created = cls.objects.update_or_create(
employee=employee,
date=target_date,
- defaults={'shift_type': shift_type}
+ defaults={"shift_type": shift_type},
)
assigned_employees.add(employee)
assigned_work_employees.add(employee)
@@ -325,50 +354,64 @@ def create_auto_shifts(cls, year, month, creation_mode='fill_gaps'):
created_count += 1
else:
updated_count += 1
-
+
# 2. 役割別最低人数を満たすように追加割り当て
for shift_type in work_shift_types:
if not shift_type.role_min_workers.exists():
continue
-
+
# そのシフト種別に既に割り当てられている従業員
- assigned_to_shift = cls.objects.filter(date=target_date, shift_type=shift_type)
- assigned_employee_ids = set(assigned_to_shift.values_list('employee_id', flat=True))
-
+ assigned_to_shift = cls.objects.filter(
+ date=target_date, shift_type=shift_type
+ )
+ assigned_employee_ids = set(
+ assigned_to_shift.values_list("employee_id", flat=True)
+ )
+
# 各役割の現在の人数をカウント
role_counts = {}
for role in roles:
role_counts[role.name] = 0
-
+
for emp_id in assigned_employee_ids:
employee = Employee.objects.get(id=emp_id)
for role in employee.roles.all():
role_counts[role.name] = role_counts.get(role.name, 0) + 1
-
+
# 役割別最低人数を満たすために必要な追加人数
- candidates = [e for e in available_employees if e not in assigned_employees]
- candidates_sorted = sorted(candidates, key=lambda e: work_days_count[e.id])
-
+ candidates = [
+ e for e in available_employees if e not in assigned_employees
+ ]
+ candidates_sorted = sorted(
+ candidates, key=lambda e: work_days_count[e.id]
+ )
+
for role_min_worker in shift_type.role_min_workers.all():
role_name = role_min_worker.role.name
min_count = role_min_worker.min_workers
current_count = role_counts.get(role_name, 0)
needed = max(0, min_count - current_count)
-
+
if needed > 0:
# その役割を持つ従業員を優先的に選択
- role_employees = [e for e in candidates_sorted if e.roles.filter(name=role_name).exists()]
- other_employees = [e for e in candidates_sorted if e not in role_employees]
-
+ role_employees = [
+ e
+ for e in candidates_sorted
+ if e.roles.filter(name=role_name).exists()
+ ]
+ other_employees = [
+ e for e in candidates_sorted if e not in role_employees
+ ]
+
# 役割を持つ従業員を優先
priority_candidates = role_employees + other_employees
-
+
for employee in priority_candidates[:needed]:
if employee not in assigned_employees:
shift, created = cls.objects.update_or_create(
employee=employee,
date=target_date,
- defaults={'shift_type': shift_type}
+ defaults={"shift_type": shift_type},
)
assigned_employees.add(employee)
assigned_work_employees.add(employee)
@@ -378,17 +421,28 @@ def create_auto_shifts(cls, year, month, creation_mode='fill_gaps'):
created_count += 1
else:
updated_count += 1
-
+
# 3. 全体のmin_workersを満たすまでmax_workersの範囲内で追加割り当て
current_workers = len(assigned_work_employees)
needed_workers = max(0, settings.min_workers - current_workers)
if needed_workers > 0:
- candidates = [e for e in available_employees if e not in assigned_employees]
- candidates_sorted = sorted(candidates, key=lambda e: work_days_count[e.id])
+ candidates = [
+ e for e in available_employees if e not in assigned_employees
+ ]
+ candidates_sorted = sorted(
+ candidates, key=lambda e: work_days_count[e.id]
+ )
for shift_type in work_shift_types:
- already_assigned = cls.objects.filter(date=target_date, shift_type=shift_type).count() + \
- sum(1 for e in assigned_work_employees if e in assigned_work_employees)
- max_allowed = (shift_type.max_workers or len(employees)) - already_assigned
+ already_assigned = cls.objects.filter(
+ date=target_date, shift_type=shift_type
+ ).count() + sum(
+ 1
+ for e in assigned_work_employees
+ if e in assigned_work_employees
+ )
+ max_allowed = (
+ shift_type.max_workers or len(employees)
+ ) - already_assigned
can_assign = min(needed_workers, max_allowed)
if can_assign <= 0:
continue
@@ -396,7 +450,7 @@ def create_auto_shifts(cls, year, month, creation_mode='fill_gaps'):
shift, created = cls.objects.update_or_create(
employee=employee,
date=target_date,
- defaults={'shift_type': shift_type}
+ defaults={"shift_type": shift_type},
)
assigned_employees.add(employee)
assigned_work_employees.add(employee)
@@ -411,13 +465,13 @@ def create_auto_shifts(cls, year, month, creation_mode='fill_gaps'):
break
if needed_workers <= 0:
break
-
+
# 4. 残りは休み
for employee in set(available_employees) - assigned_employees:
shift, created = cls.objects.update_or_create(
employee=employee,
date=target_date,
- defaults={'shift_type': rest_shift_type}
+ defaults={"shift_type": rest_shift_type},
)
if created:
created_count += 1
@@ -433,14 +487,16 @@ def create_auto_shifts(cls, year, month, creation_mode='fill_gaps'):
class ShiftTypeRoleMinWorker(models.Model):
- shift_type = models.ForeignKey('ShiftType', on_delete=models.CASCADE, related_name='role_min_workers')
- role = models.ForeignKey('Role', on_delete=models.CASCADE)
+ shift_type = models.ForeignKey(
+ "ShiftType", on_delete=models.CASCADE, related_name="role_min_workers"
+ )
+ role = models.ForeignKey("Role", on_delete=models.CASCADE)
min_workers = models.PositiveIntegerField(default=0)
class Meta:
- unique_together = ('shift_type', 'role')
- verbose_name = 'シフト種別役割別最低人数'
- verbose_name_plural = 'シフト種別役割別最低人数'
+ unique_together = ("shift_type", "role")
+ verbose_name = "シフト種別役割別最低人数"
+ verbose_name_plural = "シフト種別役割別最低人数"
def __str__(self):
return f"{self.shift_type} - {self.role}: {self.min_workers}"
diff --git a/shift/test_files/__init__.py b/shift/test_files/__init__.py
index 07aa1d1..804863d 100644
--- a/shift/test_files/__init__.py
+++ b/shift/test_files/__init__.py
@@ -1,2 +1,2 @@
-# Tests package
-# This file is intentionally empty to prevent Django from treating this as an app
\ No newline at end of file
+# Tests package
+# This file is intentionally empty to prevent Django from treating this as an app
diff --git a/shift/test_files/test_admin.py b/shift/test_files/test_admin.py
index 140d49b..3458553 100644
--- a/shift/test_files/test_admin.py
+++ b/shift/test_files/test_admin.py
@@ -4,25 +4,30 @@
import os
import django
-import json
-from datetime import date, timedelta
-from django.test import TestCase, Client
-from django.urls import reverse
-from django.contrib.auth.models import User
from django.contrib.admin.sites import site
-from ..models import Employee, ShiftType, CompanyHoliday, LaborLawSettings, Shift
+from django.test import Client, TestCase
+from django.urls import reverse
+
from ..admin import (
- EmployeeAdmin, ShiftTypeAdmin, CompanyHolidayAdmin,
- LaborLawSettingsAdmin, ShiftAdmin
+ CompanyHolidayAdmin,
+ EmployeeAdmin,
+ LaborLawSettingsAdmin,
+ ShiftAdmin,
+ ShiftTypeAdmin,
)
from ..factories import (
- EmployeeFactory, ShiftTypeFactory, WorkShiftTypeFactory,
- RestShiftTypeFactory, CompanyHolidayFactory, LaborLawSettingsFactory,
- ShiftFactory, UserFactory, RoleFactory
+ CompanyHolidayFactory,
+ EmployeeFactory,
+ LaborLawSettingsFactory,
+ RoleFactory,
+ ShiftFactory,
+ ShiftTypeFactory,
+ UserFactory,
)
+from ..models import CompanyHoliday, Employee, LaborLawSettings, Shift, ShiftType
# Django設定を確実に読み込む
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shift_table.settings_test')
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shift_table.settings_test")
django.setup()
@@ -80,117 +85,117 @@ def setUp(self):
def test_employee_admin_list_view(self):
"""Test employee admin list view."""
- employee = EmployeeFactory()
- url = reverse('admin:shift_employee_changelist')
+ EmployeeFactory()
+ url = reverse("admin:shift_employee_changelist")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_shift_type_admin_list_view(self):
"""Test shift type admin list view."""
- shift_type = ShiftTypeFactory(name=f"出勤_{self._testMethodName}")
- url = reverse('admin:shift_shifttype_changelist')
+ ShiftTypeFactory(name=f"出勤_{self._testMethodName}")
+ url = reverse("admin:shift_shifttype_changelist")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_company_holiday_admin_list_view(self):
"""Test company holiday admin list view."""
- holiday = CompanyHolidayFactory()
- url = reverse('admin:shift_companyholiday_changelist')
+ CompanyHolidayFactory()
+ url = reverse("admin:shift_companyholiday_changelist")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_labor_law_settings_admin_list_view(self):
"""Test labor law settings admin list view."""
- settings = LaborLawSettingsFactory()
- url = reverse('admin:shift_laborlawsettings_changelist')
+ LaborLawSettingsFactory()
+ url = reverse("admin:shift_laborlawsettings_changelist")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_shift_admin_list_view(self):
"""Test shift admin list view."""
- shift = ShiftFactory()
- url = reverse('admin:shift_shift_changelist')
+ ShiftFactory()
+ url = reverse("admin:shift_shift_changelist")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_employee_admin_add_view(self):
"""Test employee admin add view."""
- url = reverse('admin:shift_employee_add')
+ url = reverse("admin:shift_employee_add")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_shift_type_admin_add_view(self):
"""Test shift type admin add view."""
# 役割を作成
- role1 = RoleFactory(name="ホール")
- role2 = RoleFactory(name="キッチン")
-
- url = reverse('admin:shift_shifttype_add')
+ RoleFactory(name="ホール")
+ RoleFactory(name="キッチン")
+
+ url = reverse("admin:shift_shifttype_add")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
-
+
# 基本フィールドが含まれていることを確認
self.assertContains(response, 'name="name"')
self.assertContains(response, 'name="is_work"')
self.assertContains(response, 'name="color"')
self.assertContains(response, 'name="min_workers"')
self.assertContains(response, 'name="max_workers"')
-
+
# インラインが含まれていることを確認
- self.assertContains(response, '役割別最低人数')
- self.assertContains(response, 'role_min_workers-TOTAL_FORMS')
- self.assertContains(response, 'role_min_workers-INITIAL_FORMS')
+ self.assertContains(response, "役割別最低人数")
+ self.assertContains(response, "role_min_workers-TOTAL_FORMS")
+ self.assertContains(response, "role_min_workers-INITIAL_FORMS")
def test_company_holiday_admin_add_view(self):
"""Test company holiday admin add view."""
- url = reverse('admin:shift_companyholiday_add')
+ url = reverse("admin:shift_companyholiday_add")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_labor_law_settings_admin_add_view(self):
"""Test labor law settings admin add view."""
- url = reverse('admin:shift_laborlawsettings_add')
+ url = reverse("admin:shift_laborlawsettings_add")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_shift_admin_add_view(self):
"""Test shift admin add view."""
- url = reverse('admin:shift_shift_add')
+ url = reverse("admin:shift_shift_add")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_employee_admin_change_view(self):
"""Test employee admin change view."""
employee = EmployeeFactory()
- url = reverse('admin:shift_employee_change', args=[employee.id])
+ url = reverse("admin:shift_employee_change", args=[employee.id])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_shift_type_admin_change_view(self):
"""Test shift type admin change view."""
shift_type = ShiftTypeFactory(name=f"出勤_{self._testMethodName}")
- url = reverse('admin:shift_shifttype_change', args=[shift_type.id])
+ url = reverse("admin:shift_shifttype_change", args=[shift_type.id])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_company_holiday_admin_change_view(self):
"""Test company holiday admin change view."""
holiday = CompanyHolidayFactory()
- url = reverse('admin:shift_companyholiday_change', args=[holiday.id])
+ url = reverse("admin:shift_companyholiday_change", args=[holiday.id])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_labor_law_settings_admin_change_view(self):
"""Test labor law settings admin change view."""
settings = LaborLawSettingsFactory()
- url = reverse('admin:shift_laborlawsettings_change', args=[settings.id])
+ url = reverse("admin:shift_laborlawsettings_change", args=[settings.id])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_shift_admin_change_view(self):
"""Test shift admin change view."""
shift = ShiftFactory()
- url = reverse('admin:shift_shift_change', args=[shift.id])
+ url = reverse("admin:shift_shift_change", args=[shift.id])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
@@ -208,53 +213,53 @@ def setUp(self):
def test_bulk_add_view_get(self):
"""Test bulk add view GET request."""
- url = reverse('admin:shift_companyholiday_bulk_add')
+ url = reverse("admin:shift_companyholiday_bulk_add")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
- self.assertContains(response, '会社休日一括追加')
+ self.assertContains(response, "会社休日一括追加")
def test_bulk_add_view_post_custom_weekday(self):
"""Test bulk add view POST with custom weekday holidays."""
- url = reverse('admin:shift_companyholiday_bulk_add')
+ url = reverse("admin:shift_companyholiday_bulk_add")
data = {
- 'holiday_type': 'custom_weekday',
- 'start_date': '2025-01-01',
- 'end_date': '2025-01-31',
- 'weekday': '0', # 月曜日
- 'name': '月曜休日'
+ "holiday_type": "custom_weekday",
+ "start_date": "2025-01-01",
+ "end_date": "2025-01-31",
+ "weekday": "0", # 月曜日
+ "name": "月曜休日",
}
response = self.client.post(url, data)
self.assertEqual(response.status_code, 302)
def test_bulk_add_view_post_date_range(self):
"""Test bulk add view POST with date range holidays."""
- url = reverse('admin:shift_companyholiday_bulk_add')
+ url = reverse("admin:shift_companyholiday_bulk_add")
data = {
- 'holiday_type': 'date_range',
- 'start_date': '2025-01-01',
- 'end_date': '2025-01-05',
- 'name': '期間休日'
+ "holiday_type": "date_range",
+ "start_date": "2025-01-01",
+ "end_date": "2025-01-05",
+ "name": "期間休日",
}
response = self.client.post(url, data)
self.assertEqual(response.status_code, 302)
def test_bulk_add_view_post_holidays(self):
"""Test bulk add view POST with holidays."""
- url = reverse('admin:shift_companyholiday_bulk_add')
+ url = reverse("admin:shift_companyholiday_bulk_add")
data = {
- 'holiday_type': 'holidays',
- 'start_date': '2025-01-01',
- 'end_date': '2025-01-31',
- 'name': '祝日'
+ "holiday_type": "holidays",
+ "start_date": "2025-01-01",
+ "end_date": "2025-01-31",
+ "name": "祝日",
}
response = self.client.post(url, data)
self.assertEqual(response.status_code, 302)
def test_bulk_add_view_post_invalid_data(self):
"""Test bulk add view POST with invalid data."""
- url = reverse('admin:shift_companyholiday_bulk_add')
+ url = reverse("admin:shift_companyholiday_bulk_add")
data = {
- 'holiday_type': 'custom_weekday',
+ "holiday_type": "custom_weekday",
# 必要なフィールドが不足
}
response = self.client.post(url, data)
@@ -262,18 +267,18 @@ def test_bulk_add_view_post_invalid_data(self):
def test_bulk_add_view_contains_holiday_name_field(self):
"""一括追加画面に休日名テキストボックスが表示されること"""
- url = reverse('admin:shift_companyholiday_bulk_add')
+ url = reverse("admin:shift_companyholiday_bulk_add")
response = self.client.get(url)
self.assertContains(response, 'name="name"')
- self.assertContains(response, '休日名')
+ self.assertContains(response, "休日名")
def test_bulk_add_view_contains_weekday_select(self):
"""一括追加画面に曜日プルダウンがHTMLとして含まれていること"""
- url = reverse('admin:shift_companyholiday_bulk_add')
+ url = reverse("admin:shift_companyholiday_bulk_add")
response = self.client.get(url)
self.assertContains(response, 'name="weekday"')
- self.assertContains(response, '月曜日')
- self.assertContains(response, '火曜日')
+ self.assertContains(response, "月曜日")
+ self.assertContains(response, "火曜日")
class AutoShiftCreationTest(TestCase):
@@ -292,37 +297,25 @@ def test_auto_create_view_get(self):
url = reverse("admin:shift_auto_create")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
- self.assertContains(response, '自動シフト作成')
+ self.assertContains(response, "自動シフト作成")
def test_auto_create_view_post_fill_gaps(self):
"""Test auto create view POST with fill gaps mode."""
url = reverse("admin:shift_auto_create")
- data = {
- 'year': 2025,
- 'month': 1,
- 'creation_mode': 'fill_gaps'
- }
+ data = {"year": 2025, "month": 1, "creation_mode": "fill_gaps"}
response = self.client.post(url, data)
self.assertEqual(response.status_code, 200)
def test_auto_create_view_post_overwrite(self):
"""Test auto create view POST with overwrite mode."""
url = reverse("admin:shift_auto_create")
- data = {
- 'year': 2025,
- 'month': 1,
- 'creation_mode': 'overwrite'
- }
+ data = {"year": 2025, "month": 1, "creation_mode": "overwrite"}
response = self.client.post(url, data)
self.assertEqual(response.status_code, 200)
def test_auto_create_view_post_invalid_data(self):
"""Test auto create view POST with invalid data."""
url = reverse("admin:shift_auto_create")
- data = {
- 'year': 2025,
- 'month': 13, # 無効な月
- 'creation_mode': 'fill_gaps'
- }
+ data = {"year": 2025, "month": 13, "creation_mode": "fill_gaps"} # 無効な月
response = self.client.post(url, data)
- self.assertEqual(response.status_code, 200) # フォームエラーで再表示
\ No newline at end of file
+ self.assertEqual(response.status_code, 200) # フォームエラーで再表示
diff --git a/shift/test_files/test_basic.py b/shift/test_files/test_basic.py
index f12bffa..14d665f 100644
--- a/shift/test_files/test_basic.py
+++ b/shift/test_files/test_basic.py
@@ -20,10 +20,10 @@ def test_django_setup(self):
def test_admin_url(self):
"""Test that admin URL is accessible."""
- url = reverse('admin:index')
- self.assertEqual(url, '/admin/')
+ url = reverse("admin:index")
+ self.assertEqual(url, "/admin/")
def test_shift_url(self):
"""Test that shift URL is accessible."""
- url = reverse('shift_table')
- self.assertEqual(url, '/shift/')
\ No newline at end of file
+ url = reverse("shift_table")
+ self.assertEqual(url, "/shift/")
diff --git a/shift/test_files/test_direct_password_change.py b/shift/test_files/test_direct_password_change.py
index db2f914..b8bddd9 100644
--- a/shift/test_files/test_direct_password_change.py
+++ b/shift/test_files/test_direct_password_change.py
@@ -1,13 +1,11 @@
import os
import django
-from django.conf import settings
# Django設定を読み込み
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shift_table.settings')
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shift_table.settings")
django.setup()
-import pytest
-from django.test import TestCase, Client
+from django.test import Client, TestCase
from django.contrib.auth.models import User
from django.urls import reverse
@@ -17,130 +15,124 @@ def setUp(self):
"""テスト用のユーザーを作成"""
self.client = Client()
self.user = User.objects.create_user(
- username='testuser',
- password='oldpassword123',
- email='test@example.com'
+ username="testuser", password="oldpassword123", email="test@example.com"
)
self.admin_user = User.objects.create_superuser(
- username='admin',
- password='adminpass123',
- email='admin@example.com'
+ username="admin", password="adminpass123", email="admin@example.com"
)
def test_direct_password_change_page_access(self):
"""直接パスワード変更ページにアクセスできることを確認"""
- response = self.client.get(reverse('direct_password_change'))
+ response = self.client.get(reverse("direct_password_change"))
self.assertEqual(response.status_code, 200)
- self.assertContains(response, '直接パスワード変更')
- self.assertContains(response, 'ユーザー名')
- self.assertContains(response, '新しいパスワード')
+ self.assertContains(response, "直接パスワード変更")
+ self.assertContains(response, "ユーザー名")
+ self.assertContains(response, "新しいパスワード")
def test_direct_password_change_success(self):
"""正常なパスワード変更が成功することを確認"""
data = {
- 'username': 'testuser',
- 'new_password': 'newpassword123',
- 'confirm_password': 'newpassword123'
+ "username": "testuser",
+ "new_password": "newpassword123",
+ "confirm_password": "newpassword123",
}
- response = self.client.post(reverse('direct_password_change'), data)
+ response = self.client.post(reverse("direct_password_change"), data)
self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'パスワードが正常に変更されました')
-
+ self.assertContains(response, "パスワードが正常に変更されました")
+
# 新しいパスワードでログインできることを確認
- login_success = self.client.login(username='testuser', password='newpassword123')
+ login_success = self.client.login(
+ username="testuser", password="newpassword123"
+ )
self.assertTrue(login_success)
def test_direct_password_change_admin_user(self):
"""管理者ユーザーのパスワード変更が成功することを確認"""
data = {
- 'username': 'admin',
- 'new_password': 'newadminpass123',
- 'confirm_password': 'newadminpass123'
+ "username": "admin",
+ "new_password": "newadminpass123",
+ "confirm_password": "newadminpass123",
}
- response = self.client.post(reverse('direct_password_change'), data)
+ response = self.client.post(reverse("direct_password_change"), data)
self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'パスワードが正常に変更されました')
-
+ self.assertContains(response, "パスワードが正常に変更されました")
+
# 新しいパスワードでログインできることを確認
- login_success = self.client.login(username='admin', password='newadminpass123')
+ login_success = self.client.login(username="admin", password="newadminpass123")
self.assertTrue(login_success)
def test_direct_password_change_missing_fields(self):
"""必須フィールドが不足している場合のエラー処理"""
data = {
- 'username': 'testuser',
- 'new_password': 'newpassword123'
+ "username": "testuser",
+ "new_password": "newpassword123",
# confirm_password が不足
}
- response = self.client.post(reverse('direct_password_change'), data)
+ response = self.client.post(reverse("direct_password_change"), data)
self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'すべての項目を入力してください')
+ self.assertContains(response, "すべての項目を入力してください")
def test_direct_password_change_password_mismatch(self):
"""パスワードが一致しない場合のエラー処理"""
data = {
- 'username': 'testuser',
- 'new_password': 'newpassword123',
- 'confirm_password': 'differentpassword123'
+ "username": "testuser",
+ "new_password": "newpassword123",
+ "confirm_password": "differentpassword123",
}
- response = self.client.post(reverse('direct_password_change'), data)
+ response = self.client.post(reverse("direct_password_change"), data)
self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'パスワードが一致しません')
+ self.assertContains(response, "パスワードが一致しません")
def test_direct_password_change_short_password(self):
"""パスワードが短すぎる場合のエラー処理"""
data = {
- 'username': 'testuser',
- 'new_password': 'short',
- 'confirm_password': 'short'
+ "username": "testuser",
+ "new_password": "short",
+ "confirm_password": "short",
}
- response = self.client.post(reverse('direct_password_change'), data)
+ response = self.client.post(reverse("direct_password_change"), data)
self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'パスワードは8文字以上で入力してください')
+ self.assertContains(response, "パスワードは8文字以上で入力してください")
def test_direct_password_change_nonexistent_user(self):
"""存在しないユーザー名の場合のエラー処理"""
data = {
- 'username': 'nonexistentuser',
- 'new_password': 'newpassword123',
- 'confirm_password': 'newpassword123'
+ "username": "nonexistentuser",
+ "new_password": "newpassword123",
+ "confirm_password": "newpassword123",
}
- response = self.client.post(reverse('direct_password_change'), data)
+ response = self.client.post(reverse("direct_password_change"), data)
self.assertEqual(response.status_code, 200)
- self.assertContains(response, '指定されたユーザー名が見つかりません')
+ self.assertContains(response, "指定されたユーザー名が見つかりません")
def test_direct_password_change_empty_fields(self):
"""空のフィールドが送信された場合のエラー処理"""
- data = {
- 'username': '',
- 'new_password': '',
- 'confirm_password': ''
- }
- response = self.client.post(reverse('direct_password_change'), data)
+ data = {"username": "", "new_password": "", "confirm_password": ""}
+ response = self.client.post(reverse("direct_password_change"), data)
self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'すべての項目を入力してください')
+ self.assertContains(response, "すべての項目を入力してください")
def test_direct_password_change_get_request(self):
"""GETリクエストでページが正しく表示されることを確認"""
- response = self.client.get(reverse('direct_password_change'))
+ response = self.client.get(reverse("direct_password_change"))
self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'form')
+ self.assertContains(response, "form")
self.assertContains(response, 'method="post"')
def test_direct_password_change_csrf_protection(self):
"""CSRF保護が有効であることを確認(フォームにCSRFトークンが含まれていること)"""
- response = self.client.get(reverse('direct_password_change'))
+ response = self.client.get(reverse("direct_password_change"))
self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'csrfmiddlewaretoken')
+ self.assertContains(response, "csrfmiddlewaretoken")
def test_direct_password_change_success_page_content(self):
"""成功ページの内容が正しいことを確認"""
data = {
- 'username': 'testuser',
- 'new_password': 'newpassword123',
- 'confirm_password': 'newpassword123'
+ "username": "testuser",
+ "new_password": "newpassword123",
+ "confirm_password": "newpassword123",
}
- response = self.client.post(reverse('direct_password_change'), data)
- self.assertContains(response, 'パスワードが正常に変更されました')
- self.assertContains(response, 'testuser')
- self.assertContains(response, '管理画面にログイン')
\ No newline at end of file
+ response = self.client.post(reverse("direct_password_change"), data)
+ self.assertContains(response, "パスワードが正常に変更されました")
+ self.assertContains(response, "testuser")
+ self.assertContains(response, "管理画面にログイン")
diff --git a/shift/test_files/test_e2e.py b/shift/test_files/test_e2e.py
index 8f8f688..d2fef6b 100644
--- a/shift/test_files/test_e2e.py
+++ b/shift/test_files/test_e2e.py
@@ -3,31 +3,28 @@
"""
import os
+
import django
import pytest
from django.test import LiveServerTestCase
-from django.contrib.auth.models import User
-from selenium import webdriver
-from selenium.webdriver.common.by import By
-from selenium.webdriver.support.ui import WebDriverWait
-from selenium.webdriver.support import expected_conditions as EC
-from selenium.webdriver.support.ui import Select
-from webdriver_manager.chrome import ChromeDriverManager
-from selenium.webdriver.chrome.service import Service
-from datetime import date
# Django設定を確実に読み込む
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shift_table.settings_test')
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shift_table.settings_test")
django.setup()
from ..factories import (
- UserFactory, EmployeeFactory, ShiftTypeFactory, WorkShiftTypeFactory,
- RestShiftTypeFactory, CompanyHolidayFactory, LaborLawSettingsFactory
+ EmployeeFactory,
+ LaborLawSettingsFactory,
+ RestShiftTypeFactory,
+ UserFactory,
+ WorkShiftTypeFactory,
)
from ..models import ShiftType
-@pytest.mark.skip(reason="E2E tests require Chrome browser which is not available in Docker")
+@pytest.mark.skip(
+ reason="E2E tests require Chrome browser which is not available in Docker"
+)
class ShiftTableE2ETest(LiveServerTestCase):
"""End-to-end tests for shift table functionality."""
@@ -52,7 +49,9 @@ def setUp(self):
self.employee = EmployeeFactory(name="田中太郎")
self.work_shift_type = WorkShiftTypeFactory(name=f"出勤_{self._testMethodName}")
self.rest_shift_type = RestShiftTypeFactory(name=f"休み_{self._testMethodName}")
- self.settings = LaborLawSettingsFactory(min_workers=2, max_consecutive_work_days=6)
+ self.settings = LaborLawSettingsFactory(
+ min_workers=2, max_consecutive_work_days=6
+ )
def test_shift_table_display(self):
"""Test that shift table displays correctly."""
@@ -90,7 +89,9 @@ def test_company_holiday_display(self):
self.skipTest("E2E tests are disabled in Docker environment")
-@pytest.mark.skip(reason="E2E tests require Chrome browser which is not available in Docker")
+@pytest.mark.skip(
+ reason="E2E tests require Chrome browser which is not available in Docker"
+)
class AdminE2ETest(LiveServerTestCase):
"""End-to-end tests for admin functionality."""
@@ -137,4 +138,4 @@ def test_shift_admin_list(self):
def test_shift_creation_admin(self):
"""Test creating shift through admin interface."""
# テストをスキップ
- self.skipTest("E2E tests are disabled in Docker environment")
\ No newline at end of file
+ self.skipTest("E2E tests are disabled in Docker environment")
diff --git a/shift/test_files/test_forms.py b/shift/test_files/test_forms.py
index c3a5669..d17b065 100644
--- a/shift/test_files/test_forms.py
+++ b/shift/test_files/test_forms.py
@@ -3,20 +3,17 @@
"""
import os
+from datetime import date
+
import django
from django.test import TestCase
-from django.core.exceptions import ValidationError
-from datetime import date
-from ..forms import CompanyHolidayBulkAddForm, AutoShiftForm
-from ..factories import (
- EmployeeFactory, ShiftTypeFactory, WorkShiftTypeFactory,
- RestShiftTypeFactory, CompanyHolidayFactory, LaborLawSettingsFactory,
- RoleFactory, ShiftTypeRoleMinWorkerFactory
-)
-from ..models import ShiftType, LaborLawSettings, Role
+
+from ..factories import RestShiftTypeFactory, WorkShiftTypeFactory
+from ..forms import AutoShiftForm, CompanyHolidayBulkAddForm
+from ..models import LaborLawSettings, ShiftType
# Django設定を確実に読み込む
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shift_table.settings_test')
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shift_table.settings_test")
django.setup()
@@ -32,11 +29,11 @@ def setUp(self):
def test_form_valid_custom_weekday(self):
"""Test form with valid custom weekday data."""
form_data = {
- 'holiday_type': 'custom_weekday',
- 'start_date': '2025-01-01',
- 'end_date': '2025-01-31',
- 'weekday': '0',
- 'name': '月曜休日'
+ "holiday_type": "custom_weekday",
+ "start_date": "2025-01-01",
+ "end_date": "2025-01-31",
+ "weekday": "0",
+ "name": "月曜休日",
}
form = CompanyHolidayBulkAddForm(data=form_data)
self.assertTrue(form.is_valid())
@@ -44,10 +41,10 @@ def test_form_valid_custom_weekday(self):
def test_form_valid_date_range(self):
"""Test form with valid date range data."""
form_data = {
- 'holiday_type': 'date_range',
- 'start_date': '2025-01-01',
- 'end_date': '2025-01-05',
- 'name': '期間休日'
+ "holiday_type": "date_range",
+ "start_date": "2025-01-01",
+ "end_date": "2025-01-05",
+ "name": "期間休日",
}
form = CompanyHolidayBulkAddForm(data=form_data)
self.assertTrue(form.is_valid())
@@ -55,10 +52,10 @@ def test_form_valid_date_range(self):
def test_form_valid_holidays(self):
"""Test form with valid holidays data."""
form_data = {
- 'holiday_type': 'holidays',
- 'start_date': '2025-01-01',
- 'end_date': '2025-01-31',
- 'name': '祝日'
+ "holiday_type": "holidays",
+ "start_date": "2025-01-01",
+ "end_date": "2025-01-31",
+ "name": "祝日",
}
form = CompanyHolidayBulkAddForm(data=form_data)
self.assertTrue(form.is_valid())
@@ -66,7 +63,7 @@ def test_form_valid_holidays(self):
def test_form_missing_required_fields(self):
"""Test form with missing required fields."""
form_data = {
- 'holiday_type': 'custom_weekday',
+ "holiday_type": "custom_weekday",
# Missing start_date, end_date, weekday
}
form = CompanyHolidayBulkAddForm(data=form_data)
@@ -75,10 +72,10 @@ def test_form_missing_required_fields(self):
def test_form_end_date_before_start_date(self):
"""Test form with end date before start date."""
form_data = {
- 'holiday_type': 'date_range',
- 'start_date': '2025-01-31',
- 'end_date': '2025-01-01',
- 'name': '期間休日'
+ "holiday_type": "date_range",
+ "start_date": "2025-01-31",
+ "end_date": "2025-01-01",
+ "name": "期間休日",
}
form = CompanyHolidayBulkAddForm(data=form_data)
self.assertTrue(form.is_valid()) # フォームは日付の順序をチェックしない
@@ -86,52 +83,52 @@ def test_form_end_date_before_start_date(self):
def test_form_clean_custom_weekday(self):
"""Test form clean method for custom weekday holidays."""
form_data = {
- 'holiday_type': 'custom_weekday',
- 'weekday': '1',
- 'start_date': '2025-01-01',
- 'end_date': '2025-01-31',
- 'name': '火曜休日'
+ "holiday_type": "custom_weekday",
+ "weekday": "1",
+ "start_date": "2025-01-01",
+ "end_date": "2025-01-31",
+ "name": "火曜休日",
}
form = CompanyHolidayBulkAddForm(data=form_data)
self.assertTrue(form.is_valid())
-
+
cleaned_data = form.clean()
- self.assertEqual(cleaned_data['holiday_type'], 'custom_weekday')
- self.assertEqual(cleaned_data['weekday'], '1')
- self.assertEqual(cleaned_data['start_date'], date(2025, 1, 1))
- self.assertEqual(cleaned_data['end_date'], date(2025, 1, 31))
+ self.assertEqual(cleaned_data["holiday_type"], "custom_weekday")
+ self.assertEqual(cleaned_data["weekday"], "1")
+ self.assertEqual(cleaned_data["start_date"], date(2025, 1, 1))
+ self.assertEqual(cleaned_data["end_date"], date(2025, 1, 31))
def test_form_clean_date_range(self):
"""Test form clean method for date range holidays."""
form_data = {
- 'holiday_type': 'date_range',
- 'start_date': '2025-01-01',
- 'end_date': '2025-01-05',
- 'name': '年末年始休暇'
+ "holiday_type": "date_range",
+ "start_date": "2025-01-01",
+ "end_date": "2025-01-05",
+ "name": "年末年始休暇",
}
form = CompanyHolidayBulkAddForm(data=form_data)
self.assertTrue(form.is_valid())
-
+
cleaned_data = form.clean()
- self.assertEqual(cleaned_data['holiday_type'], 'date_range')
- self.assertEqual(cleaned_data['start_date'], date(2025, 1, 1))
- self.assertEqual(cleaned_data['end_date'], date(2025, 1, 5))
+ self.assertEqual(cleaned_data["holiday_type"], "date_range")
+ self.assertEqual(cleaned_data["start_date"], date(2025, 1, 1))
+ self.assertEqual(cleaned_data["end_date"], date(2025, 1, 5))
def test_form_clean_holidays(self):
"""Test form clean method for holidays."""
form_data = {
- 'holiday_type': 'holidays',
- 'start_date': '2025-01-01',
- 'end_date': '2025-01-31',
- 'name': '祝日'
+ "holiday_type": "holidays",
+ "start_date": "2025-01-01",
+ "end_date": "2025-01-31",
+ "name": "祝日",
}
form = CompanyHolidayBulkAddForm(data=form_data)
self.assertTrue(form.is_valid())
-
+
cleaned_data = form.clean()
- self.assertEqual(cleaned_data['holiday_type'], 'holidays')
- self.assertEqual(cleaned_data['start_date'], date(2025, 1, 1))
- self.assertEqual(cleaned_data['end_date'], date(2025, 1, 31))
+ self.assertEqual(cleaned_data["holiday_type"], "holidays")
+ self.assertEqual(cleaned_data["start_date"], date(2025, 1, 1))
+ self.assertEqual(cleaned_data["end_date"], date(2025, 1, 31))
class AutoShiftFormTest(TestCase):
@@ -145,31 +142,27 @@ def setUp(self):
def test_form_valid_data(self):
"""Test form with valid data."""
- form_data = {
- 'year': 2025,
- 'month': 1,
- 'creation_mode': 'fill_gaps'
- }
+ form_data = {"year": 2025, "month": 1, "creation_mode": "fill_gaps"}
form = AutoShiftForm(data=form_data)
self.assertTrue(form.is_valid())
def test_form_missing_required_fields(self):
"""Test form with missing required fields."""
form_data = {
- 'year': 2025,
+ "year": 2025,
# Missing month and creation_mode
}
form = AutoShiftForm(data=form_data)
self.assertFalse(form.is_valid())
- self.assertIn('month', form.errors)
- self.assertIn('creation_mode', form.errors)
+ self.assertIn("month", form.errors)
+ self.assertIn("creation_mode", form.errors)
def test_form_invalid_year(self):
"""Test form with invalid year."""
form_data = {
- 'year': 1800, # Too early
- 'month': 1,
- 'creation_mode': 'fill_gaps'
+ "year": 1800, # Too early
+ "month": 1,
+ "creation_mode": "fill_gaps",
}
form = AutoShiftForm(data=form_data)
self.assertFalse(form.is_valid())
@@ -177,98 +170,70 @@ def test_form_invalid_year(self):
def test_form_invalid_month(self):
"""Test form with invalid month."""
form_data = {
- 'year': 2025,
- 'month': 13, # Invalid month
- 'creation_mode': 'fill_gaps'
+ "year": 2025,
+ "month": 13, # Invalid month
+ "creation_mode": "fill_gaps",
}
form = AutoShiftForm(data=form_data)
self.assertFalse(form.is_valid())
def test_form_invalid_creation_mode(self):
"""Test form with invalid creation mode."""
- form_data = {
- 'year': 2025,
- 'month': 1,
- 'creation_mode': 'invalid_mode'
- }
+ form_data = {"year": 2025, "month": 1, "creation_mode": "invalid_mode"}
form = AutoShiftForm(data=form_data)
self.assertFalse(form.is_valid())
def test_form_clean_valid_data(self):
"""Test form clean method with valid data."""
- form_data = {
- 'year': 2025,
- 'month': 1,
- 'creation_mode': 'fill_gaps'
- }
+ form_data = {"year": 2025, "month": 1, "creation_mode": "fill_gaps"}
form = AutoShiftForm(data=form_data)
self.assertTrue(form.is_valid())
-
+
cleaned_data = form.clean()
- self.assertEqual(cleaned_data['year'], 2025)
- self.assertEqual(cleaned_data['month'], 1)
- self.assertEqual(cleaned_data['creation_mode'], 'fill_gaps')
+ self.assertEqual(cleaned_data["year"], 2025)
+ self.assertEqual(cleaned_data["month"], 1)
+ self.assertEqual(cleaned_data["creation_mode"], "fill_gaps")
def test_form_clean_overwrite_mode(self):
"""Test form clean method with overwrite mode."""
- form_data = {
- 'year': 2025,
- 'month': 1,
- 'creation_mode': 'overwrite'
- }
+ form_data = {"year": 2025, "month": 1, "creation_mode": "overwrite"}
form = AutoShiftForm(data=form_data)
self.assertTrue(form.is_valid())
-
+
cleaned_data = form.clean()
- self.assertEqual(cleaned_data['creation_mode'], 'overwrite')
+ self.assertEqual(cleaned_data["creation_mode"], "overwrite")
def test_form_year_range_validation(self):
"""Test form year range validation."""
# Test year too early
- form_data = {
- 'year': 1800,
- 'month': 1,
- 'creation_mode': 'fill_gaps'
- }
+ form_data = {"year": 1800, "month": 1, "creation_mode": "fill_gaps"}
form = AutoShiftForm(data=form_data)
self.assertFalse(form.is_valid())
- self.assertIn('year', form.errors)
+ self.assertIn("year", form.errors)
# Test year too late
- form_data = {
- 'year': 2100,
- 'month': 1,
- 'creation_mode': 'fill_gaps'
- }
+ form_data = {"year": 2100, "month": 1, "creation_mode": "fill_gaps"}
form = AutoShiftForm(data=form_data)
self.assertFalse(form.is_valid())
- self.assertIn('year', form.errors)
+ self.assertIn("year", form.errors)
def test_form_month_range_validation(self):
"""Test form month range validation."""
# Test month too low
- form_data = {
- 'year': 2025,
- 'month': 0,
- 'creation_mode': 'fill_gaps'
- }
+ form_data = {"year": 2025, "month": 0, "creation_mode": "fill_gaps"}
form = AutoShiftForm(data=form_data)
self.assertFalse(form.is_valid())
- self.assertIn('month', form.errors)
+ self.assertIn("month", form.errors)
# Test month too high
- form_data = {
- 'year': 2025,
- 'month': 13,
- 'creation_mode': 'fill_gaps'
- }
+ form_data = {"year": 2025, "month": 13, "creation_mode": "fill_gaps"}
form = AutoShiftForm(data=form_data)
self.assertFalse(form.is_valid())
- self.assertIn('month', form.errors)
+ self.assertIn("month", form.errors)
def test_form_default_values(self):
"""Test form default values."""
form = AutoShiftForm()
- self.assertEqual(form.fields['year'].initial, None)
- self.assertEqual(form.fields['month'].initial, None)
- self.assertEqual(form.fields['creation_mode'].initial, 'fill_gaps')
\ No newline at end of file
+ self.assertEqual(form.fields["year"].initial, None)
+ self.assertEqual(form.fields["month"].initial, None)
+ self.assertEqual(form.fields["creation_mode"].initial, "fill_gaps")
diff --git a/shift/test_files/test_models.py b/shift/test_files/test_models.py
index 8511d76..c1c55ee 100644
--- a/shift/test_files/test_models.py
+++ b/shift/test_files/test_models.py
@@ -5,20 +5,26 @@
import os
import django
import pytest
-from datetime import date, timedelta
-from django.test import TestCase
+from datetime import date
+
from django.core.exceptions import ValidationError
-from django.db import IntegrityError
+from django.test import TestCase
# Django設定を確実に読み込む
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shift_table.settings_test')
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shift_table.settings_test")
django.setup()
from ..models import Employee, ShiftType, CompanyHoliday, LaborLawSettings, Shift, Role
from ..factories import (
- EmployeeFactory, ShiftTypeFactory, WorkShiftTypeFactory,
- RestShiftTypeFactory, CompanyHolidayFactory, LaborLawSettingsFactory,
- ShiftFactory, RestShiftFactory, RoleFactory, ShiftTypeRoleMinWorkerFactory
+ CompanyHolidayFactory,
+ EmployeeFactory,
+ LaborLawSettingsFactory,
+ RestShiftTypeFactory,
+ RoleFactory,
+ ShiftFactory,
+ ShiftTypeFactory,
+ ShiftTypeRoleMinWorkerFactory,
+ WorkShiftTypeFactory,
)
@@ -47,7 +53,7 @@ def test_employee_roles(self):
role1 = RoleFactory(name="ホール")
role2 = RoleFactory(name="キッチン")
employee = EmployeeFactory(roles=[role1, role2])
-
+
self.assertEqual(employee.roles.count(), 2)
self.assertIn(role1, employee.roles.all())
self.assertIn(role2, employee.roles.all())
@@ -122,11 +128,11 @@ def test_shift_type_role_min_workers(self):
role1 = RoleFactory(name="ホール")
role2 = RoleFactory(name="キッチン")
shift_type = ShiftTypeFactory()
-
+
# 役割別最低人数を設定
ShiftTypeRoleMinWorkerFactory(shift_type=shift_type, role=role1, min_workers=2)
ShiftTypeRoleMinWorkerFactory(shift_type=shift_type, role=role2, min_workers=1)
-
+
# 確認
self.assertEqual(shift_type.role_min_workers.count(), 2)
self.assertEqual(shift_type.role_min_workers.get(role=role1).min_workers, 2)
@@ -211,7 +217,7 @@ def test_labor_law_settings_min_workers(self):
def test_get_current_settings(self):
# 既存の設定をクリア
LaborLawSettings.objects.all().delete()
-
+
# 新しい設定を作成
settings = LaborLawSettingsFactory()
current_settings = LaborLawSettings.get_current_settings()
@@ -220,7 +226,7 @@ def test_get_current_settings(self):
def test_get_current_settings_creates_default(self):
# 既存の設定をクリア
LaborLawSettings.objects.all().delete()
-
+
# 設定が存在しない場合、デフォルト設定が作成される
current_settings = LaborLawSettings.get_current_settings()
self.assertIsNotNone(current_settings)
@@ -264,223 +270,217 @@ def test_shift_unique_together(self):
employee = EmployeeFactory()
shift_type = ShiftTypeFactory()
date_obj = date(2025, 1, 1)
-
+
ShiftFactory(employee=employee, shift_type=shift_type, date=date_obj)
with self.assertRaises(Exception): # IntegrityError or ValidationError
ShiftFactory(employee=employee, shift_type=shift_type, date=date_obj)
def test_check_consecutive_work_days_no_warning(self):
"""Test consecutive work days check with no warning."""
- settings = LaborLawSettingsFactory(max_consecutive_work_days=6)
+ LaborLawSettingsFactory(max_consecutive_work_days=6)
employee = EmployeeFactory()
work_shift_type = WorkShiftTypeFactory()
-
+
# Create shifts with gaps (no consecutive work days)
- shift1 = ShiftFactory(
- employee=employee,
- shift_type=work_shift_type,
- date=date(2025, 1, 1)
+ ShiftFactory(
+ employee=employee, shift_type=work_shift_type, date=date(2025, 1, 1)
)
shift2 = ShiftFactory(
employee=employee,
shift_type=work_shift_type,
- date=date(2025, 1, 3) # Gap of 1 day
+ date=date(2025, 1, 3), # Gap of 1 day
)
-
+
warning = shift2.check_consecutive_work_days()
self.assertIsNone(warning)
def test_check_consecutive_work_days_with_warning(self):
"""Test consecutive work days check with warning."""
- settings = LaborLawSettingsFactory(max_consecutive_work_days=2)
+ LaborLawSettingsFactory(max_consecutive_work_days=2)
employee = EmployeeFactory()
work_shift_type = WorkShiftTypeFactory()
-
+
# Create consecutive work days
ShiftFactory(
- employee=employee,
- shift_type=work_shift_type,
- date=date(2025, 1, 1)
+ employee=employee, shift_type=work_shift_type, date=date(2025, 1, 1)
)
ShiftFactory(
- employee=employee,
- shift_type=work_shift_type,
- date=date(2025, 1, 2)
+ employee=employee, shift_type=work_shift_type, date=date(2025, 1, 2)
)
-
+
# This should trigger a warning
shift3 = ShiftFactory(
- employee=employee,
- shift_type=work_shift_type,
- date=date(2025, 1, 3)
+ employee=employee, shift_type=work_shift_type, date=date(2025, 1, 3)
)
-
+
warning = shift3.check_consecutive_work_days()
self.assertIsNotNone(warning)
- self.assertTrue(warning['warning'])
- self.assertIn('連続勤務日数が3日となり', warning['message'])
- self.assertEqual(warning['consecutive_days'], 3)
- self.assertEqual(warning['max_days'], 2)
+ self.assertTrue(warning["warning"])
+ self.assertIn("連続勤務日数が3日となり", warning["message"])
+ self.assertEqual(warning["consecutive_days"], 3)
+ self.assertEqual(warning["max_days"], 2)
def test_check_consecutive_work_days_with_holidays(self):
"""Test consecutive work days check excluding holidays."""
- settings = LaborLawSettingsFactory(max_consecutive_work_days=2)
+ LaborLawSettingsFactory(max_consecutive_work_days=2)
employee = EmployeeFactory()
work_shift_type = WorkShiftTypeFactory()
rest_shift_type = RestShiftTypeFactory()
-
+
# Create shifts with holidays in between
ShiftFactory(
- employee=employee,
- shift_type=work_shift_type,
- date=date(2025, 1, 1)
+ employee=employee, shift_type=work_shift_type, date=date(2025, 1, 1)
)
ShiftFactory(
employee=employee,
shift_type=rest_shift_type, # Holiday
- date=date(2025, 1, 2)
+ date=date(2025, 1, 2),
)
ShiftFactory(
- employee=employee,
- shift_type=work_shift_type,
- date=date(2025, 1, 3)
+ employee=employee, shift_type=work_shift_type, date=date(2025, 1, 3)
)
-
+
# This should not trigger a warning because of the holiday
shift4 = ShiftFactory(
- employee=employee,
- shift_type=work_shift_type,
- date=date(2025, 1, 4)
+ employee=employee, shift_type=work_shift_type, date=date(2025, 1, 4)
)
-
+
warning = shift4.check_consecutive_work_days()
self.assertIsNone(warning)
def test_check_min_workers_no_warning(self):
"""Test minimum workers check with no warning."""
- settings = LaborLawSettingsFactory(min_workers=2)
+ LaborLawSettingsFactory(min_workers=2)
work_shift_type = WorkShiftTypeFactory()
-
+
# Create enough workers
employee1 = EmployeeFactory()
employee2 = EmployeeFactory()
- ShiftFactory(employee=employee1, shift_type=work_shift_type, date=date(2025, 1, 1))
- ShiftFactory(employee=employee2, shift_type=work_shift_type, date=date(2025, 1, 1))
-
+ ShiftFactory(
+ employee=employee1, shift_type=work_shift_type, date=date(2025, 1, 1)
+ )
+ ShiftFactory(
+ employee=employee2, shift_type=work_shift_type, date=date(2025, 1, 1)
+ )
+
# This should not trigger a warning
shift = ShiftFactory(
employee=EmployeeFactory(),
shift_type=RestShiftTypeFactory(), # Rest shift
- date=date(2025, 1, 1)
+ date=date(2025, 1, 1),
)
-
+
warning = shift.check_min_workers()
self.assertIsNone(warning)
def test_check_min_workers_with_warning(self):
"""Test minimum workers check with warning."""
- settings = LaborLawSettingsFactory(min_workers=2)
+ LaborLawSettingsFactory(min_workers=2)
work_shift_type = WorkShiftTypeFactory()
-
+
# Create only one worker
employee1 = EmployeeFactory()
- ShiftFactory(employee=employee1, shift_type=work_shift_type, date=date(2025, 1, 1))
-
+ ShiftFactory(
+ employee=employee1, shift_type=work_shift_type, date=date(2025, 1, 1)
+ )
+
# This should trigger a warning
shift = ShiftFactory(
employee=EmployeeFactory(),
shift_type=RestShiftTypeFactory(), # Rest shift
- date=date(2025, 1, 1)
+ date=date(2025, 1, 1),
)
-
+
warning = shift.check_min_workers()
self.assertIsNotNone(warning)
- self.assertTrue(warning['warning'])
- self.assertIn('勤務者数が1人となり', warning['message'])
- self.assertEqual(warning['current_workers'], 1)
- self.assertEqual(warning['min_workers'], 2)
+ self.assertTrue(warning["warning"])
+ self.assertIn("勤務者数が1人となり", warning["message"])
+ self.assertEqual(warning["current_workers"], 1)
+ self.assertEqual(warning["min_workers"], 2)
def test_check_min_workers_company_holiday(self):
"""Test minimum workers check on company holiday."""
- settings = LaborLawSettingsFactory(min_workers=2)
+ LaborLawSettingsFactory(min_workers=2)
CompanyHolidayFactory(date=date(2025, 1, 1))
-
+
# This should not trigger a warning on company holiday
shift = ShiftFactory(
employee=EmployeeFactory(),
shift_type=RestShiftTypeFactory(),
- date=date(2025, 1, 1)
+ date=date(2025, 1, 1),
)
-
+
warning = shift.check_min_workers()
self.assertIsNone(warning)
def test_check_min_workers_update_scenario(self):
"""Test minimum workers check when updating a shift."""
- settings = LaborLawSettingsFactory(min_workers=2)
+ LaborLawSettingsFactory(min_workers=2)
work_shift_type = WorkShiftTypeFactory()
rest_shift_type = RestShiftTypeFactory()
-
+
# Create one worker
employee1 = EmployeeFactory()
- ShiftFactory(employee=employee1, shift_type=work_shift_type, date=date(2025, 1, 1))
-
+ ShiftFactory(
+ employee=employee1, shift_type=work_shift_type, date=date(2025, 1, 1)
+ )
+
# Create a shift that will be updated
employee2 = EmployeeFactory()
shift = ShiftFactory(
employee=employee2,
shift_type=work_shift_type, # Initially work shift
- date=date(2025, 1, 1)
+ date=date(2025, 1, 1),
)
-
+
# Update to rest shift - should trigger warning
shift.shift_type = rest_shift_type
warning = shift.check_min_workers()
self.assertIsNotNone(warning)
- self.assertTrue(warning['warning'])
- self.assertIn('勤務者数が1人となり', warning['message'])
- self.assertEqual(warning['current_workers'], 1)
+ self.assertTrue(warning["warning"])
+ self.assertIn("勤務者数が1人となり", warning["message"])
+ self.assertEqual(warning["current_workers"], 1)
@pytest.mark.slow
def test_create_auto_shifts(self):
"""Test auto shift creation."""
- settings = LaborLawSettingsFactory(min_workers=2, max_consecutive_work_days=6)
- employees = [EmployeeFactory() for _ in range(3)]
- work_shift_type = WorkShiftTypeFactory()
- rest_shift_type = RestShiftTypeFactory()
-
+ LaborLawSettingsFactory(min_workers=2, max_consecutive_work_days=6)
+ [EmployeeFactory() for _ in range(3)]
+ WorkShiftTypeFactory()
+ RestShiftTypeFactory()
+
# Create company holiday
CompanyHolidayFactory(date=date(2025, 1, 15))
-
- result = Shift.create_auto_shifts(2025, 1, 'fill_gaps')
-
- self.assertTrue(result['success'])
- self.assertGreater(result['created_count'], 0)
- self.assertIn('作成し', result['message'])
+
+ result = Shift.create_auto_shifts(2025, 1, "fill_gaps")
+
+ self.assertTrue(result["success"])
+ self.assertGreater(result["created_count"], 0)
+ self.assertIn("作成し", result["message"])
def test_create_auto_shifts_no_employees(self):
"""Test auto shift creation with no employees."""
- result = Shift.create_auto_shifts(2025, 1, 'fill_gaps')
- self.assertFalse(result['success'])
- self.assertIn('従業員が登録されていません', result['error'])
+ result = Shift.create_auto_shifts(2025, 1, "fill_gaps")
+ self.assertFalse(result["success"])
+ self.assertIn("従業員が登録されていません", result["error"])
def test_create_auto_shifts_no_work_shift_types(self):
"""Test auto shift creation with no work shift types."""
EmployeeFactory()
RestShiftTypeFactory()
-
- result = Shift.create_auto_shifts(2025, 1, 'fill_gaps')
- self.assertFalse(result['success'])
- self.assertIn('勤務シフト種別が登録されていません', result['error'])
+
+ result = Shift.create_auto_shifts(2025, 1, "fill_gaps")
+ self.assertFalse(result["success"])
+ self.assertIn("勤務シフト種別が登録されていません", result["error"])
def test_create_auto_shifts_no_rest_shift_type(self):
"""Test auto shift creation with no rest shift type."""
EmployeeFactory()
WorkShiftTypeFactory()
-
- result = Shift.create_auto_shifts(2025, 1, 'fill_gaps')
- self.assertFalse(result['success'])
- self.assertIn('休みシフト種別が登録されていません', result['error'])
+
+ result = Shift.create_auto_shifts(2025, 1, "fill_gaps")
+ self.assertFalse(result["success"])
+ self.assertIn("休みシフト種別が登録されていません", result["error"])
def test_shift_employee_relationship(self):
employee = EmployeeFactory()
@@ -505,36 +505,37 @@ def test_shift_date_field(self):
def test_create_auto_shifts_min_workers_compliance(self):
"""Test that auto shift creation respects minimum workers requirement."""
settings = LaborLawSettingsFactory(min_workers=2, max_consecutive_work_days=6)
- employees = [EmployeeFactory() for _ in range(3)]
- work_shift_type = WorkShiftTypeFactory()
- rest_shift_type = RestShiftTypeFactory()
-
+ [EmployeeFactory() for _ in range(3)]
+ WorkShiftTypeFactory()
+ RestShiftTypeFactory()
+
# Create auto shifts
- result = Shift.create_auto_shifts(2025, 1, 'overwrite')
-
- self.assertTrue(result['success'])
- self.assertGreater(result['created_count'], 0)
-
+ result = Shift.create_auto_shifts(2025, 1, "overwrite")
+
+ self.assertTrue(result["success"])
+ self.assertGreater(result["created_count"], 0)
+
# Check that each day has at least min_workers working
for day in range(1, 32):
try:
target_date = date(2025, 1, day)
work_shifts = Shift.objects.filter(
- date=target_date,
- shift_type__is_work=True
+ date=target_date, shift_type__is_work=True
)
work_count = work_shifts.count()
-
+
# Skip company holidays
- company_holiday = CompanyHoliday.objects.filter(date=target_date).first()
+ company_holiday = CompanyHoliday.objects.filter(
+ date=target_date
+ ).first()
if company_holiday:
continue
-
+
# Check minimum workers requirement
self.assertGreaterEqual(
- work_count,
+ work_count,
settings.min_workers,
- f"Day {day} has only {work_count} workers, but minimum is {settings.min_workers}"
+ f"Day {day} has only {work_count} workers, but minimum is {settings.min_workers}",
)
except ValueError:
# Skip invalid dates (e.g., February 30th)
@@ -544,31 +545,33 @@ def test_create_auto_shifts_min_workers_compliance(self):
def test_create_auto_shifts_consecutive_work_days_override(self):
"""Test that consecutive work days limit can be overridden for minimum workers."""
# 設定: 最低労働者数2人、最大連続勤務日数3日
- settings = LaborLawSettingsFactory(min_workers=2, max_consecutive_work_days=3)
-
+ LaborLawSettingsFactory(min_workers=2, max_consecutive_work_days=3)
+
# 従業員3人を作成
- employees = [EmployeeFactory() for _ in range(3)]
-
+ [EmployeeFactory() for _ in range(3)]
+
# シフト種別を作成
- work_shift = WorkShiftTypeFactory()
- rest_shift = RestShiftTypeFactory()
-
+ WorkShiftTypeFactory()
+ RestShiftTypeFactory()
+
# 2025年1月の自動シフト作成
result = Shift.create_auto_shifts(2025, 1)
-
- self.assertTrue(result['success'])
- self.assertGreater(result['created_count'], 0)
-
+
+ self.assertTrue(result["success"])
+ self.assertGreater(result["created_count"], 0)
+
# 各日について最低労働者数が満たされていることを確認
for day in range(1, 32):
try:
target_date = date(2025, 1, day)
work_shifts = Shift.objects.filter(
- date=target_date,
- shift_type__is_work=True
+ date=target_date, shift_type__is_work=True
+ )
+ self.assertGreaterEqual(
+ work_shifts.count(),
+ 2,
+ f"Day {day} has only {work_shifts.count()} workers, need at least 2",
)
- self.assertGreaterEqual(work_shifts.count(), 2,
- f"Day {day} has only {work_shifts.count()} workers, need at least 2")
except ValueError:
# 1月は31日までなので、32日目以降は無視
pass
@@ -577,21 +580,21 @@ def test_create_auto_shifts_consecutive_work_days_override(self):
def test_create_auto_shifts_work_days_distribution(self):
"""Test that work days are distributed evenly among employees."""
# 設定: 最低労働者数1人
- settings = LaborLawSettingsFactory(min_workers=1, max_consecutive_work_days=6)
-
+ LaborLawSettingsFactory(min_workers=1, max_consecutive_work_days=6)
+
# 従業員3人を作成
employees = [EmployeeFactory() for _ in range(3)]
-
+
# シフト種別を作成
- work_shift = WorkShiftTypeFactory()
- rest_shift = RestShiftTypeFactory()
-
+ WorkShiftTypeFactory()
+ RestShiftTypeFactory()
+
# 2025年1月の自動シフト作成
result = Shift.create_auto_shifts(2025, 1)
-
- self.assertTrue(result['success'])
- self.assertGreater(result['created_count'], 0)
-
+
+ self.assertTrue(result["success"])
+ self.assertGreater(result["created_count"], 0)
+
# 各従業員の勤務日数をカウント
work_days_by_employee = {}
for employee in employees:
@@ -599,29 +602,34 @@ def test_create_auto_shifts_work_days_distribution(self):
employee=employee,
date__year=2025,
date__month=1,
- shift_type__is_work=True
+ shift_type__is_work=True,
).count()
work_days_by_employee[employee.id] = work_days
-
+
# 勤務日数の分布を確認
work_days_list = list(work_days_by_employee.values())
min_work_days = min(work_days_list)
max_work_days = max(work_days_list)
-
+
# 勤務日数の差が7日以内であることを確認(均等化の効果)
- self.assertLessEqual(max_work_days - min_work_days, 7,
- f"Work days distribution is too uneven: min={min_work_days}, max={max_work_days}")
-
+ self.assertLessEqual(
+ max_work_days - min_work_days,
+ 7,
+ f"Work days distribution is too uneven: min={min_work_days}, max={max_work_days}",
+ )
+
# 最低労働者数が満たされていることを確認
for day in range(1, 32):
try:
target_date = date(2025, 1, day)
work_shifts = Shift.objects.filter(
- date=target_date,
- shift_type__is_work=True
+ date=target_date, shift_type__is_work=True
+ )
+ self.assertGreaterEqual(
+ work_shifts.count(),
+ 1,
+ f"Day {day} has only {work_shifts.count()} workers, need at least 1",
)
- self.assertGreaterEqual(work_shifts.count(), 1,
- f"Day {day} has only {work_shifts.count()} workers, need at least 1")
except ValueError:
# 1月は31日までなので、32日目以降は無視
pass
@@ -630,27 +638,35 @@ def test_create_auto_shifts_work_days_distribution(self):
def test_create_auto_shifts_work_days_distribution_with_existing_shifts(self):
"""Test work days distribution when some shifts already exist."""
# 設定: 最低労働者数1人
- settings = LaborLawSettingsFactory(min_workers=1, max_consecutive_work_days=6)
-
+ LaborLawSettingsFactory(min_workers=1, max_consecutive_work_days=6)
+
# 従業員3人を作成
employees = [EmployeeFactory() for _ in range(3)]
-
+
# シフト種別を作成
work_shift = WorkShiftTypeFactory()
rest_shift = RestShiftTypeFactory()
-
+
# 既存のシフトを作成(一部の従業員に偏りを持たせる)
- ShiftFactory(employee=employees[0], date=date(2025, 1, 1), shift_type=work_shift)
- ShiftFactory(employee=employees[0], date=date(2025, 1, 2), shift_type=work_shift)
- ShiftFactory(employee=employees[0], date=date(2025, 1, 3), shift_type=work_shift)
- ShiftFactory(employee=employees[1], date=date(2025, 1, 1), shift_type=rest_shift)
-
+ ShiftFactory(
+ employee=employees[0], date=date(2025, 1, 1), shift_type=work_shift
+ )
+ ShiftFactory(
+ employee=employees[0], date=date(2025, 1, 2), shift_type=work_shift
+ )
+ ShiftFactory(
+ employee=employees[0], date=date(2025, 1, 3), shift_type=work_shift
+ )
+ ShiftFactory(
+ employee=employees[1], date=date(2025, 1, 1), shift_type=rest_shift
+ )
+
# 2025年1月の自動シフト作成(fill_gapsモード)
- result = Shift.create_auto_shifts(2025, 1, creation_mode='fill_gaps')
-
- self.assertTrue(result['success'])
- self.assertGreater(result['created_count'], 0)
-
+ result = Shift.create_auto_shifts(2025, 1, creation_mode="fill_gaps")
+
+ self.assertTrue(result["success"])
+ self.assertGreater(result["created_count"], 0)
+
# 各従業員の勤務日数をカウント
work_days_by_employee = {}
for employee in employees:
@@ -658,15 +674,10 @@ def test_create_auto_shifts_work_days_distribution_with_existing_shifts(self):
employee=employee,
date__year=2025,
date__month=1,
- shift_type__is_work=True
+ shift_type__is_work=True,
).count()
work_days_by_employee[employee.id] = work_days
-
- # 勤務日数の分布を確認
- work_days_list = list(work_days_by_employee.values())
- min_work_days = min(work_days_list)
- max_work_days = max(work_days_list)
-
+
# 既存のシフトがある場合でも、新しく作成されるシフトは均等に分配される
# ただし、既存の偏りがあるため、完全な均等化は期待できない
# 最低労働者数は満たされていることを確認
@@ -674,11 +685,13 @@ def test_create_auto_shifts_work_days_distribution_with_existing_shifts(self):
try:
target_date = date(2025, 1, day)
work_shifts = Shift.objects.filter(
- date=target_date,
- shift_type__is_work=True
+ date=target_date, shift_type__is_work=True
+ )
+ self.assertGreaterEqual(
+ work_shifts.count(),
+ 1,
+ f"Day {day} has only {work_shifts.count()} workers, need at least 1",
)
- self.assertGreaterEqual(work_shifts.count(), 1,
- f"Day {day} has only {work_shifts.count()} workers, need at least 1")
except ValueError:
# 1月は31日までなので、32日目以降は無視
pass
@@ -687,63 +700,72 @@ def test_create_auto_shifts_work_days_distribution_with_existing_shifts(self):
def test_create_auto_shifts_work_days_distribution_with_company_holidays(self):
"""Test work days distribution when company holidays exist."""
# 設定: 最低労働者数1人
- settings = LaborLawSettingsFactory(min_workers=1, max_consecutive_work_days=6)
-
+ LaborLawSettingsFactory(min_workers=1, max_consecutive_work_days=6)
+
# 従業員3人を作成
employees = [EmployeeFactory() for _ in range(3)]
-
+
# シフト種別を作成
- work_shift = WorkShiftTypeFactory()
- rest_shift = RestShiftTypeFactory()
-
+ WorkShiftTypeFactory()
+ RestShiftTypeFactory()
+
# 会社休日を作成
CompanyHolidayFactory(date=date(2025, 1, 1), name="元日")
CompanyHolidayFactory(date=date(2025, 1, 2), name="振替休日")
-
+
# 2025年1月の自動シフト作成
result = Shift.create_auto_shifts(2025, 1)
-
- self.assertTrue(result['success'])
- self.assertGreater(result['created_count'], 0)
-
+
+ self.assertTrue(result["success"])
+ self.assertGreater(result["created_count"], 0)
+
# 会社休日は全員休みになっていることを確認
for holiday_date in [date(2025, 1, 1), date(2025, 1, 2)]:
shifts = Shift.objects.filter(date=holiday_date)
for shift in shifts:
- self.assertFalse(shift.shift_type.is_work,
- f"Employee {shift.employee.name} is working on holiday {holiday_date}")
-
+ self.assertFalse(
+ shift.shift_type.is_work,
+ f"Employee {shift.employee.name} is working on holiday {holiday_date}",
+ )
+
# 各従業員の勤務日数をカウント(会社休日を除く)
work_days_by_employee = {}
for employee in employees:
- work_days = Shift.objects.filter(
- employee=employee,
- date__year=2025,
- date__month=1,
- shift_type__is_work=True
- ).exclude(date__in=[date(2025, 1, 1), date(2025, 1, 2)]).count()
+ work_days = (
+ Shift.objects.filter(
+ employee=employee,
+ date__year=2025,
+ date__month=1,
+ shift_type__is_work=True,
+ )
+ .exclude(date__in=[date(2025, 1, 1), date(2025, 1, 2)])
+ .count()
+ )
work_days_by_employee[employee.id] = work_days
-
+
# 勤務日数の分布を確認
work_days_list = list(work_days_by_employee.values())
min_work_days = min(work_days_list)
max_work_days = max(work_days_list)
-
+
# 会社休日を除いても、勤務日数は均等に分配される
- self.assertLessEqual(max_work_days - min_work_days, 7,
- f"Work days distribution is too uneven: min={min_work_days}, max={max_work_days}")
+ self.assertLessEqual(
+ max_work_days - min_work_days,
+ 7,
+ f"Work days distribution is too uneven: min={min_work_days}, max={max_work_days}",
+ )
def test_shift_check_shift_type_worker_limits_min_workers(self):
"""Test shift check shift type worker limits for min workers."""
shift_type = ShiftTypeFactory(min_workers=2, max_workers=None)
employee = EmployeeFactory()
shift = ShiftFactory(employee=employee, shift_type=shift_type)
-
+
# 最低人数を下回る場合の警告
warning = shift.check_shift_type_worker_limits()
self.assertIsNotNone(warning)
- self.assertTrue(warning['warning'])
- self.assertIn('最低人数', warning['message'])
+ self.assertTrue(warning["warning"])
+ self.assertIn("最低人数", warning["message"])
def test_shift_check_shift_type_worker_limits_max_workers(self):
"""Test shift check shift type worker limits for max workers."""
@@ -751,25 +773,27 @@ def test_shift_check_shift_type_worker_limits_max_workers(self):
employee1 = EmployeeFactory()
employee2 = EmployeeFactory()
employee3 = EmployeeFactory()
-
+
# 既に2人のシフトがある状態で3人目を追加
ShiftFactory(employee=employee1, shift_type=shift_type, date=date(2025, 1, 1))
ShiftFactory(employee=employee2, shift_type=shift_type, date=date(2025, 1, 1))
-
- shift3 = ShiftFactory(employee=employee3, shift_type=shift_type, date=date(2025, 1, 1))
-
+
+ shift3 = ShiftFactory(
+ employee=employee3, shift_type=shift_type, date=date(2025, 1, 1)
+ )
+
# 最大人数を超える場合の警告
warning = shift3.check_shift_type_worker_limits()
self.assertIsNotNone(warning)
- self.assertTrue(warning['warning'])
- self.assertIn('最大人数', warning['message'])
+ self.assertTrue(warning["warning"])
+ self.assertIn("最大人数", warning["message"])
def test_shift_check_shift_type_worker_limits_rest_shift(self):
"""Test shift check shift type worker limits for rest shift."""
shift_type = ShiftTypeFactory(is_work=False, min_workers=1, max_workers=2)
employee = EmployeeFactory()
shift = ShiftFactory(employee=employee, shift_type=shift_type)
-
+
# 休みの場合はチェックしない
warning = shift.check_shift_type_worker_limits()
self.assertIsNone(warning)
@@ -799,4 +823,4 @@ def test_role_unique_name(self):
"""Test role name uniqueness."""
RoleFactory(name="ホール")
with self.assertRaises(Exception):
- RoleFactory(name="ホール")
\ No newline at end of file
+ RoleFactory(name="ホール")
diff --git a/shift/test_files/test_views.py b/shift/test_files/test_views.py
index 76187e0..d7fe6a8 100644
--- a/shift/test_files/test_views.py
+++ b/shift/test_files/test_views.py
@@ -2,27 +2,30 @@
Tests for views.
"""
-import os
-import django
import json
+import os
from datetime import date, timedelta
-from django.test import TestCase, Client
-from django.urls import reverse
-from django.contrib.auth.models import User
+
+import django
from django.contrib.auth.tokens import default_token_generator
-from django.utils.http import urlsafe_base64_encode
+from django.test import Client, TestCase
+from django.urls import reverse
from django.utils.encoding import force_bytes
+from django.utils.http import urlsafe_base64_encode
# Django設定を確実に読み込む
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shift_table.settings_test')
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shift_table.settings_test")
django.setup()
-from ..models import Employee, ShiftType, CompanyHoliday, LaborLawSettings, Shift
from ..factories import (
- EmployeeFactory, ShiftTypeFactory, WorkShiftTypeFactory,
- RestShiftTypeFactory, CompanyHolidayFactory, LaborLawSettingsFactory,
- ShiftFactory, UserFactory
+ CompanyHolidayFactory,
+ EmployeeFactory,
+ LaborLawSettingsFactory,
+ ShiftFactory,
+ ShiftTypeFactory,
+ UserFactory,
)
+from ..models import Employee, LaborLawSettings, Shift, ShiftType
class ShiftTableViewTest(TestCase):
@@ -41,73 +44,70 @@ def setUp(self):
def test_shift_table_view_get(self):
"""Test shift table view GET request."""
- response = self.client.get(reverse('shift_table'))
+ response = self.client.get(reverse("shift_table"))
self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'shift/shift_table.html')
+ self.assertTemplateUsed(response, "shift/shift_table.html")
def test_shift_table_view_with_year_month_params(self):
"""Test shift table view with year and month parameters."""
- response = self.client.get(reverse('shift_table'), {
- 'year': 2025,
- 'month': 1
- })
+ response = self.client.get(reverse("shift_table"), {"year": 2025, "month": 1})
self.assertEqual(response.status_code, 200)
- self.assertIn('year', response.context)
- self.assertIn('month', response.context)
- self.assertEqual(response.context['year'], 2025)
- self.assertEqual(response.context['month'], 1)
+ self.assertIn("year", response.context)
+ self.assertIn("month", response.context)
+ self.assertEqual(response.context["year"], 2025)
+ self.assertEqual(response.context["month"], 1)
def test_shift_table_view_context_data(self):
"""Test shift table view context data."""
# Create test data
employee = EmployeeFactory()
shift_type = ShiftTypeFactory()
- shift = ShiftFactory(employee=employee, shift_type=shift_type, date=self.date)
-
- response = self.client.get(reverse('shift_table'))
-
- self.assertIn('employees', response.context)
- self.assertIn('shift_dict', response.context)
- self.assertIn('shift_types', response.context)
- self.assertIn('days', response.context)
- self.assertIn('day_info', response.context)
+ ShiftFactory(employee=employee, shift_type=shift_type, date=self.date)
+
+ response = self.client.get(reverse("shift_table"))
+
+ self.assertIn("employees", response.context)
+ self.assertIn("shift_dict", response.context)
+ self.assertIn("shift_types", response.context)
+ self.assertIn("days", response.context)
+ self.assertIn("day_info", response.context)
def test_shift_table_view_day_info(self):
"""Test shift table view day information."""
- response = self.client.get(reverse('shift_table'))
- day_info = response.context['day_info']
-
+ response = self.client.get(reverse("shift_table"))
+ day_info = response.context["day_info"]
+
# Check that day_info contains expected keys
for day, info in day_info.items():
- self.assertIn('weekday', info)
- self.assertIn('color', info)
- self.assertIn('is_holiday', info)
- self.assertIn('is_company_holiday', info)
+ self.assertIn("weekday", info)
+ self.assertIn("color", info)
+ self.assertIn("is_holiday", info)
+ self.assertIn("is_company_holiday", info)
def test_shift_table_view_company_holidays(self):
"""Test shift table view with company holidays."""
holiday = CompanyHolidayFactory()
-
- response = self.client.get(reverse('shift_table'), {
- 'year': holiday.date.year,
- 'month': holiday.date.month
- })
-
- day_info = response.context['day_info']
+
+ response = self.client.get(
+ reverse("shift_table"),
+ {"year": holiday.date.year, "month": holiday.date.month},
+ )
+
+ day_info = response.context["day_info"]
holiday_info = day_info[holiday.date]
- self.assertTrue(holiday_info['is_company_holiday'])
- self.assertEqual(holiday_info['company_holiday_name'], holiday.name)
+ self.assertTrue(holiday_info["is_company_holiday"])
+ self.assertEqual(holiday_info["company_holiday_name"], holiday.name)
def test_shift_table_view_pagination(self):
"""Test shift table view pagination."""
- response = self.client.get(reverse('shift_table'))
-
- self.assertIn('prev_year', response.context)
- self.assertIn('prev_month', response.context)
- self.assertIn('next_year', response.context)
- self.assertIn('next_month', response.context)
- self.assertIn('show_prev', response.context)
- self.assertIn('show_next', response.context)
+ response = self.client.get(reverse("shift_table"))
+
+ self.assertIn("prev_year", response.context)
+ self.assertIn("prev_month", response.context)
+ self.assertIn("next_year", response.context)
+ self.assertIn("next_month", response.context)
+ self.assertIn("show_prev", response.context)
+ self.assertIn("show_next", response.context)
class SaveShiftViewTest(TestCase):
@@ -127,185 +127,179 @@ def setUp(self):
def test_save_shift_new_shift(self):
data = {
- 'employee_id': self.employee.id,
- 'date': self.date.isoformat(),
- 'shift_type_id': self.shift_type.id
+ "employee_id": self.employee.id,
+ "date": self.date.isoformat(),
+ "shift_type_id": self.shift_type.id,
}
response = self.client.post(
- reverse('save_shift'),
+ reverse("save_shift"),
data=json.dumps(data),
- content_type='application/json'
+ content_type="application/json",
)
result = json.loads(response.content)
print(result)
- self.assertTrue(result['success'])
+ self.assertTrue(result["success"])
def test_save_shift_update_existing(self):
# 既存のシフトを作成
shift = ShiftFactory(
- employee=self.employee,
- date=self.date,
- shift_type=self.shift_type
+ employee=self.employee, date=self.date, shift_type=self.shift_type
)
-
+
# 新しいシフト種別を作成
new_shift_type = ShiftTypeFactory(name=f"休み_{self._testMethodName}")
-
+
data = {
- 'shift_id': shift.id,
- 'employee_id': self.employee.id,
- 'date': self.date.isoformat(),
- 'shift_type_id': new_shift_type.id
+ "shift_id": shift.id,
+ "employee_id": self.employee.id,
+ "date": self.date.isoformat(),
+ "shift_type_id": new_shift_type.id,
}
response = self.client.post(
- reverse('save_shift'),
+ reverse("save_shift"),
data=json.dumps(data),
- content_type='application/json'
+ content_type="application/json",
)
result = json.loads(response.content)
- self.assertTrue(result['success'])
+ self.assertTrue(result["success"])
def test_save_shift_duplicate_shift(self):
- ShiftFactory(
- employee=self.employee,
- date=self.date,
- shift_type=self.shift_type
- )
+ ShiftFactory(employee=self.employee, date=self.date, shift_type=self.shift_type)
data = {
- 'employee_id': self.employee.id,
- 'date': self.date.isoformat(),
- 'shift_type_id': self.shift_type.id
+ "employee_id": self.employee.id,
+ "date": self.date.isoformat(),
+ "shift_type_id": self.shift_type.id,
}
response = self.client.post(
- reverse('save_shift'),
+ reverse("save_shift"),
data=json.dumps(data),
- content_type='application/json'
+ content_type="application/json",
)
result = json.loads(response.content)
print(result)
- self.assertIn('既にシフトが登録されています', result['error'])
+ self.assertIn("既にシフトが登録されています", result["error"])
def test_save_shift_nonexistent_shift_id(self):
data = {
- 'shift_id': 999, # 存在しないID
- 'employee_id': self.employee.id,
- 'date': self.date.isoformat(),
- 'shift_type_id': self.shift_type.id
+ "shift_id": 999, # 存在しないID
+ "employee_id": self.employee.id,
+ "date": self.date.isoformat(),
+ "shift_type_id": self.shift_type.id,
}
response = self.client.post(
- reverse('save_shift'),
+ reverse("save_shift"),
data=json.dumps(data),
- content_type='application/json'
+ content_type="application/json",
)
result = json.loads(response.content)
- self.assertIn('シフトが見つかりません', result['error'])
+ self.assertIn("シフトが見つかりません", result["error"])
def test_save_shift_invalid_data(self):
data = {
- 'employee_id': 999, # 存在しないID
- 'date': 'invalid-date',
- 'shift_type_id': 999 # 存在しないID
+ "employee_id": 999, # 存在しないID
+ "date": "invalid-date",
+ "shift_type_id": 999, # 存在しないID
}
response = self.client.post(
- reverse('save_shift'),
+ reverse("save_shift"),
data=json.dumps(data),
- content_type='application/json'
+ content_type="application/json",
)
result = json.loads(response.content)
- self.assertIn('無効な日付です', result['error'])
+ self.assertIn("無効な日付です", result["error"])
def test_save_shift_consecutive_work_days_warning(self):
# 連続勤務日数制限の設定を作成
- settings = LaborLawSettingsFactory(max_consecutive_work_days=3)
-
+ LaborLawSettingsFactory(max_consecutive_work_days=3)
+
# 連続で勤務日を作成
- work_shift_type = ShiftTypeFactory(name=f"出勤_{self._testMethodName}", is_work=True)
+ work_shift_type = ShiftTypeFactory(
+ name=f"出勤_{self._testMethodName}", is_work=True
+ )
for i in range(3):
ShiftFactory(
employee=self.employee,
date=date(2025, 1, 1) + timedelta(days=i),
- shift_type=work_shift_type
+ shift_type=work_shift_type,
)
-
+
# 4日目の勤務を追加(警告が発生するはず)
data = {
- 'employee_id': self.employee.id,
- 'date': date(2025, 1, 4).isoformat(),
- 'shift_type_id': work_shift_type.id
+ "employee_id": self.employee.id,
+ "date": date(2025, 1, 4).isoformat(),
+ "shift_type_id": work_shift_type.id,
}
response = self.client.post(
- reverse('save_shift'),
+ reverse("save_shift"),
data=json.dumps(data),
- content_type='application/json'
+ content_type="application/json",
)
result = json.loads(response.content)
- self.assertFalse(result['success'])
- self.assertIn('連続勤務日数が4日となり', result['warning'])
+ self.assertFalse(result["success"])
+ self.assertIn("連続勤務日数が4日となり", result["warning"])
def test_save_shift_min_workers_warning(self):
# 最低労働者数制限の設定を作成
- settings = LaborLawSettingsFactory(min_workers=2)
-
+ LaborLawSettingsFactory(min_workers=2)
+
# 他の従業員のシフトを作成(休み)
other_employee = EmployeeFactory()
- rest_shift_type = ShiftTypeFactory(name=f"休み_{self._testMethodName}", is_work=False)
+ rest_shift_type = ShiftTypeFactory(
+ name=f"休み_{self._testMethodName}", is_work=False
+ )
ShiftFactory(
- employee=other_employee,
- date=self.date,
- shift_type=rest_shift_type
+ employee=other_employee, date=self.date, shift_type=rest_shift_type
)
-
+
# 勤務シフトを追加(最低労働者数警告が発生するはず)
- work_shift_type = ShiftTypeFactory(name=f"出勤_{self._testMethodName}", is_work=True)
+ work_shift_type = ShiftTypeFactory(
+ name=f"出勤_{self._testMethodName}", is_work=True
+ )
data = {
- 'employee_id': self.employee.id,
- 'date': self.date.isoformat(),
- 'shift_type_id': work_shift_type.id
+ "employee_id": self.employee.id,
+ "date": self.date.isoformat(),
+ "shift_type_id": work_shift_type.id,
}
response = self.client.post(
- reverse('save_shift'),
+ reverse("save_shift"),
data=json.dumps(data),
- content_type='application/json'
+ content_type="application/json",
)
result = json.loads(response.content)
# 最低労働者数が1人の場合、警告は発生しない
- self.assertTrue(result['success'])
+ self.assertTrue(result["success"])
def test_save_shift_force_save(self):
"""Test force saving shift with warnings."""
- settings = LaborLawSettingsFactory(max_consecutive_work_days=2)
+ LaborLawSettingsFactory(max_consecutive_work_days=2)
work_shift_type = ShiftTypeFactory()
-
+
# Create consecutive work days
ShiftFactory(
- employee=self.employee,
- shift_type=work_shift_type,
- date=date(2025, 1, 1)
+ employee=self.employee, shift_type=work_shift_type, date=date(2025, 1, 1)
)
ShiftFactory(
- employee=self.employee,
- shift_type=work_shift_type,
- date=date(2025, 1, 2)
+ employee=self.employee, shift_type=work_shift_type, date=date(2025, 1, 2)
)
-
+
# Force save with warning
data = {
- 'employee_id': self.employee.id,
- 'date': date(2025, 1, 3).isoformat(),
- 'shift_type_id': work_shift_type.id,
- 'force_save': True
+ "employee_id": self.employee.id,
+ "date": date(2025, 1, 3).isoformat(),
+ "shift_type_id": work_shift_type.id,
+ "force_save": True,
}
-
+
response = self.client.post(
- reverse('save_shift'),
+ reverse("save_shift"),
data=json.dumps(data),
- content_type='application/json'
+ content_type="application/json",
)
-
+
self.assertEqual(response.status_code, 200)
result = json.loads(response.content)
- self.assertTrue(result['success'])
-
+ self.assertTrue(result["success"])
+
# Check that shift was created despite warning
shift = Shift.objects.get(employee=self.employee, date=date(2025, 1, 3))
self.assertEqual(shift.shift_type, work_shift_type)
@@ -313,15 +307,13 @@ def test_save_shift_force_save(self):
def test_save_shift_invalid_json(self):
"""Test saving shift with invalid JSON."""
response = self.client.post(
- reverse('save_shift'),
- data='invalid json',
- content_type='application/json'
+ reverse("save_shift"), data="invalid json", content_type="application/json"
)
-
+
self.assertEqual(response.status_code, 200)
result = json.loads(response.content)
- self.assertFalse(result['success'])
- self.assertIn('無効なJSONデータです', result['error'])
+ self.assertFalse(result["success"])
+ self.assertIn("無効なJSONデータです", result["error"])
class DeleteShiftViewTest(TestCase):
@@ -337,33 +329,37 @@ def setUp(self):
self.employee = EmployeeFactory()
self.shift_type = ShiftTypeFactory()
self.date = date(2025, 1, 1)
- self.shift = ShiftFactory(employee=self.employee, shift_type=self.shift_type, date=self.date)
+ self.shift = ShiftFactory(
+ employee=self.employee, shift_type=self.shift_type, date=self.date
+ )
def test_delete_shift_success(self):
"""Test successful shift deletion."""
- response = self.client.post(reverse('delete_shift', args=[self.shift.id]))
-
+ response = self.client.post(reverse("delete_shift", args=[self.shift.id]))
+
self.assertEqual(response.status_code, 200)
result = json.loads(response.content)
- self.assertTrue(result['success'])
-
+ self.assertTrue(result["success"])
+
# Check that shift was deleted
with self.assertRaises(Shift.DoesNotExist):
Shift.objects.get(id=self.shift.id)
def test_delete_shift_nonexistent(self):
"""Test deleting nonexistent shift."""
- response = self.client.post(reverse('delete_shift', args=[99999]))
-
+ response = self.client.post(reverse("delete_shift", args=[99999]))
+
self.assertEqual(response.status_code, 200)
result = json.loads(response.content)
- self.assertFalse(result['success'])
- self.assertIn('シフトが見つかりません', result['error'])
+ self.assertFalse(result["success"])
+ self.assertIn("シフトが見つかりません", result["error"])
def test_shift_creation(self):
# 別の日付で作成
another_date = self.date + timedelta(days=1)
- shift = ShiftFactory(employee=self.employee, shift_type=self.shift_type, date=another_date)
+ shift = ShiftFactory(
+ employee=self.employee, shift_type=self.shift_type, date=another_date
+ )
self.assertIsNotNone(shift.id)
self.assertIsNotNone(shift.employee)
self.assertIsInstance(shift.date, date)
@@ -386,54 +382,54 @@ def setUp(self):
def test_password_reset_view_get(self):
"""Test password reset view GET request."""
- response = self.client.get(reverse('password_reset'))
+ response = self.client.get(reverse("password_reset"))
self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'registration/password_reset_form.html')
+ self.assertTemplateUsed(response, "registration/password_reset_form.html")
def test_password_reset_view_post(self):
"""Test password reset view POST request."""
- response = self.client.post(reverse('password_reset'), {
- 'email': self.user.email
- })
+ response = self.client.post(
+ reverse("password_reset"), {"email": self.user.email}
+ )
self.assertEqual(response.status_code, 302)
- self.assertRedirects(response, reverse('password_reset_done'))
+ self.assertRedirects(response, reverse("password_reset_done"))
def test_password_reset_view_invalid_email(self):
"""Test password reset view with invalid email."""
- response = self.client.post(reverse('password_reset'), {
- 'email': 'invalid-email@example.com'
- })
+ response = self.client.post(
+ reverse("password_reset"), {"email": "invalid-email@example.com"}
+ )
# Djangoは無効なメールアドレスでも成功レスポンスを返す(セキュリティのため)
self.assertEqual(response.status_code, 302)
- self.assertRedirects(response, reverse('password_reset_done'))
+ self.assertRedirects(response, reverse("password_reset_done"))
def test_password_reset_view_inactive_user(self):
"""Test password reset view with inactive user."""
self.user.is_active = False
self.user.save()
- response = self.client.post(reverse('password_reset'), {
- 'email': self.user.email
- })
+ response = self.client.post(
+ reverse("password_reset"), {"email": self.user.email}
+ )
# Djangoは非アクティブユーザーでも成功レスポンスを返す(セキュリティのため)
self.assertEqual(response.status_code, 302)
- self.assertRedirects(response, reverse('password_reset_done'))
+ self.assertRedirects(response, reverse("password_reset_done"))
def test_password_reset_view_user_not_found(self):
"""Test password reset view with user not found."""
- response = self.client.post(reverse('password_reset'), {
- 'email': 'nonexistent@example.com'
- })
+ response = self.client.post(
+ reverse("password_reset"), {"email": "nonexistent@example.com"}
+ )
# Djangoは存在しないユーザーでも成功レスポンスを返す(セキュリティのため)
self.assertEqual(response.status_code, 302)
- self.assertRedirects(response, reverse('password_reset_done'))
+ self.assertRedirects(response, reverse("password_reset_done"))
def test_password_reset_view_token_generation(self):
"""Test password reset view token generation."""
- response = self.client.post(reverse('password_reset'), {
- 'email': self.user.email
- })
+ response = self.client.post(
+ reverse("password_reset"), {"email": self.user.email}
+ )
self.assertEqual(response.status_code, 302)
- self.assertRedirects(response, reverse('password_reset_done'))
+ self.assertRedirects(response, reverse("password_reset_done"))
# Check that a password reset token was created
reset_token = default_token_generator.make_token(self.user)
@@ -445,7 +441,9 @@ def test_password_reset_view_token_validation(self):
"""Test password reset view token validation."""
token = default_token_generator.make_token(self.user)
uidb64 = urlsafe_base64_encode(force_bytes(self.user.pk))
- response = self.client.get(reverse('password_reset_confirm', args=[uidb64, token]))
+ response = self.client.get(
+ reverse("password_reset_confirm", args=[uidb64, token])
+ )
# Djangoは有効なトークンでもリダイレクトする場合がある
self.assertIn(response.status_code, [200, 302])
@@ -453,10 +451,10 @@ def test_password_reset_view_password_change(self):
"""Test password reset view password change."""
token = default_token_generator.make_token(self.user)
uidb64 = urlsafe_base64_encode(force_bytes(self.user.pk))
- response = self.client.post(reverse('password_reset_confirm', args=[uidb64, token]), {
- 'new_password1': 'newpassword123',
- 'new_password2': 'newpassword123'
- })
+ response = self.client.post(
+ reverse("password_reset_confirm", args=[uidb64, token]),
+ {"new_password1": "newpassword123", "new_password2": "newpassword123"},
+ )
# Djangoの実際の動作に合わせて調整
self.assertIn(response.status_code, [200, 302])
@@ -464,10 +462,10 @@ def test_password_reset_view_password_change_mismatch(self):
"""Test password reset view password change mismatch."""
token = default_token_generator.make_token(self.user)
uidb64 = urlsafe_base64_encode(force_bytes(self.user.pk))
- response = self.client.post(reverse('password_reset_confirm', args=[uidb64, token]), {
- 'new_password1': 'newpassword123',
- 'new_password2': 'newpassword1234'
- })
+ response = self.client.post(
+ reverse("password_reset_confirm", args=[uidb64, token]),
+ {"new_password1": "newpassword123", "new_password2": "newpassword1234"},
+ )
# Djangoの実際の動作に合わせて調整
self.assertIn(response.status_code, [200, 302])
@@ -475,10 +473,10 @@ def test_password_reset_view_password_change_invalid(self):
"""Test password reset view password change invalid."""
token = default_token_generator.make_token(self.user)
uidb64 = urlsafe_base64_encode(force_bytes(self.user.pk))
- response = self.client.post(reverse('password_reset_confirm', args=[uidb64, token]), {
- 'new_password1': '123',
- 'new_password2': '123'
- })
+ response = self.client.post(
+ reverse("password_reset_confirm", args=[uidb64, token]),
+ {"new_password1": "123", "new_password2": "123"},
+ )
# Djangoの実際の動作に合わせて調整
self.assertIn(response.status_code, [200, 302])
@@ -486,110 +484,120 @@ def test_password_reset_views_accessible_without_authentication(self):
"""Test that password reset views are accessible without authentication."""
# ログアウト状態でテスト
self.client.logout()
-
+
# パスワードリセットフォームページ
- response = self.client.get(reverse('password_reset'))
+ response = self.client.get(reverse("password_reset"))
self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'registration/password_reset_form.html')
-
+ self.assertTemplateUsed(response, "registration/password_reset_form.html")
+
# パスワードリセット完了ページ
- response = self.client.get(reverse('password_reset_done'))
+ response = self.client.get(reverse("password_reset_done"))
self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'registration/password_reset_done.html')
-
+ self.assertTemplateUsed(response, "registration/password_reset_done.html")
+
# パスワードリセット確認ページ(有効なトークンで)
token = default_token_generator.make_token(self.user)
uidb64 = urlsafe_base64_encode(force_bytes(self.user.pk))
- response = self.client.get(reverse('password_reset_confirm', args=[uidb64, token]))
+ response = self.client.get(
+ reverse("password_reset_confirm", args=[uidb64, token])
+ )
self.assertIn(response.status_code, [200, 302])
-
+
# パスワードリセット完了ページ
- response = self.client.get(reverse('password_reset_complete'))
+ response = self.client.get(reverse("password_reset_complete"))
self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'registration/password_reset_complete.html')
+ self.assertTemplateUsed(response, "registration/password_reset_complete.html")
def test_admin_password_reset_views_accessible_without_authentication(self):
"""Test that admin password reset views are accessible without authentication."""
# ログアウト状態でテスト
self.client.logout()
-
+
# 管理画面用パスワードリセットフォームページ
- response = self.client.get(reverse('admin_password_reset'))
+ response = self.client.get(reverse("admin_password_reset"))
self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'registration/password_reset_form.html')
-
+ self.assertTemplateUsed(response, "registration/password_reset_form.html")
+
# 管理画面用パスワードリセット完了ページ
- response = self.client.get(reverse('admin_password_reset_done'))
+ response = self.client.get(reverse("admin_password_reset_done"))
self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'registration/password_reset_done.html')
-
+ self.assertTemplateUsed(response, "registration/password_reset_done.html")
+
# 管理画面用パスワードリセット確認ページ(有効なトークンで)
token = default_token_generator.make_token(self.user)
uidb64 = urlsafe_base64_encode(force_bytes(self.user.pk))
- response = self.client.get(reverse('admin_password_reset_confirm', args=[uidb64, token]))
+ response = self.client.get(
+ reverse("admin_password_reset_confirm", args=[uidb64, token])
+ )
self.assertIn(response.status_code, [200, 302])
-
+
# 管理画面用パスワードリセット完了ページ
- response = self.client.get(reverse('admin_password_reset_complete'))
+ response = self.client.get(reverse("admin_password_reset_complete"))
self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'registration/password_reset_complete.html')
+ self.assertTemplateUsed(response, "registration/password_reset_complete.html")
def test_password_reset_views_not_redirected_to_login(self):
"""Test that password reset views are not redirected to login page."""
# ログアウト状態でテスト
self.client.logout()
-
+
# パスワードリセットフォームページ
- response = self.client.get(reverse('password_reset'))
+ response = self.client.get(reverse("password_reset"))
self.assertNotEqual(response.status_code, 302)
- self.assertNotIn('/admin/login/', response.get('Location', ''))
-
+ self.assertNotIn("/admin/login/", response.get("Location", ""))
+
# 管理画面用パスワードリセットフォームページ
- response = self.client.get(reverse('admin_password_reset'))
+ response = self.client.get(reverse("admin_password_reset"))
self.assertNotEqual(response.status_code, 302)
- self.assertNotIn('/admin/login/', response.get('Location', ''))
+ self.assertNotIn("/admin/login/", response.get("Location", ""))
def test_password_reset_confirm_with_valid_token_accessible_without_auth(self):
"""Test that password reset confirm page with valid token is accessible without authentication."""
# ログアウト状態でテスト
self.client.logout()
-
+
# 有効なトークンとuidb64を生成
token = default_token_generator.make_token(self.user)
uidb64 = urlsafe_base64_encode(force_bytes(self.user.pk))
-
+
# パスワードリセット確認ページにアクセス
- response = self.client.get(reverse('password_reset_confirm', args=[uidb64, token]))
-
+ response = self.client.get(
+ reverse("password_reset_confirm", args=[uidb64, token])
+ )
+
# 認証なしでアクセス可能であることを確認
self.assertNotEqual(response.status_code, 403) # Forbidden
self.assertNotEqual(response.status_code, 401) # Unauthorized
-
+
# リダイレクトされる場合は、ログインページにリダイレクトされていないことを確認
if response.status_code == 302:
- self.assertNotIn('/admin/login/', response.get('Location', ''))
- self.assertNotIn('/login/', response.get('Location', ''))
+ self.assertNotIn("/admin/login/", response.get("Location", ""))
+ self.assertNotIn("/login/", response.get("Location", ""))
- def test_admin_password_reset_confirm_with_valid_token_accessible_without_auth(self):
+ def test_admin_password_reset_confirm_with_valid_token_accessible_without_auth(
+ self,
+ ):
"""Test that admin password reset confirm page with valid token is accessible without authentication."""
# ログアウト状態でテスト
self.client.logout()
-
+
# 有効なトークンとuidb64を生成
token = default_token_generator.make_token(self.user)
uidb64 = urlsafe_base64_encode(force_bytes(self.user.pk))
-
+
# 管理画面用パスワードリセット確認ページにアクセス
- response = self.client.get(reverse('admin_password_reset_confirm', args=[uidb64, token]))
-
+ response = self.client.get(
+ reverse("admin_password_reset_confirm", args=[uidb64, token])
+ )
+
# 認証なしでアクセス可能であることを確認
self.assertNotEqual(response.status_code, 403) # Forbidden
self.assertNotEqual(response.status_code, 401) # Unauthorized
-
+
# リダイレクトされる場合は、ログインページにリダイレクトされていないことを確認
if response.status_code == 302:
- self.assertNotIn('/admin/login/', response.get('Location', ''))
- self.assertNotIn('/login/', response.get('Location', ''))
+ self.assertNotIn("/admin/login/", response.get("Location", ""))
+ self.assertNotIn("/login/", response.get("Location", ""))
class DirectPasswordChangeViewTest(TestCase):
@@ -608,90 +616,114 @@ def setUp(self):
def test_direct_password_change_view_get(self):
"""Test direct password change view GET request."""
- response = self.client.get(reverse('direct_password_change'))
+ response = self.client.get(reverse("direct_password_change"))
self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'registration/direct_password_change.html')
+ self.assertTemplateUsed(response, "registration/direct_password_change.html")
def test_direct_password_change_view_post_success(self):
"""Test direct password change view POST request with valid data."""
- response = self.client.post(reverse('direct_password_change'), {
- 'username': self.user.username,
- 'new_password': 'newpassword123',
- 'confirm_password': 'newpassword123'
- })
+ response = self.client.post(
+ reverse("direct_password_change"),
+ {
+ "username": self.user.username,
+ "new_password": "newpassword123",
+ "confirm_password": "newpassword123",
+ },
+ )
self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'registration/direct_password_change_success.html')
-
+ self.assertTemplateUsed(
+ response, "registration/direct_password_change_success.html"
+ )
+
# パスワードが実際に変更されたことを確認
self.user.refresh_from_db()
- self.assertTrue(self.user.check_password('newpassword123'))
+ self.assertTrue(self.user.check_password("newpassword123"))
def test_direct_password_change_view_post_missing_fields(self):
"""Test direct password change view POST request with missing fields."""
- response = self.client.post(reverse('direct_password_change'), {
- 'username': self.user.username,
- 'new_password': 'newpassword123'
- # confirm_password が欠けている
- })
+ response = self.client.post(
+ reverse("direct_password_change"),
+ {
+ "username": self.user.username,
+ "new_password": "newpassword123",
+ # confirm_password が欠けている
+ },
+ )
self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'registration/direct_password_change.html')
- self.assertIn('すべての項目を入力してください', response.context['error'])
+ self.assertTemplateUsed(response, "registration/direct_password_change.html")
+ self.assertIn("すべての項目を入力してください", response.context["error"])
def test_direct_password_change_view_post_password_mismatch(self):
"""Test direct password change view POST request with password mismatch."""
- response = self.client.post(reverse('direct_password_change'), {
- 'username': self.user.username,
- 'new_password': 'newpassword123',
- 'confirm_password': 'differentpassword123'
- })
+ response = self.client.post(
+ reverse("direct_password_change"),
+ {
+ "username": self.user.username,
+ "new_password": "newpassword123",
+ "confirm_password": "differentpassword123",
+ },
+ )
self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'registration/direct_password_change.html')
- self.assertIn('パスワードが一致しません', response.context['error'])
+ self.assertTemplateUsed(response, "registration/direct_password_change.html")
+ self.assertIn("パスワードが一致しません", response.context["error"])
def test_direct_password_change_view_post_short_password(self):
"""Test direct password change view POST request with short password."""
- response = self.client.post(reverse('direct_password_change'), {
- 'username': self.user.username,
- 'new_password': '123',
- 'confirm_password': '123'
- })
+ response = self.client.post(
+ reverse("direct_password_change"),
+ {
+ "username": self.user.username,
+ "new_password": "123",
+ "confirm_password": "123",
+ },
+ )
self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'registration/direct_password_change.html')
- self.assertIn('パスワードは8文字以上で入力してください', response.context['error'])
+ self.assertTemplateUsed(response, "registration/direct_password_change.html")
+ self.assertIn(
+ "パスワードは8文字以上で入力してください", response.context["error"]
+ )
def test_direct_password_change_view_post_user_not_found(self):
"""Test direct password change view POST request with non-existent user."""
- response = self.client.post(reverse('direct_password_change'), {
- 'username': 'nonexistentuser',
- 'new_password': 'newpassword123',
- 'confirm_password': 'newpassword123'
- })
+ response = self.client.post(
+ reverse("direct_password_change"),
+ {
+ "username": "nonexistentuser",
+ "new_password": "newpassword123",
+ "confirm_password": "newpassword123",
+ },
+ )
self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'registration/direct_password_change.html')
- self.assertIn('指定されたユーザー名が見つかりません', response.context['error'])
+ self.assertTemplateUsed(response, "registration/direct_password_change.html")
+ self.assertIn("指定されたユーザー名が見つかりません", response.context["error"])
def test_direct_password_change_view_accessible_without_authentication(self):
"""Test that direct password change view is accessible without authentication."""
# ログアウト状態でテスト
self.client.logout()
-
- response = self.client.get(reverse('direct_password_change'))
+
+ response = self.client.get(reverse("direct_password_change"))
self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'registration/direct_password_change.html')
+ self.assertTemplateUsed(response, "registration/direct_password_change.html")
def test_direct_password_change_view_post_without_authentication(self):
"""Test that direct password change view POST works without authentication."""
# ログアウト状態でテスト
self.client.logout()
-
- response = self.client.post(reverse('direct_password_change'), {
- 'username': self.user.username,
- 'new_password': 'newpassword123',
- 'confirm_password': 'newpassword123'
- })
+
+ response = self.client.post(
+ reverse("direct_password_change"),
+ {
+ "username": self.user.username,
+ "new_password": "newpassword123",
+ "confirm_password": "newpassword123",
+ },
+ )
self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'registration/direct_password_change_success.html')
-
+ self.assertTemplateUsed(
+ response, "registration/direct_password_change_success.html"
+ )
+
# パスワードが実際に変更されたことを確認
self.user.refresh_from_db()
- self.assertTrue(self.user.check_password('newpassword123'))
\ No newline at end of file
+ self.assertTrue(self.user.check_password("newpassword123"))
diff --git a/shift/tests.py b/shift/tests.py
index 7ce503c..a39b155 100644
--- a/shift/tests.py
+++ b/shift/tests.py
@@ -1,3 +1 @@
-from django.test import TestCase
-
# Create your tests here.
diff --git a/shift/views.py b/shift/views.py
index 6005e85..cf42dba 100644
--- a/shift/views.py
+++ b/shift/views.py
@@ -1,20 +1,19 @@
-from django.shortcuts import render, get_object_or_404
-from django.utils import timezone
+import json
from calendar import monthrange
-from datetime import date, timedelta
-from .models import Employee, Shift, ShiftType, CompanyHoliday, LaborLawSettings
+from datetime import date
+
+from django.http import JsonResponse
+from django.shortcuts import render
+from django.utils import timezone
+from django.views.decorators.csrf import csrf_exempt
+from django.views.decorators.http import require_http_methods
+
+from .models import CompanyHoliday, Employee, Shift, ShiftType
+
try:
import jpholiday
except ImportError:
jpholiday = None
-from django.http import JsonResponse
-from django.views.decorators.csrf import csrf_exempt
-from django.views.decorators.http import require_http_methods
-import json
-from django.contrib.auth.decorators import login_required
-from django.contrib.auth import views as auth_views
-from django.urls import reverse_lazy
-from .forms import ShiftForm
# Create your views here.
@@ -94,14 +93,22 @@ def shift_table(request):
employee_shifts[emp.id][d] = shift_dict.get(key)
# 各従業員ごとの勤務日数・休み日数を計算
- work_shift_type_ids = set(ShiftType.objects.filter(is_work=True).values_list('id', flat=True))
- rest_shift_type_ids = set(ShiftType.objects.filter(is_work=False).values_list('id', flat=True))
+ work_shift_type_ids = set(
+ ShiftType.objects.filter(is_work=True).values_list("id", flat=True)
+ )
+ rest_shift_type_ids = set(
+ ShiftType.objects.filter(is_work=False).values_list("id", flat=True)
+ )
employee_work_days = {}
employee_rest_days = {}
for emp in employees:
emp_shifts = [shift_dict.get(f"{emp.id}_{d.isoformat()}") for d in days]
- work_days = sum(1 for s in emp_shifts if s and s.shift_type_id in work_shift_type_ids)
- rest_days = sum(1 for s in emp_shifts if s and s.shift_type_id in rest_shift_type_ids)
+ work_days = sum(
+ 1 for s in emp_shifts if s and s.shift_type_id in work_shift_type_ids
+ )
+ rest_days = sum(
+ 1 for s in emp_shifts if s and s.shift_type_id in rest_shift_type_ids
+ )
employee_work_days[emp.id] = work_days
employee_rest_days[emp.id] = rest_days
@@ -164,28 +171,26 @@ def save_shift(request):
if not data.get("force_save"):
# 仮のシフトオブジェクトを作成して警告チェック
temp_shift = Shift(employee=employee, date=date_obj, shift_type=shift_type)
-
+
# 更新の場合は既存のシフトIDを設定
if shift_id:
temp_shift.id = shift_id
-
+
# 勤務日かどうかでチェック内容を分ける
if shift_type.is_work:
# 勤務日:連続勤務日数制限の警告チェック
warning = temp_shift.check_consecutive_work_days()
if warning:
- return JsonResponse({
- 'success': False,
- 'warning': warning['message']
- })
+ return JsonResponse(
+ {"success": False, "warning": warning["message"]}
+ )
else:
# 休み:最低労働者数制限の警告チェック
min_workers_warning = temp_shift.check_min_workers()
if min_workers_warning:
- return JsonResponse({
- 'success': False,
- 'warning': min_workers_warning['message']
- })
+ return JsonResponse(
+ {"success": False, "warning": min_workers_warning["message"]}
+ )
# シフトの保存または更新
if shift_id:
@@ -207,8 +212,8 @@ def save_shift(request):
return JsonResponse(
{"success": False, "error": "既にシフトが登録されています"}
)
-
- return JsonResponse({'success': True})
+
+ return JsonResponse({"success": True})
except json.JSONDecodeError:
return JsonResponse({"success": False, "error": "無効なJSONデータです"})
@@ -232,42 +237,53 @@ def delete_shift(request, shift_id):
def direct_password_change(request):
"""直接パスワード変更機能"""
- if request.method == 'POST':
- username = request.POST.get('username')
- new_password = request.POST.get('new_password')
- confirm_password = request.POST.get('confirm_password')
-
+ if request.method == "POST":
+ username = request.POST.get("username")
+ new_password = request.POST.get("new_password")
+ confirm_password = request.POST.get("confirm_password")
+
# バリデーション
if not username or not new_password or not confirm_password:
- return render(request, 'registration/direct_password_change.html', {
- 'error': 'すべての項目を入力してください。'
- })
-
+ return render(
+ request,
+ "registration/direct_password_change.html",
+ {"error": "すべての項目を入力してください。"},
+ )
+
if new_password != confirm_password:
- return render(request, 'registration/direct_password_change.html', {
- 'error': 'パスワードが一致しません。'
- })
-
+ return render(
+ request,
+ "registration/direct_password_change.html",
+ {"error": "パスワードが一致しません。"},
+ )
+
if len(new_password) < 8:
- return render(request, 'registration/direct_password_change.html', {
- 'error': 'パスワードは8文字以上で入力してください。'
- })
-
+ return render(
+ request,
+ "registration/direct_password_change.html",
+ {"error": "パスワードは8文字以上で入力してください。"},
+ )
+
# ユーザーの存在確認
try:
from django.contrib.auth.models import User
+
user = User.objects.get(username=username)
except User.DoesNotExist:
- return render(request, 'registration/direct_password_change.html', {
- 'error': '指定されたユーザー名が見つかりません。'
- })
-
+ return render(
+ request,
+ "registration/direct_password_change.html",
+ {"error": "指定されたユーザー名が見つかりません。"},
+ )
+
# パスワード変更
user.set_password(new_password)
user.save()
-
- return render(request, 'registration/direct_password_change_success.html', {
- 'username': username
- })
-
- return render(request, 'registration/direct_password_change.html')
+
+ return render(
+ request,
+ "registration/direct_password_change_success.html",
+ {"username": username},
+ )
+
+ return render(request, "registration/direct_password_change.html")
diff --git a/shift_table/settings.py b/shift_table/settings.py
index 5045817..0cee463 100644
--- a/shift_table/settings.py
+++ b/shift_table/settings.py
@@ -128,7 +128,9 @@
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Email settings
-EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' # 開発用(コンソール出力)
+EMAIL_BACKEND = (
+ "django.core.mail.backends.console.EmailBackend" # 開発用(コンソール出力)
+)
# 本番環境では以下のように設定してください:
# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# EMAIL_HOST = 'smtp.gmail.com' # または他のSMTPサーバー
diff --git a/shift_table/settings_test.py b/shift_table/settings_test.py
index 1ca1a50..699f028 100644
--- a/shift_table/settings_test.py
+++ b/shift_table/settings_test.py
@@ -6,12 +6,12 @@
# Use in-memory SQLite database for testing
DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': ':memory:',
- 'TEST': {
- 'NAME': ':memory:',
- }
+ "default": {
+ "ENGINE": "django.db.backends.sqlite3",
+ "NAME": ":memory:",
+ "TEST": {
+ "NAME": ":memory:",
+ },
}
}
@@ -20,31 +20,31 @@
# Use a fast password hasher for testing
PASSWORD_HASHERS = [
- 'django.contrib.auth.hashers.MD5PasswordHasher',
+ "django.contrib.auth.hashers.MD5PasswordHasher",
]
# Disable logging during tests
LOGGING = {
- 'version': 1,
- 'disable_existing_loggers': True,
+ "version": 1,
+ "disable_existing_loggers": True,
}
# Test secret key
-SECRET_KEY = 'test-secret-key-for-testing-only'
+SECRET_KEY = "test-secret-key-for-testing-only"
# Disable CSRF for testing
-MIDDLEWARE = [m for m in MIDDLEWARE if 'csrf' not in m.lower()]
+MIDDLEWARE = [m for m in MIDDLEWARE if "csrf" not in m.lower()]
# Static files settings for testing
-STATIC_URL = '/static/'
-STATIC_ROOT = BASE_DIR / 'staticfiles'
+STATIC_URL = "/static/"
+STATIC_ROOT = BASE_DIR / "staticfiles"
# Media files settings for testing
-MEDIA_URL = '/media/'
-MEDIA_ROOT = BASE_DIR / 'media'
+MEDIA_URL = "/media/"
+MEDIA_ROOT = BASE_DIR / "media"
# Disable apps that might interfere with testing
-INSTALLED_APPS = [app for app in INSTALLED_APPS if app != 'shift.tests']
+INSTALLED_APPS = [app for app in INSTALLED_APPS if app != "shift.tests"]
# Test database isolation
-TEST_RUNNER = 'django.test.runner.DiscoverRunner'
\ No newline at end of file
+TEST_RUNNER = "django.test.runner.DiscoverRunner"
diff --git a/shift_table/urls.py b/shift_table/urls.py
index 9f9108e..f747ed7 100644
--- a/shift_table/urls.py
+++ b/shift_table/urls.py
@@ -24,35 +24,69 @@
path("admin/", admin.site.urls),
path("shift/", include("shift.urls")),
# 直接パスワード変更機能
- path('direct-password-change/', direct_password_change, name='direct_password_change'),
+ path(
+ "direct-password-change/", direct_password_change, name="direct_password_change"
+ ),
# パスワードリセット機能(管理画面用)
- path('admin-password-reset/', auth_views.PasswordResetView.as_view(
- template_name='registration/password_reset_form.html',
- email_template_name='registration/password_reset_email.html',
- subject_template_name='registration/password_reset_subject.txt'
- ), name='admin_password_reset'),
- path('admin-password-reset/done/', auth_views.PasswordResetDoneView.as_view(
- template_name='registration/password_reset_done.html'
- ), name='admin_password_reset_done'),
- path('admin-reset///', auth_views.PasswordResetConfirmView.as_view(
- template_name='registration/password_reset_confirm.html'
- ), name='admin_password_reset_confirm'),
- path('admin-reset/done/', auth_views.PasswordResetCompleteView.as_view(
- template_name='registration/password_reset_complete.html'
- ), name='admin_password_reset_complete'),
+ path(
+ "admin-password-reset/",
+ auth_views.PasswordResetView.as_view(
+ template_name="registration/password_reset_form.html",
+ email_template_name="registration/password_reset_email.html",
+ subject_template_name="registration/password_reset_subject.txt",
+ ),
+ name="admin_password_reset",
+ ),
+ path(
+ "admin-password-reset/done/",
+ auth_views.PasswordResetDoneView.as_view(
+ template_name="registration/password_reset_done.html"
+ ),
+ name="admin_password_reset_done",
+ ),
+ path(
+ "admin-reset///",
+ auth_views.PasswordResetConfirmView.as_view(
+ template_name="registration/password_reset_confirm.html"
+ ),
+ name="admin_password_reset_confirm",
+ ),
+ path(
+ "admin-reset/done/",
+ auth_views.PasswordResetCompleteView.as_view(
+ template_name="registration/password_reset_complete.html"
+ ),
+ name="admin_password_reset_complete",
+ ),
# パスワードリセット機能(一般用)
- path('password_reset/', auth_views.PasswordResetView.as_view(
- template_name='registration/password_reset_form.html',
- email_template_name='registration/password_reset_email.html',
- subject_template_name='registration/password_reset_subject.txt'
- ), name='password_reset'),
- path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(
- template_name='registration/password_reset_done.html'
- ), name='password_reset_done'),
- path('reset///', auth_views.PasswordResetConfirmView.as_view(
- template_name='registration/password_reset_confirm.html'
- ), name='password_reset_confirm'),
- path('reset/done/', auth_views.PasswordResetCompleteView.as_view(
- template_name='registration/password_reset_complete.html'
- ), name='password_reset_complete'),
+ path(
+ "password_reset/",
+ auth_views.PasswordResetView.as_view(
+ template_name="registration/password_reset_form.html",
+ email_template_name="registration/password_reset_email.html",
+ subject_template_name="registration/password_reset_subject.txt",
+ ),
+ name="password_reset",
+ ),
+ path(
+ "password_reset/done/",
+ auth_views.PasswordResetDoneView.as_view(
+ template_name="registration/password_reset_done.html"
+ ),
+ name="password_reset_done",
+ ),
+ path(
+ "reset///",
+ auth_views.PasswordResetConfirmView.as_view(
+ template_name="registration/password_reset_confirm.html"
+ ),
+ name="password_reset_confirm",
+ ),
+ path(
+ "reset/done/",
+ auth_views.PasswordResetCompleteView.as_view(
+ template_name="registration/password_reset_complete.html"
+ ),
+ name="password_reset_complete",
+ ),
]