nexo 0.1.5 → 0.1.7

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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -0
  3. data/app/controllers/nexo/element_versions_controller.rb +26 -0
  4. data/app/controllers/nexo/elements_controller.rb +70 -0
  5. data/app/controllers/nexo/folders_controller.rb +41 -0
  6. data/app/controllers/nexo/nexo_controller.rb +11 -0
  7. data/app/jobs/nexo/api_clients.rb +20 -18
  8. data/app/jobs/nexo/base_job.rb +5 -3
  9. data/app/jobs/nexo/delete_remote_resource_job.rb +23 -1
  10. data/app/jobs/nexo/fetch_remote_resource_job.rb +61 -0
  11. data/app/jobs/nexo/folder_check_status_job.rb +10 -0
  12. data/app/jobs/nexo/folder_destroy_job.rb +2 -1
  13. data/app/jobs/nexo/folder_download_job.rb +15 -0
  14. data/app/jobs/nexo/folder_sync_job.rb +6 -0
  15. data/app/jobs/nexo/synchronizable_changed_job.rb +17 -2
  16. data/app/jobs/nexo/update_remote_resource_job.rb +56 -26
  17. data/app/lib/nexo/api_client/google_auth_service.rb +9 -8
  18. data/app/lib/nexo/api_client/google_calendar_service.rb +162 -38
  19. data/app/lib/nexo/api_client/google_calendar_sync_service.rb +88 -0
  20. data/app/lib/nexo/element_service.rb +249 -0
  21. data/app/lib/nexo/errors.rb +6 -5
  22. data/app/lib/nexo/event_receiver.rb +4 -0
  23. data/app/lib/nexo/folder_service.rb +55 -28
  24. data/app/lib/nexo/import_remote_element_version.rb +75 -0
  25. data/app/lib/nexo/policy_service.rb +27 -8
  26. data/app/models/concerns/nexo/calendar_event.rb +39 -2
  27. data/app/models/concerns/nexo/synchronizable.rb +28 -9
  28. data/app/models/nexo/element.rb +23 -34
  29. data/app/models/nexo/element_version.rb +17 -2
  30. data/app/models/nexo/folder.rb +29 -17
  31. data/app/models/nexo/integration.rb +0 -3
  32. data/app/models/nexo/token.rb +2 -0
  33. data/app/views/layouts/error.html.erb +8 -0
  34. data/app/views/layouts/nexo.html.erb +40 -0
  35. data/app/views/nexo/element_versions/show.html.erb +16 -0
  36. data/app/views/nexo/elements/index.html.erb +40 -0
  37. data/app/views/nexo/elements/show.html.erb +60 -0
  38. data/app/views/nexo/folders/index.html.erb +22 -0
  39. data/app/views/nexo/folders/show.html.erb +22 -0
  40. data/config/environment.rb +1 -0
  41. data/config/routes.rb +26 -0
  42. data/config/spring.rb +1 -0
  43. data/db/migrate/20250604124821_element_sync_status.rb +13 -0
  44. data/db/migrate/20250612002919_google_sync_tokens.rb +5 -0
  45. data/db/migrate/20250623132502_folder_sync_direction.rb +8 -0
  46. data/db/migrate/20250718012839_synchronizable_nullable.rb +6 -0
  47. data/db/seeds.rb +3 -2
  48. data/lib/nexo/engine.rb +26 -5
  49. data/lib/nexo/version.rb +1 -1
  50. metadata +39 -4
  51. data/app/jobs/nexo/sync_element_job.rb +0 -93
  52. data/app/models/concerns/nexo/folder_policy.rb +0 -24
@@ -3,60 +3,87 @@ module Nexo
3
3
  #
4
4
  # Responsabilities:
5
5
  # - Creation of Element's
6
- # - Flagging Element's for deletion
7
- # - Triggering the SyncElementJob
6
+ # - Creation of ElementVersion on local changes
7
+ # - Flagging Element's for removal
8
+ # - Enqueues UpdateRemoteResourceJob, DeleteRemoteResourceJob
8
9
  class FolderService
