Commit Graph

20481 Commits

Author SHA1 Message Date
Javi Martín
6da53b5716 Add unique index to poll voters table
Note that Rails 7.1 changes `find_or_create_by!` so it calls
`create_or_find_by!` when no record is found, meaning we'll rarely get
`RecordNotUnique` exceptions when using this method during a race
condition.

Adding this index means we need to remove the uniqueness validation.
According to the `create_or_find_by` documentation [1]:

> Columns with unique database constraints should not have uniqueness
> validations defined, otherwise create will fail due to validation
> errors and find_by will never be called.

We're adding a test that checks what happens when using
`create_or_find_by!`.

Note that we're creating voters combining `create_with` with
`find_or_create_by!`. Using `find_or_create_by!(...)` with all
attributes (including non-key ones like `origin`) fails when a voter
already exists with different values, e.g. an existing `origin: "web"`
and an incoming `"booth"`. In this situation the existing record is not
matched and the unique index raises an exception.

`create_with(...).find_or_create_by!(user: ..., poll: ...)` searches by
the unique key only and applies the extra attributes only on creation.
Existing voters are returned unchanged, which is the intended behavior.

For the `take_votes_from` method, we're handling a (highly unlikely, but
theoretically possible) scenario where a user votes at the same time as
taking voters from another user. For that, we're doing something similar
to what `create_or_find_by!` does: we're updating the `user_id` column
inside a new transaction (using a new transactions avoids a
`PG::InFailedSqlTransaction` exception when there are duplicate
records), and deleting the existing voter when we get a
`RecordNotUnique` exception.

On `Poll::WebVote` we're simply raising an exception when there's
already a user who's voted via booth, because the `Poll::WebVote#update`
method should never be called in this case.

We still need to use `with_lock` in `Poll::WebVote`, but not due to
duplicate voters (`find_or_create_by!` method will now handle the unique
record scenario, even in the case of simultaneous transactions), but
because we use a uniqueness validation in `Poll::Answer`; this
validation would cause an error in simultaneous transactions.

[1] https://api.rubyonrails.org/v7.1/classes/ActiveRecord/Relation.html#method-i-create_or_find_by
2025-08-28 14:42:30 +02:00
Javi Martín
03c5533cf0 Don't allow users who voted in a booth to vote via web
For the longest time, we've disabled the buttons to vote via web when
people had already voted in a booth. However, we were still allowing
HTTP requests to the actions to vote via web.

So we're adding a condition to prevent it.

The reason why we're changing the controller instead of the abilities
model (which is what we usually do) is that there might be side-effects
to the change. For instance, in the `Polls::PollComponent` class,
there's an `elsif cannot?(:answer, poll)` condition which would have a
different behavior if we changed the abilities model.
2025-08-28 14:42:30 +02:00
Javi Martín
b5d4a32e63 Merge pull request #5897 from consuldemocracy/replace_equalizer_with_flex
Use flex and grid layouts instead of data-equalizer
2025-08-28 14:40:53 +02:00
Javi Martín
29622dcb91 Extract mixin to create a grid layout
We were using similar code and three places. And all of them used
`15rem`, which looked a lot like a magic number. So we're making it the
default value of a mixin, which means replacing it with a less arbitrary
value will be easier.

Note that, for the column gap, we're now using the standard
grid-column-gutter we use in most places. We were using `$line-height`
in a couple of places only because writing it is less verbose.

Since we're now, for the first time, using a very long mixin definition
that we need to split in several lines, we're adding the `param`
exception to the indentation rule. As far as I know, there's no way to
define a rule in Stylelint that requires parameters in multiple lines to
be aligned with the first parameter, which is what we define in Rubocop.
2025-08-28 14:04:03 +02:00
taitus
3b08fa2e05 Use grid instead of Equalizer in dashboard polls 2025-08-27 17:40:45 +02:00
Javi Martín
de303aa1df Extract component to render dashboard polls
Just like we usually do when reorganizing code.
2025-08-27 17:40:45 +02:00
taitus
fc0d79b47b Move dashboard poll partial to component 2025-08-27 17:40:45 +02:00
Javi Martín
150af75e3e Make resources component tests more readable
We're testing things from the user's point of view by finding elements
given their texts, instead of checking for elements with a certain ID.
2025-08-27 17:40:45 +02:00
taitus
1e06e676a4 Move dashboard system tests to resources component specs
We're making the `new_actions_since_last_login` parameter optional in
order to simplify the tests.
2025-08-27 17:40:45 +02:00
taitus
1dc4f1c534 Use grid instead of equalizer in dashboard resources
Move resource cards layout inside #available-resources-section and switch
from equalizer alignment to a responsive grid layout.

