Skip to content

Commit b063d80

Browse files
authored
Merge pull request #160 from Freika/visit_detection
Visit detection
2 parents a6b2cd2 + 656dc97 commit b063d80

File tree

73 files changed

+1452
-90
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+1452
-90
lines changed

.app_version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.11.2
1+
0.12.0

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8+
## [0.12.0] — 2024-08-25
9+
10+
### The visit suggestion release
11+
12+
1. With this release deployment, data migration will work, starting visits suggestion process for all users.
13+
2. After initial visit suggestion process, new suggestions will be calculated every 24 hours, based on points for last 24 hours.
14+
3. If you have enabled reverse geocoding and (optionally) provided Photon Api Host, Dawarich will try to reverse geocode your visit and suggest specific places you might have visited, such as cafes, restaurants, parks, etc. If reverse geocoding is not enabled, or Photon Api Host is not provided, Dawarich will not try to suggest places but you'll be able to rename the visit yourself.
15+
4. You can confirm or decline the visit suggestion. If you confirm the visit, it will be added to your timeline. If you decline the visit, it will be removed from your timeline. You'll be able to see all your confirmed, declined and suggested visits on the Visits page.
16+
17+
18+
### Added
19+
20+
- A "Map" button to each visit on the Visits page to allow user to see the visit on the map
21+
- Visits suggestion functionality. Read more on that in the release description
22+
- Click on the visit name allows user to rename the visit
23+
- Tabs to the Visits page to allow user to switch between confirmed, declined and suggested visits
24+
- Places page to see and delete places suggested by Dawarich's visit suggestion process
25+
- Importing a file will now trigger the visit suggestion process for the user
26+
827
## [0.11.2] — 2024-08-22
928

1029
### Changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ Feel free to change them both in the Account section.
117117
| APPLICATION_HOSTS | list of host of the application, e.g. `localhost,dawarich.example.com` |
118118
| BACKGROUND_PROCESSING_CONCURRENCY (only for dawarich_sidekiq service) | Number of simultaneously processed background jobs, default is 10 |
119119
| REVERSE_GEOCODING_ENABLED | `true` or `false`, this env var allows you to disable reverse geocoding feature entirely |
120+
| PHOTON_API_HOST | Photon reverse geocoding api host. Useful, if you're running your own Photon instance |
120121

121122
## Star History
122123

app/assets/builds/tailwind.css

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controllers/api/v1/areas_controller.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# frozen_string_literal: true
22

3-
class Api::V1::AreasController < ApplicationController
4-
skip_forgery_protection
5-
before_action :authenticate_api_key
3+
class Api::V1::AreasController < ApiController
64
before_action :set_area, only: %i[update destroy]
75

86
def index

app/controllers/api/v1/overland/batches_controller.rb

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
# frozen_string_literal: true
22

3-
class Api::V1::Overland::BatchesController < ApplicationController
4-
skip_forgery_protection
5-
before_action :authenticate_api_key
6-
3+
class Api::V1::Overland::BatchesController < ApiController
74
def create
85
Overland::BatchCreatingJob.perform_later(batch_params, current_api_user.id)
96

app/controllers/api/v1/owntracks/points_controller.rb

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
# frozen_string_literal: true
22

3-
class Api::V1::Owntracks::PointsController < ApplicationController
4-
skip_forgery_protection
5-
before_action :authenticate_api_key
6-
3+
class Api::V1::Owntracks::PointsController < ApiController
74
def create
85
Owntracks::PointCreatingJob.perform_later(point_params, current_api_user.id)
96

app/controllers/api/v1/points_controller.rb

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
# frozen_string_literal: true
22

3-
class Api::V1::PointsController < ApplicationController
4-
skip_forgery_protection
5-
before_action :authenticate_api_key
6-
3+
class Api::V1::PointsController < ApiController
74
def index
85
start_at = params[:start_at]&.to_datetime&.to_i
96
end_at = params[:end_at]&.to_datetime&.to_i || Time.zone.now.to_i

app/controllers/api/v1/stats_controller.rb

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
# frozen_string_literal: true
22

3-
class Api::V1::StatsController < ApplicationController
4-
skip_forgery_protection
5-
before_action :authenticate_api_key
6-
3+
class Api::V1::StatsController < ApiController
74
def index
85
render json: StatsSerializer.new(current_api_user).call
96
end
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# frozen_string_literal: true
2+
3+
class Api::V1::VisitsController < ApiController
4+
def update
5+
visit = current_api_user.visits.find(params[:id])
6+
visit = update_visit(visit)
7+
8+
render json: visit
9+
end
10+
11+
private
12+
13+
def visit_params
14+
params.require(:visit).permit(:name, :place_id)
15+
end
16+
17+
def update_visit(visit)
18+
visit_params.each do |key, value|
19+
visit[key] = value
20+
visit.name = visit.place.name if visit_params[:place_id].present?
21+
end
22+
23+
visit.save!
24+
25+
visit
26+
end
27+
end

