Skip to content

Commit

Permalink
Merge pull request #62 from Freika/exports
Browse files Browse the repository at this point in the history
Exports
  • Loading branch information
Freika authored Jun 12, 2024
2 parents e736f66 + b7f648d commit 3875429
Show file tree
Hide file tree
Showing 38 changed files with 502 additions and 128 deletions.
2 changes: 1 addition & 1 deletion .app_version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.5.3
0.6.0
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
!/tmp/storage/.keep

/public/assets
/public/exports
/public/imports

# Ignore master key for decrypting credentials and more.
/config/master.key
Expand Down
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,51 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [0.6.0] — 2024-06-12

### Added

- Exports page to list existing exports download them or delete them

### Changed

- Exporting process now is done in the background, so user can close the browser tab and come back later to download the file. The status of the export can be checked on the Exports page.

ℹ️ Deleting Export file will only delete the file, not the points in the database. ℹ️

⚠️ BREAKING CHANGES: ⚠️

Volume, exposed to the host machine for placing files to import was changed. See the changes below.

Path for placing files to import was changed from `tmp/imports` to `public/imports`.

```diff
...

dawarich_app:
image: freikin/dawarich:latest
container_name: dawarich_app
volumes:
- gem_cache:/usr/local/bundle/gems
- - tmp:/var/app/tmp
+ - public:/var/app/public/imports

...
```

```diff
...

volumes:
db_data:
gem_cache:
shared_data:
- tmp:
+ public:
```

---

## [0.5.3] — 2024-06-10

### Added
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ You can see the number of countries and cities visited, the distance traveled, a

You can import your Google Maps Timeline data into Dawarich as well as Owntracks data.

⚠️ **Note**: Import of huge Google Maps Timeline files may take a long time and consume a lot of memory. It also might temporarily consume a lot of disk space due to logs. Please make sure you have enough resources before starting the import. After import is completed, you can restart your docker container and logs will be removed.

## How to start the app locally

`docker-compose up` to start the app. The app will be available at `http://localhost:3000`.
Expand Down
2 changes: 1 addition & 1 deletion app/assets/builds/tailwind.css

Large diffs are not rendered by default.

32 changes: 0 additions & 32 deletions app/controllers/export_controller.rb

This file was deleted.

39 changes: 39 additions & 0 deletions app/controllers/exports_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

class ExportsController < ApplicationController
before_action :authenticate_user!
before_action :set_export, only: %i[destroy]

def index
@exports = current_user.exports.order(created_at: :desc).page(params[:page])
end

def create
export_name = "#{params[:start_at].to_date}_#{params[:end_at].to_date}"
export = current_user.exports.create(name: export_name, status: :created)

ExportJob.perform_later(export.id, params[:start_at], params[:end_at])

redirect_to exports_url, notice: 'Export was successfully initiated. Please wait until it\'s finished.'
rescue StandardError => e
export&.destroy

redirect_to exports_url, alert: "Export failed to initiate: #{e.message}", status: :unprocessable_entity
end

def destroy
@export.destroy

redirect_to exports_url, notice: 'Export was successfully destroyed.', status: :see_other
end

private

def set_export
@export = current_user.exports.find(params[:id])
end

def export_params
params.require(:export).permit(:name, :url, :status)
end
end
5 changes: 5 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,9 @@ def sidebar_points(points)
def active_class?(link_path)
'btn-active' if current_page?(link_path)
end

def full_title(page_title = '')
base_title = 'Dawarich'
page_title.empty? ? base_title : "#{page_title} | #{base_title}"
end
end
2 changes: 2 additions & 0 deletions app/helpers/exports_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module ExportsHelper
end
11 changes: 11 additions & 0 deletions app/jobs/export_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

class ExportJob < ApplicationJob
queue_as :exports

def perform(export_id, start_at, end_at)
export = Export.find(export_id)

Exports::Create.new(export:, start_at:, end_at:).call
end
end
19 changes: 19 additions & 0 deletions app/models/export.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

class Export < ApplicationRecord
belongs_to :user

enum status: { created: 0, processing: 1, completed: 2, failed: 3 }

validates :name, presence: true

before_destroy :delete_export_file

private

def delete_export_file
file_path = Rails.root.join('public', 'exports', "#{name}.json")

File.delete(file_path) if File.exist?(file_path)
end
end
19 changes: 1 addition & 18 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,10 @@ class User < ApplicationRecord
has_many :points, through: :imports
has_many :stats, dependent: :destroy
has_many :tracked_points, class_name: 'Point', dependent: :destroy
has_many :exports, dependent: :destroy

after_create :create_api_key

def export_data(start_at: nil, end_at: nil)
geopoints = time_framed_points(start_at, end_at)

::ExportSerializer.new(geopoints, email).call
end

def countries_visited
stats.pluck(:toponyms).flatten.map { _1['country'] }.uniq.compact
end
Expand Down Expand Up @@ -59,16 +54,4 @@ def create_api_key

