mensa 0.6.2 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f85cdb0a26916745236f68e8a5e2cc5e0241f9f1555f4c4a4dcc408cd94c3786
4
- data.tar.gz: 9d12eb7e55e07a595490511329bbf87fb171bf7ca2d1f07f50c92581ce688488
3
+ metadata.gz: b6c3d12b5f69c5a0893eae56ffda155d75a658dc46620aa18cbd379d0773e16e
4
+ data.tar.gz: 9e26900083f2aba091e70e5cbcc695f575432c0705b2cc6cda56df78b2d97ed3
5
5
  SHA512:
6
- metadata.gz: 465070cf2e47b03b4d240a9d3f36a1f1129574a25d1492cb3a57612d5e6156a57ba40dd9cf887f4ee1ee14e5fbbe8ca20a217c73241da4ac12440d0b8ffcb059
7
- data.tar.gz: ebdbace0651138de47cc6c26237df580b6c30f49b1735a686d98061b16f14e844d5f7edebd9936ea42e8a8dfd5c57f3f82580bad9a1c9f31f68bd1e77c7d21c7
6
+ metadata.gz: c5edc68c51ba784fb05689a538fdeec88dccfaeadee54df43a10af537700b442eb16344848a60fa7b8da20ed4c4dde8754e3c2a9bad3848ba71ed255e2598f07
7
+ data.tar.gz: 327b344e58309db72a11b1d90c8d8eae950b92571434144c3f237e6aa4161ac5eb9221bb3d33609f883e1a38bd6a6c4f0eb5a11a564adf79890fe9f69083ae25
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mensa (0.6.1)
4
+ mensa (0.6.2)
5
5
  csv
6
6
  importmap-rails
7
7
  pagy (>= 43)
@@ -89,6 +89,18 @@
89
89
  @apply text-xs font-normal text-gray-400 dark:text-gray-500;
90
90
  }
91
91
 
92
+ &__item-password {
93
+ @apply inline-flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400;
94
+ }
95
+
96
+ &__item-password-value {
97
+ @apply rounded bg-gray-100 dark:bg-gray-700 px-1 py-0.5 font-mono text-[0.6875rem] text-gray-700 dark:text-gray-200;
98
+ }
99
+
100
+ &__item-password-copy {
101
+ @apply inline-flex h-5 w-5 items-center justify-center rounded text-gray-400 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-400 cursor-pointer;
102
+ }
103
+
92
104
  &__download {
93
105
  @apply inline-flex items-center gap-1.5 rounded-md bg-white dark:bg-gray-700 px-2.5 py-1.5 text-sm font-medium text-primary-600 dark:text-primary-300 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600;
94
106
  }
@@ -0,0 +1,32 @@
1
+ import ApplicationController from "mensa/controllers/application_controller";
2
+
3
+ export default class CopyableComponentController extends ApplicationController {
4
+ static values = {
5
+ text: String,
6
+ };
7
+
8
+ async copy(event) {
9
+ event.preventDefault();
10
+
11
+ if (!this.hasTextValue) return;
12
+
13
+ if (navigator.clipboard?.writeText) {
14
+ await navigator.clipboard.writeText(this.textValue);
15
+ } else {
16
+ this._copyWithFallback(this.textValue);
17
+ }
18
+ }
19
+
20
+ _copyWithFallback(text) {
21
+ const input = document.createElement("textarea");
22
+ input.value = text;
23
+ input.setAttribute("readonly", "");
24
+ input.style.position = "fixed";
25
+ input.style.top = "-9999px";
26
+ input.style.left = "-9999px";
27
+ document.body.appendChild(input);
28
+ input.select();
29
+ document.execCommand("copy");
30
+ input.remove();
31
+ }
32
+ }
@@ -30,6 +30,9 @@ application.register("mensa-selection", SelectionComponentController);
30
30
  import ColumnCustomizerController from "mensa/components/column_customizer/component_controller";
31
31
  application.register("mensa-column-customizer", ColumnCustomizerController);
32
32
 
33
+ import CopyableComponentController from "mensa/components/copyable/component_controller";
34
+ application.register("mensa-copyable", CopyableComponentController);
35
+
33
36
  // Eager load all controllers defined in the import map under controllers/**/*_controller
34
37
  // import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
35
38
  // eagerLoadControllersFrom("controllers", application)
@@ -1,6 +1,6 @@
1
1
  require "csv"
2
2
  require "securerandom"
3
- require "stringio"
3
+ require "tempfile"
4
4
 