app/controllers/api_controller.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# frozen_string_literal: true
2+
3+
class ApiController < ApplicationController
4+
skip_before_action :verify_authenticity_token
5+
before_action :authenticate_api_key
6+
7+
private
8+
9+
def authenticate_api_key
10+
return head :unauthorized unless current_api_user
11+
12+
true
13+
end
14+
15+
def current_api_user
16+
@current_api_user ||= User.find_by(api_key: params[:api_key])
17+
end
18+
end

app/controllers/application_controller.rb

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,4 @@ def authenticate_admin!
1818

1919
redirect_to root_path, notice: 'You are not authorized to perform this action.', status: :see_other
2020
end
21-
22-
def authenticate_api_key
23-
return head :unauthorized unless current_api_user
24-
25-
true
26-
end
27-
28-
def current_api_user
29-
@current_api_user ||= User.find_by(api_key: params[:api_key])
30-
end
3121
end

app/controllers/places_controller.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# frozen_string_literal: true
2+
3+
class PlacesController < ApplicationController
4+
before_action :authenticate_user!
5+
before_action :set_place, only: :destroy
6+
7+
def index
8+
@places = current_user.places.page(params[:page]).per(20)
9+
end
10+
11+
def destroy
12+
@place.destroy!
13+
14+
redirect_to places_url, notice: 'Place was successfully destroyed.', status: :see_other
15+
end
16+
17+
private
18+
19+
def set_place
20+
@place = current_user.places.find(params[:id])
21+
end
22+
end

app/controllers/visits_controller.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,23 @@ class VisitsController < ApplicationController
66

77
def index
88
order_by = params[:order_by] || 'asc'
9+
status = params[:status] || 'confirmed'
910

1011
visits = current_user
1112
.visits
12-
.where(status: :pending)
13-
.or(current_user.visits.where(status: :confirmed))
13+
.where(status:)
1414
.order(started_at: order_by)
1515
.group_by { |visit| visit.started_at.to_date }
1616
.map { |k, v| { date: k, visits: v } }
1717

18+
@suggested_visits_count = current_user.visits.suggested.count
19+
1820
@visits = Kaminari.paginate_array(visits).page(params[:page]).per(10)
1921
end
2022

2123
def update
2224
if @visit.update(visit_params)
23-
redirect_to visits_url, notice: 'Visit was successfully updated.', status: :see_other
25+
redirect_back(fallback_location: visits_path(status: :suggested))
2426
else
2527
render :edit, status: :unprocessable_entity
2628
end

app/helpers/exports_helper.rb