save
end

def time_framed_points(start_at, end_at)
return tracked_points.without_raw_data if start_at.nil? && end_at.nil?

if start_at && end_at
tracked_points.without_raw_data.where('timestamp >= ? AND timestamp <= ?', start_at, end_at)
elsif start_at
tracked_points.without_raw_data.where('timestamp >= ?', start_at)
elsif end_at
tracked_points.without_raw_data.where('timestamp <= ?', end_at)
end
end
end
34 changes: 34 additions & 0 deletions app/services/exports/create.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

class Exports::Create
def initialize(export:, start_at:, end_at:)
@export = export
@user = export.user
@start_at = start_at
@end_at = end_at
end

def call
export.update!(status: :processing)

points = time_framed_points(start_at, end_at, user)
data = ::ExportSerializer.new(points, user.email).call
file_path = Rails.root.join('public', 'exports', "#{export.name}.json")

File.open(file_path, 'w') { |file| file.write(data) }

export.update!(status: :completed, url: "exports/#{export.name}.json")
rescue StandardError => e
Rails.logger.error("====Export failed to create: #{e.message}")

export.update!(status: :failed)
end

private

attr_reader :user, :export, :start_at, :end_at

def time_framed_points(start_at, end_at, user)
user.tracked_points.without_raw_data.where('timestamp >= ? AND timestamp <= ?', start_at.to_i, end_at.to_i)
end
end
47 changes: 47 additions & 0 deletions app/views/exports/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<% content_for :title, "Exports" %>

<div class="w-full">
<div class="flex justify-between items-center mb-5">
<h1 class="font-bold text-4xl">Exports</h1>
</div>

<div id="exports" class="min-w-full">
<% if @exports.empty? %>
<div class="hero min-h-80 bg-base-200">
<div class="hero-content text-center">
<div class="max-w-md">
<h1 class="text-5xl font-bold">Hello there!</h1>
<p class="py-6">
Here you'll find your exports, created on <%= link_to 'Points', points_url, class: 'link' %> page. But now there are none.
</p>
</div>
</div>
</div>
<% else %>
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% @exports.each do |export| %>
<tr>
<td><%= export.name %></td>
<td><%= export.status %></td>
<td>
<% if export.completed? %>
<%= link_to 'Download', export.url, class: "px-4 py-2 bg-blue-500 text-white rounded-md", download: export.name %>
<% end %>
<%= link_to 'Delete', export, data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?", turbo_method: :delete }, method: :delete, class: "px-4 py-2 bg-red-500 text-white rounded-md" %>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
</div>
</div>
67 changes: 41 additions & 26 deletions app/views/imports/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,37 +1,52 @@
<% content_for :title, 'Imports' %>

<div class="w-full">
<div class="flex justify-between items-center">
<h1 class="font-bold text-4xl">Imports</h1>
<%= link_to "New import", new_import_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium" %>
</div>

<div id="imports" class="min-w-full">
<div class="overflow-x-auto">
<table class="table">
<!-- head -->
<thead>
<tr>
<th>Name</th>
<th>Processed</th>
<th>Doubles</th>
<th>Created at</th>
</tr>
</thead>
<tbody>
<% @imports.each do |import| %>
<% if @imports.empty? %>
<div class="hero min-h-80 bg-base-200">
<div class="hero-content text-center">
<div class="max-w-md">
<h1 class="text-5xl font-bold">Hello there!</h1>
<p class="py-6">
Here you'll find your imports, But now there are none. Let's <%= link_to 'create one', new_import_path, class: 'link' %>!
</p>
</div>
</div>
</div>
<% else %>
<div class="overflow-x-auto">
<table class="table">
<!-- head -->
<thead>
<tr>
<td>
<%= link_to import.name, import, class: 'underline hover:no-underline' %> (<%= import.source %>)
</td>
<td>
<%= "✅" if import.processed == import.raw_points %>
<%= "#{import.processed}/#{import.raw_points}" %>
</td>
<td><%= import.doubles %></td>
<td><%= import.created_at.strftime("%d.%m.%Y, %H:%M") %></td>
<th>Name</th>
<th>Processed</th>
<th>Doubles</th>
<th>Created at</th>
</tr>
<% end %>
</tbody>
</table>
</div>
</thead>
<tbody>
<% @imports.each do |import| %>
<tr>
<td>
<%= link_to import.name, import, class: 'underline hover:no-underline' %> (<%= import.source %>)
</td>
<td>
<%= "✅" if import.processed == import.raw_points %>
<%= "#{import.processed}/#{import.raw_points}" %>
</td>
<td><%= import.doubles %></td>
<td><%= import.created_at.strftime("%d.%m.%Y, %H:%M") %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
</div>
</div>
Loading

0 comments on commit 3875429

Please sign in to comment.