Skip to content

models

importing.models ¤

Attributes¤

MAX_FILE_SIZE = settings.MAX_LAYER_FILE_SIZE_MB module-attribute ¤

User = get_user_model() module-attribute ¤

Classes¤

DataImport ¤

Bases: PermissionsBase

Model to store the data imports.

This model stores the data imports, which are, often, files with data that are uploaded to the system. The data is then processed asynchronously and stored in the database.

Attributes:

Name Type Description
station ForeignKey

Station to which the data belongs.

format ForeignKey

Format of the data.

rawfile FileField

File with the data to be imported.

date DateTimeField

Date of submission of the data.

start_date DateTimeField

Start date of the data.

end_date DateTimeField

End date of the data.

records IntegerField

Number of records in the data.

observations TextField

Notes or observations about the data.

status TextField

Status of the import.

log TextField

Log of the data ingestion, indicating any errors.

Functions¤
clean() ¤

Validate information and uploads the measurement data.

Source code in importing/models.py
138
139
140
141
142
143
144
145
def clean(self) -> None:
    """Validate information and uploads the measurement data."""
    tz = self.station.timezone
    if not tz:
        raise ValidationError("Station must have a timezone set.")

    if self.origin.origin == "Thingsboard" and not self.format.thingsboard:
        raise ValidationError("Ensure a Thingsboard-specific format is specified.")

Format ¤

Bases: PermissionsBase

Details of the data file format, describing how to read the file.

It combines several properties, such as the file extension, the delimiter, the date and time formats, and the column indices for the date and time columns, instructing how to read the data file and parse the dates. It is mostly used to ingest data from text files, like CSV. For Thingsboard imports, only the name, description and thingsboard fields are applicable.

Attributes:

Name Type Description
format_id AutoField

Primary key.

name CharField

Short name of the format entry.

description TextField

Description of the format.

extension ForeignKey

The extension of the data file.

delimiter ForeignKey

The delimiter between columns in the data file. Only required for text files.

first_row PositiveSmallIntegerField

Index of the first row with data, starting in 0.

footer_rows PositiveSmallIntegerField

Number of footer rows to be ignored at the end.

date ForeignKey

Format for the date column. Only required for text files.

date_column PositiveSmallIntegerField

Index of the date column, starting in 0.

time ForeignKey

Format for the time column. Only required for text files.

time_column PositiveSmallIntegerField

Index of the time column, starting in 0.

thingsboard BooleanField

Whether the data is being imported from Thingsboard.

Attributes¤
datetime_format property ¤

Obtain the datetime format string.

Functions¤
__str__() ¤

Return the string representation of the object.

Source code in formatting/models.py
272
273
274
def __str__(self) -> str:
    """Return the string representation of the object."""
    return str(self.name)
clean() ¤

Validate the model instance.

Checks that the required fields for non-Thingsboard data are provided.

Source code in formatting/models.py
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
def clean(self) -> None:
    """Validate the model instance.

    Checks that the required fields for non-Thingsboard data are provided.
    """
    super().clean()
    errors = {}
    if not self.thingsboard:
        required_fields = (
            "extension",
            "first_row",
            "footer_rows",
            "date_column",
            "time_column",
        )
        for field in required_fields:
            if getattr(self, field) is None:
                errors[field] = "Field is required for non-Thingsboard data."

    if errors:
        raise ValidationError(errors)
datetime_columns(delimiter) ¤

Column indices that correspond to the date and time columns in the dataset.

Parameters:

Name Type Description Default
delimiter str

The delimiter used to split the date and time codes.

required

Returns:

Type Description
list[int]

list[int]: A list of column indices.

Source code in formatting/models.py
285
286
287
288
289
290
291
292
293
294
295
296
297
298
def datetime_columns(self, delimiter: str) -> list[int]:
    """Column indices that correspond to the date and time columns in the dataset.

    Args:
        delimiter (str): The delimiter used to split the date and time codes.

    Returns:
        list[int]: A list of column indices.
    """
    date_items = self.date.code.split(delimiter)
    date_cols = list(range(self.date_column, self.date_column + len(date_items)))
    time_items = self.time.code.split(delimiter)
    time_cols = list(range(self.time_column, self.time_column + len(time_items)))
    return date_cols + time_cols
get_absolute_url() ¤

Get the absolute URL of the object.

Source code in formatting/models.py
276
277
278
def get_absolute_url(self) -> str:
    """Get the absolute URL of the object."""
    return reverse("formatting:format_detail", kwargs={"pk": self.pk})

ImportOrigin ¤

Bases: Model

Class that contains the origin of the data import, eg. file, API, etc.

Functions¤
get_default() classmethod ¤

Get default import origin, 'file'.

It should exist, as it is created in a data migration, but just in case it is not, we use get_or_create.

