Skip to content

models

main.models ¤

Models module for main app.

Classes¤

AnalysisCode ¤

Bases: Model

Analysis code to use during charging.

Functions¤
__str__() ¤

String representation of the Analysis Code object.

Source code in main/models.py
86
87
88
def __str__(self) -> str:
    """String representation of the Analysis Code object."""
    return f"{self.code} - {self.description}"

Capacity ¤

Bases: Model

Proportion of working time that team members are able to work on projects.

Classes¤
Meta ¤

Meta class for the model.

Functions¤
__str__() ¤

String representation of the Capacity object.

Source code in main/models.py
614
615
616
def __str__(self) -> str:
    """String representation of the Capacity object."""
    return f"From {self.start_date}, the capacity of {self.user} is {self.value}."

Department ¤

Bases: Model

Model to manage the departments.

You can find the faculties and potential departments in:

https://www.imperial.ac.uk/faculties-and-departments/

Functions¤
__str__() ¤

String representation of the Department object.

Source code in main/models.py
57
58
59
def __str__(self) -> str:
    """String representation of the Department object."""
    return f"{self.name} - {self.faculty}"

Funding ¤

Bases: Model

Funding associated with a project.

Attributes¤
effort property ¤

Provide the effort in days, calculated based on the budget and daily rate.

Returns:

Type Description
float

The total number of days of effort provided by the funding.

effort_left property ¤

Provide the effort left in days.

Returns:

Type Description
float

The number of days worth of effort left.

funding_left property ¤

Provide the funding left in currency.

Funding left is calculated based on 'Confirmed' monthly charges.

Returns:

Type Description
Decimal

The amount of funding left.

monthly_pro_rata_charge property ¤

Calculate the charge per month if the project has Pro-rata charging.

Calculates the number of months between project start and end date regardless of the day of the month so the monthly charge will be the same regardless of the number of days in the month.

project_code property ¤

Provide the project code, containing the cost centre and activity code.

Returns:

Type Description
str

The designated project code.

Classes¤
Meta ¤

Meta class for the model.

Functions¤
__str__() ¤

String representation of the Funding object.

Source code in main/models.py
469
470
471
def __str__(self) -> str:
    """String representation of the Funding object."""
    return f"{self.project} - £{self.budget:.2f} - {self.project_code}"
clean() ¤

Ensure that the activity code has a valid value.

Source code in main/models.py
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
def clean(self) -> None:
    """Ensure that the activity code has a valid value."""
    if (
        self.project
        and self.project.status in ("Active", "Confirmed")
        and not self.is_complete()
    ):
        raise ValidationError(
            "Funding of Active and Confirmed projects must be complete."
        )

    allowed_characters = ["P", "F", "G", "I"]
    if self.activity and (
        len(self.activity) != 6
        or not self.activity.isalnum()
        or self.activity[0] not in allowed_characters
    ):
        raise ValidationError(
            "Activity code must be 6 alphanumeric characters starting with P, F, "
            "G or I."
        )
is_complete() ¤

Checks if funding record is complete.

This is only relevant to funding where source is external.

Source code in main/models.py
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
def is_complete(self) -> bool:
    """Checks if funding record is complete.

    This is only relevant to funding where source is external.
    """
    if self.source == "Internal":
        return True

    return bool(
        self.funding_body
        and self.cost_centre
        and self.activity
        and self.analysis_code
        and self.expiry_date
    )

MonthlyCharge ¤

Bases: Model

Monthly charge for a specific project, account and analysis code.

Functions¤
__str__() ¤

String representation of the MonthlyCharge object.

Source code in main/models.py
676
677
678
def __str__(self) -> str:
    """String representation of the MonthlyCharge object."""
    return self.description
clean() ¤

Ensure the charge has valid funding attached and description if Manual.

Source code in main/models.py
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
def clean(self) -> None:
    """Ensure the charge has valid funding attached and description if Manual."""
    super().clean()
    if not self.funding.expiry_date:
        raise ValidationError("Funding source must have an expiry date.")

    if (
        self.date > self.funding.expiry_date
        or self.funding.funding_left < 0  # After deducting charge amount
    ):
        raise ValidationError(
            "Monthly charge must not exceed the funding date or amount."
        )

    if self.project.charging == "Manual":
        if not self.description:
            raise ValidationError(
                "Line description needed for manual charging method."
            )
    else:
        self.description = (
            f"RSE Project {self.project} ({self.funding.project_code}): "
            f"{self.date.month}/{self.date.year} [rcs-manager@imperial.ac.uk]"
        )