10
+ # @raise [ActiveRecord::RecordNotUnique] on ElementVersion creation
9
11
  def find_element_and_sync(folder, synchronizable)
10
- # TODO: handle conflicted synchronizable
11
- element = find_element(folder, synchronizable)
12
+ validate_synchronizable!(synchronizable)
13
+
14
+ element = folder.find_element(synchronizable:)
12
15
 
13
16
  if element.present?
17
+ Nexo.logger.debug { "Element found" }
18
+ validate_element_state!(element)
14
19
  sync_element(element)
15
20
  else
21
+ Nexo.logger.debug { "Element not found" }
16
22
  create_and_sync_element(folder, synchronizable)
17
23
  end
18
24
  end
19
25
 
20
- def destroy_elements(synchronizable, reason)
21
- synchronizable.nexo_elements.each do |element|
22
- element.flag_for_deletion!(reason)
26
+ def destroy_elements(synchronizable, reason, exclude_elements: [])
27
+ Nexo.logger.debug("Destroying elements for synchronizable")
28
+
29
+ scope = synchronizable.nexo_elements
30
+
31
+ if exclude_elements.any?
32
+ Nexo.logger.debug("Excluding elements: #{exclude_elements}")
23
33
 
24
- SyncElementJob.perform_later(element)
34
+ scope = scope.where.not(id: exclude_elements)
25
35
  end
26
- end
27
36
 
28
- private
37
+ scope.each do |element|
38
+ unless element.folder.sync_internal_changes?
39
+ Nexo.logger.debug("Folder dont syncs internal changes, skipping")
40
+ next
41
+ end
29
42
 
30
- def find_element(folder, synchronizable)
31
- folder.find_element(synchronizable:)
43
+ ElementService.new(element:).flag_for_removal!(reason)
44
+
45
+ DeleteRemoteResourceJob.perform_later(element)
46
+ end
32
47
  end
33
48
 
34
- def create_and_sync_element(folder, synchronizable)
35
- must_be_included = folder.policy_match?(synchronizable)
49
+ private
36
50
 
37
- if must_be_included
38
- element = Element.create!(
39
- synchronizable:,
40
- folder:
41
- )
51
+ def validate_synchronizable!(synchronizable)
52
+ synchronizable.validate_synchronizable!
42
53
 
43
- SyncElementJob.perform_later(element)
54
+ if synchronizable.sequence.nil?
55
+ raise Errors::SynchronizableSequenceIsNull
44
56
  end
45
57
  end
46
58
 
59
+ def validate_element_state!(element)
60
+ # unless element.synced?
61
+ # raise Errors::Error, <<~STR
62
+ # element ne_status is invalid: #{element.ne_status} \
63
+ # (should be synced)
64
+ # STR
65
+ # end
66
+ end
67
+
47
68
  def sync_element(element)
48
- synchronizable = element.synchronizable
69
+ if element.policy_still_applies?
70
+ ElementService.new(element:).create_internal_version_if_none!
71
+ else
72
+ Nexo.logger.debug("Flagging for removal and enqueuing DeleteRemoteResourceJob")
73
+ ElementService.new(element:).flag_for_removal!(:no_longer_included_in_folder)
49
74
 
50
- if synchronizable.conflicted?
51
- raise Nexo::Errors::ElementConflicted, element
75
+ DeleteRemoteResourceJob.perform_later(element)
52
76
  end
77
+ end
53
78
 
54
- # Check if Synchronizable still must be included in folder
55
- if !element.policy_still_match?
56
- element.flag_for_deletion!(:no_longer_included_in_folder)
57
- end
79
+ def create_and_sync_element(folder, synchronizable)
80
+ must_be_included = folder.policy_applies?(synchronizable)
58
81
 
59
- SyncElementJob.perform_later(element)
82
+ if must_be_included
83
+ ElementService.new.create_element_for!(folder, synchronizable)
84
+ else
85
+ Nexo.logger.debug { "Policy not applies, skipping creation" }
86
+ end
60
87
  end
