Skip to content

views

measurement.views ¤

Attributes¤

logger = logging.getLogger(__name__) module-attribute ¤

Classes¤

DailyValidation ¤

Bases: LoginRequiredMixin, View

View for displaying the Daily Validation dash app.

DataReport ¤

Bases: View

View for displaying the Data Report dash app.

MeasurementDataDownloadAPIView ¤

Bases: APIView

API endpoint for downloading measurement data.

Access to measurement data is controlled via object-level permissions using django-guardian. Users can only access data from Station objects for which they have the view_measurements permission assigned. This is configured via django-guardian's object-level permission system, typically by assigning the view_measurements permission to users or groups for specific Station instances.

Attributes¤
paginator property ¤

Return the paginator instance.

Functions¤
get(request) ¤

Retrieve measurement data based on query parameters.

Source code in measurement/views.py
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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
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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
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
@extend_schema(
    summary="Download measurement data",
    description="""
    Download measurement data within a date range.

    **Permissions**: Users can only access data from stations they have
    `view_measurements` permission for.

    **Report Types**:
    - `measurement`: Raw measurement data
    - `validated`: Validated measurement data only
    - `hourly`: Hourly aggregated report
    - `daily`: Daily aggregated report
    - `monthly`: Monthly aggregated report

    **Traces**: Select which data columns to include in the response.
    Common traces include `value`, `maximum`, `minimum`, `depth`, and `direction`.

    **Pagination**: Results are paginated with a default page size of 1000 records.
    Use `page` and `page_size` query parameters to control pagination.
    Maximum page size is 10000 records.
    """,
    parameters=[
        OpenApiParameter(
            name="station",
            type=str,
            location=OpenApiParameter.QUERY,
            description="Station code",
            required=True,
        ),
        OpenApiParameter(
            name="variable",
            type=str,
            location=OpenApiParameter.QUERY,
            description="Variable code",
            required=True,
        ),
        OpenApiParameter(
            name="start_date",
            type=str,
            location=OpenApiParameter.QUERY,
            description="Start date (YYYY-MM-DD)",
            required=True,
        ),
        OpenApiParameter(
            name="end_date",
            type=str,
            location=OpenApiParameter.QUERY,
            description="End date (YYYY-MM-DD)",
            required=True,
        ),
        OpenApiParameter(
            name="report_type",
            type=str,
            location=OpenApiParameter.QUERY,
            description="Type of report",
            required=True,
            enum=["measurement", "validated", "hourly", "daily", "monthly"],
        ),
        OpenApiParameter(
            name="traces",
            type={"type": "array", "items": {"type": "string"}},
            location=OpenApiParameter.QUERY,
            description="Data traces to include",
            required=False,
            enum=[
                v for v, _ in MeasurementDataDownloadRequestSerializer.TRACE_CHOICES
            ],
        ),
        OpenApiParameter(
            name="page",
            type=int,
            location=OpenApiParameter.QUERY,
            description="Page number for pagination",
            required=False,
        ),
        OpenApiParameter(
            name="page_size",
            type=int,
            location=OpenApiParameter.QUERY,
            description="Number of records per page (default: 1000, max: 10000)",
            required=False,
        ),
    ],
    responses={
        200: OpenApiResponse(
            response=MeasurementDataDownloadResponseSerializer(many=True),
            description="Measurement data retrieved successfully",
            examples=[
                OpenApiExample(
                    "Successful response",
                    value=[
                        {
                            "id": 1,
                            "time": "2024-01-01T00:00:00Z",
                            "value": "12.5000",
                            "maximum": "15.0000",
                            "minimum": "10.0000",
                        },
                        {
                            "id": 2,
                            "time": "2024-01-01T01:00:00Z",
                            "value": "13.2000",
                            "maximum": "16.0000",
                            "minimum": "11.0000",
                        },
                    ],
                )
            ],
        ),
        400: OpenApiResponse(description="Invalid request parameters"),
        403: OpenApiResponse(
            description="User does not have permission to access the station's data"
        ),
        404: OpenApiResponse(description="Station or variable not found"),
    },
    tags=["measurements"],
)
def get(self, request):
    """Retrieve measurement data based on query parameters."""
    # Handle traces as a list from query params
    traces = request.query_params.getlist("traces")
    if not traces:
        traces = ["value", "maximum", "minimum", "depth", "direction"]

    # Build data dict for serializer
    data = {
        "station": request.query_params.get("station"),
        "variable": request.query_params.get("variable"),
        "start_date": request.query_params.get("start_date"),
        "end_date": request.query_params.get("end_date"),
        "report_type": request.query_params.get("report_type"),
        "traces": traces,
    }

    # Validate request parameters
    serializer = MeasurementDataDownloadRequestSerializer(data=data)
    if not serializer.is_valid():
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    validated_data = serializer.validated_data
    station = validated_data["station"]

    # Check user permissions for this station
    permitted_stations = self.get_permitted_stations(request.user)
    if station not in permitted_stations:
        return Response(
            {
                "detail": "You do not have permission to access data "
                f"for station '{station.station_code}'."
            },
            status=status.HTTP_403_FORBIDDEN,
        )

    # Get the data using existing function
    try:
        df = get_report_data_from_db(
            station=station.station_code,
            variable=validated_data["variable"].variable_code,
            start_time=validated_data["start_date"].strftime("%Y-%m-%d"),
            end_time=validated_data["end_date"].strftime("%Y-%m-%d"),
            report_type=validated_data["report_type"],
            whole_months=False,
        )
    except Exception:
        logger.exception("Error retrieving data")
        return Response(
            {"detail": "An internal error occurred retrieving data."},
            status=status.HTTP_500_INTERNAL_SERVER_ERROR,
        )

    if df.empty:
        return Response([])

    # Filter to requested traces plus required columns
    available_traces = [t for t in validated_data["traces"] if t in df.columns]
    columns_to_include = ["id", "time", *available_traces]

    # Add completeness and report_type if available (for report data)
    if "completeness" in df.columns:
        columns_to_include.append("completeness")
    if "report_type" in df.columns:
        columns_to_include.append("report_type")

    # Filter columns that exist in the dataframe
    columns_to_include = [c for c in columns_to_include if c in df.columns]
    result_df = df[columns_to_include]

    # Convert to list of dicts for pagination
    result = result_df.to_dict(orient="records")

    # Paginate the results
    page = self.paginate_queryset(result)
    response_serializer = MeasurementDataDownloadResponseSerializer(page, many=True)
    return self.get_paginated_response(response_serializer.data)
