vkk.workhours.forms
A collection of forms used throughout the workhours
module.
1""" 2A collection of forms used throughout the `workhours` module. 3""" 4 5import datetime 6from django import forms 7from django.db.models import Sum 8from django.utils import timezone 9from django.utils.translation import gettext_lazy as _ 10from vkk.workhours.models import WorkHours, WorkHoursCorrection, Period, PeriodClosure, Project 11 12 13def date_iterator(start, end): 14 """ 15 Returns an iterator over a range of dates. 16 """ 17 delta = end - start 18 for day in range(delta.days+1): 19 yield start + datetime.timedelta(day) 20 21 22class CustomDateInput(forms.DateInput): 23 """ 24 This subclass of `DateInput` provides the HTML input type "date" for widgets. 25 """ 26 input_type = 'date' 27 28 def format_value(self, value): 29 """ 30 Returns a date in ISO-8601 format. 31 """ 32 if isinstance(value, datetime.date): 33 return value.isoformat() 34 else: 35 return super().format_value(value) 36 37 38class CustomDateTimeInput(forms.DateTimeInput): 39 """ 40 This subclass of `DateTimeInput` provides the HTML input type "datetime-local" 41 for widgets. 42 """ 43 input_type = 'datetime-local' 44 45 46class PeriodSelectForm(forms.Form): 47 """ 48 A form for selecting a `Period` instance. 49 """ 50 51 def __init__(self, queryset, *args, **kwargs): 52 """ 53 Constructs a `PeriodSelectForm` object. 54 """ 55 super().__init__(*args, **kwargs) 56 self.fields["period"] = forms.ModelChoiceField( 57 queryset, 58 empty_label=None, 59 label=_('Period') 60 ) 61 62 63class WorkhourSheetForm(forms.Form): 64 """ 65 A form closely resembling a sheet of paper for keeping track of hour worked 66 on a project by individual contributors. 67 """ 68 template_name_sheet = 'vkk/workhours/workhours_sheet_form.html' 69 sums = dict() 70 closures = dict() 71 72 def __init__(self, *args, period_pk=None, assignments=None, closure_lock=True, invoice_number=None, **kwargs): 73 """ 74 This constructor takes a primary key value of a `Period` instance, an 75 `ProjectAssingment` instance, a projects invoice number and whether 76 the inputs should be disabled according to `PeriodClosure`instances. 77 78 Returns a newly constructed `WorkhourSheetForm` object. 79 """ 80 super().__init__(*args, **kwargs) 81 82 today = datetime.date.today() 83 field_class = WorkHours.hours.field.formfield 84 field_class_correction = WorkHoursCorrection.ammount.field.formfield 85 86 self._period = Period.objects.get(pk=period_pk) 87 self._dates = list(date_iterator(self._period.start, self._period.end)) 88 self._assignments = assignments.select_related('contributor') 89 self._closure = PeriodClosure.objects.filter( 90 period=self._period, 91 ) 92 self._project = None 93 if invoice_number is not None: 94 projects = Project.objects.filter(invoice_number=invoice_number) 95 if projects.exists(): 96 self._project = projects[0] 97 98 for assignment in self._assignments: 99 closed = closure_lock and self._period.dead_line < timezone.now() 100 closure = self._closure.filter( 101 project_assignment=assignment 102 ) 103 if closure_lock and not closed and closure.exists(): 104 closed = closure[0].is_closed_manager 105 expired = False 106 for date in self._dates: 107 if self._project is not None: 108 expired = self._project.start > date or self._project.end < date 109 # create fields for each assignment 110 is_future = date > today 111 field = field_class( 112 min_value=0, 113 max_value=24, 114 required=False, 115 label=date.day, 116 label_suffix='', 117 disabled=(is_future or closed or expired) 118 ) 119 self.fields[str(assignment.pk) + '_' + 120 date.isoformat()] = field 121 self.fields[str(assignment.pk) + '_C'] = field = field_class_correction( 122 label=_('C'), 123 required=False, 124 label_suffix='', 125 disabled=closed, 126 ) 127 128 # provide initial data 129 workhours = WorkHours.objects.filter( 130 period__pk=period_pk, 131 project_assignment=assignment 132 ) 133 for entry in workhours: 134 field = self.fields.get( 135 str(assignment.pk) + '_' + entry.day.isoformat()) 136 if field is not None: 137 field.initial = entry.hours 138 139 assignment_sum = workhours.aggregate(Sum('hours')) 140 self.sums[str(assignment.pk)] = assignment_sum['hours__sum'] or 0 141 142 correction = WorkHoursCorrection.objects.filter( 143 period__pk=period_pk, 144 project_assignment=assignment 145 ) 146 if correction.exists(): 147 field = self.fields.get(str(assignment.pk) + '_C') 148 if field is not None: 149 field.initial = correction[0].ammount 150 self.sums[str(assignment.pk)] += correction[0].ammount 151 152 # feedback for closed periods 153 feedback = "" 154 if closure.exists() and closure[0].is_closed_contributor: 155 feedback += "☺️" 156 if closure.exists() and closure[0].is_closed_manager: 157 feedback += "☺️" 158 self.closures[str(assignment.pk) + '_closures'] = feedback 159 160 def _get_field_sheet_structure(self): 161 """ 162 Returns a list for building the HTML representation of the form. 163 """ 164 return [ 165 ( 166 assignment, 167 [ 168 self[str(assignment.pk) + '_' + date.isoformat()] for date in self._dates 169 ] + [self[str(assignment.pk) + '_C'], self.sums[str(assignment.pk)], self.closures[str(assignment.pk) + '_closures']] 170 ) for assignment in self._assignments 171 ] 172 173 def as_sheet(self): 174 """ 175 Renders and returns the HTML form. 176 """ 177 context = super().get_context() 178 context.update({ 179 'start': self._period.start, 180 'fields_more': self._get_field_sheet_structure() 181 }) 182 return self.render( 183 template_name=self.template_name_sheet, 184 context=context 185 ) 186 187 def save(self): 188 """ 189 Modifies and saves all associated `WorkHours` instances. 190 """ 191 if self.is_valid() and self.has_changed(): 192 for assignment in self._assignments: 193 add = [] 194 delete = [] 195 field_names = [(date, str(assignment.pk) + '_' + 196 date.isoformat()) for date in self._dates] 197 for date, field_name in field_names: 198 if field_name in self.changed_data: 199 initial = self.fields[field_name].initial 200 value = self.cleaned_data.get(field_name) 201 if value is None or value == 0.0: 202 delete.append( 203 date 204 ) 205 elif initial is None: 206 add.append(WorkHours( 207 project_assignment=assignment, 208 period=self._period, 209 day=date, 210 hours=value 211 )) 212 else: 213 WorkHours.objects.filter( 214 project_assignment=assignment, 215 period=self._period, 216 day=date, 217 ).update(hours=value) 218 WorkHours.objects.filter( 219 project_assignment=assignment, 220 period=self._period, 221 day__in=delete 222 ).delete() 223 WorkHours.objects.bulk_create(add) 224 correction_name = str(assignment.pk) + '_C' 225 if correction_name in self.changed_data: 226 value = self.cleaned_data.get(correction_name) 227 if value is None or value == 0.0: 228 WorkHoursCorrection.objects.filter( 229 project_assignment=assignment, 230 period=self._period 231 ).delete() 232 else: 233 WorkHoursCorrection.objects.update_or_create( 234 project_assignment=assignment, 235 period=self._period, 236 defaults={'ammount': value} 237 )
14def date_iterator(start, end): 15 """ 16 Returns an iterator over a range of dates. 17 """ 18 delta = end - start 19 for day in range(delta.days+1): 20 yield start + datetime.timedelta(day)
Returns an iterator over a range of dates.
23class CustomDateInput(forms.DateInput): 24 """ 25 This subclass of `DateInput` provides the HTML input type "date" for widgets. 26 """ 27 input_type = 'date' 28 29 def format_value(self, value): 30 """ 31 Returns a date in ISO-8601 format. 32 """ 33 if isinstance(value, datetime.date): 34 return value.isoformat() 35 else: 36 return super().format_value(value)
This subclass of DateInput
provides the HTML input type "date" for widgets.
29 def format_value(self, value): 30 """ 31 Returns a date in ISO-8601 format. 32 """ 33 if isinstance(value, datetime.date): 34 return value.isoformat() 35 else: 36 return super().format_value(value)
Returns a date in ISO-8601 format.
Inherited Members
- django.forms.widgets.DateTimeBaseInput
- DateTimeBaseInput
- supports_microseconds
- format
- django.forms.widgets.DateInput
- format_key
- template_name
- django.forms.widgets.Input
- get_context
- django.forms.widgets.Widget
- needs_multipart_form
- is_localized
- is_required
- use_fieldset
- attrs
- subwidgets
- render
- build_attrs
- value_from_datadict
- value_omitted_from_data
- id_for_label
- use_required_attribute
39class CustomDateTimeInput(forms.DateTimeInput): 40 """ 41 This subclass of `DateTimeInput` provides the HTML input type "datetime-local" 42 for widgets. 43 """ 44 input_type = 'datetime-local'
This subclass of DateTimeInput
provides the HTML input type "datetime-local"
for widgets.
Inherited Members
- django.forms.widgets.DateTimeBaseInput
- DateTimeBaseInput
- supports_microseconds
- format
- format_value
- django.forms.widgets.DateTimeInput
- format_key
- template_name
- django.forms.widgets.Input
- get_context
- django.forms.widgets.Widget
- needs_multipart_form
- is_localized
- is_required
- use_fieldset
- attrs
- subwidgets
- render
- build_attrs
- value_from_datadict
- value_omitted_from_data
- id_for_label
- use_required_attribute
47class PeriodSelectForm(forms.Form): 48 """ 49 A form for selecting a `Period` instance. 50 """ 51 52 def __init__(self, queryset, *args, **kwargs): 53 """ 54 Constructs a `PeriodSelectForm` object. 55 """ 56 super().__init__(*args, **kwargs) 57 self.fields["period"] = forms.ModelChoiceField( 58 queryset, 59 empty_label=None, 60 label=_('Period') 61 )
A form for selecting a Period
instance.
52 def __init__(self, queryset, *args, **kwargs): 53 """ 54 Constructs a `PeriodSelectForm` object. 55 """ 56 super().__init__(*args, **kwargs) 57 self.fields["period"] = forms.ModelChoiceField( 58 queryset, 59 empty_label=None, 60 label=_('Period') 61 )
Constructs a PeriodSelectForm
object.
Inherited Members
- django.forms.forms.BaseForm
- default_renderer
- field_order
- prefix
- use_required_attribute
- template_name_div
- template_name_p
- template_name_table
- template_name_ul
- template_name_label
- is_bound
- data
- files
- auto_id
- initial
- error_class
- label_suffix
- empty_permitted
- fields
- renderer
- order_fields
- errors
- is_valid
- add_prefix
- add_initial_prefix
- template_name
- get_context
- non_field_errors
- add_error
- has_error
- full_clean
- clean
- has_changed
- changed_data
- is_multipart
- visible_fields
- get_initial_for_field
- django.forms.utils.RenderableFormMixin
- as_p
- as_table
- as_ul
- as_div
- django.forms.utils.RenderableMixin
- render
64class WorkhourSheetForm(forms.Form): 65 """ 66 A form closely resembling a sheet of paper for keeping track of hour worked 67 on a project by individual contributors. 68 """ 69 template_name_sheet = 'vkk/workhours/workhours_sheet_form.html' 70 sums = dict() 71 closures = dict() 72 73 def __init__(self, *args, period_pk=None, assignments=None, closure_lock=True, invoice_number=None, **kwargs): 74 """ 75 This constructor takes a primary key value of a `Period` instance, an 76 `ProjectAssingment` instance, a projects invoice number and whether 77 the inputs should be disabled according to `PeriodClosure`instances. 78 79 Returns a newly constructed `WorkhourSheetForm` object. 80 """ 81 super().__init__(*args, **kwargs) 82 83 today = datetime.date.today() 84 field_class = WorkHours.hours.field.formfield 85 field_class_correction = WorkHoursCorrection.ammount.field.formfield 86 87 self._period = Period.objects.get(pk=period_pk) 88 self._dates = list(date_iterator(self._period.start, self._period.end)) 89 self._assignments = assignments.select_related('contributor') 90 self._closure = PeriodClosure.objects.filter( 91 period=self._period, 92 ) 93 self._project = None 94 if invoice_number is not None: 95 projects = Project.objects.filter(invoice_number=invoice_number) 96 if projects.exists(): 97 self._project = projects[0] 98 99 for assignment in self._assignments: 100 closed = closure_lock and self._period.dead_line < timezone.now() 101 closure = self._closure.filter( 102 project_assignment=assignment 103 ) 104 if closure_lock and not closed and closure.exists(): 105 closed = closure[0].is_closed_manager 106 expired = False 107 for date in self._dates: 108 if self._project is not None: 109 expired = self._project.start > date or self._project.end < date 110 # create fields for each assignment 111 is_future = date > today 112 field = field_class( 113 min_value=0, 114 max_value=24, 115 required=False, 116 label=date.day, 117 label_suffix='', 118 disabled=(is_future or closed or expired) 119 ) 120 self.fields[str(assignment.pk) + '_' + 121 date.isoformat()] = field 122 self.fields[str(assignment.pk) + '_C'] = field = field_class_correction( 123 label=_('C'), 124 required=False, 125 label_suffix='', 126 disabled=closed, 127 ) 128 129 # provide initial data 130 workhours = WorkHours.objects.filter( 131 period__pk=period_pk, 132 project_assignment=assignment 133 ) 134 for entry in workhours: 135 field = self.fields.get( 136 str(assignment.pk) + '_' + entry.day.isoformat()) 137 if field is not None: 138 field.initial = entry.hours 139 140 assignment_sum = workhours.aggregate(Sum('hours')) 141 self.sums[str(assignment.pk)] = assignment_sum['hours__sum'] or 0 142 143 correction = WorkHoursCorrection.objects.filter( 144 period__pk=period_pk, 145 project_assignment=assignment 146 ) 147 if correction.exists(): 148 field = self.fields.get(str(assignment.pk) + '_C') 149 if field is not None: 150 field.initial = correction[0].ammount 151 self.sums[str(assignment.pk)] += correction[0].ammount 152 153 # feedback for closed periods 154 feedback = "" 155 if closure.exists() and closure[0].is_closed_contributor: 156 feedback += "☺️" 157 if closure.exists() and closure[0].is_closed_manager: 158 feedback += "☺️" 159 self.closures[str(assignment.pk) + '_closures'] = feedback 160 161 def _get_field_sheet_structure(self): 162 """ 163 Returns a list for building the HTML representation of the form. 164 """ 165 return [ 166 ( 167 assignment, 168 [ 169 self[str(assignment.pk) + '_' + date.isoformat()] for date in self._dates 170 ] + [self[str(assignment.pk) + '_C'], self.sums[str(assignment.pk)], self.closures[str(assignment.pk) + '_closures']] 171 ) for assignment in self._assignments 172 ] 173 174 def as_sheet(self): 175 """ 176 Renders and returns the HTML form. 177 """ 178 context = super().get_context() 179 context.update({ 180 'start': self._period.start, 181 'fields_more': self._get_field_sheet_structure() 182 }) 183 return self.render( 184 template_name=self.template_name_sheet, 185 context=context 186 ) 187 188 def save(self): 189 """ 190 Modifies and saves all associated `WorkHours` instances. 191 """ 192 if self.is_valid() and self.has_changed(): 193 for assignment in self._assignments: 194 add = [] 195 delete = [] 196 field_names = [(date, str(assignment.pk) + '_' + 197 date.isoformat()) for date in self._dates] 198 for date, field_name in field_names: 199 if field_name in self.changed_data: 200 initial = self.fields[field_name].initial 201 value = self.cleaned_data.get(field_name) 202 if value is None or value == 0.0: 203 delete.append( 204 date 205 ) 206 elif initial is None: 207 add.append(WorkHours( 208 project_assignment=assignment, 209 period=self._period, 210 day=date, 211 hours=value 212 )) 213 else: 214 WorkHours.objects.filter( 215 project_assignment=assignment, 216 period=self._period, 217 day=date, 218 ).update(hours=value) 219 WorkHours.objects.filter( 220 project_assignment=assignment, 221 period=self._period, 222 day__in=delete 223 ).delete() 224 WorkHours.objects.bulk_create(add) 225 correction_name = str(assignment.pk) + '_C' 226 if correction_name in self.changed_data: 227 value = self.cleaned_data.get(correction_name) 228 if value is None or value == 0.0: 229 WorkHoursCorrection.objects.filter( 230 project_assignment=assignment, 231 period=self._period 232 ).delete() 233 else: 234 WorkHoursCorrection.objects.update_or_create( 235 project_assignment=assignment, 236 period=self._period, 237 defaults={'ammount': value} 238 )
A form closely resembling a sheet of paper for keeping track of hour worked on a project by individual contributors.
73 def __init__(self, *args, period_pk=None, assignments=None, closure_lock=True, invoice_number=None, **kwargs): 74 """ 75 This constructor takes a primary key value of a `Period` instance, an 76 `ProjectAssingment` instance, a projects invoice number and whether 77 the inputs should be disabled according to `PeriodClosure`instances. 78 79 Returns a newly constructed `WorkhourSheetForm` object. 80 """ 81 super().__init__(*args, **kwargs) 82 83 today = datetime.date.today() 84 field_class = WorkHours.hours.field.formfield 85 field_class_correction = WorkHoursCorrection.ammount.field.formfield 86 87 self._period = Period.objects.get(pk=period_pk) 88 self._dates = list(date_iterator(self._period.start, self._period.end)) 89 self._assignments = assignments.select_related('contributor') 90 self._closure = PeriodClosure.objects.filter( 91 period=self._period, 92 ) 93 self._project = None 94 if invoice_number is not None: 95 projects = Project.objects.filter(invoice_number=invoice_number) 96 if projects.exists(): 97 self._project = projects[0] 98 99 for assignment in self._assignments: 100 closed = closure_lock and self._period.dead_line < timezone.now() 101 closure = self._closure.filter( 102 project_assignment=assignment 103 ) 104 if closure_lock and not closed and closure.exists(): 105 closed = closure[0].is_closed_manager 106 expired = False 107 for date in self._dates: 108 if self._project is not None: 109 expired = self._project.start > date or self._project.end < date 110 # create fields for each assignment 111 is_future = date > today 112 field = field_class( 113 min_value=0, 114 max_value=24, 115 required=False, 116 label=date.day, 117 label_suffix='', 118 disabled=(is_future or closed or expired) 119 ) 120 self.fields[str(assignment.pk) + '_' + 121 date.isoformat()] = field 122 self.fields[str(assignment.pk) + '_C'] = field = field_class_correction( 123 label=_('C'), 124 required=False, 125 label_suffix='', 126 disabled=closed, 127 ) 128 129 # provide initial data 130 workhours = WorkHours.objects.filter( 131 period__pk=period_pk, 132 project_assignment=assignment 133 ) 134 for entry in workhours: 135 field = self.fields.get( 136 str(assignment.pk) + '_' + entry.day.isoformat()) 137 if field is not None: 138 field.initial = entry.hours 139 140 assignment_sum = workhours.aggregate(Sum('hours')) 141 self.sums[str(assignment.pk)] = assignment_sum['hours__sum'] or 0 142 143 correction = WorkHoursCorrection.objects.filter( 144 period__pk=period_pk, 145 project_assignment=assignment 146 ) 147 if correction.exists(): 148 field = self.fields.get(str(assignment.pk) + '_C') 149 if field is not None: 150 field.initial = correction[0].ammount 151 self.sums[str(assignment.pk)] += correction[0].ammount 152 153 # feedback for closed periods 154 feedback = "" 155 if closure.exists() and closure[0].is_closed_contributor: 156 feedback += "☺️" 157 if closure.exists() and closure[0].is_closed_manager: 158 feedback += "☺️" 159 self.closures[str(assignment.pk) + '_closures'] = feedback
This constructor takes a primary key value of a Period
instance, an
ProjectAssingment
instance, a projects invoice number and whether
the inputs should be disabled according to PeriodClosure
instances.
Returns a newly constructed WorkhourSheetForm
object.
174 def as_sheet(self): 175 """ 176 Renders and returns the HTML form. 177 """ 178 context = super().get_context() 179 context.update({ 180 'start': self._period.start, 181 'fields_more': self._get_field_sheet_structure() 182 }) 183 return self.render( 184 template_name=self.template_name_sheet, 185 context=context 186 )
Renders and returns the HTML form.
188 def save(self): 189 """ 190 Modifies and saves all associated `WorkHours` instances. 191 """ 192 if self.is_valid() and self.has_changed(): 193 for assignment in self._assignments: 194 add = [] 195 delete = [] 196 field_names = [(date, str(assignment.pk) + '_' + 197 date.isoformat()) for date in self._dates] 198 for date, field_name in field_names: 199 if field_name in self.changed_data: 200 initial = self.fields[field_name].initial 201 value = self.cleaned_data.get(field_name) 202 if value is None or value == 0.0: 203 delete.append( 204 date 205 ) 206 elif initial is None: 207 add.append(WorkHours( 208 project_assignment=assignment, 209 period=self._period, 210 day=date, 211 hours=value 212 )) 213 else: 214 WorkHours.objects.filter( 215 project_assignment=assignment, 216 period=self._period, 217 day=date, 218 ).update(hours=value) 219 WorkHours.objects.filter( 220 project_assignment=assignment, 221 period=self._period, 222 day__in=delete 223 ).delete() 224 WorkHours.objects.bulk_create(add) 225 correction_name = str(assignment.pk) + '_C' 226 if correction_name in self.changed_data: 227 value = self.cleaned_data.get(correction_name) 228 if value is None or value == 0.0: 229 WorkHoursCorrection.objects.filter( 230 project_assignment=assignment, 231 period=self._period 232 ).delete() 233 else: 234 WorkHoursCorrection.objects.update_or_create( 235 project_assignment=assignment, 236 period=self._period, 237 defaults={'ammount': value} 238 )
Modifies and saves all associated WorkHours
instances.
Inherited Members
- django.forms.forms.BaseForm
- default_renderer
- field_order
- prefix
- use_required_attribute
- template_name_div
- template_name_p
- template_name_table
- template_name_ul
- template_name_label
- is_bound
- data
- files
- auto_id
- initial
- error_class
- label_suffix
- empty_permitted
- fields
- renderer
- order_fields
- errors
- is_valid
- add_prefix
- add_initial_prefix
- template_name
- get_context
- non_field_errors
- add_error
- has_error
- full_clean
- clean
- has_changed
- changed_data
- is_multipart
- visible_fields
- get_initial_for_field
- django.forms.utils.RenderableFormMixin
- as_p
- as_table
- as_ul
- as_div
- django.forms.utils.RenderableMixin
- render