working on poll evaluation
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -56,6 +56,7 @@ public class Field {
|
|||||||
public static final String END_TIME = "end_time";
|
public static final String END_TIME = "end_time";
|
||||||
public static final String ENTITY_ID = "entity_id";
|
public static final String ENTITY_ID = "entity_id";
|
||||||
public static final String EST_TIME = "est_time";
|
public static final String EST_TIME = "est_time";
|
||||||
|
public static final String EVALUATION = "evaluation";
|
||||||
public static final String EXPECTED = "expected";
|
public static final String EXPECTED = "expected";
|
||||||
public static final String EXPIRATION = "expiration";
|
public static final String EXPIRATION = "expiration";
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public class Text {
|
|||||||
public static final String DOCUMENT_WITH_ID = "document ({id})";
|
public static final String DOCUMENT_WITH_ID = "document ({id})";
|
||||||
|
|
||||||
public static final String EMAILS_FOR_RECEIVER = "emails for {email}";
|
public static final String EMAILS_FOR_RECEIVER = "emails for {email}";
|
||||||
|
public static final String EVALUATION = "evaluation";
|
||||||
|
|
||||||
public static final String FILES = "files";
|
public static final String FILES = "files";
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import de.srsoftware.umbrella.core.constants.Field;
|
|||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static de.srsoftware.umbrella.core.constants.Field.*;
|
import static de.srsoftware.umbrella.core.constants.Field.*;
|
||||||
import static java.text.MessageFormat.format;
|
import static java.text.MessageFormat.format;
|
||||||
@@ -104,6 +105,22 @@ public class Poll implements Mappable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
public static class Evaluation {
|
||||||
|
// Map<Option → Map<Weight → Count>>
|
||||||
|
private HashMap<Integer,Map<Integer,Integer>> selections = new HashMap<>();
|
||||||
|
|
||||||
|
public void count(ResultSet rs) throws SQLException {
|
||||||
|
var optionId = rs.getInt(OPTION_ID);
|
||||||
|
var userId = rs.getObject(USER_ID);
|
||||||
|
var weight = rs.getInt(WEIGHT);
|
||||||
|
var optionStats = selections.computeIfAbsent(optionId, k -> new HashMap<>());
|
||||||
|
optionStats.compute(weight, (k, sum) -> sum == null ? 1 : sum + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HashMap<Integer, Map<Integer, Integer>> toMap() {
|
||||||
|
return selections;
|
||||||
|
}
|
||||||
|
}
|
||||||
private Owner owner;
|
private Owner owner;
|
||||||
|
|
||||||
private String id, name, description;
|
private String id, name, description;
|
||||||
@@ -212,7 +229,7 @@ public class Poll implements Mappable {
|
|||||||
Field.SOURCE,description,
|
Field.SOURCE,description,
|
||||||
Field.RENDERED,Util.markdown(description)
|
Field.RENDERED,Util.markdown(description)
|
||||||
),
|
),
|
||||||
Field.OPTIONS, options.stream().map(Option::toMap).toList(),
|
Field.OPTIONS, options.stream().collect(Collectors.toMap(Option::id,Option::toMap)),
|
||||||
Field.SHARES, mapShares(),
|
Field.SHARES, mapShares(),
|
||||||
Field.PRIVATE, isPrivate,
|
Field.PRIVATE, isPrivate,
|
||||||
Field.WEIGHTS, weights
|
Field.WEIGHTS, weights
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each poll.options as option (option.id)}
|
{#each Object.entries(poll.options) as [option_id, option]}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<LineEditor editable={true} value={option.name} onSet={name => patch_option(option,'name',name)} title={t('clear to remove')} />
|
<LineEditor editable={true} value={option.name} onSet={name => patch_option(option,'name',name)} title={t('clear to remove')} />
|
||||||
|
|||||||
@@ -1,8 +1,24 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { api, get } from '../../urls.svelte';
|
import { api, get } from '../../urls.svelte';
|
||||||
|
import { error, yikes } from '../../warn.svelte';
|
||||||
|
import { t } from '../../translations.svelte';
|
||||||
let { id } = $props();
|
let { id } = $props();
|
||||||
|
let poll = $state(null);
|
||||||
|
|
||||||
|
function average(hist){
|
||||||
|
let count = 0;
|
||||||
|
let sum = 0;
|
||||||
|
for (let [k,v] of Object.entries(hist)) {
|
||||||
|
count += v;
|
||||||
|
sum += k*v;
|
||||||
|
}
|
||||||
|
return count > 0 ? sum/count : '?';
|
||||||
|
}
|
||||||
|
|
||||||
|
function max_val(hist){
|
||||||
|
return Math.max(...Object.values(hist));
|
||||||
|
}
|
||||||
|
|
||||||
async function load(){
|
async function load(){
|
||||||
let url = api('poll/evaluate/'+id);
|
let url = api('poll/evaluate/'+id);
|
||||||
@@ -16,4 +32,56 @@
|
|||||||
onMount(load);
|
onMount(load);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
Evaluate
|
<style>
|
||||||
|
.histogram > span{
|
||||||
|
display: inline-block;
|
||||||
|
border: 1px solid lime;
|
||||||
|
vertical-align: bottom;
|
||||||
|
position: relative;
|
||||||
|
width: 15px;
|
||||||
|
}
|
||||||
|
.histogram{
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.histogram span span{
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{#if poll}
|
||||||
|
<fieldset>
|
||||||
|
<legend>{poll.name}</legend>
|
||||||
|
<div class="description">{@html poll.description.rendered}</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>{t('option')}</td>
|
||||||
|
<td>{t('average')}</td>
|
||||||
|
<td>{t('histogram')}</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each Object.entries(poll.evaluation) as [option_id,hist]}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{poll.options[option_id].name}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{average(hist)}
|
||||||
|
</td>
|
||||||
|
<td class="histogram">
|
||||||
|
{#each Object.entries(hist) as [weight,count]}
|
||||||
|
<span style="height: {100*count/max_val(hist)}%">
|
||||||
|
<span>
|
||||||
|
{weight}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
{/if}
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each poll.options as option}
|
{#each Object.entries(poll.options) as [option_id,option]}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="option">
|
<td class="option">
|
||||||
{option.name}
|
{option.name}
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
</td>
|
</td>
|
||||||
{#each Object.entries(poll.weights) as [weight,name]}
|
{#each Object.entries(poll.weights) as [weight,name]}
|
||||||
<td class="radio" onclick={e => select(option,weight)} title={t('click to select')} >
|
<td class="radio" onclick={e => select(option,weight)} title={t('click to select')} >
|
||||||
{#if selection[option.id] == weight}
|
{#if selection[option_id] == weight}
|
||||||
X
|
X
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -17,4 +17,6 @@ public interface PollDb {
|
|||||||
|
|
||||||
void saveSelection(Poll poll, Map<Integer,Integer> optionsToWeights, String guestName);
|
void saveSelection(Poll poll, Map<Integer,Integer> optionsToWeights, String guestName);
|
||||||
void saveSelection(Poll poll, Map<Integer,Integer> optionsToWeights, UmbrellaUser user);
|
void saveSelection(Poll poll, Map<Integer,Integer> optionsToWeights, UmbrellaUser user);
|
||||||
|
|
||||||
|
Poll.Evaluation loadEvaluation(String id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import static de.srsoftware.umbrella.core.constants.Field.ID;
|
|||||||
import static de.srsoftware.umbrella.core.constants.Path.*;
|
import static de.srsoftware.umbrella.core.constants.Path.*;
|
||||||
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
|
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
|
||||||
import static de.srsoftware.umbrella.poll.Constants.CONFIG_DATABASE;
|
import static de.srsoftware.umbrella.poll.Constants.CONFIG_DATABASE;
|
||||||
|
import static java.lang.System.Logger.Level.WARNING;
|
||||||
import static java.text.MessageFormat.format;
|
import static java.text.MessageFormat.format;
|
||||||
|
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
@@ -100,10 +101,13 @@ public class PollModule extends BaseHandler implements PollService {
|
|||||||
|
|
||||||
private boolean getPollEvaluation(HttpExchange ex, UmbrellaUser user, Path path) throws IOException {
|
private boolean getPollEvaluation(HttpExchange ex, UmbrellaUser user, Path path) throws IOException {
|
||||||
if (path.empty()) throw missingField(ID);
|
if (path.empty()) throw missingField(ID);
|
||||||
var poll = loadPoll(user,path.pop());
|
var poll = loadPoll(user,path.pop());
|
||||||
|
LOG.log(WARNING,"Mising permission check for poll evaluation");
|
||||||
// TODO: load data required for evaluation
|
// TODO: check permissions
|
||||||
return sendContent(ex,poll);
|
var result = new HashMap<>(poll.toMap());
|
||||||
|
var evaluation = pollDb.loadEvaluation(poll.id());
|
||||||
|
result.put(Field.EVALUATION,evaluation.toMap());
|
||||||
|
return sendContent(ex,result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean getPollList(HttpExchange ex, UmbrellaUser user) throws IOException {
|
private boolean getPollList(HttpExchange ex, UmbrellaUser user) throws IOException {
|
||||||
|
|||||||
@@ -134,12 +134,26 @@ public class SqliteDb extends BaseDb implements PollDb {
|
|||||||
poll.shares().put(user,rs.getLong(PERMISSION));
|
poll.shares().put(user,rs.getLong(PERMISSION));
|
||||||
list.add(poll);
|
list.add(poll);
|
||||||
}
|
}
|
||||||
|
rs.close();
|
||||||
return list;
|
return list;
|
||||||
} catch (SQLException sqle){
|
} catch (SQLException sqle){
|
||||||
throw failedToLoadObject(TABLE_POLLS);
|
throw failedToLoadObject(TABLE_POLLS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Poll.Evaluation loadEvaluation(String pollId) {
|
||||||
|
try {
|
||||||
|
var result = new Poll.Evaluation();
|
||||||
|
var rs = select(ALL).from(TABLE_SELECTIONS).where(POLL_ID,equal(pollId)).exec(db);
|
||||||
|
while (rs.next()) result.count(rs);
|
||||||
|
rs.close();
|
||||||
|
return result;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw failedToLoadObject(Text.EVALUATION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Poll loadPoll(String id) {
|
public Poll loadPoll(String id) {
|
||||||
try {
|
try {
|
||||||
@@ -193,7 +207,7 @@ public class SqliteDb extends BaseDb implements PollDb {
|
|||||||
try {
|
try {
|
||||||
insertInto(TABLE_POLLS,Field.ID, USER_ID, NAME, DESCRIPTION, PRIVATE)
|
insertInto(TABLE_POLLS,Field.ID, USER_ID, NAME, DESCRIPTION, PRIVATE)
|
||||||
.values(uuid,poll.owner().id(),poll.name(),poll.description(),poll.isPrivate())
|
.values(uuid,poll.owner().id(),poll.name(),poll.description(),poll.isPrivate())
|
||||||
.execute(db);
|
.execute(db).close();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw failedToStoreObject(poll);
|
throw failedToStoreObject(poll);
|
||||||
}
|
}
|
||||||
@@ -203,7 +217,7 @@ public class SqliteDb extends BaseDb implements PollDb {
|
|||||||
private Poll.Option saveNew(String pollId, Poll.Option option) throws SQLException {
|
private Poll.Option saveNew(String pollId, Poll.Option option) throws SQLException {
|
||||||
insertInto(TABLE_OPTIONS, ID, POLL_ID, NAME, DESCRIPTION, STATUS)
|
insertInto(TABLE_OPTIONS, ID, POLL_ID, NAME, DESCRIPTION, STATUS)
|
||||||
.values(option.id(), pollId, option.name(), option.description(), option.status())
|
.values(option.id(), pollId, option.name(), option.description(), option.status())
|
||||||
.execute(db);
|
.execute(db).close();
|
||||||
return option;
|
return option;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +232,8 @@ public class SqliteDb extends BaseDb implements PollDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void saveSelection(String pollId, Map<Integer, Integer> optionsToWeights, Object editor) {
|
private void saveSelection(String pollId, Map<Integer, Integer> optionsToWeights, Object editor) {
|
||||||
var query = insertInto(TABLE_SELECTIONS,POLL_ID,OPTION_ID,USER_ID,WEIGHT);
|
|
||||||
|
var query = replaceInto(TABLE_SELECTIONS,POLL_ID,OPTION_ID,USER_ID,WEIGHT);
|
||||||
for (var entry : optionsToWeights.entrySet()){
|
for (var entry : optionsToWeights.entrySet()){
|
||||||
var optionId = entry.getKey();
|
var optionId = entry.getKey();
|
||||||
var weight = entry.getValue();
|
var weight = entry.getValue();
|
||||||
@@ -233,7 +248,7 @@ public class SqliteDb extends BaseDb implements PollDb {
|
|||||||
|
|
||||||
private Poll update(Poll poll) {
|
private Poll update(Poll poll) {
|
||||||
if (poll.isDirty(NAME, DESCRIPTION)) try {
|
if (poll.isDirty(NAME, DESCRIPTION)) try {
|
||||||
replaceInto(TABLE_POLLS,ID,Field.USER_ID,NAME,DESCRIPTION).values(poll.id(),poll.owner().id(),poll.name(),poll.description()).execute(db);
|
replaceInto(TABLE_POLLS,ID,Field.USER_ID,NAME,DESCRIPTION).values(poll.id(),poll.owner().id(),poll.name(),poll.description()).execute(db).close();
|
||||||
} catch (SQLException e){
|
} catch (SQLException e){
|
||||||
throw failedToStoreObject(poll);
|
throw failedToStoreObject(poll);
|
||||||
}
|
}
|
||||||
@@ -261,7 +276,7 @@ public class SqliteDb extends BaseDb implements PollDb {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
replaceInto(TABLE_OPTIONS,POLL_ID, ID, NAME, DESCRIPTION, STATUS)
|
replaceInto(TABLE_OPTIONS,POLL_ID, ID, NAME, DESCRIPTION, STATUS)
|
||||||
.values(pollId, option.id(), option.name(), option.description(), option.status()).execute(db);
|
.values(pollId, option.id(), option.name(), option.description(), option.status()).execute(db).close();
|
||||||
return option;
|
return option;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user