61
88
  end
62
89
  end
@@ -0,0 +1,75 @@
1
+ module Nexo
2
+ class ImportRemoteElementVersion
3
+ class VersionSuperseded < Errors::Error; end
4
+ class ImportRemoteVersionFailed < Errors::Error; end
5
+
6
+ def perform(element_version)
7
+ validate_element_state!(element_version)
8
+
9
+ ElementService.new(element_version:).update_synchronizable!
10
+ rescue ImportRemoteVersionFailed => e
11
+ Nexo.logger.warn(e.inspect)
12
+ rescue VersionSuperseded
13
+ Nexo.logger.info("ImportRemoteElementVersion: version superseded")
14
+ end
15
+
16
+ private
17
+
18
+ def validate_element_state!(element_version)
19
+ element = element_version.element
20
+
21
+ raise Nexo::Errors::Error, "element version must be external" if element_version.internal?
22
+ raise Nexo::Errors::Error, "etag must be present" if element_version.etag.blank?
23
+
24
+ unless element_version.nev_status == "pending_sync"
25
+ raise Nexo::Errors::Error, "nev_status invalid, should be pending_sync"
26
+ end
27
+
28
+ # NOTE: this is actually very coupled to the way Google manages etag as a
29
+ # sequential number, if a new protocol or service is added in the future
30
+ # and it doesnt manage it that way, it would have to be delegated to the
31
+ # protocol service
32
+ if element.element_versions.where(nev_status: :synced)
33
+ .where("etag > ?", element_version.etag).any?
34
+
35
+ ElementService.new(element_version:).update_element_version!(nev_status: :superseded)
36
+
37
+ raise VersionSuperseded
38
+ end
39
+
40
+ if element.conflicted?
41
+ raise ImportRemoteVersionFailed, "element conflicted"
42
+ end
43
+
44
+ # if element.synchronizable.blank?
45
+ # # TODO!: this could be that an external element was restored. i.e.:
46
+ # # google calendar event cancelled and restored.
47
+ # # this should be handled in some way, maybe configurable per folder
48
+ # # options are:
49
+ # # - ignore the element
50
+ # # - create synchronizable as if it were new
51
+ # # - discard/undiscard, for this the synchronizable should have been
52
+ # # deleted
53
+ # raise ImportRemoteVersionFailed, "synchronizable not found"
54
+ # end
55
+
56
+ # :nocov: borderline
57
+ if element.discarded?
58
+ # TODO!: this could be that an external element was restored. i.e.:
59
+ # Event excluded from folder and then restored from Google Calendar
60
+ raise ImportRemoteVersionFailed, "element discarded"
61
+ end
62
+
63
+ if element.synchronizable.present?
64
+ if element.synchronizable.conflicted?
65
+ raise ImportRemoteVersionFailed, "synchronizable conflicted"
66
+ end
67
+
68
+ if element.synchronizable.sequence.nil?
69
+ raise ImportRemoteVersionFailed, "synchronizable sequence is null"
70
+ end
71
+ end
72
+ # :nocov:
73
+ end
74
+ end
75
+ end
@@ -1,9 +1,13 @@
1
1
  module Nexo
2
2
  class PolicyService
3
- def initialize
3
+ def clear_finders!
4
4
  @finders = []
5
5
  end
6
6
 
7
+ def initialize
8
+ clear_finders!
9
+ end
10
+
7
11
  @instance = new
8
12
 
9
13
  private_class_method :new
@@ -12,29 +16,44 @@ module Nexo
12
16
  @instance
13
17
  end
14
18
 
15
- def register_folder_policy_finder(&block)
19
+ def register_folder_rule_finder(&block)
16
20
  @finders << block
17
21
  end
18
22
 
19
23
  attr_reader :finders
20
24
 
21
- def match?(folder, synchronizable)
25
+ def applies?(folder, synchronizable)
22
26
  policies = policies_for(folder)