Lines changed: 0 additions & 2 deletions
This file was deleted.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Controller } from "@hotwired/stimulus"
2+
import L, { latLng } from "leaflet";
3+
import { osmMapLayer } from "../maps/layers";
4+
5+
// Connects to data-controller="visit-modal-map"
6+
export default class extends Controller {
7+
static targets = ["container"];
8+
9+
connect() {
10+
console.log("Visits maps controller connected");
11+
this.coordinates = JSON.parse(this.element.dataset.coordinates);
12+
this.center = JSON.parse(this.element.dataset.center);
13+
this.radius = this.element.dataset.radius;
14+
this.map = L.map(this.containerTarget).setView([this.center[0], this.center[1]], 17);
15+
16+
osmMapLayer(this.map),
17+
this.addMarkers();
18+
19+
L.circle([this.center[0], this.center[1]], {
20+
radius: this.radius,
21+
color: 'red',
22+
fillColor: '#f03',
23+
fillOpacity: 0.5
24+
}).addTo(this.map);
25+
}
26+
27+
addMarkers() {
28+
this.coordinates.forEach((coordinate) => {
29+
L.circleMarker([coordinate[0], coordinate[1]], { radius: 4 }).addTo(this.map);
30+
});
31+
}
32+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Controller } from "@hotwired/stimulus";
2+
3+
export default class extends Controller {
4+
5+
connect() {
6+
this.visitId = this.element.dataset.id;
7+
this.apiKey = this.element.dataset.api_key;
8+
}
9+
10+
// Action to handle selection change
11+
selectPlace(event) {
12+
const selectedPlaceId = event.target.value; // Get the selected place ID
13+
14+
// Send PATCH request to update the place for the visit
15+
this.updateVisitPlace(selectedPlaceId);
16+
}
17+
18+
updateVisitPlace(placeId) {
19+
const url = `/api/v1/visits/${this.visitId}?api_key=${this.apiKey}`;
20+
21+
fetch(url, {
22+
method: 'PATCH',
23+
headers: {'Content-Type': 'application/json'},
24+
body: JSON.stringify({ place_id: placeId })
25+
})
26+
.then(response => {
27+
if (!response.ok) {
28+
throw new Error('Network response was not ok');
29+
}
30+
return response.json();
31+
})
32+
.then(data => {
33+
console.log('Success:', data);
34+
this.updateVisitNameOnPage(data.name);
35+
})
36+
.catch((error) => {
37+
console.error('Error:', error);
38+
});
39+
}
40+
41+
updateVisitNameOnPage(newName) {
42+
document.querySelectorAll(`[data-visit-name="${this.visitId}"]`).forEach(element => {
43+
element.textContent = newName;
44+
});
45+
}
46+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// app/javascript/controllers/visit_name_controller.js
2+
3+
import { Controller } from "@hotwired/stimulus";
4+
5+
export default class extends Controller {
6+
static targets = ["name", "input"];
7+
8+
connect() {
9+
this.apiKey = this.element.dataset.api_key;
10+
this.visitId = this.element.dataset.id;
11+
12+
// Listen for custom event to update all instances
13+
this.element.addEventListener("visit-name:updated", this.updateAll.bind(this));
14+
}
15+
16+
edit() {
17+
this.nameTargets.forEach((nameTarget, index) => {
18+
nameTarget.classList.add("hidden");
19+
this.inputTargets[index].classList.remove("hidden");
20+
this.inputTargets[index].focus();
21+
});
22+
}
23+
24+
save() {
25+
const newName = this.inputTargets[0].value; // Assuming both inputs have the same value
26+
27+
fetch(`/api/v1/visits/${this.visitId}?api_key=${this.apiKey}`, {
28+
method: "PATCH",
29+
headers: {
30+
"Content-Type": "application/json"
31+
},
32+
body: JSON.stringify({ visit: { name: newName } })
33+
})
34+
.then(response => {
35+
if (response.ok) {
36+
this.updateAllInstances(newName);
37+
} else {
38+
return response.json().then(errors => Promise.reject(errors));
39+
}
40+
})
41+
.catch(() => {
42+
alert("Error updating visit name.");
43+
});
44+
}
45+
46+
updateAllInstances(newName) {
47+
// Dispatch a custom event that other instances of this controller can listen to
48+
const event = new CustomEvent("visit-name:updated", { detail: { newName } });
49+
document.querySelectorAll(`[data-id="${this.visitId}"]`).forEach(element => {
50+
element.dispatchEvent(event);
51+
});
52+
}
53+
54+
updateAll(event) {
55+
const newName = event.detail.newName;
56+
57+
// Update all name displays
58+
this.nameTargets.forEach(nameTarget => {
59+
nameTarget.textContent = newName;
60+
nameTarget.classList.remove("hidden");
61+
});
62+
63+
// Update all input fields
64+
this.inputTargets.forEach(inputTarget => {
65+
inputTarget.value = newName;
66+
inputTarget.classList.add("hidden");
67+
});
68+
}
69+
70+
cancel() {
71+
this.nameTargets.forEach((nameTarget, index) => {
72+
nameTarget.classList.remove("hidden");
73+
this.inputTargets[index].classList.add("hidden");
74+
});
75+
}
76+
77+
handleEnter(event) {
78+
if (event.key === "Enter") {
79+
this.save();
80+
} else if (event.key === "Escape") {
81+
this.cancel();
82+
}
83+
}
84+
}

app/jobs/import_job.rb

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ def perform(user_id, import_id)
1515

1616
create_import_finished_notification(import, user)
1717

18-
StatCreatingJob.perform_later(user_id)
18+
schedule_stats_creating(user_id)
19+
schedule_visit_suggesting(user_id, import)
1920
rescue StandardError => e
2021
create_import_failed_notification(import, user, e)
2122
end
@@ -34,6 +35,18 @@ def parser(source)
3435
end
3536
end
3637

38+
def schedule_stats_creating(user_id)
39+
StatCreatingJob.perform_later(user_id)
40+
end
41+
42+
def schedule_visit_suggesting(user_id, import)
43+
points = import.points.order(:timestamp)
44+
start_at = Time.zone.at(points.first.timestamp)
45+
end_at = Time.zone.at(points.last.timestamp)
46+
47+
VisitSuggestingJob.perform_later(user_ids: [user_id], start_at:, end_at:)
48+
end
49+
3750
def create_import_finished_notification(import, user)
3851
Notifications::Create.new(
3952
user:,

0 commit comments

Comments
 (0)