Skip to content

Bug: fave() cross-campaign collision when same image appears in multiple campaigns #526

Description

@lgelauff

Description

`JurorDAO.fave()` in `montage/rdb.py` does not scope its existing-fave lookup to the current campaign. If the same image appears in two campaigns, faving it in the second campaign reactivates the first campaign's record instead of creating a new one — attributing the fave to the wrong campaign.

Setup

Two campaigns, both containing the same image (`File:Photo.jpg`):

Campaign Round Image
Campaign A Round 1 `File:Photo.jpg`
Campaign B Round 1 `File:Photo.jpg`

Reproduction

Verified against local dev instance. Requires two rounds that share at least one entry, with the same juror in both. In the default dev database, entry 6 (`Test_WLM_2015_image_001.jpg`) appears in round 4 (campaign 2) and round 6 (campaign 4), with Effeietsanders as a juror in both.

Generate a dev cookie first:

from clastic.middleware.cookie import JSONCookie
cookie = JSONCookie(
    {'userid': 409, 'username': 'Effeietsanders'},
    secret_key='Replace221432ThisWithSomethingSomewhatSecret'
)
print(cookie.serialize())

Then run:

import requests, sqlite3

BASE = 'http://localhost:5001/v1'
COOKIE = '<output from above>'

s = requests.Session()
s.cookies.set('clastic_cookie', COOKIE)
s.headers.update({'Content-Type': 'application/json'})

ROUND_A_ID = 4  # campaign_id = 2
ROUND_B_ID = 6  # campaign_id = 4
ENTRY_ID = 6    # Test_WLM_2015_image_001.jpg, present in both rounds

def db_faves():
    conn = sqlite3.connect('tmp_montage.db')
    cur = conn.cursor()
    cur.execute('SELECT id, entry_id, campaign_id, status FROM favorites WHERE entry_id = ?', (ENTRY_ID,))
    rows = cur.fetchall()
    conn.close()
    return rows

# Clean slate
conn = sqlite3.connect('tmp_montage.db')
conn.execute('DELETE FROM favorites WHERE entry_id = ?', (ENTRY_ID,))
conn.commit()

# Step 1: fave in Campaign A
s.post(f'{BASE}/juror/round/{ROUND_A_ID}/{ENTRY_ID}/fave', data='{}')
print("After fave in Campaign A:", db_faves())
# [(1, 6, 2, 'active')]

# Step 2: unfave in Campaign A
s.post(f'{BASE}/juror/round/{ROUND_A_ID}/{ENTRY_ID}/unfave', data='{}')
print("After unfave in Campaign A:", db_faves())
# [(1, 6, 2, 'cancelled')]

# Step 3: fave the same image in Campaign B
s.post(f'{BASE}/juror/round/{ROUND_B_ID}/{ENTRY_ID}/fave', data='{}')
rows = db_faves()
print("After fave in Campaign B:", rows)
# BUG: [(1, 6, 2, 'active')] — campaign_id=2 (Campaign A), not campaign_id=4 (Campaign B)
# EXPECTED: a new record with campaign_id=4

Actual output (bug):

After fave in Campaign A: [(1, 6, 2, 'active')]
After unfave in Campaign A: [(1, 6, 2, 'cancelled')]
After fave in Campaign B: [(1, 6, 2, 'active')]   ← campaign_id=2, wrong campaign

Cause

In `rdb.py`, `JurorDAO.fave()`:

existing_fave = (self.query(Favorite)
                     .filter_by(entry_id=entry_id,
                                user=self.user)
                     .first())  # missing campaign_id filter

`campaign_id` is not included in the lookup, so faves are matched across all campaigns.

Fix

Resolve `round_entry` before the existing-fave check and add `campaign_id` to the filter:

round_entry = self.get_round_entry(round_id, entry_id)
campaign_id = round_entry.round.campaign.id

existing_fave = (self.query(Favorite)
                     .filter_by(entry_id=entry_id,
                                campaign_id=campaign_id,
                                user=self.user)
                     .first())

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions