def _init(import_demo_activities=False): _build_home() if Path(WORKOUTIZER_DB_PATH).is_file(): execute_from_command_line(["manage.py", "check"]) from wizer import models try: if models.Settings.objects.count() == 1: if models.Activity.objects.count() == 0 and import_demo_activities: click.echo("Found initialized db, but with no demo activity, importing...") else: click.echo( f"Found initialized db at {WORKOUTIZER_DB_PATH}, maybe you want to run wkz \n" "instead. If you really want to initialize wkz consider removing the existing db file. \n" "Aborting." ) return except OperationalError: pass # means required tables are not set up - continuing with applying migrations execute_from_command_line(["manage.py", "collectstatic", "--noinput"]) execute_from_command_line(["manage.py", "migrate"]) from wizer import models # insert settings models.get_settings() _check() if import_demo_activities: # import demo activities from wizer.file_importer import import_activity_files, prepare_import_of_demo_activities prepare_import_of_demo_activities(models) import_activity_files(models, importing_demo_data=True) click.echo(f"Database and track files are stored in: {WORKOUTIZER_DIR}")
def test_settings_page__no_demo_activity(live_server, webdriver): models.get_settings() webdriver.get(live_server.url + reverse("settings")) assert webdriver.find_element_by_tag_name("h3").text == "Settings" headings = [h.text for h in webdriver.find_elements_by_tag_name("h5")] assert "File Importer" in headings assert "Reimporter" in headings # verify the text of the input field labels input_labels = [ link.text for link in webdriver.find_elements_by_class_name("col-sm-4") ] assert "Path to Traces Directory:" in input_labels input_labels.remove("Path to Traces Directory:") assert "Path to Garmin Device:" in input_labels input_labels.remove("Path to Garmin Device:") assert "Delete fit Files after Copying:" in input_labels input_labels.remove("Delete fit Files after Copying:") assert "Reimport all Files:" in input_labels input_labels.remove("Reimport all Files:") # verify that the list is empty after remove all given input labels assert len(input_labels) == 0 # verify no demo activity is present assert len(models.Activity.objects.filter(is_demo_activity=True)) == 0 # no Demo heading present assert "Demo" not in headings
def test_settings_page__demo_activity_present__delete_it( import_demo_data, live_server, webdriver): models.get_settings() webdriver.get(live_server.url + reverse("settings")) assert webdriver.find_element_by_tag_name("h3").text == "Settings" headings = [h.text for h in webdriver.find_elements_by_tag_name("h5")] assert "File Importer" in headings assert "Reimporter" in headings # verify no demo activity is present assert len(models.Activity.objects.filter(is_demo_activity=True)) == 19 # Demo heading is present assert "Demo" in headings # also delete demo activity button is present first_delete_button = webdriver.find_element_by_id("delete-demo-data") assert first_delete_button.text == " Delete" # click button to verify demo data gets deleted first_delete_button.click() assert webdriver.current_url == live_server.url + reverse( "delete-demo-data") # on the new page find the additional delete button and click it second_delete_button = webdriver.find_element_by_class_name("btn-space") assert second_delete_button.text == " Delete" second_delete_button.click() # verify that all demo data got deleted assert len(models.Activity.objects.filter(is_demo_activity=True)) == 0
def import_one_activity(db, tracks_in_tmpdir): models.get_settings() assert models.Settings.objects.count() == 1 def _copy_activity(file_name: str): copy_demo_fit_files_to_track_dir( source_dir=django_settings.INITIAL_TRACE_DATA_DIR, targe_dir=models.get_settings().path_to_trace_dir, list_of_files_to_copy=[file_name], ) import_activity_files(models, importing_demo_data=False) assert models.Activity.objects.count() == 1 return _copy_activity
def test_settings_page__edit_and_submit_form(live_server, webdriver): # get settings and check that all values are at their default configuration settings = models.get_settings() assert settings.path_to_trace_dir == django_settings.TRACKS_DIR assert settings.path_to_garmin_device == "/run/user/1000/gvfs/" assert settings.delete_files_after_import is False assert settings.number_of_days == 30 # go to settings page webdriver.get(live_server.url + reverse("settings")) assert webdriver.find_element_by_tag_name("h3").text == "Settings" # modify values by inserting into input fields trace_dir_input_field = webdriver.find_element_by_css_selector( "#id_path_to_trace_dir") trace_dir_input_field.clear() trace_dir_input_field.send_keys("some/dummy/path") garmin_device_input_field = webdriver.find_element_by_css_selector( "#id_path_to_garmin_device") garmin_device_input_field.clear() garmin_device_input_field.send_keys("garmin/dummy/path") # got removed, should not be accessible with pytest.raises(NoSuchElementException): webdriver.find_element_by_css_selector("#id_reimporter_updates_all") delete_files_input_field = webdriver.find_element_by_css_selector( "#id_delete_files_after_import") delete_files_input_field.click() # verify that the number of days field is not present nor editable with pytest.raises(NoSuchElementException): webdriver.find_element_by_css_selector("#id_number_of_days") # find button and submit button = webdriver.find_element_by_id("button") button.click() # again get settings and check that the values are the once entered above settings = models.get_settings() assert settings.path_to_trace_dir == "some/dummy/path" assert settings.path_to_garmin_device == "garmin/dummy/path" assert settings.delete_files_after_import is True # number of days should not be changed assert settings.number_of_days == 30 # this attribute got removed, so verifying that is does no longer exist with pytest.raises(AttributeError): assert settings.reimporter_updates_all is True
def test_fake_device(db, fake_device, device_dir, activity_dir, fit_file): # initialize fake device device = fake_device(activity_files=[fit_file]) settings = models.get_settings() mount_path = Path(settings.path_to_garmin_device) # once fake device is initialized, mount path should be available assert mount_path.is_dir() # now mount the fake device, which should also contain a fit file device.mount() assert (mount_path / device_dir).is_dir() assert (mount_path / device_dir / activity_dir).is_dir() assert (mount_path / device_dir / activity_dir / fit_file).is_file() # check that mounting again does not have any effect device.mount() assert (mount_path / device_dir / activity_dir / fit_file).is_file() # unmount device again and verify dirs are gone device.unmount() assert not (mount_path / device_dir).is_dir() assert not (mount_path / device_dir / activity_dir / fit_file).is_file() # check that unmounting again does not have any effect device.unmount() assert not (mount_path / device_dir).is_dir() assert not (mount_path / device_dir / activity_dir / fit_file).is_file() # and again mounting should also work device.mount() assert (mount_path / device_dir / activity_dir / fit_file).is_file()
def get(self, request, list_of_activities: list): self.settings = models.get_settings() setattr(self.settings, "trace_width", django_settings.trace_line_width) setattr(self.settings, "trace_opacity", django_settings.trace_line_opacity) self.number_of_days = self.settings.number_of_days self.days_choices = models.Settings.days_choices traces = [] for activity in list_of_activities: if activity.trace_file: coordinates = json.dumps( get_list_of_coordinates( json.loads(activity.trace_file.longitude_list), json.loads(activity.trace_file.latitude_list) ) ) sport = activity.sport.name if coordinates != "[]": traces.append(GeoTrace(pk=activity.pk, name=activity.name, sport=sport, coordinates=coordinates)) has_traces = True if traces else False if traces: traces, colors = cut_list_to_have_same_length(traces, lines_colors, mode="fill end", modify_only_list2=True) traces = zip(traces, colors) return { "traces": traces, "settings": self.settings, "days": self.number_of_days, "choices": self.days_choices, "has_traces": has_traces, }
def test__start_device_watchdog(transactional_db, fake_device, device_dir, activity_dir, fit_file_a, fit_file_b): # initialize fake device with two fit files device = fake_device(activity_files=[fit_file_a, fit_file_b]) settings = models.get_settings() mount_path = Path(settings.path_to_garmin_device) trace_dir = Path(settings.path_to_trace_dir) # verify mount path exists assert mount_path.is_dir() # ensure fit files are not already present in trace dir assert not (trace_dir / fit_file_a).is_file() assert not (trace_dir / fit_file_b).is_file() # start device watch dog _start_device_watchdog(mount_path, trace_dir, settings.delete_files_after_import) # now mount device which contains fit files device.mount() # verify the fit files are present on the mounted device assert (mount_path / device_dir / activity_dir / fit_file_a).is_file() assert (mount_path / device_dir / activity_dir / fit_file_b).is_file() # verify that the fit files are now present in the trace dir assert condition((trace_dir / "garmin" / fit_file_a).is_file, operator.is_, True) assert condition((trace_dir / "garmin" / fit_file_b).is_file, operator.is_, True)
def test_mount_device__success(db, monkeypatch, tmpdir, client): # prepare settings target_dir = tmpdir.mkdir("tracks") settings = models.get_settings() settings.path_to_garmin_device = tmpdir # source settings.path_to_trace_dir = target_dir # target settings.save() def check_output(dummy): return "dummy\nstring\nsome\ncontent\ncontaining\nGarmin" monkeypatch.setattr(subprocess, "check_output", check_output) # mock output of subprocess to prevent function from failing def try_to_mount_device(): return "dummy-string" monkeypatch.setattr(fit_collector, "try_to_mount_device", try_to_mount_device) # mock output of actual mounting command def mount(bus, dev): return "Mounted" monkeypatch.setattr(fit_collector, "_mount_device_using_gio", mount) # create directory to import the fit files from fake_device_dir = os.path.join(tmpdir, "mtp:host/Primary/GARMIN/Activity/") os.makedirs(fake_device_dir) res = client.post("/mount-device/") assert res.status_code == 200
def fake_device(tmp_path, device_dir, activity_dir): settings = models.get_settings() mount_path = tmp_path / "mount_path" mount_path.mkdir() assert mount_path.is_dir() settings.path_to_garmin_device = mount_path trace_dir = tmp_path / "trace_dir" trace_dir.mkdir() assert trace_dir.is_dir() settings.path_to_trace_dir = trace_dir settings.save() def _get_device(activity_files: List[str], device_dir: str = device_dir, activity_dir: str = activity_dir): return FakeDevice( mount_path=settings.path_to_garmin_device, device_dir=device_dir, activity_dir=activity_dir, activity_files=activity_files, ) return _get_device
def test__start_file_importer_watchdog__missing_dir(db, caplog): invalid_dir = "/some/random/non_existent/path/" settings = models.get_settings() settings.path_to_trace_dir = invalid_dir settings.save() _start_file_importer_watchdog(invalid_dir, models=models) assert f"Path to trace dir {invalid_dir} does not exist. File Importer watchdog is disabled." in caplog.text
def set_number_of_days(request, number_of_days): settings = models.get_settings() settings.number_of_days = number_of_days log.debug(f"number of days: {number_of_days}") settings.save() if request.META.get("HTTP_REFERER"): return redirect(request.META.get("HTTP_REFERER")) else: return HttpResponseRedirect(reverse("home"))
def test__start_device_watchdog__missing_dir(db, caplog): invalid_dir = "/some/random/non_existent/path/" settings = models.get_settings() settings.path_to_garmin_device = invalid_dir settings.save() _start_device_watchdog(invalid_dir, settings.path_to_trace_dir, settings.delete_files_after_import) assert f"Device mount path {invalid_dir} does not exist. Device watchdog is disabled." in caplog.text
def get(self, request, sports_name_slug): log.debug(f"got sports name: {sports_name_slug}") settings = models.get_settings() if sports_name_slug == "undefined": log.warning("could not find sport - redirecting to home") return HttpResponseRedirect(reverse("home")) sport = models.Sport.objects.get(slug=sports_name_slug) activities = self.get_activity_data_for_plots(sport_id=sport.id) context = {} sports = models.Sport.objects.all().order_by("name") summary = get_summary_of_all_activities(sport_slug=sports_name_slug) if activities: script_history, div_history = plot_history( activities=activities, sport_model=models.Sport, number_of_days=settings.number_of_days, ) context["script_history"] = script_history context["div_history"] = div_history context["activities_selected_for_plot"] = True else: context["activities_selected_for_plot"] = False map_context = super(SportsView, self).get(request=request, list_of_activities=activities) if sport.evaluates_for_awards: top_awards = get_flat_list_of_pks_of_activities_in_top_awards( configuration.rank_limit, sports_name_slug) context["top_awards"] = top_awards try: sport = model_to_dict( models.Sport.objects.get(slug=sports_name_slug)) sport["slug"] = sports_name_slug except ObjectDoesNotExist: log.critical("this sport does not exist") raise Http404 page = 0 return render( request, self.template_name, { **map_context, **context, "current_page": page, "is_last_page": False, "sports": sports, "summary": summary, "sport": sport, "form_field_ids": get_all_form_field_ids(), }, )
def test__start_file_importer_watchdog_basic(transactional_db, tmp_path, test_data_dir, demo_data_dir, fit_file_a): assert models.Activity.objects.count() == 0 assert models.BestSection.objects.count() == 0 # update path_to_trace_dir in db accordingly, since import_activity_files will read it from the db settings = models.get_settings() trace_dir = tmp_path / "trace_dir" trace_dir.mkdir() assert trace_dir.is_dir() settings.path_to_trace_dir = trace_dir settings.save() _start_file_importer_watchdog(trace_dir, models=models) # put an activity fit file into the watched dir copy_demo_fit_files_to_track_dir( source_dir=demo_data_dir, targe_dir=trace_dir, list_of_files_to_copy=[fit_file_a], ) assert (Path(trace_dir) / fit_file_a).is_file() # watchdog should now have triggered the file imported and activity should be in db assert condition(models.Activity.objects.count, operator.eq, 1) assert condition(models.BestSection.objects.count, operator.gt, 0) bs1 = models.BestSection.objects.count() # now put a activity GPX file into the watched dir copy_demo_fit_files_to_track_dir( source_dir=test_data_dir, targe_dir=trace_dir, list_of_files_to_copy=["example.gpx"], ) assert condition(models.Activity.objects.count, operator.eq, 2) assert condition(models.BestSection.objects.count, operator.gt, bs1) bs2 = models.BestSection.objects.count() # create a non fit/gpx file to verify it won't be imported dummy_file = trace_dir / "fake_activity.txt" dummy_file.write_text("not a real activity", encoding="utf-8") # check that the dummy file was actually created assert Path(dummy_file).is_file() # but assert that the number of activities and best sections did not increase assert condition(models.Activity.objects.count, operator.eq, 2) assert condition(models.BestSection.objects.count, operator.eq, bs2)
def ready(self): # ensure to only run with 'manage.py runserver' and not in auto reload thread if _was_runserver_triggered( sys.argv) and os.environ.get("RUN_MAIN", None) != "true": log.info( f"using workoutizer home at {django_settings.WORKOUTIZER_DIR}") from wizer import models # start watchdog to monitor whether a new device was mounted settings = models.get_settings() path_to_garmin_device = settings.path_to_garmin_device _start_device_watchdog(path_to_garmin_device, settings.path_to_trace_dir, settings.delete_files_after_import) # start watchdog for new files being placed into the tracks directory _start_file_importer_watchdog(path=django_settings.TRACKS_DIR, models=models)
def test_device_and_file_importer_watchdog(transactional_db, tmpdir, test_data_dir, demo_data_dir, fake_device, device_dir, activity_dir, fit_file_a, fit_file_b): assert models.Activity.objects.count() == 0 assert models.BestSection.objects.count() == 0 device = fake_device(activity_files=[fit_file_a, fit_file_b]) settings = models.get_settings() mount_path = Path(settings.path_to_garmin_device) trace_dir = Path(settings.path_to_trace_dir) # verify mount path exists assert mount_path.is_dir() # ensure fit files are not already present in trace dir assert not (trace_dir / fit_file_a).is_file() assert not (trace_dir / fit_file_b).is_file() # start watchdogs _start_device_watchdog(mount_path, trace_dir, settings.delete_files_after_import) _start_file_importer_watchdog(trace_dir, models=models) # mounting the device will: # 1. trigger device watchdog to copy fit files to trace dir, what in turn will # 2. trigger file importer watchdog to import the fit files into workoutizer activities device.mount() # verify the fit files are present on the mounted device assert (mount_path / device_dir / activity_dir / fit_file_a).is_file() assert (mount_path / device_dir / activity_dir / fit_file_b).is_file() # verify that the fit files are now present in the trace dir assert condition((trace_dir / "garmin" / fit_file_a).is_file, operator.is_, True) assert condition((trace_dir / "garmin" / fit_file_b).is_file, operator.is_, True) # check that the activities got imported assert condition(models.Activity.objects.count, operator.eq, 2) assert condition(models.BestSection.objects.count, operator.gt, 2)
def plot_trend(activities, sport_model): number_of_days = models.get_settings().number_of_days df = pd.DataFrame.from_records( activities.values("sport_id", "duration", "date")) df["date"] = pd.to_datetime(df["date"]) df = df.set_index("date") days = int(number_of_days / 5) freq = days if days > 1 else 1 df = df.groupby([pd.Grouper(freq=f"{freq}D"), "sport_id"]).agg({ "duration": np.sum }).reset_index() df = df.pivot(index="date", columns="sport_id", values="duration").fillna("0") sports = sport_model.objects.exclude(name="unknown").order_by("id").values( "id", "name", "color") id_color_mapping = {} for sport in sports: id_color_mapping[sport["id"]] = sport["color"] df = df.rename(columns=id_color_mapping) p = figure(width=280, height=200, x_axis_type="datetime", y_axis_type="datetime") p.multi_line(xs=[df.index.values] * len(df.columns), ys=[df[name].values for name in df], line_color=df.columns, line_width=2) # render zero hours properly p.yaxis.major_label_overrides = {0: "0h"} p.toolbar.logo = None p.toolbar_location = None p.background_fill_color = "whitesmoke" p.border_fill_color = "whitesmoke" p.tools = [] script_trend, div_trend = components(p) return script_trend, div_trend
def get(self, request): page = 0 settings = models.get_settings() self.sports = models.Sport.objects.all().order_by("name") activities = self.get_activity_data_for_plots() summary = get_summary_of_all_activities() context = { "sports": self.sports, "current_page": page, "is_last_page": False, "days": self.number_of_days, "choices": self.days_choices, "summary": summary, "page": "dashboard", "form_field_ids": get_all_form_field_ids(), } if activities: script_history, div_history = plot_history( activities=activities, sport_model=models.Sport, number_of_days=settings.number_of_days ) script_pc, div_pc = plot_pie_chart(activities=activities) script_trend, div_trend = plot_trend(activities=activities, sport_model=models.Sport) plotting_context = { "script_history": script_history, "div_history": div_history, "script_pc": script_pc, "div_pc": div_pc, "script_trend": script_trend, "div_trend": div_trend, "activities_selected_for_plot": True, } return render(request, self.template_name, {**context, **plotting_context}) else: log.warning("no activities found...") context["activities_selected_for_plot"] = False return render(request, self.template_name, {**context})
def settings_view(request): sports = models.Sport.objects.all().order_by("name") settings = models.get_settings() activities = models.Activity.objects.filter(is_demo_activity=True).count() form = forms.EditSettingsForm(request.POST or None, instance=settings) if request.method == "POST": if form.is_valid(): log.debug(f"got valid form: {form.cleaned_data}") form.save() messages.success(request, "Successfully saved Settings!") return HttpResponseRedirect(reverse("settings")) else: log.warning(f"form invalid: {form.errors}") return render( request, "lib/settings.html", { "sports": sports, "form": form, "settings": settings, "form_field_ids": get_all_form_field_ids(), "delete_demos": True if activities else False, }, )
def tracks_in_tmpdir(tmpdir): target_dir = tmpdir.mkdir("tracks") settings = models.get_settings() settings.path_to_trace_dir = target_dir settings.save()
def get_days_config(self): self.settings = models.get_settings() self.number_of_days = self.settings.number_of_days self.days_choices = models.Settings.days_choices