Add bulma css
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Dominic Grimm 2022-11-23 20:17:14 +01:00
parent 100f7c8ad6
commit 05d77329ab
No known key found for this signature in database
GPG Key ID: 6F294212DEAAC530
20 changed files with 175 additions and 131 deletions

View File

@ -103,7 +103,7 @@ module Backend
@[GraphQL::Field]
# Starts assignment job of mentors to students
def assign_students(context : Context) : Bool?
def start_assignment(context : Context) : Bool?
context.admin!
Worker::Jobs::AssignStudentsJob.new.enqueue

View File

@ -54,115 +54,8 @@ module Backend
teachers = Db::Teacher.query
.where do
raw("EXISTS (SELECT 1 FROM teacher_votes WHERE teacher_id = teachers.id)") &
(max_students > 0)
max_students > 0
end
.with_teacher_votes
.to_a
vote_index = Hash.zip(teachers.map(&.id), [0] * teachers.size)
teacher_votes : Hash(Int32, Array(TeacherVote)) = Hash.zip(
teachers.map(&.id),
teachers.map do |t|
t.teacher_votes.map do |tv|
vote_index[t.id] += 1
{
student: tv.vote.student.id,
priority: tv.priority,
}
end
end
)
teachers.sort_by! { |t| vote_index[t.id] }
students = Db::Student.query
.with_vote(&.with_teacher_votes(&.order_by(priority: :asc)))
.to_a
student_ids = students.map(&.id)
votes = Hash.zip(
student_ids,
students.map do |s|
s.vote.not_nil!.teacher_votes
.to_a
.select { |tv| teacher_votes.has_key?(tv.teacher.id) }
.map do |tv|
{
teacher: tv.teacher.id,
teacher_max_students: tv.teacher.max_students,
}
end
end
)
best_assignment = {
assignment: {} of Int32 => Assignment,
score: Float32::INFINITY,
}
Backend.config.assignment_possibility_count.times.each do
assignment = {} of Int32 => Assignment
assignment_count = Hash.zip(teachers.map(&.id), [0] * teachers.size)
# teachers.each do |t|
# queue = Deque.new(teacher_votes[t.id].shuffle)
# count = 1
# while count < t.max_students
# break unless x = queue.shift?
# tv = x.not_nil!
# if assignment[tv[:student]]?.nil? || assignment[tv[:student]][:priority] <= tv[:priority]
# assignment[tv[:student]] = {teacher: t.id, priority: tv[:priority]}
# count += 1
# end
# end
# end
votes.to_a.shuffle.each do |s, tvs|
tvs.each_with_index do |tv, i|
if assignment[s]?.nil?
assignment_count[tv[:teacher]] += 1
assignment[s] = {teacher: tv[:teacher], priority: i}
elsif assignment_count[tv[:teacher]] < tv[:teacher_max_students]
assignment_count[assignment[s][:teacher]] -= 1
assignment_count[tv[:teacher]] += 1
assignment[s] = {teacher: tv[:teacher], priority: i}
end
end
end
pp! assignment, assignment_count
score = 0_f32
# positivity = 0
# assignment.each do |s, a|
# ratio = (vote_sizes[s] - a[:priority]) / vote_sizes[s]
# score += 2 ** ratio
# positivity += ratio > 0.5 ? 1 : -1
# end
assignment.each do |s, a|
size = votes[s].size
p! a[:priority], (votes[s].size - a[:priority]) / size
# score += 1 if ((votes[s].size - a[:priority]) / size) >= 0.5
score += a[:priority]
end
# full_score = score ** positivity
if score < best_assignment[:score]
best_assignment = {
assignment: assignment,
score: score,
}
end
end
pp! best_assignment
str = String.build do |str|
str << "===========================\n"
best_assignment[:assignment].each do |s, a|
str << "#{Db::Student.query.find!(s).user.username} : #{Db::Teacher.query.find!(a[:teacher]).user.username} (#{a[:priority]} / #{votes[s].size})\n"
end
str << "===========================\n"
end
print str
end
end
end

View File

@ -2,7 +2,6 @@ FROM lukemathwalker/cargo-chef:latest-rust-1.65.0 as chef
WORKDIR /usr/src/frontend
FROM chef as planner
WORKDIR /usr/src/frontend
RUN mkdir src && touch src/main.rs
COPY ./Cargo.toml .
RUN cargo chef prepare --recipe-path recipe.json
@ -11,10 +10,10 @@ FROM chef as builder
WORKDIR /usr/local/bin
ARG TRUNK_VERSION="v0.16.0"
RUN wget -qO- https://github.com/thedodd/trunk/releases/download/${TRUNK_VERSION}/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf-
WORKDIR /usr/src/frontend
RUN rustup target add wasm32-unknown-unknown
COPY ./.cargo ./.cargo
WORKDIR /usr/src/frontend
COPY --from=planner /usr/src/frontend/recipe.json .
COPY ./.cargo ./.cargo
RUN cargo chef cook --release --recipe-path recipe.json
COPY ./index.html .
COPY ./graphql ./graphql

