Squashed commit of the following:

commit 7db48f592f
Author: Alex Lion <dev@alexandrelion.com>
Date:   Fri Jun 5 10:16:10 2026 +0200

    Update changelog

commit 3039b70edc
Author: Peter Rundqvist <47314226+pete1854@users.noreply.github.com>
Date:   Fri Jun 5 10:11:07 2026 +0200

    Add swedish translation (#228)

    * Initial translation

    * Initial error msg translation

    * Fixes suggested by Copilot.

    * Fixed spacing before ! and ?

    * Added missing provider translation.

    * Upddated translation for "Unpin"

    * Added missing Content-Type header.

commit d18e5d7c32
Author: fgallarday <fgallarday@users.noreply.github.com>
Date:   Fri Jun 5 03:07:25 2026 -0500

    Add PageUp and PageDown slide navigation support (#227)

commit 24e18c2764
Author: Alex Lion <dev@alexandrelion.com>
Date:   Fri Jun 5 10:06:54 2026 +0200

    Bump version to 2.5.2

commit d5df9a73f8
Author: Alex Lion <dev@alexandrelion.com>
Date:   Fri May 8 22:42:04 2026 +0200

    Fix form submissions losing values when field names contain spaces or non-word characters

commit 57596952d4
Author: Alex Lion <dev@alexandrelion.com>
Date:   Fri May 8 14:50:33 2026 +0200

    Update package-lock.json: add asset name, upgrade dependencies, and remove unused packages

commit e246530c23
Author: Alex Lion <dev@alexandrelion.com>
Date:   Fri May 8 14:34:53 2026 +0200

    Bump version to 2.5.1

commit 534d915782
Author: Alex Lion <dev@alexandrelion.com>
Date:   Fri May 8 10:55:38 2026 +0200

    Handle Ecto.NoResultsError in event retrieval and redirect with error message

commit b762e21325
Author: Alex Lion <dev@alexandrelion.com>
Date:   Fri May 8 10:47:01 2026 +0200

    Fix crash on event manager pages when an event has multiple activity leaders

commit 153cadb42b
Author: alxlion <dev@alexandrelion.com>
Date:   Wed Apr 29 13:42:05 2026 +0000

    Fix presentation views without slide count

commit 3cfb3cab7f
Author: alxlion <dev@alexandrelion.com>
Date:   Wed Apr 29 13:16:48 2026 +0000

    Fix event code normalization validation
This commit is contained in:
Alex Lion
2026-06-05 10:24:46 +02:00
parent d1590720dd
commit dd5dd82daa
16 changed files with 3369 additions and 74 deletions

View File

@@ -1,4 +1,21 @@
### v.2.5.0
## v.2.5.2
### Fixes and improvements
- Add Swedish translation (#228)
- Add PageUp and PageDown support for slide navigation (#227)
## v.2.5.1
### Security
- Update JS dependencies with high CVE
### Fixes and improvements
- Fix form submissions losing values when field names contain spaces or non-word characters
## v.2.5.0
### Features
@@ -24,8 +41,10 @@
- Fix quizz real time average score update and id duplication
- Fix crash when broadcasting events to leaders with unregistered emails
- Fix OIDC compatibility with providers like Authelia and Microsoft Entra ID (#216) (#143) (#195)
- Fix manager and presenter views while presentation conversion has no slide count yet
- Fix crash on event manager pages when an event has multiple activity leaders
### v.2.4.0
## v.2.4.0
### ⚠️ Breaking changes
@@ -57,7 +76,7 @@
- Fix italian translation (#179)
- Fix random poll choices (#184)
### v.2.3.2
## v.2.3.2
### Fixes and improvements
@@ -69,7 +88,7 @@
- Fix event code length validation (min: 5, max: 10)
- Fix presentation upload progress when editing an event
### v.2.3.1
## v.2.3.1
### Fixes and improvements
@@ -80,7 +99,7 @@
- Add option to force login to submit quizzes
- Fix url with question mark being flagged as a question
### v.2.3.0
## v.2.3.0
### Features

View File

@@ -36,10 +36,12 @@ export class Manager {
switch (e.key) {
case "ArrowLeft":
case "PageUp":
e.preventDefault();
this.prevPage();
break;
case "ArrowRight":
case "PageDown":
e.preventDefault();
this.nextPage();
break;

View File

@@ -116,12 +116,14 @@ export class Presenter {
this.fullscreen();
break;
case "ArrowLeft":
case "PageUp":
e.preventDefault();
window.opener.dispatchEvent(
new KeyboardEvent("keydown", { key: "ArrowLeft" })
);
break;
case "ArrowRight":
case "PageDown":
e.preventDefault();
window.opener.dispatchEvent(
new KeyboardEvent("keydown", { key: "ArrowRight" })

182
assets/package-lock.json generated
View File

@@ -773,6 +773,66 @@
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.4.3",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.0.2",
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.4.3",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.0.2",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.11",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@tybys/wasm-util": "^0.9.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.9.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
"version": "2.8.0",
"dev": true,
"inBundle": true,
"license": "0BSD",
"optional": true
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz",
@@ -1238,9 +1298,9 @@
}
},
"node_modules/minizlib": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1250,22 +1310,6 @@
"node": ">= 18"
}
},
"node_modules/mkdirp": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
"dev": true,
"license": "MIT",
"bin": {
"mkdirp": "dist/cjs/src/bin.js"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
@@ -1324,9 +1368,9 @@
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"version": "8.5.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
"dev": true,
"funding": [
{
@@ -1403,17 +1447,16 @@
}
},
"node_modules/tar": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
"version": "7.5.15",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz",
"integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==",
"dev": true,
"license": "ISC",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
"minipass": "^7.1.2",
"minizlib": "^3.0.1",
"mkdirp": "^3.0.1",
"minizlib": "^3.1.0",
"yallist": "^5.0.0"
},
"engines": {
@@ -1787,6 +1830,62 @@
"@napi-rs/wasm-runtime": "^0.2.11",
"@tybys/wasm-util": "^0.9.0",
"tslib": "^2.8.0"
},
"dependencies": {
"@emnapi/core": {
"version": "1.4.3",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"@emnapi/wasi-threads": "1.0.2",
"tslib": "^2.4.0"
}
},
"@emnapi/runtime": {
"version": "1.4.3",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"tslib": "^2.4.0"
}
},
"@emnapi/wasi-threads": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"tslib": "^2.4.0"
}
},
"@napi-rs/wasm-runtime": {
"version": "0.2.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@tybys/wasm-util": "^0.9.0"
}
},
"@tybys/wasm-util": {
"version": "0.9.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"tslib": "^2.4.0"
}
},
"tslib": {
"version": "2.8.0",
"bundled": true,
"dev": true,
"optional": true
}
}
},
"@tailwindcss/oxide-win32-arm64-msvc": {
@@ -2042,20 +2141,14 @@
"dev": true
},
"minizlib": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
"dev": true,
"requires": {
"minipass": "^7.1.2"
}
},
"mkdirp": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
"dev": true
},
"moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
@@ -2110,9 +2203,9 @@
"dev": true
},
"postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"version": "8.5.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
"dev": true,
"requires": {
"nanoid": "^3.3.11",
@@ -2162,16 +2255,15 @@
"dev": true
},
"tar": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
"version": "7.5.15",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz",
"integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==",
"dev": true,
"requires": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
"minipass": "^7.1.2",
"minizlib": "^3.0.1",
"mkdirp": "^3.0.1",
"minizlib": "^3.1.0",
"yallist": "^5.0.0"
}
},