Note that using `grid-auto-rows: 1fr` requires us to change the CSS of
the card itself so the "see resource" link remains at the bottom of the
card.
2025-08-27 17:40:45 +02:00
taitus
1f97a996f8 Move resource partial to a component
We're renaming it to ActiveResource in order to better differentiate it
from the DefaultResource component.
2025-08-27 17:40:45 +02:00
taitus
9b0675aa06 Unify dashboard default resources partials to a component 2025-08-27 17:40:45 +02:00
taitus
048c8ce917 Move dashboard resources partial to a component
We're renaming the HTML class (and removing the id attribute, which was
only used in tests) in order to follow our naming conventions.
2025-08-27 17:40:45 +02:00
Javi Martín
4e129e7d9c Use grid instead of equalizer in recommendations
It's August 2025 and support for grid layout has been available in about
99% of the browsers for some time now. All major browsers added support
for grid layouts in 2017, which means our rule to support browsers that
are 7 years old allows us to start using `display: grid`.

Using a grid layout allows displaying a dynamic number of rows while
keepin all of them the same height, the same foundation's equalizer
does, by setting `grid-auto-rows: 1fr`.

And the `grid-template-columns` property lets us use dynamic columns for
all screen sizes, always filling the available space. No need to use
breakpoints.
2025-08-27 17:38:37 +02:00
Javi Martín
34d87dff98 Use a list of links to display recommendations
We were using an <h3> tag with no content associated to it. This is like
having a chapter in a book with only the title.
2025-08-22 13:45:04 +02:00
Javi Martín
054fbf585e Simplify recommended index component parameters
We can get the path if we know the namespace, so we don't need to pass
both of them.
2025-08-22 13:42:41 +02:00
Javi Martín
3347e85017 Use a different parameter name in recommendex index
The problem with "recommended" is that it had identical singular and
plural, which means it wasn't clear whether we were using it to refer to
an item of a collection or to refer to the collection itself.
2025-08-22 13:40:45 +02:00
Javi Martín
34c8559065 Remove helper that returned a polymorphic path
This is similar to what we did in commits 1a902a967 and fa3781059.
2025-08-22 13:38:03 +02:00
Javi Martín
f5793013c9 Remove unused block parameter in recommended index 2025-08-22 13:37:52 +02:00
Javi Martín
a548c497fc Move recommended index partial to a component
Thanks to it, we can slightly simplify the debates view rendering it.
2025-08-22 13:32:04 +02:00
Javi Martín
d34c9905a4 Use flex instead of equalizer in following
We're also removing the padding in the (now called) followables element,
since it caused a gap on the right side of the border of the
`.menu.simple` element.
2025-08-22 13:31:55 +02:00
Javi Martín
909272d879 Move users following partial to a component 2025-08-22 13:25:50 +02:00
Javi Martín
c4d69416ca Use flex instead of equalizer in polls results
Note that the sidebar class isn't used since commit b91b766e9, so we're
removing it.
2025-08-22 13:25:30 +02:00
Javi Martín
22b0c04cf4 Use a component to render poll results
We're also renaming the `poll-results-stats` class to `poll-results`.
The former name was confusing because it had nothing to do with stats.
2025-08-22 13:19:04 +02:00
Javi Martín
8ca941ab02 Use flex in a poll in the polls index
Note we aren't using flex-with-gap because currently (until we decide to
use the `gap` property) this mixin sets a negative margin that would
move the border of this element to the left.
2025-08-22 13:12:10 +02:00
Javi Martín
b00ddfc931 Move helper method to the component 2025-08-22 13:12:10 +02:00
Javi Martín
edfea8f06b Move image-container CSS to the polls component
This is the only place where we use the `image-container` HTML class.
2025-08-22 13:12:10 +02:00
Javi Martín
1b9d321c4e Extract methods in poll status component
This way we remove duplication in the HTML.

