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                        )
def date_iterator(start, end):
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.

class CustomDateInput(django.forms.widgets.DateInput):
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.

input_type = 'date'
def format_value(self, value):
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.

media
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
is_hidden
subwidgets
render
build_attrs
value_from_datadict
value_omitted_from_data
id_for_label
use_required_attribute
class CustomDateTimeInput(django.forms.widgets.DateTimeInput):
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.

input_type = 'datetime-local'
media
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
is_hidden
subwidgets
render
build_attrs
value_from_datadict
value_omitted_from_data
id_for_label
use_required_attribute
class PeriodSelectForm(django.forms.forms.Form):
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.

PeriodSelectForm(queryset, *args, **kwargs)
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.

media

Return all media required to render the widgets on this form.

declared_fields = {}
base_fields = {}
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
hidden_fields
visible_fields
get_initial_for_field
django.forms.utils.RenderableFormMixin
as_p
as_table
as_ul
as_div
django.forms.utils.RenderableMixin
render
class WorkhourSheetForm(django.forms.forms.Form):
 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.

WorkhourSheetForm( *args, period_pk=None, assignments=None, closure_lock=True, invoice_number=None, **kwargs)
 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 PeriodClosureinstances.

Returns a newly constructed WorkhourSheetForm object.

template_name_sheet = 'vkk/workhours/workhours_sheet_form.html'
sums = {}
closures = {}
def as_sheet(self):
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.

def save(self):
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.

media

Return all media required to render the widgets on this form.

declared_fields = {}
base_fields = {}
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
hidden_fields
visible_fields
get_initial_for_field
django.forms.utils.RenderableFormMixin
as_p
as_table
as_ul
as_div
django.forms.utils.RenderableMixin
render