Enunciato del problema
Se utilizzi il plugin Discourse Gamification per assegnare punti agli utenti per i loro contributi alla tua community, potresti aver riscontrato la necessità di modificare i valori dei punti in base alle tendenze emergenti. Nella SailPoint Developer Community, utilizziamo il plugin Gamification per gestire il nostro programma Ambassador. Gli utenti che apportano contributi preziosi alla nostra community guadagnano punti, che a loro volta determinano il livello di benefit che ricevono. Man mano che la nostra community è cresciuta e il numero di contributi è aumentato drasticamente, abbiamo stabilito che i valori originali assegnati per determinati tipi di contributi necessitavano di una modifica. La modifica dei valori dei punti funziona bene se non si esegue mai una ricalcolazione dei punti, ma spesso dobbiamo ricalcolare i punti quando eseguiamo la fusione degli utenti o quando utilizziamo l’API di gamification esterna per assegnare punti per contributi passati. Se modifichi i valori dei punti della gamification ed esegui una ricalcolazione, i contributi precedenti valutati con i vecchi valori dei punti verranno ora valutati con i nuovi valori. Questo è un problema se desideri che i nuovi valori dei punti siano assegnati ai nuovi contributi, lasciando i vecchi contributi allo stesso valore. In questa guida, discuterò una soluzione che ho creato per garantire che i punti precedenti dei tuoi utenti non vengano alterati quando cambi i valori dei punti ed esegui una ricalcolazione.
Utilizzo di Data Explorer per calcolare i punti attuali e proposti
Importazione della query SQL
Il primo passo in questo processo è importare la seguente query SQL nel tuo plugin Data Explorer. Questa query SQL è stata modificata rispetto alla query SQL originale per includere la paginazione, aggiungere colonne aggiuntive per user_name e name, e ordinare per user_id. Questa query produrrà una tabella dei punteggi degli utenti per un determinato intervallo di tempo e utilizzando i valori di punteggio forniti.
-- [params]
-- date :start_date
-- date :end_date
-- int :day_visited_score_value = 0
-- int :time_read_score_value = 0
-- int :posts_read_score_value = 0
-- int :posts_created_score_value = 6
-- int :topics_created_score_value = 0
-- int :likes_received_score_value = 3
-- int :likes_given_score_value = 0
-- int :solutions_score_value = 60
-- int :flag_created_score_value = 6
-- int :user_invited_score_value = 0
-- int :limit = 1000
-- int :page = 0
WITH visits AS (
SELECT
uv.user_id,
COUNT(*) AS user_visits,
COUNT(*) * :day_visited_score_value AS visits_score
FROM user_visits uv
WHERE uv.visited_at BETWEEN :start_date AND :end_date
GROUP BY uv.user_id
),
time_read AS (
SELECT
uv.user_id,
SUM(uv.time_read) /3600 AS time_read,
SUM(uv.time_read) /3600 * :time_read_score_value AS time_read_score
FROM user_visits uv
WHERE uv.visited_at BETWEEN :start_date AND :end_date
AND uv.time_read >= 60
GROUP BY uv.user_id
),
posts_read AS (
SELECT
uv.user_id,
SUM(uv.posts_read) AS posts_read,
SUM(uv.posts_read) /100 * :posts_read_score_value AS posts_read_score
FROM user_visits uv
WHERE uv.visited_at BETWEEN :start_date AND :end_date
AND uv.posts_read >= 5
GROUP BY uv.user_id
),
posts_created AS (
SELECT
p.user_id,
COUNT(*) AS posts_created,
COUNT(*) * :posts_created_score_value AS posts_created_score
FROM posts p
INNER JOIN topics t ON t.id = p.topic_id
WHERE p.deleted_at IS NULL
AND t.archetype <> 'private_message'
AND p.wiki IS FALSE
AND p.created_at::date BETWEEN :start_date AND :end_date
GROUP BY p.user_id
),
topics_created AS (
SELECT
t.user_id,
COUNT(*) AS topics_created,
COUNT(*) * :topics_created_score_value AS topics_created_score
FROM topics t
WHERE t.deleted_at IS NULL
AND t.archetype <> 'private_message'
AND t.created_at::date BETWEEN :start_date AND :end_date
GROUP BY t.user_id
),
likes_received AS (
SELECT
p.user_id,
COUNT(*) AS likes_received,
COUNT(*) * :likes_received_score_value AS likes_received_score
FROM post_actions pa
INNER JOIN posts p ON p.id = pa.post_id
INNER JOIN topics t ON t.id = p.topic_id
WHERE p.deleted_at IS NULL
AND t.archetype <> 'private_message'
AND p.wiki IS FALSE
AND post_action_type_id = 2
AND pa.created_at::date BETWEEN :start_date AND :end_date
GROUP BY p.user_id
),
likes_given AS (
SELECT
pa.user_id AS user_id,
COUNT(*) AS likes_given,
COUNT(*) * :likes_given_score_value AS likes_given_score
FROM post_actions pa
INNER JOIN posts p ON p.id = pa.post_id
INNER JOIN topics t ON t.id = p.topic_id
WHERE p.deleted_at IS NULL
AND t.archetype <> 'private_message'
AND p.wiki IS FALSE
AND post_action_type_id = 2
AND pa.created_at::date BETWEEN :start_date AND :end_date
GROUP BY pa.user_id
),
solutions AS (
SELECT
p.user_id,
COUNT(st.topic_id) AS solutions,
COUNT(st.topic_id) * :solutions_score_value AS solutions_score
FROM discourse_solved_solved_topics st
INNER JOIN topics t ON st.topic_id = t.id
INNER JOIN posts p ON p.id = st.answer_post_id
WHERE p.deleted_at IS NULL
AND t.deleted_at IS NULL
AND t.archetype <> 'private_message'
AND p.user_id <> t.user_id
AND st.updated_at::date BETWEEN :start_date AND :end_date
GROUP BY p.user_id
),
flags AS (
SELECT
r.created_by_id AS user_id,
COUNT(*) AS flags,
COUNT(*) * :flag_created_score_value AS flags_score
FROM reviewables r
WHERE created_at::date BETWEEN :start_date AND :end_date
AND status = 1
GROUP BY user_id
),
invites AS (
SELECT
inv.invited_by_id AS user_id,
SUM(inv.redemption_count) AS invites,
(SUM(inv.redemption_count) * :user_invited_score_value)::int AS invites_score
FROM invites inv
WHERE inv.created_at::date BETWEEN :start_date AND :end_date
AND inv.redemption_count > 0
GROUP BY inv.invited_by_id
)
SELECT
u.id AS user_id,
u.username AS username,
u.name AS name,
(
COALESCE(v.visits_score,0) +
COALESCE(tr.time_read_score,0) +
COALESCE(pr.posts_read_score,0) +
COALESCE(pc.posts_created_score,0) +
COALESCE(tc.topics_created_score,0) +
COALESCE(lr.likes_received_score,0) +
COALESCE(lg.likes_given_score,0) +
COALESCE(s.solutions_score,0) +
COALESCE(f.flags_score,0) +
COALESCE(i.invites_score,0)
) AS "Total Cheers",
COALESCE(v.user_visits,0) || ' (' || COALESCE(v.visits_score,0) || ')' AS "Visits (cheers)",
COALESCE(tr.time_read,0) || 'hrs' || ' (' || COALESCE(tr.time_read_score,0) || ')' AS "Time Read (cheers)",
COALESCE(pr.posts_read,0) || ' (' || COALESCE(pr.posts_read_score,0) || ')' AS "Posts Read (cheers)",
COALESCE(pc.posts_created,0) || ' (' || COALESCE(pc.posts_created_score,0) || ')' AS "Posts Created (cheers)",
COALESCE(tc.topics_created,0) || ' (' || COALESCE(tc.topics_created_score,0) || ')'AS "Topics Created (cheers)",
COALESCE(lr.likes_received,0) || ' (' || COALESCE(lr.likes_received_score,0) || ')' AS "Likes Received (cheers)",
COALESCE(lg.likes_given,0) || ' (' || COALESCE(lg.likes_given_score,0) || ')' AS "Likes Given (cheers)",
COALESCE(s.solutions,0) || ' (' || COALESCE(s.solutions_score,0) || ')'AS "Solutions (cheers)",
COALESCE(f.flags,0) || ' (' || COALESCE(f.flags_score,0) || ')' AS "Agreed Flags (cheers)",
COALESCE(i.invites,0) || ' (' || COALESCE(i.invites_score,0) || ')' AS "Invites Redeemed (cheers)"
FROM users u
LEFT JOIN visits v ON v.user_id = u.id
LEFT JOIN posts_read pr USING (user_id)
LEFT JOIN time_read tr USING (user_id)
LEFT JOIN flags f USING (user_id)
LEFT JOIN posts_created pc USING (user_id)
LEFT JOIN topics_created tc USING (user_id)
LEFT JOIN likes_given lg USING (user_id)
LEFT JOIN likes_received lr USING (user_id)
LEFT JOIN solutions s USING (user_id)
LEFT JOIN invites i USING (user_id)
WHERE u.id > 0
AND u.id NOT IN (SELECT user_id FROM group_users WHERE group_id = 3)
ORDER BY user_id ASC
OFFSET :page * :limit
LIMIT :limit
Test della query utilizzando i valori di punti attuali e proposti
Una volta importata la query SQL, dovresti testarla utilizzando un intervallo di tempo che riflette il periodo in cui non desideri che i punti vengano modificati. Per noi, questo significava dall’inizio del nostro forum (2020-01-01) al giorno prima di voler apportare l’aggiornamento ai nostri valori dei punti (2024-05-15). Aggiorna i valori di punteggio per riflettere i tuoi valori di punti attuali e testa la query per assicurarti che i valori siano corretti.
Successivamente, mantieni lo stesso intervallo di tempo ma modifica i valori dei punti in quelli che desideri. Prendi nota della differenza nei valori per gli stessi utenti. In questo esempio, Neil aveva un totale di 1104 punti (aka cheers) utilizzando i valori di punti attuali, e i suoi punti sono aumentati a 1245 utilizzando i nuovi valori di punti. Abbiamo bisogno di un metodo per calcolare la differenza tra i punti prima e dopo e regolare i punti dell’utente in modo che non vedano un aumento o una diminuzione dei punti dopo l’implementazione dei nuovi valori di punti.
Utilizzo dell’API di gamification esterna per regolare i punti
Utilizzando Neil come esempio, i suoi punti attuali sono 1104 e i suoi punti dopo la modifica proposta saranno 1245. Per assicurarsi che non veda un aumento o una diminuzione dei punti, dobbiamo calcolare la differenza tra questi due valori e quindi assegnare la differenza al suo punteggio. La differenza è calcolata come valoreCorrente - valoreNuovo, che nel caso di Neil sarebbe 1104 - 1245 = -141. Questo significa che a Neil devono essere assegnati -141 punti. Possiamo utilizzare l’API di gamification esterna per assegnare questi punti al suo account utente in modo che si riflettano nel suo punteggio della classifica. La chiamata API che dobbiamo effettuare è la seguente:
curl --location 'https://my.discourse.com/admin/plugins/gamification/score_events' \
--header 'Api-Key: <tua chiavi>' \
--header 'Api-Username: <il tuo nome utente>' \
--header 'Content-Type: application/json' \
--data '{
"user_id": "101",
"date": "2024-05-15",
"points": "-141",
"description": "Regolazione punti gamification"
}'
L’esecuzione di questa chiamata API regolerà il punteggio totale di Neil in modo che i nuovi valori di punti proposti influenzino solo i nuovi eventi punteggiabili, mentre qualsiasi differenza nei vecchi eventi punteggiabili verrà annullata dalla regolazione. Ora dobbiamo solo applicare questo processo a ogni utente della community.
Automazione delle regolazioni dei punti
Per automatizzare questo processo, ho creato uno script Python che sfrutta le API di Discourse per eseguire le query SQL, calcolare la differenza e assegnare le regolazioni a ogni utente. Lo script genera anche un file CSV con un registro di tutti gli utenti che hanno ricevuto una regolazione. I commenti nello script fanno un buon lavoro nel descrivere i valori che devi modificare e come funziona. Se hai domande su come far funzionare tutto questo, lascia un commento qui sotto.
Requisiti
Avrai bisogno di una versione recente di Python 3. Io uso Python 3.9.6. Dovrai anche installare il pacchetto requests da PyPi.
python3 -m pip install requests
Script
import requests
import secrets
from datetime import date
from datetime import timedelta
import time
import csv
import json
# Le credenziali API sono memorizzate in un file segreto. Come fornire i segreti è a tua discrezione.
api_key = secrets.api_key
api_username = secrets.api_username
host = 'https://developer.sailpoint.com/discuss' # Il nome host della tua istanza Discourse
# Un link a un argomento nel tuo forum che descrive le modifiche attuali e proposte dei punti. È una buona cosa avere
# questo in modo da poterlo consultare in futuro per capire cosa è cambiato e rendere le modifiche trasparenti ai tuoi utenti.
point_adjustment_link = 'https://developer.sailpoint.com/discuss/t/update-to-ambassador-point-values-may-15th-2024/54178'
# Il nome che desideri dare al tuo file CSV. La data di inizio e fine verranno aggiunte quando viene creato.
csv_name = 'sailpoint_developer_community_point_adjustment'
max_requests_per_minute = 200 # Numero massimo di richieste per periodo di 1 minuto. Regola questo valore per essere pari o inferiore al limite di velocità della tua API Discourse.
start_date = '2020-01-01' # Il forum è iniziato dopo questa data, quindi questo garantirà che tutti i punti vengano considerati.
# Questa data non può essere oggi, altrimenti la query genererà un errore. Questa formula userà la data di ieri. Puoi modificarla
# per essere più indietro nel tempo se lo desideri.
end_date = (date.today() - timedelta(days = 1)).strftime('%Y-%m-%d')
# L'ID della tua query SQL. Puoi trovarlo cliccando sulla tua query nel plugin Data Explorer e cercando
# il parametro "id" nell'URL. Ad esempio, il mio ID è 66 come mostrato in questo URL:
# https://developer.sailpoint.com/discuss/admin/plugins/explorer?id=66
query_id = '66'
# Questi sono i valori di punti attuali per le tue impostazioni di punteggio Gamification. Questi quattro erano applicabili al nostro forum
# ma puoi aggiungere altri tipi di punti punteggiabili se necessario.
current_likes_received_score_value = '3'
current_solutions_score_value = '60'
current_posts_created_score_value = '6'
current_flag_created_score_value = '6'
# Questi sono i valori di punti proposti che verranno applicati a tutti i nuovi punti.
new_likes_received_score_value = '6'
new_solutions_score_value = '60'
new_posts_created_score_value = '3'
new_flag_created_score_value = '6'
# Questa funzione esegue la query SQL per ottenere i valori dei punti per tutti gli utenti. Paginerà automaticamente fino a quando non ci saranno più record.
def getPointValues(query_id, start_date, end_date, limit, likes_received_score_value, solutions_score_value, posts_created_score_value, flag_created_score_value):
rows = []
page = 0
headers = {
'Api-Key': api_key,
'Api-Username': api_username,
'Content-Type': 'application/x-www-form-urlencoded'
}
payload = f'params={{"start_date":"{start_date}","end_date":"{end_date}","limit":"{str(limit)}","page":"{str(page)}","likes_received_score_value":"{likes_received_score_value}","solutions_score_value":"{solutions_score_value}","posts_created_score_value":"{posts_created_score_value}","flag_created_score_value":"{flag_created_score_value}"}}'
r = requests.post(f'{host}/admin/plugins/explorer/queries/{query_id}/run', headers=headers, data=payload).json()
rows += r['rows']
# Pagina fino a raggiungere l'ultima pagina
while len(r['rows']) == limit:
page += 1
payload = f'params={{"start_date":"{start_date}","end_date":"{end_date}","limit":"{str(limit)}","page":"{str(page)}","likes_received_score_value":"{likes_received_score_value}","solutions_score_value":"{solutions_score_value}","posts_created_score_value":"{posts_created_score_value}","flag_created_score_value":"{flag_created_score_value}"}}'
r = requests.post(f'{host}/admin/plugins/explorer/queries/{query_id}/run', headers=headers, data=payload).json()
rows += r['rows']
return rows
# Crea l'array di regolazioni dei punti che devono essere effettuate. Se un utente ha 0 punti, o non c'è differenza nei punti, verranno
# filtrati in modo da non effettuare chiamate API non necessarie. Questo array includerà tutte le informazioni necessarie per effettuare
# la chiamata API di gamification esterna nonché per popolare il file CSV.
def calculatePointAdjustments(old_values, new_values):
point_adjustments = []
for row in old_values:
user_id = row[0]
username = row[1]
name = row[2]
current_cheers = row[3]
for i in range(len(new_values)):
new_cheers = new_values[i][3]
if new_values[i][0] == user_id:
# Calcola la diff solo se c'è una differenza nei punti.
if current_cheers != new_cheers:
point_adjustments.append({
'user_id': user_id,
'username': username,
'name': name,
'current_points': current_cheers,
'new_points': new_cheers,
'difference': current_cheers - new_cheers,
'external_gamification_point_id': -1
})
new_values.pop(i) # Rimuovi l'elemento dalla lista per accelerare l'elaborazione
break
else:
new_values.pop(i) # Rimuovi l'elemento dalla lista per accelerare l'elaborazione
break
return point_adjustments
# Salva l'array di regolazioni dei punti in un file CSV per la tua archiviazione
def exportCSV(differences):
fields = ['User ID', 'Username', 'Name', 'Current Points', 'New Points', 'Difference', 'External Gamification Point ID']
rows = []
for diff in differences:
rows.append([diff['user_id'], diff['username'], diff['name'], diff['current_points'], diff['new_points'], diff['difference'], diff['external_gamification_point_id']])
with open(f'{csv_name}_{start_date}_to_{end_date}.csv', 'w') as f:
csv_writer = csv.writer(f)
csv_writer.writerow(fields)
csv_writer.writerows(rows)
# Esegui l'API di gamification per assegnare la differenza nei punti per ogni utente. Questa funzione rallenterà intenzionalmente
# per rimanere entro il limite di velocità. Nel caso in cui venga raggiunto il limite di velocità, questa funzione continuerà ad attendere fino a quando
# il limite di velocità non scadrà e potrà continuare. Se in qualsiasi momento si verifica un errore nella chiamata API, le regolazioni dei punti completate con successo
# verranno salvate in un file CSV in modo da sapere quali sono complete e dove riprendere.
def assignPointAdjustments(point_adjustments):
assigned_point_adjustments = []
# Auto-limitazione della velocità per non influenzare altre integrazioni
sleep_time = 60 / max_requests_per_minute # Numero di secondi da attendere tra ogni chiamata per rimanere entro il limite massimo di richieste
endpoint = f'{host}/admin/plugins/gamification/score_events'
headers = {
'Api-Key': api_key,
'Api-Username': api_username,
'Content-Type': 'application/json'
}
for adjustment in point_adjustments:
payload = json.dumps({
"user_id": str(adjustment['user_id']),
"date": end_date,
"points": str(adjustment['difference']),
"description": f'Aggiustamento punti basato sui nuovi valori di punti gamification. Si prega di consultare {point_adjustment_link}.'
})
start = time.time() # Tieni traccia del tempo di esecuzione. Poiché le chiamate API richiedono diversi millisecondi, non contare questo tempo nel tempo di attesa massimo per le richieste.
try:
r = requests.post(endpoint, headers=headers, data=payload)
except Exception as e:
# In caso di fallimento, salva le regolazioni assegnate in un file CSV in modo da sapere quali sono già complete
exportCSV(assigned_point_adjustments)
raise
# Se il limite massimo di richieste è inferiore a 300, non dovremmo raggiungere un limite di velocità. Se per caso lo raggiungiamo, gestiscilo in modo appropriato.
while r.status_code == 429:
wait_time = r.json()["extras"]["wait_seconds"] + 1
print(f'Limite di velocità raggiunto. In attesa di {wait_time} secondi')
time.sleep(wait_time)
try:
r = requests.post(endpoint, headers=headers, data=payload)
adjustment['external_gamification_point_id'] = r.json()["id"]
except Exception as e:
# In caso di fallimento, salva le regolazioni assegnate in un file CSV in modo da sapere quali sono già complete
exportCSV(assigned_point_adjustments)
raise
if r.status_code != 200:
exportCSV(assigned_point_adjustments)
print(f'L'ultima richiesta ha restituito un errore {r.status_code} con il seguente messaggio di errore\n{r.text}')
print('Interrompi gli aggiustamenti e scrivi gli aggiustamenti riusciti nel file')
raise Exception("HTTP Error")
end = time.time()
if end - start < sleep_time:
# Dormi solo se il tempo della richiesta è stato inferiore al tempo di attesa massimo per le richieste
time.sleep(sleep_time - (end - start))
adjustment['external_gamification_point_id'] = r.json()["id"]
print(f'Assegnati {adjustment["difference"]} punti a {adjustment["username"]}')
assigned_point_adjustments.append(adjustment)
return assigned_point_adjustments
limit = 1000 # Questo è il limite massimo per le query SQL.
current_values = getPointValues(query_id, start_date, end_date, limit, current_likes_received_score_value, current_solutions_score_value, current_posts_created_score_value, current_flag_created_score_value)
new_values = getPointValues(query_id, start_date, end_date, limit, new_likes_received_score_value, new_solutions_score_value, new_posts_created_score_value, new_flag_created_score_value)
point_adjustments = calculatePointAdjustments(current_values, new_values)
assigned_point_adjustments = assignPointAdjustments(point_adjustments)
exportCSV(assigned_point_adjustments)
I passaggi per eseguire la regolazione dei punti
Ora che abbiamo tutti i pezzi in posizione, dovrai seguire questi passaggi per eseguire il processo di regolazione dei punti. Si consiglia di testare questo processo in un’istanza di test del tuo ambiente Discourse prima per risolvere eventuali problemi.
- Aggiorna le variabili nello script Python con i tuoi valori desiderati.
- Modifica i valori dei punti nelle impostazioni di Gamification con i nuovi valori.
- Ricalcola i punteggi per “Tutto il tempo”.
- Esegui lo script Python e attendi che si completi. A seconda di quanti utenti ci sono nel tuo sistema e qual è il tuo limite di velocità, questo può richiedere diversi minuti per completarsi. Avevamo 2.200 utenti che necessitavano di una regolazione. Con un limite di velocità di 200 richieste/minuto, ci sono voluti circa 15 minuti per completarsi.
- Una volta completato, ricalcola nuovamente i punteggi per “Tutto il tempo”, solo per sicurezza.
- Salva il file CSV in un luogo sicuro per i tuoi registri, nel caso tu debba annullare eventuali modifiche.
Il ricalcolo dei punti può richiedere alcune ore per completarsi. Dovrai essere paziente per vedere le regolazioni avere effetto. Una volta completato il ricalcolo, hai ora regolato con successo i punti dei tuoi utenti, e i punti futuri verranno calcolati dai nuovi valori di punti.
Effettuare ulteriori regolazioni in futuro
Se in qualsiasi momento hai bisogno di apportare regolazioni ai valori dei punti, puoi seguire lo stesso processo. L’unica differenza è che devi impostare la start_date sulla end_date dell’ultima volta che hai eseguito la regolazione dei punti. Ad esempio, la prima volta che ho eseguito questa regolazione dei punti ho impostato la end_date su 2024-05-15. Questo ha assicurato che tutti i punti guadagnati tra l’inizio del mio forum e il 2024-05-15 fossero regolati per preservare i vecchi valori. Supponiamo che passi un anno e io voglia cambiare di nuovo i valori dei punti. Ora devo impostare la start_date su 2024-05-15 e la end_date su 2025-05-15. Questo garantirà che la regolazione dei punti si applichi solo a quel periodo e non sovrascriva nulla dalla prima volta che hai eseguito la regolazione dei punti.