5
5
  module Mensa
6
6
  # Generates the CSV for a Mensa::Export, attaches it to the export's +asset+
@@ -21,17 +21,19 @@ module Mensa
21
21
  return
22
22
  end
23
23
 
24
- data, filename, content_type = generate(table, export)
24
+ tempfile, filename, content_type = generate(table, export)
25
25
 
26
26
  export.asset.purge if export.asset.attached?
27
- export.asset.attach(io: StringIO.new(data), filename: filename, content_type: content_type)
27
+ export.asset.attach(io: tempfile, filename: filename, content_type: content_type)
28
28
  finalize(export, status: "completed", filename: filename)
29
29
 
30
30
  Mensa.config.callbacks[:export_complete]&.call(export)
31
31
  rescue => e
32
- Mensa.config.logger&.error("Mensa::ExportJob failed for export #{export_id}: #{e.class}: #{e.message}")
32
+ Mensa.config.logger&.error("Mensa::ExportJob failed for export #{export&.id}: #{e.class}: #{e.message}")
33
33
  finalize(export, status: "failed") if export
34
34
  raise
35
+ ensure
36
+ tempfile&.close!
35
37
  end
36
38
 
37
39
  private
@@ -56,35 +58,63 @@ module Mensa
56
58
  end
57
59
 
58
60
  def generate(table, export)
59
- io = StringIO.new
61
+ base_filename = "#{export.table_name}_export_#{export.created_at.strftime("%Y-%m-%d-%H%M%S")}"
62
+ csv_file = write_csv_file(table, export, base_filename)
63
+
64
+ if table.export_with_password?
65
+ zip_file = write_zip_file(csv_file, export, base_filename)
66
+ csv_file.close!
67
+ [zip_file, "#{base_filename}.zip", "application/zip"]
68
+ else
69
+ [csv_file, "#{base_filename}.csv", "text/csv"]
70
+ end
71
+ end
72
+
73
+ def write_csv_file(table, export, base_filename)
74
+ tempfile = Tempfile.new([base_filename, ".csv"], binmode: true)
75
+
60
76
  # A UTF-8 BOM makes spreadsheet programs such as Excel detect the encoding
61
77
  # correctly. The "plain" CSV variant omits it for maximum compatibility
62
78
  # with programmatic consumers.
63
- io.write("\uFEFF") if export.format == "csv_excel"
79
+ tempfile.write("\uFEFF") if export.format == "csv_excel"
64
80
 
65
- csv = CSV.new(io)
81
+ csv = CSV.new(tempfile)
66
82
  csv << table.display_columns.map(&:name)
67
83
  export_rows(table, export).each do |row|
68
84
  csv << table.display_columns.map { |column| Mensa::Cell.new(row: row, column: column).render(:csv) }
69
85
  end
70
- io.rewind
71
- data = io.read
86
+ csv.close
87
+ tempfile.open
88
+ tempfile.binmode
89
+ tempfile.rewind
90
+ tempfile
91
+ rescue
92
+ tempfile&.close!
93
+ raise
94
+ end
72
95
 
73
- base_filename = "#{export.table_name}_export_#{export.created_at.strftime("%Y-%m-%d-%H%M%S")}"
96
+ def write_zip_file(csv_file, export, base_filename)
97
+ require "zip"
74
98
 
75
- if table.export_with_password?
76
- require "zip"
77
- password = SecureRandom.hex(6)
78
- encrypter = Zip::TraditionalEncrypter.new(password)
79
- zip_io = Zip::OutputStream.write_buffer(encrypter: encrypter) do |zio|
80
- zio.put_next_entry("#{base_filename}.csv")
81
- zio.write data
82
- end
83
- zip_io.rewind
84
- [zip_io.read, "#{base_filename}.zip", "application/zip"]
85
- else
86
- [data, "#{base_filename}.csv", "text/csv"]
99
+ zip_file = Tempfile.new([base_filename, ".zip"], binmode: true)
100
+ zip_path = zip_file.path
101
+ zip_file.close
102
+
103
+ export.password = SecureRandom.hex(6)
104
+ encrypter = Zip::TraditionalEncrypter.new(export.password)
105
+ Zip::OutputStream.open(zip_path, encrypter: encrypter) do |zio|
106
+ zio.put_next_entry("#{base_filename}.csv")
107
+ csv_file.rewind
108
+ IO.copy_stream(csv_file, zio)
87
109
  end
