Skip to content

utils

main.utils ¤

General utilities for ProCAT.

Classes¤

Functions¤

create_HoRSE_group(*args) ¤

Create HoRSE group.

Source code in main/utils.py
58
59
60
def create_HoRSE_group(*args: Any) -> None:  # type: ignore [explicit-any]
    """Create HoRSE group."""
    Group.objects.get_or_create(name="HoRSE")[0]

create_analysis(*args) ¤

Create default analysis codes.

Source code in main/utils.py
45
46
47
48
49
def create_analysis(*args: Any) -> None:  # type: ignore [explicit-any]
    """Create default analysis codes."""
    models.AnalysisCode.objects.bulk_create(
        [models.AnalysisCode(**ac) for ac in ANALYSIS_CODES]
    )

destroy_HoRSE_group(*args) ¤

Delete HoRSE group.

Source code in main/utils.py
63
64
65
def destroy_HoRSE_group(*args: Any) -> None:  # type: ignore [explicit-any]
    """Delete HoRSE group."""
    Group.objects.filter(name="HoRSE").delete()

destroy_analysis(*args) ¤

Delete default analysis codes.

Source code in main/utils.py
52
53
54
55
def destroy_analysis(*args: Any) -> None:  # type: ignore [explicit-any]
    """Delete default analysis codes."""
    codes = [ac["code"] for ac in ANALYSIS_CODES]
    models.AnalysisCode.objects.filter(code__in=codes).delete()

format_currency(value) ¤

Format a float value as a GBP currency with two decimal places.

Source code in main/utils.py
239
240
241
def format_currency(value: Decimal) -> str:
    """Format a float value as a GBP currency with two decimal places."""
    return f{value:.2f}"

get_budget_status(date=None) ¤

Get the budget status of a funding.

Source code in main/utils.py
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
def get_budget_status(
    date: date | None = None,
) -> tuple[list[Funding], list[Funding]]:
    """Get the budget status of a funding."""
    if date is None:
        date = timezone.now().date()

    funds_ran_out_not_expired = list(Funding.objects.filter(expiry_date__gt=date))
    funds_ran_out_not_expired = [
        fund for fund in funds_ran_out_not_expired if fund.funding_left <= 0
    ]

    funding_expired_budget_left = list(Funding.objects.filter(expiry_date__lt=date))
    funding_expired_budget_left = [
        fund for fund in funding_expired_budget_left if fund.funding_left > 0
    ]
    return funds_ran_out_not_expired, funding_expired_budget_left

get_calendar_year_dates() ¤

Get the start and end dates for the current calendar year.

Source code in main/utils.py
219
220
221
222
223
224
def get_calendar_year_dates() -> tuple[datetime, datetime]:
    """Get the start and end dates for the current calendar year."""
    today = timezone.now()
    start = today.replace(day=1, month=1)
    end = today.replace(day=31, month=12)
    return start, end

get_current_and_last_month(date=None) ¤

Get the start of the last month and current month, and their names.

Source code in main/utils.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def get_current_and_last_month(
    date: datetime | None = None,
) -> tuple[datetime, str, datetime, str]:
    """Get the start of the last month and current month, and their names."""
    if date is None:
        date = timezone.now()

    current_month_start = datetime(year=date.year, month=date.month, day=1)
    last_month_start = (current_month_start - timedelta(days=1)).replace(day=1)

    last_month_name = last_month_start.strftime("%B")
    current_month_name = current_month_start.strftime("%B")

    return (
        last_month_start,
        last_month_name,
        current_month_start,
        current_month_name,
    )

get_financial_year_dates() ¤

Get the start and end dates for the current financial year.

Source code in main/utils.py
227
228
229
230
231
232
233
234
235
236
def get_financial_year_dates() -> tuple[datetime, datetime]:
    """Get the start and end dates for the current financial year."""
    today = timezone.now()
    if today.month > 8:
        start = today.replace(day=1, month=8)
        end = today.replace(day=31, month=7, year=today.year + 1)
    else:
        start = today.replace(day=1, month=8, year=today.year - 1)
        end = today.replace(day=31, month=7, year=today.year)
    return start, end

get_head_email() ¤

Get the emails of the HoRSE group users.