Source code in importing/models.py
45
46
47
48
49
50
51
52
53
@classmethod
def get_default(cls) -> ImportOrigin:
    """Get default import origin, 'file'.

    It should exist, as it is created in a data migration, but just in case it
    is not, we use get_or_create.
    """
    obj, _ = cls.objects.get_or_create(origin="file")
    return obj.pk

MapLayerImport ¤

Bases: PermissionsBase

Functions¤
clean() ¤

Validate the uploaded GeoTIFF and its transformed lon/lat bounds.

Source code in importing/models.py
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
def clean(self) -> None:
    """Validate the uploaded GeoTIFF and its transformed lon/lat bounds."""

    if not self.file:
        return

    import rasterio
    from rasterio.warp import transform_bounds

    file_obj = self.file.file
    try:
        with rasterio.MemoryFile(file_obj.read()) as memfile:
            with memfile.open() as dataset:
                if dataset.count == 0:
                    raise ValidationError(
                        {"file": "File contains no raster bands."}
                    )
                if dataset.crs is None:
                    raise ValidationError(
                        {"file": "File has no coordinate reference system (CRS)."}
                    )

                left, bottom, right, top = transform_bounds(
                    dataset.crs,
                    "EPSG:4326",
                    dataset.bounds.left,
                    dataset.bounds.bottom,
                    dataset.bounds.right,
                    dataset.bounds.top,
                )

                if not (-180 <= left <= 180 and -180 <= right <= 180):
                    raise ValidationError(
                        {
                            "file": (
                                "GeoTIFF longitude values are out of range "
                                "[-180, 180]."
                            )
                        }
                    )
                if not (-90 <= bottom <= 90 and -90 <= top <= 90):
                    raise ValidationError(
                        {
                            "file": (
                                "GeoTIFF latitude values are out of range "
                                "[-90, 90]."
                            )
                        }
                    )
    except ValidationError:
        raise
    except Exception as e:
        raise ValidationError(
            {"file": f"Could not open as a valid GeoTIFF or read coordinates: {e}"}
        )
    finally:
        file_obj.seek(0)

SensorInstallation ¤

Bases: PermissionsBase

Represents an installation of a Sensor at a Station, which measures a Variable.

It includes metadata for installation and finishing date, as well as state (active or not).

Attributes:

Name Type Description
sensorinstallation_id AutoField

Primary key.

variable ForeignKey

Variable measured by the sensor.

station ForeignKey

Station where the sensor is installed.

sensor ForeignKey

Sensor used for the measurement.

start_date DateField

Start date of the installation.

end_date DateField

End date of the installation.

state BooleanField

Is the sensor active?

Functions¤
get_absolute_url() ¤

Get the absolute URL of the object.

Source code in variable/models.py
245
246
247
def get_absolute_url(self) -> str:
    """Get the absolute URL of the object."""
    return reverse("variable:sensorinstallation_detail", kwargs={"pk": self.pk})

Station ¤

Bases: PermissionsBase

Main representation of a station, including several metadata.

Attributes:

Name Type Description
visibility str

Visibility level of the object, including an "internal" option.

station_id int

Primary key.

station_code str

Unique code for the station.

station_name str

Brief description of the station.

station_type StationType

Type of the station.

country Country

Country where the station is located.

region Region

Region within the Country where the station is located.

ecosystem Ecosystem

Ecosystem associated with the station.

institution Institution

Institutional partner responsible for the station.

place_basin PlaceBasin

Place-Basin association.

station_state bool

Is the station operational?

timezone str

Timezone of the station.

station_latitude Decimal

Latitude of the station, in degrees [-90 to 90].

station_longitude Decimal

Longitude of the station, in degrees [-180 to 180].

station_altitude int

Altitude of the station.

influence_km Decimal

Area of influence in km2.

station_file ImageField

Photography of the station.

station_external bool

Is the station external?

variables str

Comma-separated list of variables measured by the station.

Attributes¤
variables_list property ¤

Return the list of variables measured by the station.

Only variables with data in the database are returned.

Returns:

Type Description
list[str]

list[str]: List of variables measured by the station.

Functions¤
__str__() ¤

Return the station code.

Source code in station/models.py
458
459
460
def __str__(self) -> str:
    """Return the station code."""
    return str(self.station_code)
get_absolute_url() ¤

Return the absolute url of the station.

Source code in station/models.py
462
463
464
def get_absolute_url(self) -> str:
    """Return the absolute url of the station."""
    return reverse("station:station_detail", kwargs={"pk": self.pk})
set_object_permissions() ¤

Set object-level permissions.

This method is called by the save method of the model to set the object-level permissions based on the visibility level of the object. In addition to the standard permissions for the station, the view_measurements permission is set which controls who can view the measurements associated to the station.