110
+
111
+ zip_file.open
112
+ zip_file.binmode
113
+ zip_file.rewind
114
+ zip_file
115
+ rescue
116
+ zip_file&.close!
117
+ raise
88
118
  end
89
119
 
90
120
  def export_rows(table, export)
@@ -25,7 +25,8 @@ module Mensa
25
25
  [:lt, I18n.t("mensa.operators.lt"), true],
26
26
  [:lteq, I18n.t("mensa.operators.lteq"), true],
27
27
  [:is_current, I18n.t("mensa.operators.is_current"), false],
28
- [:is_empty, I18n.t("mensa.operators.is_empty"), false]
28
+ [:is_empty, I18n.t("mensa.operators.is_empty"), false],
29
+ [:isnt_empty, I18n.t("mensa.operators.isnt_empty"), false]
29
30
  ].freeze
30
31
  end
31
32
  end
@@ -85,6 +86,12 @@ module Mensa
85
86
  else
86
87
  record_scope.where("#{column.attribute_for_condition} IS NULL")
87
88
  end
89
+ when :isnt_empty
90
+ if column.type == :string
91
+ record_scope.where("#{column.attribute_for_condition} IS NOT NULL AND #{column.attribute_for_condition} != ''")
92
+ else
93
+ record_scope.where("#{column.attribute_for_condition} IS NOT NULL")
94
+ end
88
95
  when :is_current
89
96
  record_scope.where("#{column.attribute_for_condition} = ?", Current.send(column.name))
90
97
  when :matches
@@ -19,6 +19,15 @@
19
19
  <% if export.repeating? %>
20
20
  <span class="mensa-table__export-dialog__item-repeat"><%= export.repeat_label %></span>
21
21
  <% end %>
22
+ <% if export.password.present? %>
23
+ <span class="mensa-table__export-dialog__item-password" data-controller="mensa-copyable" data-mensa-copyable-text-value="<%= export.password %>">
24
+ <span class="mensa-table__export-dialog__item-password-label">Password:</span>
25
+ <code class="mensa-table__export-dialog__item-password-value"><%= export.password %></code>
26
+ <button class="mensa-table__export-dialog__item-password-copy" type="button" data-action="mensa-copyable#copy" aria-label="Copy password">
27
+ <i class="fas fa-copy" aria-hidden="true"></i>
28
+ </button>
29
+ </span>
30
+ <% end %>
22
31
  <span class="mensa-table__export-dialog__item-meta"><%= export.created_at.strftime("%Y-%m-%d %H:%M") %></span>
23
32
  </div>
24
33
  <div class="mensa-table__export-dialog__item-action">
@@ -19,6 +19,7 @@ en:
19
19
  gteq: greater than or equal to
20
20
  lteq: less than or equal to
21
21
  is_empty: is empty
22
+ isnt_empty: is not empty
22
23
  add_filter:
23
24
  add: Add filter
24
25
  component:
@@ -19,6 +19,7 @@ nl:
19
19
  gteq: groter dan of gelijk aan
20
20
  lteq: kleiner dan of gelijk aan
21
21
  is_empty: is leeg
22
+ isnt_empty: is niet leeg
22
23
  add_filter:
23
24
  add: Filter toevoegen
24
25
  component:
@@ -0,0 +1,5 @@
1
+ class AddPasswordToExport < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :mensa_exports, :password, :string
4
+ end
5
+ end
data/lib/mensa/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Mensa
2
- VERSION = "0.6.2"
2
+ VERSION = "0.6.3"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mensa
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.6.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom de Grunt
@@ -193,6 +193,7 @@ files:
193
193
  - app/components/mensa/control_bar/component.css
194
194
  - app/components/mensa/control_bar/component.html.erb
195
195
  - app/components/mensa/control_bar/component.rb
196
+ - app/components/mensa/copyable/component_controller.js
196
197
  - app/components/mensa/empty_state/component.css
197
198
  - app/components/mensa/empty_state/component.html.erb
198
199
  - app/components/mensa/empty_state/component.rb
@@ -294,6 +295,7 @@ files:
294
295
  - db/migrate/20251112143558_add_description_to_table_view.rb
295
296
  - db/migrate/20260604120000_create_mensa_exports.rb
296
297
  - db/migrate/20260612110000_add_repeat_to_mensa_exports.rb
298
+ - db/migrate/20260616113603_add_password_to_export.rb
297
299
  - docs/columns.png
298
300
  - docs/export.png
299
301
  - docs/filters.png