23
- matching_policies = policies.select { |policy| policy.match?(synchronizable) }
24
- if matching_policies.any?
25
- aplicable_policy = matching_policies.sort_by { |policy| policy.priority }.last
26
- aplicable_policy.sync_policy == :include
27
+ applied_policies = policies.select { |policy| policy.applies?(synchronizable) }
28
+ # :nocov: TODO
29
+ if applied_policies.any?
30
+ aplicable_policy = applied_policies.sort_by { |policy| policy.priority }.last
31
+ logger.debug { "Aplicable policy: #{aplicable_policy.inspect}" }
32
+ logger.debug { "sync_policy: #{aplicable_policy.sync_policy}" }
33
+ aplicable_policy.sync_policy.to_s == "include"
27
34
  else
35
+ logger.debug { "No applicable policies" }
28
36
  false
29
37
  end
38
+ # :nocov:
30
39
  end
31
40
 
32
41
  def policies_for(folder)
42
+ logger.debug { "Found #{@finders.length} finders" }
43
+
33
44
  aux = @finders.map do |finder|
34
45
  finder.call(folder)
35
46
  end
36
47
 
37
- aux.flatten.compact
48
+ aux.flatten.compact.tap do |ret|
49
+ logger.debug { "Found #{ret.length} policies" }
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def logger
56
+ logger = Nexo.logger
38
57
  end
39
58
  end
40
59
  end
@@ -4,6 +4,11 @@ module Nexo
4
4
 
5
5
  extend ActiveSupport::Concern
6
6
 
7
+ # nce_status (Nexo Calendar Event Status)
8
+ #
9
+ # - confirmed
10
+ # - tentative (unused)
11
+ # - cancelled (deleted)
7
12
  define_protocol(:nexo_calendar_event, %i[
8
13
  date_from
9
14
  date_to
@@ -11,6 +16,7 @@ module Nexo
11
16
  time_to
12
17
  summary
13
18
  description
19
+ nce_status
14
20
  ])
15
21
 
16
22
  def change_is_significative_to_sequence?
@@ -33,6 +39,38 @@ module Nexo
33
39
  build_date_time(date_to, time_to)
34
40
  end
35
41
 
42
+ def all_day?
43
+ time_from.blank? && time_to.blank?
44
+ end
45
+
46
+ # transparent: non blocking
47
+ # opaque: blocking
48
+ def transparency
49
+ if all_day?
50
+ "transparent"
51
+ else
52
+ "opaque"
53
+ end
54
+ end
55
+
56
+ def validate_synchronizable!
57
+ unless datetime_from.present?
58
+ raise Errors::SynchronizableInvalid, "datetime_from is nil"
59
+ end
60
+
61
+ unless datetime_to.present?
62
+ raise Errors::SynchronizableInvalid, "datetime_to is nil"
63
+ end
64
+
65
+ unless datetime_from != datetime_to
66
+ raise Errors::SynchronizableInvalid, "datetime_from and datetime_to are equal"
67
+ end
68
+
69
+ unless summary.present?
70
+ raise Errors::SynchronizableInvalid, "summary is nil"
71
+ end
72
+ end
73
+
36
74
  private
37
75
 
38
76
  # @param [Date] date
@@ -40,7 +78,7 @@ module Nexo
40
78
  #
41
79
  # @return [Date, DateTime]
42
80
  def build_date_time(date, time = nil)