Project ¤

Bases: Model

Software project details.

Attributes¤
days_left property ¤

Provide the days worth of effort left.

Returns:

Type Description
tuple[float, float] | None

The number of days and percentage worth of effort left, or None if there is

tuple[float, float] | None

no funding information.

effort_per_day property ¤

Calculate the estimated effort per day.

Considers only working (business) days.

Returns:

Type Description
float | None

Float representing the estimated effort per day over project lifespan.

percent_effort_left property ¤

Provide the percentage of effort left.

Returns:

Type Description
float | None

The percentage of effort left, or None if there is no funding information.

total_effort property ¤

Provide the total days worth of effort available from funding.

Returns:

Type Description
float | None

The total number of days effort, or None if there is no funding information.

total_funding_left property ¤

Provide the total funding left after deducting confirmed charges.

Returns:

Type Description
Decimal | None

The total monetary amount of funding left, or none if there is no funding

Decimal | None

information.

total_working_days property ¤

Provide the total number of working (business) days given the dates.

Returns:

Type Description
int | None

Number of working days between the project start and end date.

weeks_to_deadline property ¤

Provide the number of weeks left until project deadline.

Only relevant for active projects.

Returns:

Type Description
tuple[int, float] | None

The number of weeks left or None if the project is Tentative or Not done.

Functions¤
__str__() ¤

String representation of the Project object.

Source code in main/models.py
191
192
193
def __str__(self) -> str:
    """String representation of the Project object."""
    return self.name
check_and_notify_status() ¤

Check the project status and notify accordingly.

Source code in main/models.py
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
def check_and_notify_status(self) -> None:
    """Check the project status and notify accordingly."""
    from .tasks import notify_left_threshold

    check = False

    assert self.lead and hasattr(self.lead, "email")

    for threshold in sorted(EFFORT_LEFT_THRESHOLD):
        if self.percent_effort_left is None or self.percent_effort_left > threshold:
            continue

        if str(threshold) in self.notifications_effort:
            # Already notified for this threshold in the past
            break

        notify_left_threshold(
            email=self.lead.email,
            lead=self.lead.get_full_name(),
            project_name=self.name,
            threshold_type="effort",
            threshold=threshold,
            value=self.days_left[0] if self.days_left else 0,
        )
        self.notifications_effort[str(threshold)] = (
            timezone.now().date().isoformat()
        )
        check = True
        break

    for threshold in sorted(WEEKS_LEFT_THRESHOLD):
        if self.weeks_to_deadline is None or self.weeks_to_deadline[1] > threshold:
            continue

        if str(threshold) in self.notifications_weeks:
            # Already notified for this threshold in the past
            break

        notify_left_threshold(
            email=self.lead.email,
            lead=self.lead.get_full_name(),
            project_name=self.name,
            threshold_type="weeks",
            threshold=threshold,
            value=self.weeks_to_deadline[0] if self.weeks_to_deadline else 0,
        )
        self.notifications_weeks[str(threshold)] = timezone.now().date().isoformat()
        check = True
        break

    if check:
        self.save(update_fields=["notifications_effort", "notifications_weeks"])
clean() ¤

Ensure all fields have a value unless status is 'Tentative' or 'Not done'.

It also checks that, if present, the end date is after the start date.

Source code in main/models.py
195
196
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
def clean(self) -> None:
    """Ensure all fields have a value unless status is 'Tentative' or 'Not done'.

    It also checks that, if present, the end date is after the start date.
    """
    if self.status == "Tentative" or self.status == "Not done":
        return super().clean()

    if not self.start_date or not self.end_date or not self.lead:
        raise ValidationError(
            "All fields are mandatory except if Project status is 'Tentative' or "
            "'Not done'."
        )

    if self.end_date <= self.start_date:
        raise ValidationError("The end date must be after the start date.")

    if self.pk is not None and self.status in ("Active", "Confirmed"):
        if not self.funding_source.exists():
            raise ValidationError(
                "Active and Confirmed projects must have at least 1 funding source."
            )

        if not all([f.is_complete() for f in self.funding_source.all()]):
            raise ValidationError(
                "Funding of Active and Confirmed projects must be complete."
            )

TimeEntry ¤

Bases: Model

Time entry for a user.

Functions¤
__str__() ¤

String representation of the Time Entry object.

Source code in main/models.py
750
751
752
def __str__(self) -> str:
    """String representation of the Time Entry object."""
    return f"{self.user} - {self.project} - {self.start_time} to {self.end_time}"

User ¤

Bases: AbstractUser

Custom user model.