View File

@ -1,5 +1,8 @@
mutation Login($username: String!, $password: String!) {
login(username: $username, password: $password) {
token
user {
admin
}
}
}

View File

@ -0,0 +1,3 @@
mutation StartAssignment {
startAssignment
}

View File

@ -86,7 +86,6 @@ type LoginPayload {
}
type Mutation {
assignStudents: Boolean
createUser(checkLdap: Boolean! = true, input: UserCreateInput!): User
createVote(input: VoteCreateInput!): Vote
deleteUser(id: Int!): Int
@ -94,6 +93,7 @@ type Mutation {
logout: UUID
registerTeacher(input: TeacherInput!): Teacher
revokeToken(token: UUID!): UUID!
startAssignment: Boolean
}
input TeacherInput {

View File

@ -5,6 +5,10 @@
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.9.4/css/bulma.min.css"
integrity="sha512-HqxHUkJM0SYcbvxUw5P60SzdOTy/QVwA1JJrvaXJv4q7lmbDZCmZaqz01UPOaQveoxfYRv1tHozWGPMcuTBuvQ=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body>

View File

@ -3,6 +3,12 @@ events {
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
server_tokens off;
more_clear_headers Server;

View File

@ -3,7 +3,7 @@ use yew_agent::{Bridge, Bridged};
use yew_router::prelude::*;
use crate::agents;
use crate::cookie_names;
use crate::cookies;
use crate::routes;
pub enum Msg {
@ -38,7 +38,7 @@ impl Component for LoggedInHandler {
Msg::LoggedIn(x) => {
if self.logged_in && !x {
log::info!("Global logout!");
wasm_cookies::delete(cookie_names::TOKEN);
cookies::logout_clear();
ctx.link().history().unwrap().push(routes::Route::Login);
}
self.logged_in = x;

View File

@ -1,4 +0,0 @@
use const_format::concatcp;
pub const BASE: &str = "mentorenwahl_";
pub const TOKEN: &str = concatcp!(BASE, "token");

14
frontend/src/cookies.rs Normal file
View File

@ -0,0 +1,14 @@
use const_format::concatcp;
const BASE: &str = "mentorenwahl_";
pub const TOKEN: &str = concatcp!(BASE, "token");
pub const ADMIN: &str = concatcp!(BASE, "admin");
pub const DELETE_ON_LOGOUT: [&str; 2] = [TOKEN, ADMIN];
pub fn logout_clear() {
for x in DELETE_ON_LOGOUT {
wasm_cookies::delete(x);
}
}

View File

@ -2,4 +2,5 @@ pub mod login;
pub mod logout;
pub mod register_teacher;
pub mod revoke_token;
pub mod start_assignment;
pub mod vote;

View File

@ -0,0 +1,10 @@
use graphql_client::GraphQLQuery;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "graphql/schema.graphql",
query_path = "graphql/mutations/start_assignment.graphql",
response_derives = "Debug",
skip_serializing_none
)]
pub struct StartAssignment;

View File

@ -1,6 +1,6 @@
pub mod agents;
pub mod components;
pub mod cookie_names;
pub mod cookies;
pub mod graphql;
pub mod layouts;
pub mod routes;

View File

@ -33,6 +33,7 @@ impl Component for App {
}
fn main() {
#[cfg(target_arch = "wasm32")]
wasm_logger::init(wasm_logger::Config::default());
yew::start_app::<App>();

View File

@ -5,7 +5,7 @@ use yew_router::prelude::*;
use yew_side_effect::title::Title;
use crate::components;
use crate::cookie_names;
use crate::cookies;
use crate::graphql;
use crate::routes;
@ -50,13 +50,24 @@ impl Component for Login {
.unwrap();
if response.errors.is_some() {
wasm_cookies::delete(cookie_names::TOKEN);
cookies::logout_clear();
} else {
let data = response.data.unwrap().login.unwrap();
wasm_cookies::set(
cookie_names::TOKEN,
&response.data.unwrap().login.unwrap().token,
cookies::TOKEN,
&data.token,
&wasm_cookies::CookieOptions::default(),
)
);
if data.user.admin {
wasm_cookies::set(
cookies::ADMIN,
"1",
&wasm_cookies::CookieOptions::default(),
);
} else {
wasm_cookies::delete(cookies::ADMIN);
}
}
Msg::Login(components::graphql_errors::convert(response.errors))

View File

@ -1,7 +1,7 @@
use yew::prelude::*;
use yew_router::prelude::*;
use crate::cookie_names;
use crate::cookies;
use crate::layouts;
pub mod home;
@ -24,12 +24,12 @@ pub enum Route {
pub fn switch(routes: &Route) -> Html {
let token = {
let tmp = wasm_cookies::get(cookie_names::TOKEN);
let tmp = wasm_cookies::get(cookies::TOKEN);
if let Some(x) = tmp {
if let Ok(y) = x {
Some(y)
} else {
wasm_cookies::delete(cookie_names::TOKEN);
wasm_cookies::delete(cookies::TOKEN);
None
}
} else {
@ -37,6 +37,20 @@ pub fn switch(routes: &Route) -> Html {
}
};
let logged_in = token.is_some();
let admin = {
let tmp = wasm_cookies::get(cookies::ADMIN);
if let Some(x) = tmp {
if let Ok(_) = x {
true
} else {
wasm_cookies::delete(cookies::ADMIN);
false
}
} else {
false
}
};
log::debug!("admin = {:?}", admin);
match routes {
Route::Home => {
@ -52,7 +66,7 @@ pub fn switch(routes: &Route) -> Html {
html! {
<layouts::logged_in::LoggedIn {logged_in}>
<layouts::main::Main token={token.to_owned()} {logged_in}>
<settings::Settings {token} />
<settings::Settings {token} {admin} />
</layouts::main::Main>
</layouts::logged_in::LoggedIn>
}

View File

@ -0,0 +1,82 @@
use graphql_client::reqwest::post_graphql;
use yew::prelude::*;
use crate::components;
use crate::graphql;
pub enum Msg {
StartAssignment,
StartAssignmentDone(Option<Vec<String>>),
}
#[derive(Properties, PartialEq, Eq)]
pub struct AssignmentsProps {
pub token: String,
}
pub struct Assignments {
errors: Option<Vec<String>>,
}
impl Component for Assignments {
type Message = Msg;
type Properties = AssignmentsProps;
fn create(_ctx: &Context<Self>) -> Self {
Self { errors: None }
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::StartAssignment => {
let client = graphql::client(Some(&ctx.props().token)).unwrap();
ctx.link().send_future(async move {
let response =
post_graphql::<graphql::mutations::start_assignment::StartAssignment, _>(
&client,
graphql::URL.as_str(),
graphql::mutations::start_assignment::start_assignment::Variables,
)
.await
.unwrap();
Msg::StartAssignmentDone(components::graphql_errors::convert(response.errors))
});
false
}
Msg::StartAssignmentDone(errors) => {
self.errors = errors;
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<fieldset>
<legend>{ "Zuweisungen" }</legend>
<table>
<tr>
<th>{ "Aktion" }</th>
<th>{ "Optionen" }</th>
</tr>
<tr>
<td>{ "Zuweisung starten" }</td>
<td>
<ul>
<li>
<button onclick={ctx.link().callback(|_| Msg::StartAssignment)}>
{ "Ausführen" }
</button>
</li>
</ul>
</td>
</tr>
</table>
<components::graphql_errors::GraphQLErrors errors={self.errors.to_owned()} />
</fieldset>
}
}
}

View File

@ -1,11 +1,13 @@
use yew::prelude::*;
use yew_side_effect::title::Title;
pub mod assignments;
pub mod tokens;
#[derive(Properties, PartialEq, Eq)]
pub struct SettingsProps {
pub token: Option<String>,
pub admin: bool,
}
pub struct Settings;
@ -25,6 +27,11 @@ impl Component for Settings {
<section>
<tokens::Tokens token={ctx.props().token.as_ref().unwrap().to_owned()} />
</section>
if ctx.props().admin {
<section>
<assignments::Assignments token={ctx.props().token.as_ref().unwrap().to_owned()} />
</section>
}
</>
}
}

View File

@ -82,7 +82,7 @@ impl Component for Tokens {
}
});
true
false
}
Msg::RevokeDone { errors, id } => {
self.errors = errors;
@ -122,7 +122,7 @@ impl Component for Tokens {
}) }
</table>
<components::graphql_errors::GraphQLErrors errors={self.errors.clone()} />
<components::graphql_errors::GraphQLErrors errors={self.errors.to_owned()} />
}
</fieldset>
}