43
- if time.present?
81
+ if time.present? && date.present?
44
82
  DateTime.new(
45
83
  date.year,
46
84
  date.month,
@@ -55,7 +93,6 @@ module Nexo
55
93
  end
56
94
  end
57
95
 
58
-
59
96
  # TODO: refactor https://api.rubyonrails.org/classes/ActiveSupport/Concern.html
60
97
  included do
61
98
  include Nexo::Synchronizable::Associations
@@ -8,6 +8,10 @@ module Nexo
8
8
  # inserta en un google calendar y luego se elimina, luego ya no se
9
9
  # puede volver a insertar con el mismo uuid, sería mejor que el id se
10
10
  # genere automáticamente por google y guardarlo en Element
11
+ #
12
+ # sequence
13
+ # autoincremental integer, starting at 0. its recommended that
14
+ # the attribute is always initialized with 0 value on creation
11
15
  %i[
12
16
  sequence
13
17
  ]
@@ -35,6 +39,19 @@ module Nexo
35
39
  end
36
40
  end
37
41
  end
42
+
43
+ def create_from_payload!(folder, payload)
44
+ Nexo.logger.debug("Synchronizable#create_from_payload!")
45
+ service = Nexo::ServiceBuilder.instance.build_protocol_service(folder)
46
+ fields = service.fields_from_payload(payload)
47
+
48
+ synchronizable = new
49
+ attributes = synchronizable.translate_fields(fields, for_create: true, folder:)
50
+ synchronizable.assign_attributes(attributes)
51
+ synchronizable.save!
52
+
53
+ synchronizable
54
+ end
38
55
  end
39
56
 
40
57
  def method_missing(method_name, *, &)
@@ -58,21 +75,23 @@ module Nexo
58
75
  nexo_elements.conflicted.any?
59
76
  end
60
77
 
61
- # :nocov: TODO, not yet implemented
62
- def update_from!(element_version)
63
- transaction do
64
- # TODO: parse the element_version.payload
65
- # and set the Synchronizable fields according to the Folder#nexo_protocol
78
+ # :nocov: borderline
79
+ def validate_synchronizable!
80
+ raise "must be implemented in subclass"
81
+ end
66
82
 
67
- new_sequence = increment_sequence!
68
- element_version.update_sequence!(new_sequence)
69
- end
83
+ def update_from_fields!(fields)
84
+ raise "must be implemented in subclass"
85
+ end
86
+
87
+ def translate_fields(fields, for_create: false, folder: nil)
88
+ raise "must be implemented in subclass"
70
89
  end
71
90
  # :nocov:
72
91
 
73
92
  def increment_sequence!
74
93
  if sequence.nil?
75
- Rails.logger.warn("Synchronizable sequence is nil on increment_sequence!: #{self.to_gid}")
94
+ Nexo.logger.warn("Synchronizable sequence is nil on increment_sequence!: #{self.to_gid}")
76
95
  end
77
96
 
78
97
  # This operation is performed directly in the database without the need
@@ -4,20 +4,24 @@
4
4
  #
5
5
  # id :bigint not null, primary key
6
6
  # folder_id :bigint not null
7
- # synchronizable_id :integer not null
8
- # synchronizable_type :string not null
7
+ # synchronizable_id :integer
8
+ # synchronizable_type :string
9
9
  # uuid :string
10
10
  # flagged_for_removal :boolean not null
11
11
  # removal_reason :integer
12
- # conflicted :boolean default(FALSE), not null
13
12
  # discarded_at :datetime
14
13
  # created_at :datetime not null
15
14
  # updated_at :datetime not null
15
+ # ne_status :integer not null
16
16
  #
17
17
  module Nexo
18
18
  class Element < ApplicationRecord
19
19
  belongs_to :folder, class_name: "Nexo::Folder"
20
- belongs_to :synchronizable, polymorphic: true
20
+
21
+ # when blank it's a brand new remote element that needs a synchronizable to
22
+ # be created
23
+ belongs_to :synchronizable, polymorphic: true, optional: true
24
+
21
25
  has_many :element_versions, dependent: :destroy, class_name: "Nexo::ElementVersion"
22
26
 
23
27
  after_initialize do
@@ -29,46 +33,31 @@ module Nexo
29
33
 
30
34
  enum :removal_reason, no_longer_included_in_folder: 0, synchronizable_destroyed: 1
31
35
 
32
- scope :conflicted, -> { where(conflicted: true) }
33
-
34
- def policy_still_match?
35
- folder.policy_match?(synchronizable)
36
- end
37
-
38
- def last_synced_sequence
39
- element_versions.pluck(:sequence).max || -1
40
- end
41
-
42
- def external_unsynced_change?
43
- last_external_unsynced_version.present?
44
- end
36
+ enum :ne_status,
37
+ synced: 0,
38
+ pending_external_sync: 1,
39
+ pending_local_sync: 2,
40
+ conflicted: 3,
41
+ pending_remote_delete: 4
45
42
 
46
- def last_external_unsynced_version
47
- element_versions.where(sequence: nil).order(created_at: :desc).first
48
- end
43
+ scope :conflicted, -> { where(ne_status: :conflicted) }
49
44
 
50
- def flag_for_deletion!(removal_reason)
51
- update!(flagged_for_removal: true, removal_reason:)
45
+ def policy_still_applies?
46
+ # :nocov: TODO
47
+ folder.policy_applies?(synchronizable)
48
+ # :nocov:
52
49
  end
53
50
 
54
- def flagged_for_deletion?
55
- flagged_for_removal?
51
+ def etag
52
+ last_remote_version&.etag
56
53
  end
57
54
 
58
- # :nocov: TODO, not yet being called
59
- def flag_as_conflicted!
60
- update!(conflicted: true)
61
-
62
- # TODO: log "Conflicted Element: #{element.to_gid}"
55
+ def last_remote_version
56
+ element_versions.where.not(etag: nil).order(:etag).last
63
57
  end
64
- # :nocov:
65
58
 
66
59
  def discarded?
67
60
  discarded_at.present?
68
61
  end
69
-
70
- def discard!
71
- update!(discarded_at: Time.current)
72
- end
73
62
  end
74
63
  end
@@ -10,12 +10,16 @@
10
10
  # origin :integer not null
11
11
  # created_at :datetime not null
12
12
  # updated_at :datetime not null
13
+ # nev_status :integer not null
13
14
  #
14
15
  module Nexo
15
16
  # sequence
16
17
  #
17
18
  # cuando es null significa que el origin es external que debe ser
18
- # sincronizado. si está presente, significa que fue sinzronizado
19
+ # sincronizado. si está presente, significa que fue sincronizado
20
+ # incremental correlativa. puede ser discontinua si se generan updates
21
+ # a una frecuencia alta
22
+ # TODO!: rename to "version"
19
23
  #
20
24
  # etag
21
25
  #
@@ -30,9 +34,20 @@ module Nexo
30
34
  belongs_to :element, class_name: "Nexo::Element"
31
35
 
32
36
  enum :origin, internal: 0, external: 1
37
+ enum :nev_status,
38
+ pending_sync: 0,
39
+ synced: 1,
40
+ ignored_in_conflict: 2,
41
+ superseded: 3,
42
+ ignored_by_sync_direction: 4,
43
+ ignored_by_deletion: 5
33
44
 
34
45
  serialize :payload, coder: JSON
35
46
 
36
- validates :payload, :etag, :origin, presence: true
47
+ validates :origin, presence: true
48
+
49
+ def remote_status
50
+ payload["status"] if payload.is_a?(Hash)
51
+ end
37
52
  end
38
53
  end
@@ -2,37 +2,45 @@
2
2
  #
3
3
  # Table name: nexo_folders
4
4
  #
5
- # id :bigint not null, primary key
6
- # integration_id :bigint not null
7
- # nexo_protocol :integer not null
8
- # external_identifier :string
9
- # name :string
10
- # description :string
11
- # discarded_at :datetime
12
- # created_at :datetime not null
13
- # updated_at :datetime not null
5
+ # id :bigint not null, primary key
6
+ # integration_id :bigint not null
7
+ # nexo_protocol :integer not null
8
+ # external_identifier :string
9
+ # name :string
10
+ # description :string
11
+ # discarded_at :datetime
12
+ # created_at :datetime not null
13
+ # updated_at :datetime not null
14
+ # google_next_sync_token :string
15
+ # sync_direction :integer not null
16
+ # nf_status :integer default("initial"), not null
14
17
  #
15
18
  module Nexo
16
19
  class Folder < ApplicationRecord
17
20
  belongs_to :integration, class_name: "Nexo::Integration"
18
21
  has_many :elements, class_name: "Nexo::Element"
19
22
 
20
- if respond_to?(:enumerize)
21
- enumerize :nexo_protocol, in: { calendar: 0, dummy_calendar: 1 }
22
- else
23
- enum :nexo_protocol, calendar: 0, dummy_calendar: 1
23
+ enum :nexo_protocol, calendar: 0, dummy_calendar: 1
24
+ enum :sync_direction, sync_in_out: 0, sync_out_in: 1, sync_bidirectional: 2
25
+ enum :nf_status, initial: 0, ok: 1, not_found: 2
26
+
27
+ def sync_external_changes?
28
+ sync_out_in? || sync_bidirectional?
29
+ end
30
+
31
+ def sync_internal_changes?
32
+ sync_in_out? || sync_bidirectional?
24
33
  end
25
34
 
26
35
  scope :kept, -> { where(discarded_at: nil) }
27
36
 
28
37
  validates :nexo_protocol, :name, presence: true
29
38
 
30
- # TODO!: find better name
31
- # maybe policy_applies?
32
- def policy_match?(synchronizable)
33
- PolicyService.instance.match?(self, synchronizable)
39
+ def policy_applies?(synchronizable)
40
+ PolicyService.instance.applies?(self, synchronizable)
34
41
  end
35
42
 
43
+ # TODO: use find_sole_by
36
44
  def find_element(synchronizable:)
37
45
  ary = elements.where(synchronizable:, discarded_at: nil).to_a
38
46
 
@@ -54,5 +62,9 @@ module Nexo
54
62
  def discard!
55
63
  update!(discarded_at: Time.current)
56
64
  end
65
+
66
+ def to_s
67
+ name.presence || super
68
+ end
57
69
  end
58
70
  end
@@ -14,9 +14,6 @@
14
14
  #
15
15
  module Nexo
16
16
  class Integration < ApplicationRecord
17
- include Discard::Model if defined? Discard::Model
18
- include Hashid::Rails if defined? Hashid::Rails
19
-
20
17
  serialize :scope, coder: JSON
21
18
  belongs_to :user
22
19
  belongs_to :client, class_name: "Nexo::Client"
@@ -14,6 +14,8 @@ module Nexo
14
14
  class Token < ApplicationRecord
15
15
  belongs_to :integration, class_name: "Nexo::Integration"
16
16
 
17
+ scope :active, -> { where(nt_status: :active) }
18
+
17
19
  after_initialize do
18
20
  # TODO: https://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html#method-i-attribute
19
21
  self.nt_status = :active if nt_status.nil?
@@ -0,0 +1,8 @@
1
+ <h1>ERROR</h1>
2
+
3
+ <% if @exception.present? %>
4
+ <h4><%= @exception.message %></h4>
5
+ <pre>
6
+ <%= JSON.pretty_generate @exception.backtrace %>
7
+ </pre>
8
+ <% end %>
@@ -0,0 +1,40 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= content_for(:title) || "Nexo" %></title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <meta name="apple-mobile-web-app-capable" content="yes">
7
+ <%= csrf_meta_tags %>
8
+ <%= csp_meta_tag %>
9
+
10
+ <style>
11
+ table th {
12
+ font-weight: bold;
13
+ }
14
+ table th, table td {
15
+ border: 1px solid lightgrey;
16
+ padding: 0.4em;
17
+ }
18
+ </style>
19
+ </head>
20
+
21
+ <body>
22
+ <h1><%= link_to "NEXO", root_path %></h1>
23
+ <% flash.each do |flash_type, message| %>
24
+ <% if flash_type == "alert" %>
25
+ <div style="color: red">
26
+ <% else %>
27
+ <div style="color: blue">
28
+ <% end %>
29
+ <%= flash_type %>:
30
+ <%= message %>
31
+ </div>
32
+ <% end %>
33
+ <%= link_to "Folders", folders_path %>
34
+ <%= link_to "Elements", elements_path %>
35
+ |
36
+ <%= link_to "Home", main_app.root_path %>
37
+ <hr>
38
+ <%= yield %>
39
+ </body>
40
+ </html>