View File

@@ -293,6 +293,7 @@ defmodule Claper.Events do
left_join: a in ActivityLeader,
on: e.id == a.event_id,
where: e.uuid == ^uuid and (u.id == ^user.id or a.email == ^user.email),
distinct: true,
select: e
)
|> Repo.one!()
@@ -394,6 +395,9 @@ defmodule Claper.Events do
end
end
defp validate_unique_event(%Ecto.Changeset{changes: %{code: nil}} = changeset),
do: {:ok, changeset}
defp validate_unique_event(%Ecto.Changeset{changes: %{code: code} = _changes} = event) do
case get_event_with_code(code) do
%Event{} -> {:error, Ecto.Changeset.add_error(event, :code, "Already exists")}

View File

@@ -80,19 +80,28 @@ defmodule Claper.Events.Event do
|> cast(attrs, [:name, :code, :user_id, :started_at, :expired_at])
|> cast_assoc(:presentation_file)
|> cast_assoc(:leaders)
|> downcase_code
|> validate_required([:name, :code, :started_at, :user_id])
|> validate_length(:code, min: 5, max: 10)
|> validate_length(:name, min: 5, max: 50)
|> downcase_code
|> put_change(:uuid, Ecto.UUID.generate())
end
def downcase_code(changeset) do
update_change(
changeset,
:code,
&(&1 |> String.downcase() |> String.split(~r"[^\w\d]", trim: true) |> List.first())
)
case fetch_change(changeset, :code) do
{:ok, nil} ->
changeset
{:ok, code} ->
put_change(
changeset,
:code,
code |> String.downcase() |> String.split(~r"[^\w\d]", trim: true) |> List.first()
)
:error ->
changeset
end
end
def update_changeset(event, attrs) do
@@ -100,10 +109,10 @@ defmodule Claper.Events.Event do
|> cast(attrs, [:name, :code, :started_at, :expired_at, :audience_peak, :user_id])
|> cast_assoc(:presentation_file)
|> cast_assoc(:leaders)
|> downcase_code
|> validate_required([:name, :code, :started_at, :user_id])
|> validate_length(:code, min: 5, max: 10)
|> validate_length(:name, min: 5, max: 50)
|> downcase_code
end
def restart_changeset(event) do

View File

@@ -61,7 +61,7 @@ defmodule ClaperWeb.EventLive.FormComponent do
form={f}
labelClass="text-white"
fieldClass="bg-gray-700 text-white"
key={safe_field_atom(field.name)}
key={field_key(field.name)}
name={field.name}
required={field.required}
value={
@@ -75,7 +75,7 @@ defmodule ClaperWeb.EventLive.FormComponent do
form={f}
labelClass="text-white"
fieldClass="bg-gray-700 text-white"
key={safe_field_atom(field.name)}
key={field_key(field.name)}
name={field.name}
required={field.required}
value={
@@ -176,13 +176,12 @@ defmodule ClaperWeb.EventLive.FormComponent do
end
end
defp safe_field_atom(name) when is_binary(name) do
String.to_existing_atom(name)
rescue
ArgumentError ->
if Regex.match?(~r/^[\w]{1,100}$/, name),
do: String.to_atom(name),
else: :invalid_field
defp field_key(name) when is_binary(name) do
try do
String.to_existing_atom(name)
rescue
ArgumentError -> String.to_atom(name)
end
end
def toggle_form(js \\ %JS{}) do

View File

@@ -168,6 +168,11 @@ defmodule ClaperWeb.EventLive.Index do
|> assign(:page_title, gettext("Edit"))
|> assign(:event, event)
end
rescue
Ecto.NoResultsError ->
socket
|> put_flash(:error, gettext("Event doesn't exist"))
|> redirect(to: ~p"/events")
end
defp apply_action(socket, :new, _params) do

View File

@@ -12,7 +12,7 @@
<div
id="presenter"
phx-hook="Presenter"
data-max-page={@event.presentation_file.length}
data-max-page={@event.presentation_file.length || 0}
data-hash={@event.presentation_file.hash}
data-current-page={@state.position}
data-embed-active={if @current_embed, do: "true", else: "false"}
@@ -90,7 +90,7 @@
<!-- MESSAGES -->
<div
id="slider-wrapper"
class={"w-full min-h-screen grid #{if (@state.chat_visible && @event.presentation_file.length > 0) || (@current_embed && @event.presentation_file.length == 0), do: "grid-cols-[1fr_10px_1fr]", else: "grid-cols-[1fr]"} items-center justify-center relative bg-black"}
class={"w-full min-h-screen grid #{if (@state.chat_visible && (@event.presentation_file.length || 0) > 0) || (@current_embed && (@event.presentation_file.length || 0) == 0), do: "grid-cols-[1fr_10px_1fr]", else: "grid-cols-[1fr]"} items-center justify-center relative bg-black"}
phx-hook="Split"
data-type="column"
data-gutter=".gutter-1"
@@ -198,7 +198,7 @@
<% end %>
</div>
<div
class={"gutter-1 row-span-full cursor-col-resize col-2 text-center text-sm leading-3 text-white #{if (!@state.chat_visible && @event.presentation_file.length > 0) || (!@current_embed && @event.presentation_file.length == 0), do: "hidden"}"}
class={"gutter-1 row-span-full cursor-col-resize col-2 text-center text-sm leading-3 text-white #{if (!@state.chat_visible && (@event.presentation_file.length || 0) > 0) || (!@current_embed && (@event.presentation_file.length || 0) == 0), do: "hidden"}"}
style="writing-mode: vertical-rl"
>
•••

View File

@@ -1,7 +1,7 @@
defmodule Claper.MixProject do
use Mix.Project
@version "2.5.0"
@version "2.5.2"
def project do
[

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,99 @@
## `msgid`s in this file come from POT (.pot) files.
##
## Do not add, change, or remove `msgid`s manually here as
## they're tied to the ones in the corresponding POT file
## (with the same domain).
##
## Use `mix gettext.extract --merge` or `mix gettext.merge`
## to merge POT files into PO files.
msgid ""
msgstr ""
"Language: sv\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
## From Ecto.Changeset.cast/4
msgid "can't be blank"
msgstr "får inte vara tom"
## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken"
msgstr "har redan tagits"
## From Ecto.Changeset.put_change/3
msgid "is invalid"
msgstr "är ogiltig"
## From Ecto.Changeset.validate_acceptance/3
msgid "must be accepted"
msgstr "måste accepteras"
## From Ecto.Changeset.validate_format/3
msgid "has invalid format"
msgstr "har ogiltigt format"
## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry"
msgstr "har en ogiltig post"
## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved"
msgstr "är reserverad"
## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
msgstr "matchar inte bekräftelsen"
## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry"
msgstr "associeras fortfarande med den här posten"
msgid "are still associated with this entry"
msgstr "associeras fortfarande med denna post"
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
msgstr[0] "måste innehålla %{count} tecken"
msgstr[1] "måste innehålla %{count} tecken"
## From Ecto.Changeset.validate_length/3
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
msgstr[0] "måste innehålla %{count} objekt"
msgstr[1] "måste innehålla %{count} objekt"
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
msgstr[0] "måste innehålla minst %{count} tecken"
msgstr[1] "måste innehålla minst %{count} tecken"
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
msgstr[0] "måste innehålla minst %{count} objekt"
msgstr[1] "måste innehålla minst %{count} objekt"
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] "får innehålla högst %{count} tecken"
msgstr[1] "får innehålla högst %{count} tecken"
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
msgstr[0] "får innehålla högst %{count} objekt"
msgstr[1] "får innehålla högst %{count} objekt"
## From Ecto.Changeset.validate_number/3
msgid "must be less than %{number}"
msgstr "måste vara mindre än %{number}"
msgid "must be greater than %{number}"
msgstr "måste vara större än %{number}"
msgid "must be less than or equal to %{number}"
msgstr "måste vara mindre än eller lika med %{number}"
msgid "must be greater than or equal to %{number}"
msgstr "måste vara större än eller lika med %{number}"
msgid "must be equal to %{number}"
msgstr "måste vara lika med %{number}"

View File

@@ -232,6 +232,16 @@ defmodule Claper.EventsTest do
assert Events.get_managed_event!(context.alice, event.uuid) == event
end
test "get_managed_event!/3 does not duplicate when an event has multiple leaders",
context do
event = Enum.at(context.carol_active_events, 1)
activity_leader_fixture(%{event: event, email: "extra-leader-1@example.com"})
activity_leader_fixture(%{event: event, email: "extra-leader-2@example.com"})
assert Events.get_managed_event!(context.carol, event.uuid) == event
assert Events.get_managed_event!(context.alice, event.uuid) == event
end
test "get_user_event!/3 gets event by owner, raises if not", context do
event = Enum.at(context.alice_active_events, 0)
assert Events.get_user_event!(context.alice.id, event.uuid) == event
@@ -452,6 +462,16 @@ defmodule Claper.EventsTest do
assert event == Events.get_event!(event.uuid)
end
test "update_event/2 with code that normalizes to nil returns error changeset" do
event = event_fixture()
assert {:error, %Ecto.Changeset{} = changeset} =
Events.update_event(event, %{code: "------"})
assert %{code: ["can't be blank"]} = errors_on(changeset)
assert event == Events.get_event!(event.uuid)
end
test "change_event/1 returns a event changeset" do
event = event_fixture()
assert %Ecto.Changeset{} = Events.change_event(event)

View File

@@ -1,8 +1,7 @@
defmodule ClaperWeb.StatControllerTest do
use ClaperWeb.ConnCase
use ClaperWeb.ConnCase, async: true
import Claper.AccountsFixtures
import Claper.PresentationsFixtures
import Claper.{AccountsFixtures, FormsFixtures, PresentationsFixtures}
describe "export_transcriptions/2" do
test "exports timestamp, language, and text as CSV", %{conn: conn} do
@@ -38,4 +37,58 @@ defmodule ClaperWeb.StatControllerTest do
assert response(conn, 403) == "Forbidden"
end
end
describe "POST /export/forms/:form_id" do
setup %{conn: conn} do
owner = confirmed_user_fixture()
presentation_file = presentation_file_fixture(%{user: owner}, [:event])
form =
form_fixture(%{
presentation_file_id: presentation_file.id,
title: "My Form",
fields: [
%{name: "First Name", type: "text", required: false},
%{name: "Email Address", type: "email", required: false}
]
})
%{conn: log_in_user(conn, owner), form: form, event: presentation_file.event}
end
test "exports CSV with field names containing spaces as headers and values", %{
conn: conn,
form: form,
event: event
} do
{:ok, _submit} =
Claper.Forms.create_or_update_form_submit(event.uuid, %{
"attendee_identifier" => "attendee-1",
"form_id" => form.id,
"response" => %{"First Name" => "Ada", "Email Address" => "ada@example.com"}
})
conn = post(conn, ~p"/export/forms/#{form.id}")
assert response_content_type(conn, :csv)
body = response(conn, 200)
[header_line, data_line | _] = String.split(body, "\r\n", trim: true)
assert header_line =~ "First Name"
assert header_line =~ "Email Address"
assert data_line =~ "Ada"
assert data_line =~ "ada@example.com"
end
test "returns 403 for users who don't own the event", %{form: form} do
stranger = confirmed_user_fixture()
conn = build_conn() |> log_in_user(stranger)
conn = post(conn, ~p"/export/forms/#{form.id}")
assert response(conn, 403) == "Forbidden"
end
end
end

View File

@@ -0,0 +1,72 @@
defmodule ClaperWeb.EventLive.FormComponentTest do
use ClaperWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import Claper.{AccountsFixtures, FormsFixtures, PresentationsFixtures}
alias ClaperWeb.EventLive.FormComponent
defp setup_form(field_attrs) do
presentation_file = presentation_file_fixture(%{}, [:event])
form =
form_fixture(%{
presentation_file_id: presentation_file.id,
fields: field_attrs
})
%{event: presentation_file.event, form: form}
end
describe "rendering fields with non-alphanumeric names" do
test "renders an input whose name uses the original field name (with spaces)" do
%{event: event, form: form} =
setup_form([
%{name: "First Name", type: "text", required: false},
%{name: "Email Address", type: "email", required: false}
])
html =
render_component(FormComponent,
id: "form-component",
form: form,
current_user: user_fixture(),
attendee_identifier: nil,
event: event,
current_form_submit: nil
)
# The HTML name attribute must contain the actual field name —
# regression: a previous version mapped any non-\w name to :invalid_field,
# so all space-containing fields collided on `form_submit[invalid_field]`
# and submissions were saved under the wrong key.
assert html =~ ~s(name="form_submit[First Name]")
assert html =~ ~s(name="form_submit[Email Address]")
refute html =~ "invalid_field"
end
test "preserves submitted values when re-rendering for fields with spaces" do
%{event: event, form: form} =
setup_form([
%{name: "First Name", type: "text", required: false}
])
form_submit = %Claper.Forms.FormSubmit{
response: %{"First Name" => "Ada"}
}
html =
render_component(FormComponent,
id: "form-component",
form: form,
current_user: user_fixture(),
attendee_identifier: nil,
event: event,
current_form_submit: form_submit
)
assert html =~ ~s(value="Ada")
end
end
end

View File

@@ -38,7 +38,7 @@ defmodule Claper.EventsFixtures do
attrs
|> Enum.into(%{
name: "some name",
code: :crypto.strong_rand_bytes(4) |> Base.url_encode64() |> binary_part(0, 8),
code: Base.encode16(:crypto.strong_rand_bytes(4), case: :lower),
uuid: Ecto.UUID.generate(),
user_id: assoc.user.id,
started_at: NaiveDateTime.utc_now(),