We're also adding a test checking what happens when users can vote in
order to test the `render?` method we've added.
2025-08-22 13:09:49 +02:00
Javi Martín
b2a49cd291 Extract component to render the status of a poll
We're renaming the existing HTML class in order to be consistent with
the name of the component.
2025-08-22 12:13:41 +02:00
Javi Martín
6f87a0912c Merge pull request #6063 from consuldemocracy/dependabot/bundler/activestorage-7.1.5.2
Bump activestorage from 7.1.5.1 to 7.1.5.2
2025-08-15 12:51:48 +02:00
dependabot[bot]
4f4bd0f715 Bump activestorage from 7.1.5.1 to 7.1.5.2
Bumps [activestorage](https://github.com/rails/rails) from 7.1.5.1 to 7.1.5.2.
- [Release notes](https://github.com/rails/rails/releases)
- [Changelog](https://github.com/rails/rails/blob/v8.0.2.1/activestorage/CHANGELOG.md)
- [Commits](https://github.com/rails/rails/compare/v7.1.5.1...v7.1.5.2)

---
updated-dependencies:
- dependency-name: activestorage
  dependency-version: 7.1.5.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-15 10:21:10 +00:00
Javi Martín
7a76b30909 Merge pull request #6062 from consuldemocracy/allow_rails_security_updates
Relax Rails dependency to allow security updates
2025-08-15 12:19:06 +02:00
Javi Martín
24dcff3c1d Relax Rails dependency to allow security updates
Currently dependabot is failing to upgrade some gems that are part of
Rails. For example, when there's a security issue in ActiveRecord or
ActiveStorage, we get messages like:

```
Dependabot cannot update activestorage to a non-vulnerable version.

The latest possible version that can be installed is 7.1.5.1 because of
the following conflicting dependencies:

rails (7.1.5.1) requires activestorage (= 7.1.5.1) via actionmailbox (7.1.5.1)
rails (7.1.5.1) requires activestorage (= 7.1.5.1) via actiontext (7.1.5.1)
rails (7.1.5.1) requires activestorage (= 7.1.5.1)

The earliest fixed version is 7.1.5.2.
```

So we're relaxing the dependency in order to make it easier for
dependabot to upgrade gems that are part of Rails.

Note that, with this configuration, Dependabot wouldn't be able to
upgrade to Rails 7.1.6 if this releases fixed a security issues in a gem
that is part of Rails. We might still need to upgrade Rails manually in
this case.
2025-08-15 12:01:27 +02:00
Javi Martín
1108c61f01 Merge pull request #5540 from consuldemocracy/poll_form
Use checkboxes and radio buttons on poll forms
2025-08-14 14:13:26 +02:00
Javi Martín
4b7700b6f5 Add a border for each question in the poll form
This way polls look more similar to the way they did when the answers
were buttons instead of checkboxes or radio buttons.

Note the styling is tricky because we need to add a `float` property to
the legend so it's actually inside the fieldset. This forces us to add a
`::before` pseudo-element in order to add margin between the legend and
the first label. Another option would be:

```
legend {
  &:has(+ label) {
    margin-bottom: calc($line-height / 2);
  }

  + label {
    clear: $global-left;
  }
}
```

But the `:has` pseudo-class isn't universally supported yet, and we'd
still have to add `margin-top` to the first label when it comes after a
`.help-text` element.

Due to the presence of the border, we're increasing the margin between
elements a little bit.

Note that adding a pseudoelement to the label is a consequence of adding
the `float` property to the legend, so we're changing the order of the
code so the styles for `legend` appear before the styles in `label`.
2025-08-14 13:59:07 +02:00
Javi Martín
8deb1964bd Show errors when submitting too many answers
This could be the case when JavaScript is disabled.

Note that, in `Poll/WebVote` we're calling `given_answers` inside a
transaction. Putting this code before the transaction resulted in a test
failing sometimes, probably because of a bug that might be possible to
reproduce by doing simultaneous requests.
2025-08-14 13:06:43 +02:00
werdenktwas-gmbh
abf02808bf Disable other answers when reaching maximum votes
This is similar to the way we were disabling buttons in the old design.

Co-authored-by: Javi Martín <javim@elretirao.net>
2025-08-14 13:06:43 +02:00
Javi Martín
7ea4f63b07 Allow blank votes in polls via web
With the old interface, there wasn't a clear way to send a blank ballot.
But now that we've got a form, there's an easy way: clicking on "Vote"
while leaving the form blank.
2025-08-14 13:06:43 +02:00
Javi Martín
cd134ed44f Remove unused HTML class in polls callout
This class was added in commit 3d22c556e but was never used.
2025-08-14 13:06:43 +02:00
Javi Martín
34a1e65ca9 Extract methods in poll callout component 2025-08-14 13:06:43 +02:00
Javi Martín
b48682e3e4 Group code for the poll callout together
Some of the code was in its own component, while some of the code
remained in the polls/show view.

Note that we're re-structuring the code a little bit, so it's clear that
the "already voted" messages are only shown when users can vote. Also
note that now the `can?` condition involves the existence of a
`current_user` and that the poll is not expired, so we can simplify the
`voted_in_web` condition.
2025-08-14 13:06:43 +02:00
Javi Martín
5402cb6042 Move poll callout partial to a component
This way it'll be easier to refactor it.

Note there was a system test which tested both the callout and the form
when unverified users visit a poll. We've split this system test in two
component tests.
2025-08-14 13:06:43 +02:00
Javi Martín
a7e1b42b6c Use checkboxes and radio buttons on poll forms
Our original interface to vote in a poll had a few issues:

* Since there was no button to send the form, it wasn't clear that
  selecting an option would automatically store it in the database.
* The interface was almost identical for single-choice questions and
  multiple-choice questions, which made it hard to know which type of
  question we were answering.
* Adding other type of questions, like open answers, was hard since we
  would have to add a different submit button for each answer.

So we're now using radio buttons for single-choice questions and
checkboxes for multiple-choice questions, which are the native controls
designed for these purposes, and a button to send the whole form.

Since we don't have a database table for poll ballots like we have for
budget ballots, we're adding a new `Poll::WebVote` model to manage poll
ballots. We're using WebVote instead of Ballot or Vote because they
could be mistaken with other vote classes.

Note that browsers don't allow removing answers with radio buttons, so
once somebody has voted in a single-choice question, they can't remove
the vote unless they manually edit their HTML. This is the same behavior
we had before commit 7df0e9a96.

As mentioned in c2010f975, we're now adding the `ChangeByZero` rubocop
rule, since we've removed the test that used `and change`.
2025-08-14 13:06:37 +02:00
Javi Martín
fd14c55615 Make answer depend on the option in poll answer factory
Until now, when writing `create(:poll_answer, option: option)`, the
`answer` field would take the title of a random option instead of taking
the title from the `option` variable.

So now, if we're given the option, the `answer` field will be taken from
the option itself.

Note that writing something like:

```
option { question.question_options.find_by(title: answer) }
answer { option.title }
```

Would create an infinite loop when creating a poll answer if we don't
pass the `option` and/or the `answer` attributes.

So, instead, we're making the `option` depend on the `answer` attribute
exclusively when we pass the `answer` attribute to the factory.
2025-08-14 12:58:51 +02:00
Javi Martín
a5054089b8 Fix typo in poll voter test 2025-08-12 12:45:12 +02:00
Javi Martín
b81bbeaa96 Remove unused method Poll::Question.answerable_by
This method isn't used since commit 909114bcf.
2025-08-12 12:45:12 +02:00
Javi Martín
3ddba8660e Fix "more info" heading
We were using an <h3> tag when there was no <h2> tag before it.
2025-08-12 12:45:12 +02:00
Javi Martín
6c5b908ef5 Use a loop instead of with_collection to render questions
This is what we usually do in components.
2025-08-12 12:45:12 +02:00
Javi Martín
eccf906e45 Add component to render the form poll
Right now it just renders the questions as it used to.
2025-08-12 12:45:12 +02:00
Javi Martín
dae932b0d2 Fix radio and label margin in right-to-left layouts
We used `margin-left` in commit b4eba055c, but when using right-to-left
layout, the property we should use is `margin-right`. So we're using
`margin-#{$global-left}` as usual.
2025-08-12 12:43:15 +02:00