support_desk 0.1.0
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 +7 -0
- data/.rubocop.yml +32 -0
- data/.simplecov +53 -0
- data/Appraisals +18 -0
- data/CHANGELOG.md +135 -0
- data/LICENSE.txt +21 -0
- data/README.md +469 -0
- data/Rakefile +55 -0
- data/app/assets/stylesheets/support_desk.css +242 -0
- data/app/controllers/support_desk/application_controller.rb +92 -0
- data/app/controllers/support_desk/console/application_controller.rb +18 -0
- data/app/controllers/support_desk/console/tickets_controller.rb +26 -0
- data/app/controllers/support_desk/tickets_controller.rb +119 -0
- data/app/helpers/support_desk/engine_helper.rb +196 -0
- data/app/views/chats/slots/_inbox_top.html.erb +31 -0
- data/app/views/chats/slots/_locked_composer.html.erb +18 -0
- data/app/views/support_desk/console/tickets/_actions.html.erb +29 -0
- data/app/views/support_desk/console/tickets/_assignment.html.erb +60 -0
- data/app/views/support_desk/console/tickets/_composer.html.erb +71 -0
- data/app/views/support_desk/console/tickets/_context_card.html.erb +53 -0
- data/app/views/support_desk/console/tickets/_message.html.erb +48 -0
- data/app/views/support_desk/console/tickets/_nav_badge.html.erb +19 -0
- data/app/views/support_desk/console/tickets/_tabs.html.erb +15 -0
- data/app/views/support_desk/console/tickets/_ticket_row.html.erb +56 -0
- data/app/views/support_desk/console/tickets/_timeline.html.erb +37 -0
- data/app/views/support_desk/console/tickets/_transcript.html.erb +26 -0
- data/app/views/support_desk/console/tickets/index.html.erb +37 -0
- data/app/views/support_desk/console/tickets/show.html.erb +45 -0
- data/app/views/support_desk/tickets/_context_card.html.erb +14 -0
- data/app/views/support_desk/tickets/_door.html.erb +12 -0
- data/app/views/support_desk/tickets/_pick_thing.html.erb +55 -0
- data/app/views/support_desk/tickets/_pick_topic.html.erb +30 -0
- data/app/views/support_desk/tickets/_ticket_row.html.erb +31 -0
- data/app/views/support_desk/tickets/_wizard_header.html.erb +23 -0
- data/app/views/support_desk/tickets/_write.html.erb +56 -0
- data/app/views/support_desk/tickets/index.html.erb +53 -0
- data/app/views/support_desk/tickets/new.html.erb +14 -0
- data/app/views/support_desk/tickets/rate_limited.html.erb +40 -0
- data/config/console_routes.rb +16 -0
- data/config/locales/support_desk.console.en.yml +93 -0
- data/config/locales/support_desk.console.es.yml +93 -0
- data/config/locales/support_desk.en.yml +79 -0
- data/config/locales/support_desk.es.yml +84 -0
- data/config/routes.rb +24 -0
- data/context7.json +4 -0
- data/gemfiles/rails_7.2.gemfile +34 -0
- data/gemfiles/rails_8.0.gemfile +34 -0
- data/gemfiles/rails_8.1.gemfile +34 -0
- data/lib/generators/support_desk/console_generator.rb +94 -0
- data/lib/generators/support_desk/install_generator.rb +86 -0
- data/lib/generators/support_desk/templates/console/controller.rb.erb +43 -0
- data/lib/generators/support_desk/templates/console/resource.rb.erb +44 -0
- data/lib/generators/support_desk/templates/create_support_desk_tables.rb.erb +224 -0
- data/lib/generators/support_desk/templates/initializer.rb +186 -0
- data/lib/generators/support_desk/views_generator.rb +50 -0
- data/lib/support_desk/configuration.rb +675 -0
- data/lib/support_desk/console.rb +487 -0
- data/lib/support_desk/console_engine.rb +63 -0
- data/lib/support_desk/console_routes.rb +107 -0
- data/lib/support_desk/context_card.rb +90 -0
- data/lib/support_desk/current.rb +26 -0
- data/lib/support_desk/doctor.rb +220 -0
- data/lib/support_desk/engine.rb +141 -0
- data/lib/support_desk/errors.rb +49 -0
- data/lib/support_desk/events.rb +122 -0
- data/lib/support_desk/macros.rb +73 -0
- data/lib/support_desk/models/application_record.rb +11 -0
- data/lib/support_desk/models/assignment.rb +83 -0
- data/lib/support_desk/models/concerns/agent.rb +79 -0
- data/lib/support_desk/models/concerns/requester.rb +71 -0
- data/lib/support_desk/models/concerns/supportable.rb +88 -0
- data/lib/support_desk/models/desk.rb +101 -0
- data/lib/support_desk/models/event.rb +72 -0
- data/lib/support_desk/models/ticket.rb +1124 -0
- data/lib/support_desk/queue.rb +171 -0
- data/lib/support_desk/summary.rb +70 -0
- data/lib/support_desk/test_helpers.rb +137 -0
- data/lib/support_desk/timeline.rb +104 -0
- data/lib/support_desk/topic.rb +290 -0
- data/lib/support_desk/topic_tree.rb +214 -0
- data/lib/support_desk/version.rb +5 -0
- data/lib/support_desk/wizard.rb +392 -0
- data/lib/support_desk.rb +288 -0
- metadata +229 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 953cadd4b7e15695dd5d17092647e38c5595e4b81bccfc2917b491c428f4d7d6
|
|
4
|
+
data.tar.gz: 077ca8e707b0789140bd484414645ad1217b210d92d02557d7174fd32617b90b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f442d5f55ab69f61941fcd0b401e0be726d7ca1b228d68d683b52cc14e795d1f136fadf1915faf0153dca9d33a5f3720f764cacd06f3bab3567af20a36393ba6
|
|
7
|
+
data.tar.gz: 83cc58116727f3f2aebf62f8b61f4a2617d443b58f7cee8179620e5889532e2f281a6515b6c058f7baed4512a259f06592ff6d8b420b2afcbd12e2ddc1241ad6
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# rubocop-rails-omakase is the house style across the gem ecosystem: Rails'
|
|
2
|
+
# own defaults, which means generated code (migrations, initializers) stays
|
|
3
|
+
# clean in a stock Rails app without us reformatting it.
|
|
4
|
+
inherit_gem:
|
|
5
|
+
rubocop-rails-omakase: rubocop.yml
|
|
6
|
+
|
|
7
|
+
# MERGE our excludes with rubocop's defaults (vendor/, node_modules/, …) —
|
|
8
|
+
# a bare Exclude key would REPLACE them.
|
|
9
|
+
inherit_mode:
|
|
10
|
+
merge:
|
|
11
|
+
- Exclude
|
|
12
|
+
|
|
13
|
+
AllCops:
|
|
14
|
+
TargetRubyVersion: 3.2
|
|
15
|
+
Exclude:
|
|
16
|
+
# The dummy host app mirrors GENERATED code (its migration is a copy of
|
|
17
|
+
# the install template) — don't lint it against the gem's own style.
|
|
18
|
+
- test/dummy/db/**/*
|
|
19
|
+
# Generator scratch output. `rake ci` runs the tests before rubocop, so
|
|
20
|
+
# leaving this in would make the set of linted files depend on whether a
|
|
21
|
+
# generator test had run — a gate that inspects 109 files on a clean
|
|
22
|
+
# checkout and 111 after a suite is a gate nobody can reproduce. The
|
|
23
|
+
# generated console is still linted for real: test/dummy's controller is
|
|
24
|
+
# a verbatim rendering of the same template, and a test keeps them equal.
|
|
25
|
+
- test/tmp/**/*
|
|
26
|
+
# Appraisal-generated Gemfiles.
|
|
27
|
+
- gemfiles/*
|
|
28
|
+
|
|
29
|
+
Layout/LineLength:
|
|
30
|
+
Max: 120
|
|
31
|
+
Exclude:
|
|
32
|
+
- support_desk.gemspec # the long-form rubygems description is one line by design
|
data/.simplecov
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SimpleCov configuration (auto-loaded by `require "simplecov"`); the suite
|
|
4
|
+
# calls SimpleCov.start from test_helper.rb, so this file stays
|
|
5
|
+
# configuration-only. Coherent with the rest of the gem ecosystem (chats,
|
|
6
|
+
# wallets, moderate, usage_credits, …).
|
|
7
|
+
SimpleCov.configure do
|
|
8
|
+
# SimpleFormatter for terminal-only output (no HTML generation)
|
|
9
|
+
formatter SimpleCov::Formatter::SimpleFormatter
|
|
10
|
+
|
|
11
|
+
# Don't count code that ISN'T unit-testable by this suite and would only
|
|
12
|
+
# distort the numbers:
|
|
13
|
+
# - the test suite itself;
|
|
14
|
+
# - generators + their templates: they run via `rails generate
|
|
15
|
+
# support_desk:install` in a real host. The generator classes ARE
|
|
16
|
+
# exercised by Rails::Generators::TestCase, but the migration template
|
|
17
|
+
# itself is never loaded as Ruby here (the dummy migrates a copy of it);
|
|
18
|
+
# - version.rb: a single constant, nothing to cover.
|
|
19
|
+
if respond_to?(:skip)
|
|
20
|
+
# SimpleCov 1.x vocabulary.
|
|
21
|
+
skip "/test/"
|
|
22
|
+
skip "/lib/generators/"
|
|
23
|
+
skip "/lib/support_desk/version.rb"
|
|
24
|
+
cover "lib/**/*.rb"
|
|
25
|
+
else
|
|
26
|
+
# Fallback vocabulary for SimpleCov 0.22.
|
|
27
|
+
add_filter "/test/"
|
|
28
|
+
add_filter "/lib/generators/"
|
|
29
|
+
add_filter "/lib/support_desk/version.rb"
|
|
30
|
+
track_files "lib/**/*.rb"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
enable_coverage :branch
|
|
34
|
+
|
|
35
|
+
# Thresholds sit just under the current floor so the gate catches a real
|
|
36
|
+
# regression without failing on the existing baseline; raise as it grows.
|
|
37
|
+
minimum_coverage line: 94, branch: 74
|
|
38
|
+
|
|
39
|
+
# Disambiguate parallel test runs
|
|
40
|
+
command_name "Job #{ENV["TEST_ENV_NUMBER"]}" if ENV["TEST_ENV_NUMBER"]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Print coverage summary to terminal after tests complete
|
|
44
|
+
SimpleCov.at_exit do
|
|
45
|
+
SimpleCov.result.format!
|
|
46
|
+
puts "\n#{"=" * 60}"
|
|
47
|
+
puts "COVERAGE SUMMARY"
|
|
48
|
+
puts "=" * 60
|
|
49
|
+
puts "Line Coverage: #{SimpleCov.result.covered_percent.round(2)}%"
|
|
50
|
+
branch_coverage = SimpleCov.result.coverage_statistics[:branch]&.percent&.round(2) || "N/A"
|
|
51
|
+
puts "Branch Coverage: #{branch_coverage}%"
|
|
52
|
+
puts "=" * 60
|
|
53
|
+
end
|
data/Appraisals
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Test the minimum supported Rails version (matches the gemspec floor).
|
|
4
|
+
# `ActiveRecord.after_all_transactions_commit` — how every event this gem
|
|
5
|
+
# emits gets out only after the transition it describes is durable — is a
|
|
6
|
+
# Rails 7.2 API, which is what sets the floor.
|
|
7
|
+
appraise "rails-7.2" do
|
|
8
|
+
gem "rails", "~> 7.2.0"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
appraise "rails-8.0" do
|
|
12
|
+
gem "rails", "~> 8.0.0"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Test the latest Rails version — this is the default/main Gemfile anyway.
|
|
16
|
+
appraise "rails-8.1" do
|
|
17
|
+
gem "rails", "~> 8.1.0"
|
|
18
|
+
end
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-09-16
|
|
8
|
+
|
|
9
|
+
First release: the whole core of a support desk, on top of `chats` 0.2.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Tickets that are conversations.** `SupportDesk::Ticket` is a case with
|
|
14
|
+
exactly one `Chats::Conversation` behind it. Transitions (`reply!`,
|
|
15
|
+
`note!`, `assign!`, `hand_off!`, `release!`, `close!`, `reopen!`,
|
|
16
|
+
`change_topic!`, `attach_subject!`) each take `by:`, run under the
|
|
17
|
+
ticket's row lock, write one append-only event row, and emit after the
|
|
18
|
+
transaction commits. Repeating one that already happened returns `self`
|
|
19
|
+
and writes nothing; one that can't happen from here raises
|
|
20
|
+
`SupportDesk::InvalidTransition`.
|
|
21
|
+
- **Three macros.** `has_support_tickets` adds exactly four methods to
|
|
22
|
+
whoever asks for help; `supportable topic: :order` makes a domain record
|
|
23
|
+
something to ask about, with a working default for every method;
|
|
24
|
+
`acts_as_support_agent if: :admin?` makes someone able to answer, and
|
|
25
|
+
adds no verbs — the ticket is the subject of every sentence.
|
|
26
|
+
- **Topics as a tree of value objects.** Defined in code, frozen at boot,
|
|
27
|
+
stored on the ticket as a stable path (`"billing/invoice"`).
|
|
28
|
+
`ticket.topic` is a `SupportDesk::Topic`, not a string, and a path no tree
|
|
29
|
+
knows reads as a null object that still renders.
|
|
30
|
+
- **Assignment as a history.** Who held the case, when, why they stopped,
|
|
31
|
+
with `ticket.assignee` as the denormalised pointer to the one open row.
|
|
32
|
+
Reopening gives the case back to whoever handled it.
|
|
33
|
+
- **`awaiting` and the SLA clocks**, maintained by a single subscriber to
|
|
34
|
+
chats' `:message_created` and idempotent on the message id — so a message
|
|
35
|
+
typed in the app, mirrored in by email or posted by a bot all move the
|
|
36
|
+
same clock, and a redelivered event never double-counts.
|
|
37
|
+
- **Reply policies**: `:anyone` (the first agent to answer an unheld ticket
|
|
38
|
+
takes it; a drop-in on someone else's is recorded), `:take_over`,
|
|
39
|
+
`:assignee_only`.
|
|
40
|
+
- **`SupportDesk::Wizard`** — the three-step "what do you need help with?"
|
|
41
|
+
machine as a PORO, so the views are replaceable and a native app or an API
|
|
42
|
+
can drive the same steps. Subjects travel as signed GlobalIDs and are
|
|
43
|
+
re-checked against `supportable_by?` anyway.
|
|
44
|
+
- **A requester-facing engine you just mount.** `mount SupportDesk::Engine
|
|
45
|
+
=> "/support"` and the user side is done: their list of cases (open ones
|
|
46
|
+
as chats rows, closed ones folded away), the three wizard frames at one
|
|
47
|
+
URL, and a stable `/support/tickets/:id` that redirects into the
|
|
48
|
+
conversation. Every step is a real URL, so the back gesture, a bookmark
|
|
49
|
+
and a cold-boot deep link all work; `data-turbo-action="advance"` is what
|
|
50
|
+
lets them also re-render in place. Limits (`max_open_tickets`,
|
|
51
|
+
`open_rate_limit`) render a wall with the cases they already have listed
|
|
52
|
+
on it, never a 500.
|
|
53
|
+
- **`link_to_support(about:, text:, **html)` and `support_unread_badge`.**
|
|
54
|
+
The door renders nothing for a record that is not supportable or not
|
|
55
|
+
theirs, so it is safe in shared partials, and leads to the conversation
|
|
56
|
+
they already have rather than opening a second one.
|
|
57
|
+
- **Two rows on chats’ own screens**, through its view slots: the
|
|
58
|
+
"¿Necesitas ayuda? Escríbenos" door above the inbox for somebody who has
|
|
59
|
+
never written (`config.inbox_entry`), and a way out of a case closed on a
|
|
60
|
+
desk configured `closed_tickets: :locked`.
|
|
61
|
+
- **`SupportDesk.native_path_rules(mount:, title:)`** — the Hotwire Native
|
|
62
|
+
path-configuration rules for both surfaces, as pushed screens.
|
|
63
|
+
- **`config.authenticate_method`** so the engine runs the host’s own
|
|
64
|
+
authentication filter, and `rails g support_desk:views` to eject every
|
|
65
|
+
requester-facing template.
|
|
66
|
+
- **Queues and presenters**
|
|
67
|
+
— `Queue#counts` in one grouped query, a badge
|
|
68
|
+
cached 30s per agent, `ContextCard`, `Summary`, `Timeline` and
|
|
69
|
+
`actions_for(agent)`, all with no view dependency.
|
|
70
|
+
- **The agent console, in three layers** — the query objects above, then a
|
|
71
|
+
routing concern and a controller concern, then a generator. The generated
|
|
72
|
+
madmin console and the turnkey `SupportDesk::ConsoleEngine` use nothing
|
|
73
|
+
the concerns don't expose, which is what makes "bring your own UI" a
|
|
74
|
+
promise rather than a hope.
|
|
75
|
+
- `concerns: :support_console` in any route set draws member `reply`,
|
|
76
|
+
`take`, `assign`, `hand_off`, `release`, `close`, `reopen`, `note` and
|
|
77
|
+
`change_topic`, plus collection `next`.
|
|
78
|
+
- `SupportDesk::Console` scopes everything it reaches through
|
|
79
|
+
`config.visible_desks_for` — the ticket, the queue, the tab counts, the
|
|
80
|
+
badge and `next`, with `?desk=` able to name only a desk already on that
|
|
81
|
+
list. A case on a desk you may not work is a **404**; no desks at all is
|
|
82
|
+
a **403**. It asks `config.authorize_console` before every action (a
|
|
83
|
+
hook that raises denies, and is reported through `Rails.error`), checks
|
|
84
|
+
`actions_for` so it never accepts a verb it wouldn't have offered, sets
|
|
85
|
+
`SupportDesk::Current.actor`, and turns every domain refusal into a
|
|
86
|
+
translated flash — a policy never 500s.
|
|
87
|
+
`SupportDesk::Console::Index` is the optional `@queue` / `@scope` /
|
|
88
|
+
`@tickets`, preloading everything a row renders including the subject.
|
|
89
|
+
- `rails g support_desk:console madmin` writes a host-owned controller, a
|
|
90
|
+
madmin resource, and the view set: queue tabs, waiting chips coloured by
|
|
91
|
+
`at_risk_after` / `reply_within`, context card, transcript, timeline,
|
|
92
|
+
a "Reply"/"Internal note" composer, the hand-off picker, and a nav badge.
|
|
93
|
+
Idempotent, `--force` to take new defaults.
|
|
94
|
+
- `mount SupportDesk::ConsoleEngine => "/admin/support"` for hosts with no
|
|
95
|
+
admin framework, rendering those same views through
|
|
96
|
+
`config.console_parent_controller`.
|
|
97
|
+
- **Events out, policy in.** `SupportDesk.on(:ticket_opened) { … }`,
|
|
98
|
+
multi-subscriber, isolated with `Rails.error.report`, mirrored on
|
|
99
|
+
`ActiveSupport::Notifications`.
|
|
100
|
+
- **Multi-desk configuration** where every desk inherits what it doesn't
|
|
101
|
+
state, with setters that validate on assignment and boot-time
|
|
102
|
+
`ConfigurationError`s that carry the fix. `config.visible_desks_for` and
|
|
103
|
+
`config.authorize_console` are the console's two hooks into it.
|
|
104
|
+
- **`SupportDesk.doctor`** — configuration, the chats seams, and the data
|
|
105
|
+
invariants, with an `ok?` for CI.
|
|
106
|
+
- **`SupportDesk::TestHelpers`** for host suites.
|
|
107
|
+
- Install generator (migration + annotated initializer), Spanish and
|
|
108
|
+
English locales, and a mountable engine.
|
|
109
|
+
|
|
110
|
+
### Notes
|
|
111
|
+
|
|
112
|
+
- **The desk registers itself with chats at boot.** `acts_as_messager`
|
|
113
|
+
registers a class when it loads, and `Chats::Inbox` folds support threads
|
|
114
|
+
into one row only for registered grouped messagers, so under lazy
|
|
115
|
+
autoloading the inbox showed every support conversation as its own row
|
|
116
|
+
until something happened to reference `SupportDesk::Desk`. The engine's
|
|
117
|
+
`to_prepare` now touches the desk the way it already touches the helper
|
|
118
|
+
(#2). Eager-loading production hosts never saw it; development did.
|
|
119
|
+
- **`jsonb` on PostGIS.** The install migration decides jsonb-or-json by
|
|
120
|
+
adapter name, and activerecord-postgis-adapter answers `"PostGIS"`, not
|
|
121
|
+
`"PostgreSQL"` — the first cut matched the full word and silently gave
|
|
122
|
+
PostGIS hosts plain `json` columns. Caught while integrating with a PostGIS host, before
|
|
123
|
+
the gem shipped; the template now matches the prefix (`/\Apostg/i`).
|
|
124
|
+
- `SupportDesk::OffDuty` is NOT part of 0.1.0. Duty is a seam this release
|
|
125
|
+
only asks about (`agent.on_duty?` decides who is notified and what
|
|
126
|
+
`actions_for` offers); nothing in it assigns work, so nothing can
|
|
127
|
+
honestly refuse on those grounds yet. It arrives with the duty table in
|
|
128
|
+
0.3. `SupportDesk::Locked` IS raised — writing into a closed ticket on a
|
|
129
|
+
desk configured `closed_tickets: :locked` — and subclasses
|
|
130
|
+
`InvalidTransition`, so either name catches it.
|
|
131
|
+
|
|
132
|
+
### Credits
|
|
133
|
+
|
|
134
|
+
The wizard copy, the console screens and the Spanish strings began life in
|
|
135
|
+
carhey#309, and were lifted into the gem with the author's blessing.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Javi R
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|