Source code in main/utils.py
115
116
117
118
119
120
121
def get_head_email() -> list[str]:
    """Get the emails of the HoRSE group users."""
    User = get_user_model()
    head_email = User.objects.filter(groups__name="HoRSE").values_list(
        "email", flat=True
    )
    return list(head_email)

get_logged_hours(entries) ¤

Calculate total logged hours from time entries.

Source code in main/utils.py
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def get_logged_hours(
    entries: Iterable["TimeEntry"],
) -> tuple[float, str]:
    """Calculate total logged hours from time entries."""
    project_hours: defaultdict[str, float] = defaultdict(
        float
    )  # <- This defaults to 0.0
    total_hours = 0.0

    for entry in entries:
        project_name = entry.project.name
        hours = (entry.end_time - entry.start_time).total_seconds() / 3600
        total_hours += hours
        project_hours[project_name] += hours

    project_work_summary = "\n".join(
        [
            f"{project}: {round(hours / 7, 1)} days"
            # Assuming 7 hours/workday
            for project, hours in project_hours.items()
        ]
    )

    return total_hours, project_work_summary

get_month_dates_for_previous_years() ¤

Get the start and end date of each month for the previous 3 years.

Source code in main/utils.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
def get_month_dates_for_previous_years() -> list[tuple[date, date]]:
    """Get the start and end date of each month for the previous 3 years."""
    dates = []
    today = timezone.now().date()

    start_current_month = today.replace(day=1)
    for _ in range(36):
        end_prev_month = start_current_month - timedelta(days=1)
        start_prev_month = end_prev_month.replace(day=1)
        dates.append((start_prev_month, end_prev_month))
        start_current_month = start_prev_month

    dates.reverse()
    return dates

get_projects_with_days_used_exceeding_days_left() ¤

Get projects whose time entries exceed the total effort of the project.

Source code in main/utils.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
def get_projects_with_days_used_exceeding_days_left() -> list[
    tuple[Project, float, float | None]
]:
    """Get projects whose time entries exceed the total effort of the project."""
    projects = Project.objects.filter(status="Active")
    projects_with_negative_days_left = []

    for project in projects:
        if project.days_left is None:
            continue

        days_left, _ = project.days_left
        if days_left < 0:
            projects_with_negative_days_left.append(
                (project, days_left, project.total_effort)
            )

    return projects_with_negative_days_left

order_queryset_by_property(queryset, property, is_descending) ¤

Orders a queryset according to a specified Model property.

Creates a Django conditional expression to assign the position of the model in a queryset according to its model ID (using a custom ordering). The conditional expression is then provided to the QuerySet.order_by() function. This can be used to update the ordering of a queryset column in a Table.

Parameters:

Name Type Description Default
queryset QuerySet[Any]

a model queryset for ordering

required
property str

the name of the model property with which to order the queryset

required
is_descending bool

bool to indicate whether the property should be sorted by descending (or ascending) order

required

Returns:

Type Description
QuerySet[Any]

The queryset ordered according to the property.

Source code in main/utils.py
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
def order_queryset_by_property(  # type: ignore[explicit-any]
    queryset: QuerySet[Any], property: str, is_descending: bool
) -> QuerySet[Any]:
    """Orders a queryset according to a specified Model property.

    Creates a Django conditional expression to assign the position
    of the model in a queryset according to its model ID (using a
    custom ordering). The conditional expression is then provided to
    the QuerySet.order_by() function. This can be used to update the
    ordering of a queryset column in a Table.

    Args:
        queryset: a model queryset for ordering
        property: the name of the model property with which to order
            the queryset
        is_descending: bool to indicate whether the property should
            be sorted by descending (or ascending) order

    Returns:
        The queryset ordered according to the property.
    """
    queryset = queryset.order_by("id")
    model_ids = list(queryset.values_list("id", flat=True))
    values = [getattr(obj, property) for obj in queryset]
    sorted_indexes = sorted(
        range(len(values)),
        key=lambda i: (values[i] is not None, values[i]),
        reverse=is_descending,
    )
    # Create conditional expression using custom ordering
    preserved_ordering = Case(
        *[
            When(id=model_ids[id], then=position)
            for position, id in enumerate(sorted_indexes)
        ]
    )
    queryset = queryset.order_by(preserved_ordering)
    return queryset