get_paginated_response(data) ¤

Return a paginated response.

Source code in measurement/views.py
103
104
105
def get_paginated_response(self, data):
    """Return a paginated response."""
    return self.paginator.get_paginated_response(data)
get_permitted_stations(user) ¤

Get stations the user has permission to view measurements for.

Source code in measurement/views.py
107
108
109
def get_permitted_stations(self, user):
    """Get stations the user has permission to view measurements for."""
    return get_objects_for_user(user, "view_measurements", klass=Station)
paginate_queryset(queryset) ¤

Paginate a list of records.

Source code in measurement/views.py
 97
 98
 99
100
101
def paginate_queryset(self, queryset):
    """Paginate a list of records."""
    if self.paginator is None:
        return None
    return self.paginator.paginate_queryset(queryset, self.request, view=self)

MeasurementDataDownloadRequestSerializer ¤

Bases: Serializer

Serializer for validating measurement data download requests.

Functions¤
validate(attrs) ¤

Validate the request data.

Source code in measurement/serializers.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def validate(self, attrs):
    """Validate the request data."""
    if attrs["start_date"] > attrs["end_date"]:
        raise serializers.ValidationError(
            {"end_date": "End date must be after start date."}
        )

    # Validate that the variable is available for the station
    station = attrs["station"]
    variable = attrs["variable"]
    if variable.variable_code not in station.variables_list:
        raise serializers.ValidationError(
            {
                "variable": f"Variable '{variable.variable_code}' is not available "
                f"for station '{station.station_code}'."
            }
        )

    return attrs

MeasurementDataDownloadResponseSerializer ¤

Bases: Serializer

Serializer for measurement data response.

MeasurementDataPagination ¤

Bases: PageNumberPagination

Pagination for measurement data API.

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)

Functions¤

get_report_data_from_db(station, variable, start_time, end_time, report_type, whole_months=True) cached ¤

Retrieves the report data from the database.

Time is set to the station timezone and the time range is inclusive of both start and end times.

Parameters:

Name Type Description Default
station str

Station of interest.

required
variable str

Variable of interest.

required
start_time str

Start time.

required
end_time str

End time.

required
report_type str

Type of report to retrieve.

required
whole_months bool

Whether to cover whole months or not.

True

Returns:

Type Description
DataFrame

A dataframe with the report data.

Source code in measurement/reporting.py
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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
@lru_cache(1)
def get_report_data_from_db(
    station: str,
    variable: str,
    start_time: str,
    end_time: str,
    report_type: str,
    whole_months: bool = True,
) -> pd.DataFrame:
    """Retrieves the report data from the database.

    Time is set to the station timezone and the time range is inclusive of both
    start and end times.

    Args:
        station: Station of interest.
        variable: Variable of interest.
        start_time: Start time.
        end_time: End time.
        report_type: Type of report to retrieve.
        whole_months: Whether to cover whole months or not.

    Returns:
        A dataframe with the report data.
    """
    start_time_, end_time_ = reformat_dates(start_time, end_time, whole_months)

    if report_type == "measurement":
        data = pd.DataFrame.from_records(
            Measurement.objects.filter(
                station__station_code=station,
                variable__variable_code=variable,
                time__date__range=(start_time_.date(), end_time_.date()),
            ).values()
        )
        raw_cols = [col for col in data.columns if col.startswith("raw_")]
        normal = [col.strip("raw_") for col in raw_cols]
        data = data.drop(columns=normal).rename(columns=dict(zip(raw_cols, normal)))

    elif report_type == "validated":
        data = pd.DataFrame.from_records(
            Measurement.objects.filter(
                station__station_code=station,
                variable__variable_code=variable,
                time__date__range=(start_time_.date(), end_time_.date()),
                is_validated=True,
                is_active=True,
            ).values()
        )
        raw_cols = [col for col in data.columns if col.startswith("raw_")]
        data = data.drop(columns=raw_cols)

    else:
        data = pd.DataFrame.from_records(
            Report.objects.filter(
                station__station_code=station,
                variable__variable_code=variable,
                time__date__range=(start_time_.date(), end_time_.date()),
                report_type=report_type,
            ).values()
        )

    data = data.rename(columns={"station_id": "station", "variable_id": "variable"})

    if data.empty:
        return data

    tz = timezone.get_current_timezone()
    data["time"] = data["time"].dt.tz_convert(tz)
    return data.sort_values("time")