Skip to content

Commit

Permalink
23 table download (#85)
Browse files Browse the repository at this point in the history
* tabular exports time series #23

* added site filter on resource #23

* fix #23

* update README #23

* small mods README #23
  • Loading branch information
hcwinsemius authored Jun 13, 2024
1 parent 0ab0bac commit bd24226
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 6 deletions.
1 change: 1 addition & 0 deletions LiveORC/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
'users',
"admin_interface",
"colorfield",
"import_export",
'LiveORC.admin.CustomAdminConfig',
'django.contrib.auth',
'django.contrib.contenttypes',
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,9 @@ variability but also instabilities in the frame rate of the camera). Furthermore
has been resolved optically is also shown. This is a good measure of uncertainty. OpenRiverCam uses infilling
techniques to fill in missing velocities in the cross-section. If the fraction velocimetry value is high (e.g. 85%)
it means that a lot of the discharge amount was actually observed optically and only a small portion (15%) comes
from interpolated surface velocities.
from interpolated surface velocities. In the time series menu you can also export data to a preferred format using the
EXPORT button on the top-right. This may make it easy to analyze longer series in Excel or python scripts, for instance
to update your stage-discharge relationship on the site or analyze such changes, or investigate time series behaviour.

Finally, if you go to the "Sites" menu and click on your only site so far, you will also see a time series view with
only one red dot (water level) and one cyan dot (discharge). If you start adding more videos at different moments in
Expand All @@ -544,11 +546,12 @@ open, so that you can easily navigate through the results.

Congratulations! You have now processed your first video in LiveOpenRiverCam. We hope that you have understood that
in LiveOpenRiverCam, you can entirely organize all your videos around sites, maintain camera configurations, change
these as you might change your setup in the field, check out time series and more. Remember that if you have many
these as you might change your set up in the field, check out time series and more. Remember that if you have many
videos on the same site, taken with the same camera at a fixed location and orientation, you only need to add a new
video, and a new water level, and reuse the camera configuration you've already made for your first video.

If you expect to process many videos and want to scale up, remember to look into the ``
If you expect to process many videos and want to scale up, remember to look into the [Installation](#installation)
section and in particular the section on extending the amount of [workers](#more-processing-nodes).

Of course, adding videos manually can be very useful for smaller sets, but it is also quite some work, and perhaps
not very efficient once you want to process a lot of them. Furthermore, you may want to start setting up a field site,
Expand Down
1 change: 1 addition & 0 deletions api/admin/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

FOREIGN_KEYS = ["institute", "site", "profile", "camera_config"]


class BaseForm(forms.ModelForm):
class Meta:
fields = "__all__"
Expand Down
72 changes: 69 additions & 3 deletions api/admin/time_series.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,50 @@
from django import forms
from django.contrib import admin
from django.core.exceptions import PermissionDenied
from django.template.response import TemplateResponse
from import_export.admin import ExportActionModelAdmin
from import_export.forms import ExportForm

from api.models import TimeSeries
from api.models import TimeSeries, Site
from api.admin import BaseAdmin, BaseForm
from api.admin import SiteUserFilter
from api.resources import TimeSeriesResource


class CustomExportForm(ExportForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
required=True
)
start_date = forms.DateTimeField(
required=False,
widget=forms.DateTimeInput(attrs={'type': 'datetime-local'}),
help_text="Start date and time in local timezone"
)
end_date = forms.DateTimeField(
required=False,
widget=forms.DateTimeInput(attrs={'type': 'datetime-local'}),
help_text="End date and time in local timezone",
)

def __init__(self, *args, **kwargs):
request = kwargs.pop("request", None)
super(CustomExportForm, self).__init__(*args, **kwargs)
if request:
user = request.user
if not user.is_superuser:
# only show sites for which logged in user has membership
self.fields["site"].queryset = Site.objects.filter(institute__in=user.get_membership_institutes())

def clean(self):
# check start and end dates on form
cleaned_data = super().clean()
start_date = cleaned_data.get("start_date")
end_date = cleaned_data.get("end_date")
if start_date and end_date:
if end_date < start_date:
raise forms.ValidationError("End date and time cannot be earlier than start date and time.")
return cleaned_data


class TimeSeriesForm(BaseForm):
Expand All @@ -21,8 +62,9 @@ class TimeSeriesInline(admin.TabularInline):
extra = 5


class TimeSeriesAdmin(BaseAdmin):

class TimeSeriesAdmin(ExportActionModelAdmin, BaseAdmin):
resource_classes = [TimeSeriesResource]
export_form_class = CustomExportForm
list_display = ["get_site_name", "timestamp", "str_h", "str_fraction_velocimetry", "str_q_50", 'thumbnail_preview']
list_filter = ["site__name"]
readonly_fields = (
Expand Down Expand Up @@ -61,6 +103,30 @@ class TimeSeriesAdmin(BaseAdmin):
def get_site_name(self, obj):
return obj.site.name

def get_export_queryset(self, request):
return TimeSeries.objects.filter(site__institute__in=request.user.get_membership_institutes())

def export_action(self, request):
if request.POST:
return super().export_action(request)

if not self.has_export_permission(request):
raise PermissionDenied

form_type = self.get_export_form_class()
formats = self.get_export_formats()
# with GET request, the form is instantiated with the request, so that we can check which sites belong to user
form = form_type(
formats,
self.get_export_resource_classes(request),
data=request.POST or None,
request=request # this arg is added as only difference between the parent export_action method
)
context = self.init_request_context_data(request, form)
request.current_app = self.admin_site.name
return TemplateResponse(request, [self.export_template_name], context=context)


def get_readonly_fields(self, request, obj=None):
# prevent that the file or camera config can be changed afterwards. That is very risky and can lead to
# inconsistent model records
Expand Down
1 change: 1 addition & 0 deletions api/resources/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .time_series import TimeSeriesResource
39 changes: 39 additions & 0 deletions api/resources/time_series.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""resources file for tabular downloads of TimeSeries model. """
from import_export import resources
from api.models import TimeSeries


class TimeSeriesResource(resources.ModelResource):

class Meta:
model = TimeSeries
fields = ('id', 'site__name','timestamp', 'h', 'q_05', 'q_25', 'q_50', 'q_75', 'q_95', 'fraction_velocimetry')
export_order = ('id', 'site__name', 'timestamp', 'h', 'q_05', 'q_25', 'q_50', 'q_75', 'q_95', 'fraction_velocimetry')

@classmethod
def get_display_name(cls):
return "Time series" # Customize this display name as needed

def get_export_headers(self, fields=None):
headers = super().get_export_headers(fields=fields)
header_mapping = {"site__name": "site"}
# Replace the original headers with custom headers
custom_headers = [header_mapping.get(header, header) for header in headers]
return custom_headers

def export(self, queryset=None, *args, **kwargs):
data = kwargs["export_form"].data
start_date = data.get("start_date", None)
end_date = data.get("end_date", None)
site = data.get("site", None)
if not queryset:
queryset = self.get_queryset()

queryset = queryset.filter(site=site)
if start_date and end_date:
queryset = queryset.filter(timestamp__range=(start_date, end_date))
elif start_date:
queryset = queryset.filter(timestamp__gte=start_date)
elif end_date:
queryset = queryset.filter(timestamp__lte=end_date)
return super().export(queryset, *args, **kwargs)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ django-admin-interface
django-debug-toolbar
django-extensions
django-filter
django-import-export
django-json-widget
django-object-actions
django-storages
Expand Down

0 comments on commit bd24226

Please sign in to comment.