Source code in station/models.py
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
def set_object_permissions(self) -> None:
    """Set object-level permissions.

    This method is called by the save method of the model to set the object-level
    permissions based on the visibility level of the object. In addition to the
    standard permissions for the station, the view_measurements permission is set
    which controls who can view the measurements associated to the station.
    """
    super().set_object_permissions()

    standard_group = Group.objects.get(name="Standard")
    anonymous_user = get_anonymous_user()

    # Assign view_measurements permission based on permissions level
    if self.visibility == "public":
        assign_perm("view_measurements", standard_group, self)
        assign_perm("view_measurements", anonymous_user, self)
        if self.owner:
            remove_perm("view_measurements", self.owner, self)
    elif self.visibility == "internal":
        assign_perm("view_measurements", standard_group, self)
        remove_perm("view_measurements", anonymous_user, self)
        if self.owner:
            remove_perm("view_measurements", self.owner, self)
    elif self.visibility == "private":
        remove_perm("view_measurements", standard_group, self)
        remove_perm("view_measurements", anonymous_user, self)
        if self.owner:
            assign_perm("view_measurements", self.owner, self)

ThingsboardImportMap ¤

Bases: PermissionsBase

Model to store Thingsboard device mappings to station variables.

This model maps Thingsboard devices to specific variables at stations, allowing data from IoT devices to be imported and associated with the correct station and variable combinations.

Attributes:

Name Type Description
tb_variable CharField

Name of the variable in Thingsboard.

variable ForeignKey

The existing variable in Paricia associated with this mapping.

tb_device_name CharField

The name of the device in Thingsboard.

station ForeignKey

The name of the corresponding station in Paricia.

Functions¤
clean() ¤

Validate that the variable is valid for the station.

Source code in importing/models.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
def clean(self) -> None:
    """Validate that the variable is valid for the station."""
    try:
        station = self.station
    except ThingsboardImportMap.station.RelatedObjectDoesNotExist:
        raise ValidationError({"station": "Station is required."})

    try:
        variable = self.variable
    except ThingsboardImportMap.variable.RelatedObjectDoesNotExist:
        raise ValidationError({"variable": "Variable is required."})

    # Check if the variable is valid for the station through SensorInstallation
    if not SensorInstallation.objects.filter(
        variable=variable, station=station
    ).exists():
        raise ValidationError(
            {
                "variable": f"Variable '{variable}' is not configured for"
                f" station '{station}' via a sensor installation."
            }
        )

Variable ¤

Bases: PermissionsBase

A variable with a physical meaning.

Such as precipitation, wind speed, wind direction, soil moisture, including the associated unit. It also includes metadata to help identify what is a reasonable value for the data, to flag outliers and to help with the validation process.

The nature of the variable can be one of the following:

  • sum: Cumulative value over a period of time.
  • average: Average value over a period of time.
  • value: One-off value.

Attributes:

Name Type Description
variable_id AutoField

Primary key.

variable_code CharField

Code of the variable, eg. airtemperature.

name CharField

Human-readable name of the variable, eg. Air temperature.

unit ForeignKey

Unit of the variable.

maximum DecimalField

Maximum value allowed for the variable.

minimum DecimalField

Minimum value allowed for the variable.

diff_error DecimalField

If two sequential values in the time-series data of this variable differ by more than this value, the validation process can mark this with an error flag.

outlier_limit DecimalField

The statistical deviation for defining outliers, in times the standard deviation (sigma).

null_limit DecimalField

The max % of null values (missing, caused by e.g. equipment malfunction) allowed for hourly, daily, monthly data. Cumulative values are not deemed trustworthy if the number of missing values in a given period is greater than the null_limit.

nature CharField

Nature of the variable, eg. if it represents a one-off value, the average over a period of time or the cumulative value over a period

Attributes¤
is_cumulative property ¤

Return True if the nature of the variable is sum.

Functions¤
__str__() ¤

Return the string representation of the object.

Source code in variable/models.py
165
166
167
def __str__(self) -> str:
    """Return the string representation of the object."""
    return str(self.name)
clean() ¤

Validate the model fields.

Source code in variable/models.py
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def clean(self) -> None:
    """Validate the model fields."""
    if self.maximum < self.minimum:
        raise ValidationError(
            {"maximum": "The maximum value must be greater than the minimum value."}
        )
    if not self.variable_code.isidentifier():
        raise ValidationError(
            {
                "variable_code": "The variable code must be a valid Python "
                "identifier. Only letters, numbers and underscores are allowed, and"
                " it cannot start with a number."
            }
        )
    return super().clean()
get_absolute_url() ¤

Get the absolute URL of the object.

Source code in variable/models.py
169
170
171
def get_absolute_url(self) -> str:
    """Get the absolute URL of the object."""
    return reverse("variable:variable_detail", kwargs={"pk": self.pk})

Functions¤

validate_layer_file_size(file) ¤

Source code in importing/utils.py
102
103
104
105
106
107
def validate_layer_file_size(file) -> None:
    if file.size > MAX_FILE_SIZE * 1024 * 1024:
        raise ValidationError(
            f"File size must not exceed {MAX_FILE_SIZE} MB."
            f" Uploaded file is {file.size / (1024 * 1024):.1f} MB."
        )