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
56
57
58
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
43
44
45
46
47
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
61
62
63
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
50
51
52
53
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()

get_budget_status(date=None) ¤

Get the budget status of a funding.

Source code in main/utils.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def get_budget_status(
    date: date | None = None,
) -> tuple[QuerySet[Funding], QuerySet[Funding]]:
    """Get the budget status of a funding."""
    if date is None:
        date = datetime.today().date()

    funds_ran_out_not_expired = Funding.objects.filter(
        expiry_date__gt=date, budget__lt=0
    )
    funding_expired_budget_left = Funding.objects.filter(
        expiry_date__lt=date, budget__gt=0
    )
    return funds_ran_out_not_expired, funding_expired_budget_left

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
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
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 = datetime.today()

    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_head_email() ¤

Get the emails of the HoRSE group users.

Source code in main/utils.py
113
114
115
116
117
118
119
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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_year() ¤

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

Source code in main/utils.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
def get_month_dates_for_previous_year() -> list[tuple[date, date]]:
    """Get the start and end date of each month for the previous year."""
    dates = []
    today = datetime.today().date()

    start_current_month = today.replace(day=1)
    for _ in range(12):
        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(date=None) ¤

Get projects whose days used in the last month exceed the days left.

Source code in main/utils.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
def get_projects_with_days_used_exceeding_days_left(
    date: datetime | None = None,
) -> list[tuple[Project, float, float]]:
    """Get projects whose days used in the last month exceed the days left."""
    if date is None:
        date = datetime.today()

    projects_with_days_used_exceeding_days_left = []

    last_month_start, _, current_month_start, _ = get_current_and_last_month(date)

    projects = Project.objects.filter(status="Active")

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

        days_left, _ = project.days_left

        time_entries = project.timeentry_set.filter(
            start_time__gte=last_month_start,
            end_time__lt=current_month_start,
            monthly_charge__isnull=True,  # include entries that are not yet charged
        )

        if not time_entries.exists():
            continue

        total_hours = sum(
            (entry.end_time - entry.start_time).total_seconds() / 3600
            for entry in time_entries
        )

        days_used = round(total_hours / 7, 1)  # Assuming 7 hrs/workday

        if days_used > days_left:
            projects_with_days_used_exceeding_days_left.append(
                (project, days_used, days_left)
            )

    return projects_with_days_used_exceeding_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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
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.
    """
    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], 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