我正在尝试覆盖formset上的clean方法。正如文档提出的那样,我编写了自己的BaseFormSet并覆盖了它的干净方法。 FormSet的某些值取决于父母模型表单中的值。
模型是包含工作时间的应用程序中的条目。条目适用于一天,并且只能有一个条目,但条目可以有多个班次。可以想象,模型表单用于Entry,FormSet用于Shifts。所以Entry是父母,Shifts是孩子。
为了验证Shift,我需要访问父条目。我尝试通过从clean方法中调用self.instance来实现。在这种情况下,self将是FormSet。
但是,self.instance的内容始终是当天的条目,缺少它的所有者。如果我想在1月4日添加一个条目,那么self.instance应该是该日期。但是,当我访问self.instance时,结果是今天的条目。但是,条目的表格是有效的。正确输入所有值。应该有一个所有者。
我的问题是,我正确使用self.instance吗?如果没有,如何在验证期间从Shift FormSet访问父条目的值?
Shift的定义:
class Shift(m.Model):
TIME_HELP_TEXT = 'Please use the following format: <em>HH:MM</em>.'
# meta
cr_date = m.DateTimeField(auto_now_add=True, editable=False, verbose_name='Date of Creation') # date of creation
# content
entry = m.ForeignKey(Entry, on_delete=m.CASCADE, related_name='shifts')
start = m.TimeField(help_text=TIME_HELP_TEXT) # starting time
end = m.TimeField(help_text=TIME_HELP_TEXT) # ending time
recess = m.PositiveIntegerField(default=0) # recess time in minutes
project = m.ForeignKey(Project, on_delete=m.PROTECT)
参赛作品的定义:
class Entry(m.Model):
""" Represents an entry in a accounting. An entry is either for work, sick leave or VACATION. """
# meta
cr_date = m.DateTimeField(auto_now_add=True, editable=False, verbose_name='Date of Creation') # date of creation
owner = m.ForeignKey(User, on_delete=m.PROTECT, unique_for_date='date')
final = m.BooleanField(default=False)
# content
type = m.CharField(max_length=1, choices=type_choices, default='w')
date = m.DateField(default=now)
FormSet声明:
class BaseShiftFormSet(f.BaseInlineFormSet):
warnings = []
def has_messages(self):
return self.warnings
def flush_messages(self, request):
for w in self.warnings:
messages.warning(request, w)
def clean(self):
if any(self.errors):
# Don't bother validating the formset unless each form is valid on its own.
return
super(BaseShiftFormSet, self).clean()
# Standard day. Is used to be combined with Time object instances in order to get Datetime object instances,
# which allow for arithmetic comparison as opposed to Time objects.
# entry = self.instance
# day = datetime.date(entry.get_year(), entry.get_month(), entry.get_day())
day = datetime.date(1, 1, 1)
one_day = datetime.timedelta(days=1)
# previous_entry = Entry.get_entry_for_day(entry.get_owner(), day - one_day)
# next_entry = Entry.get_entry_for_day(entry.get_owner(), day + one_day)
earliest = datetime.datetime.combine(day, datetime.time(hour=6))
latest = datetime.datetime.combine(day, datetime.time(hour=20))
# shifts = entry.get_shifts()
valid = True
work_time = datetime.timedelta()
work_time_limit = datetime.timedelta(hours=10)
min_break = datetime.timedelta(hours=11)
for form in self.forms:
start = datetime.datetime.combine(day, form.cleaned_data['start'])
end = datetime.datetime.combine(day, form.cleaned_data['end'])
recess_time = datetime.timedelta(minutes=form.cleaned_data['recess'])
shift_length = end - start
# new_shift = Shift(start=form.cleaned_data['start'], end=form.cleaned_data['end'],
# recess=form.cleaned_data['recess'], project=form.cleaned_data['project'],
# entry=entry)
work_time += shift_length - recess_time
# Validation: LOGICAL ERRORS
# Start before end.
if start > end:
raise ValidationError(ERROR_MESSAGES['shift_inconsistent'])
# Recess not longer than total work time.
if recess_time >= shift_length:
raise ValidationError(ERROR_MESSAGES['shift_recess_too_long'])
# # No overlapping shifts
# for shift in shifts:
# valid = valid and (new_shift.is_before(shift) or new_shift.is_after(shift))
# if not valid:
# raise ValidationError(ERROR_MESSAGES['shifts_overlap'])
# # Validation: WARNINGS
# # No night shifts. Applies only to office work.
# if entry.is_type_work():
# # Avoid starting before 6:00.
# if start < earliest:
# self.warnings.append(WARNING_MESSAGES['no_night_shifts'])
# # Avoid starting after 22:00.
# if end > latest:
# self.warnings.append(WARNING_MESSAGES['no_night_shifts'])
#
# # No shifts longer than ten hours
# if work_time >= work_time_limit:
# self.warnings.append(WARNING_MESSAGES['shift_too_long'])
#
# # Minimum break of 11 hours between work days.
# gap = entry.calculate_time_gap(previous_entry)
# if gap:
# if gap < min_break:
# self.warnings.append(WARNING_MESSAGES['no_11_hour_break'].
# format(previous_entry.get_date().strftime('%d %m %Y')))
# gap = entry.calculate_time_gap(next_entry)
# if gap:
# if gap < min_break:
# self.warnings.append(WARNING_MESSAGES['no_11_hour_break'].
# format(next_entry.get_date().strftime('%d %m %Y')))
fields = ('entry', 'project', 'start', 'end', 'recess', )
CreateShiftFormSet = f.inlineformset_factory(Entry, Shift, formset=BaseShiftFormSet, fields=fields, extra=0,
can_delete=False, min_num=1, validate_min=True)
EditShiftFormSet = f.inlineformset_factory(Entry, Shift, formset=BaseShiftFormSet, fields=fields, extra=1,
can_delete=True, min_num=1, validate_min=True)
使用上述元素的视图:
class EntryView(FormView):
""" Base view. """
model = Entry
form_class = f.EntryForm
success_url = reverse_lazy('time_manager:time_tracking:index')
formset = CreateShiftFormSet
object = None
def get_success_url(self):
# Fallback url.
url = reverse('time_manager:time_tracking:index')
# If possible, go back to the month were the last entry was added.
if self.object:
url = reverse('time_manager:time_tracking:month',
kwargs={'year': self.object.get_year(), 'month': self.object.get_month()})
return url
def get(self, request, pk=None, *args, **kwargs):
if pk:
try:
self.object = Entry.objects.get(pk=pk)
except ObjectDoesNotExist:
self.object = None
else:
self.object = None
if self.object:
form = f.EntryForm(instance=self.object)
form_set = self.formset(instance=self.object)
else:
form = f.EntryForm(initial={'owner': request.user})
form_set = self.formset()
return_dict = {'form': form, 'shift_form': form_set}
return render(request, self.template_name, return_dict)
def post(self, request, pk=None, *args, **kwargs):
if pk:
self.object = Entry.objects.get(pk=pk)
else:
self.object = None
if self.object:
form = f.EntryForm(data=request.POST, instance=self.object)
shift_form_set = self.formset(data=request.POST, instance=self.object)
else:
form = f.EntryForm(data=request.POST)
shift_form_set = self.formset(data=request.POST)
if form.is_valid() and shift_form_set.is_valid():
return self.form_valid(request.user, form, shift_form_set)
else:
rd = {'form': form, 'shift_form': shift_form_set, 'test': self.object}
return render(request, self.template_name, rd)
def form_valid(self, user, form, shift_form):
""" Stores the entry and all shifts. """
self.object = form.save(commit=False)
self.object.owner = user
self.object.save()
shift_form.instance = self.object
shifts = shift_form.save(commit=False)
for shift in shifts:
shift.save()
return HttpResponseRedirect(self.get_success_url())
class EntryCreateView(EntryView):
""" View for creating new entries. Can be passed an ordinal to create an entry for the day, represented by the
ordinal. """
template_name = 'entry/create.html'
form_class = f.AddWorkDay
def get(self, request, ordinal=None, *args, **kwargs):
""" Initiates with a blank form or will populate the day field with the day represented by the passed
ordinal. """
if ordinal:
date = datetime.datetime.fromordinal(int(ordinal))
form = f.AddWorkDay(initial={'date': date, 'owner': request.user})
else:
form = f.AddWorkDay(initial={'owner': request.user})
shift_form_set = CreateShiftFormSet(instance=self.object)
return render(request, self.template_name, {'form': form, 'shift_form': shift_form_set})
class EntryEditView(EntryView):
template_name = 'entry/edit.html'
formset = EditShiftFormSet