Add student vote enable toggle to admin panel
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
parent
f7d32ac08c
commit
d6dbd18090
|
@ -14,7 +14,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
FROM docker.io/crystallang/crystal:1.6.2-alpine as crystal
|
||||
FROM docker.io/crystallang/crystal:1.7.2-alpine as crystal
|
||||
FROM tdewolff/minify:latest as minify
|
||||
|
||||
FROM crystal as micrate-deps
|
||||
|
|
|
@ -72,8 +72,25 @@ CREATE TABLE assignments(
|
|||
teacher_id int NOT NULL REFERENCES teachers(id)
|
||||
);
|
||||
|
||||
CREATE TABLE configs(
|
||||
id serial PRIMARY KEY,
|
||||
active boolean NOT NULL,
|
||||
can_vote boolean NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX ON configs(active)
|
||||
WHERE
|
||||
active;
|
||||
|
||||
INSERT INTO
|
||||
configs(active, can_vote)
|
||||
VALUES
|
||||
(TRUE, FALSE);
|
||||
|
||||
-- +micrate Down
|
||||
-- SQL section ' Down ' is executed when this migration is rolled back
|
||||
DROP TABLE configs;
|
||||
|
||||
DROP TABLE assignments;
|
||||
|
||||
DROP TABLE teacher_votes;
|
||||
|
|
|
@ -8,7 +8,7 @@ targets:
|
|||
micrate:
|
||||
main: src/micrate.cr
|
||||
|
||||
crystal: 1.7.1
|
||||
crystal: 1.7.2
|
||||
|
||||
dependencies:
|
||||
micrate:
|
||||
|
|
|
@ -26,7 +26,7 @@ targets:
|
|||
backend:
|
||||
main: src/backend.cr
|
||||
|
||||
crystal: 1.6.2
|
||||
crystal: 1.7.2
|
||||
|
||||
dependencies:
|
||||
clear:
|
||||
|
|
|
@ -216,6 +216,18 @@ module Backend
|
|||
|
||||
Vote.new(vote)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
# Sets if students are allowed to vote
|
||||
def set_voting(context : Context, state : Bool) : Bool?
|
||||
context.admin!
|
||||
|
||||
config = Db::Config.query.where { active == true }.first!
|
||||
config.can_vote = state
|
||||
config.save!
|
||||
|
||||
state
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -143,9 +143,10 @@ module Backend
|
|||
@[GraphQL::Field]
|
||||
# Students can vote
|
||||
def students_can_vote : Bool
|
||||
teacher_role_count = Db::User.query.where(role: Db::UserRole::Teacher).count
|
||||
# teacher_role_count = Db::User.query.where(role: Db::UserRole::Teacher).count
|
||||
|
||||
teacher_role_count > 0 && teacher_role_count == Db::Teacher.query.count
|
||||
# teacher_role_count > 0 && teacher_role_count == Db::Teacher.query.count
|
||||
Db::Config.query.select(:can_vote).where { active == true }.first!.can_vote
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
|
|
|
@ -102,13 +102,13 @@ module Backend
|
|||
|
||||
@[GraphQL::Field]
|
||||
# User's external ID
|
||||
def external_id : Int32
|
||||
case @model.role.to_api
|
||||
def external_id : Int32?
|
||||
case @model.role
|
||||
when Db::UserRole::Teacher
|
||||
@model.teacher
|
||||
@model.teacher.try(&.id)
|
||||
when Db::UserRole::Student
|
||||
@model.student
|
||||
end.not_nil!.id
|
||||
@model.student.try(&.id)
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
module Backend::Db
|
||||
class Config
|
||||
include Clear::Model
|
||||
self.table = :configs
|
||||
|
||||
primary_key type: serial
|
||||
|
||||
column active : Bool
|
||||
column can_vote : Bool
|
||||
end
|
||||
end
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Benutzeraccounts | Mentorenwahl</title>
|
||||
<title>Benutzerexport</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=jetbrains-mono:400" rel="stylesheet" />
|
||||
|
@ -68,6 +68,12 @@
|
|||
height: 1px;
|
||||
background: black;
|
||||
}
|
||||
|
||||
.link {
|
||||
font-style: italic;
|
||||
font-size: small;
|
||||
font-weight: lighter;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -123,6 +129,7 @@
|
|||
<%- group.each do |student| %>
|
||||
<%- if student -%>
|
||||
<td class="border padded">
|
||||
<span class="link">mentorenwahl.dergrimm.net</span>
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td>
|
||||
|
@ -170,10 +177,11 @@
|
|||
<%- group.each do |teacher| %>
|
||||
<%- if teacher -%>
|
||||
<td class="border padded">
|
||||
<span class="link">mentorenwahl.dergrimm.net</span>
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td>
|
||||
<%= teacher.last_name %>, <%= teacher.first_name %>
|
||||
<%= (teacher.first_name ? "#{teacher.last_name}, #{teacher.first_name}" : teacher.last_name).rstrip.rchop(',') %>
|
||||
<hr />
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
mutation SetVoting($state: Boolean!) {
|
||||
setVoting(state: $state)
|
||||
}
|
|
@ -5,6 +5,7 @@ query Users {
|
|||
lastName
|
||||
username
|
||||
role
|
||||
externalId
|
||||
admin
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ scalar UUID
|
|||
|
||||
type User {
|
||||
admin: Boolean!
|
||||
externalId: Int!
|
||||
externalId: Int
|
||||
firstName: String!
|
||||
id: Int!
|
||||
lastName: String!
|
||||
|
@ -162,11 +162,11 @@ type LoginPayload {
|
|||
|
||||
type Mutation {
|
||||
createVote(input: VoteCreateInput!): Vote
|
||||
deleteUser(id: Int!): Int
|
||||
login(password: String!, username: String!): LoginPayload
|
||||
logout: UUID
|
||||
registerTeacher(input: TeacherInput!): Teacher!
|
||||
revokeToken(token: UUID!): UUID!
|
||||
setVoting(state: Boolean!): Boolean
|
||||
startAssignment: Boolean
|
||||
}
|
||||
|
||||
|
|
|
@ -2,5 +2,6 @@ pub mod login;
|
|||
pub mod logout;
|
||||
pub mod register_teacher;
|
||||
pub mod revoke_token;
|
||||
pub mod set_voting;
|
||||
pub mod start_assignment;
|
||||
pub mod vote;
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
use graphql_client::GraphQLQuery;
|
||||
|
||||
#[derive(GraphQLQuery)]
|
||||
#[graphql(
|
||||
schema_path = "graphql/schema.graphql",
|
||||
query_path = "graphql/mutations/set_voting.graphql",
|
||||
response_derives = "Debug",
|
||||
skip_serializing_none
|
||||
)]
|
||||
pub struct SetVoting;
|
|
@ -7,15 +7,15 @@ use crate::graphql;
|
|||
|
||||
pub enum Msg {
|
||||
DoneFetchingCanVote {
|
||||
errors: Option<Vec<String>>,
|
||||
errors: graphql::Errors,
|
||||
can_vote: bool,
|
||||
},
|
||||
DoneFetchingConfig {
|
||||
errors: Option<Vec<String>>,
|
||||
errors: graphql::Errors,
|
||||
min: usize,
|
||||
},
|
||||
DoneFetchingTeachers {
|
||||
errors: Option<Vec<String>>,
|
||||
errors: graphql::Errors,
|
||||
teachers: Vec<graphql::queries::teachers::teachers::TeachersTeachers>,
|
||||
},
|
||||
RadioSelect {
|
||||
|
@ -23,7 +23,7 @@ pub enum Msg {
|
|||
teacher: i64,
|
||||
},
|
||||
Submit,
|
||||
Vote(Option<Vec<String>>),
|
||||
Vote(graphql::Errors),
|
||||
AddSlot,
|
||||
RemoveSlot,
|
||||
Reset,
|
||||
|
@ -38,7 +38,7 @@ pub struct StudentVoteProps {
|
|||
pub struct StudentVote {
|
||||
fetching: bool,
|
||||
can_vote: bool,
|
||||
errors: Option<Vec<String>>,
|
||||
errors: graphql::Errors,
|
||||
min: usize,
|
||||
slots: usize,
|
||||
teachers: Vec<graphql::queries::teachers::teachers::TeachersTeachers>,
|
||||
|
|
|
@ -83,16 +83,19 @@ impl Component for TeacherRegistration {
|
|||
html! {
|
||||
<>
|
||||
<form {onsubmit}>
|
||||
<div>
|
||||
<div class={classes!("field")}>
|
||||
<label for="max_students">
|
||||
{ "Maximale Anzahl von Schülern:" }
|
||||
<br />
|
||||
<input ref={self.max_students.clone()} type="number" id="max_students" name="max_students" min=0 />
|
||||
</label>
|
||||
<div class={classes!("control")}>
|
||||
<input ref={self.max_students.clone()} type="number" id="max_students" name="max_students" min=0 class={classes!("input")} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input type="submit" value="Submit" />
|
||||
<div class={classes!("field")}>
|
||||
<div class={classes!("control")}>
|
||||
<input type="submit" value="Speichern" class={classes!("button", "is-success")} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<components::graphql_errors::GraphQLErrors errors={self.errors.clone()} />
|
||||
|
|
|
@ -5,6 +5,12 @@ use crate::components;
|
|||
use crate::graphql;
|
||||
|
||||
pub enum Msg {
|
||||
DoneFetchingCanVote {
|
||||
errors: graphql::Errors,
|
||||
can_vote: bool,
|
||||
},
|
||||
UpdateCanVote,
|
||||
UpdateCanVoteDone(graphql::Errors),
|
||||
StartAssignment,
|
||||
StartAssignmentDone(Option<Vec<String>>),
|
||||
}
|
||||
|
@ -16,18 +22,71 @@ pub struct AssignmentsProps {
|
|||
|
||||
pub struct Assignments {
|
||||
errors: Option<Vec<String>>,
|
||||
fetching_students_can_vote: bool,
|
||||
students_can_vote: bool,
|
||||
}
|
||||
|
||||
impl Component for Assignments {
|
||||
type Message = Msg;
|
||||
type Properties = AssignmentsProps;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self { errors: None }
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let client = graphql::client(Some(&ctx.props().token)).unwrap();
|
||||
ctx.link().send_future(async move {
|
||||
let response = post_graphql::<graphql::queries::students_can_vote::StudentsCanVote, _>(
|
||||
&client,
|
||||
graphql::URL.as_str(),
|
||||
graphql::queries::students_can_vote::students_can_vote::Variables,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Msg::DoneFetchingCanVote {
|
||||
errors: graphql::convert(response.errors),
|
||||
can_vote: response.data.unwrap().students_can_vote,
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
errors: None,
|
||||
fetching_students_can_vote: true,
|
||||
students_can_vote: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::DoneFetchingCanVote { errors, can_vote } => {
|
||||
self.errors = errors;
|
||||
self.students_can_vote = can_vote;
|
||||
self.fetching_students_can_vote = false;
|
||||
|
||||
true
|
||||
}
|
||||
Msg::UpdateCanVote => {
|
||||
self.students_can_vote = !self.students_can_vote;
|
||||
|
||||
let state = self.students_can_vote;
|
||||
let client = graphql::client(Some(&ctx.props().token)).unwrap();
|
||||
ctx.link().send_future(async move {
|
||||
let response = post_graphql::<graphql::mutations::set_voting::SetVoting, _>(
|
||||
&client,
|
||||
graphql::URL.as_str(),
|
||||
graphql::mutations::set_voting::set_voting::Variables { state },
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Msg::UpdateCanVoteDone(graphql::convert(response.errors))
|
||||
});
|
||||
|
||||
true
|
||||
}
|
||||
Msg::UpdateCanVoteDone(errors) => {
|
||||
self.errors = errors;
|
||||
|
||||
true
|
||||
}
|
||||
Msg::StartAssignment => {
|
||||
let client = graphql::client(Some(&ctx.props().token)).unwrap();
|
||||
ctx.link().send_future(async move {
|
||||
|
@ -60,20 +119,34 @@ impl Component for Assignments {
|
|||
<thead>
|
||||
<tr>
|
||||
<th>{ "Aktion" }</th>
|
||||
<th>{ "Optionen" }</th>
|
||||
<th>{ "Option" }</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
if self.fetching_students_can_vote {
|
||||
<td><components::fetching::Fetching /></td>
|
||||
} else {
|
||||
<td>{ "Wahl erlauben" }</td>
|
||||
<td>
|
||||
<button onclick={ctx.link().callback(|_| Msg::UpdateCanVote)}
|
||||
class={classes!("button", if self.students_can_vote { "is-success" } else { "is-danger" })}
|
||||
>
|
||||
if self.students_can_vote {
|
||||
{ "An" }
|
||||
} else {
|
||||
{ "Aus" }
|
||||
}
|
||||
</button>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{ "Zuweisung starten" }</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>
|
||||
<button class={classes!("button")} onclick={ctx.link().callback(|_| Msg::StartAssignment)}>
|
||||
{ "Ausführen" }
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<button onclick={ctx.link().callback(|_| Msg::StartAssignment)} class={classes!("button")}>
|
||||
{ "Ausführen" }
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
@ -7,6 +7,7 @@ use crate::graphql;
|
|||
pub enum Msg {
|
||||
DoneFetching {
|
||||
errors: graphql::Errors,
|
||||
users: Option<Vec<graphql::queries::users::users::UsersUsers>>,
|
||||
students: Option<Vec<graphql::queries::users_by_role::students::StudentsStudents>>,
|
||||
teachers: Option<Vec<graphql::queries::users_by_role::teachers::TeachersTeachers>>,
|
||||
},
|
||||
|
@ -29,6 +30,7 @@ pub struct Users {
|
|||
tab: UsersTab,
|
||||
fetching: bool,
|
||||
errors: graphql::Errors,
|
||||
users: Option<Vec<graphql::queries::users::users::UsersUsers>>,
|
||||
students: Option<Vec<graphql::queries::users_by_role::students::StudentsStudents>>,
|
||||
teachers: Option<Vec<graphql::queries::users_by_role::teachers::TeachersTeachers>>,
|
||||
}
|
||||
|
@ -40,6 +42,13 @@ impl Component for Users {
|
|||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let client = graphql::client(Some(&ctx.props().token)).unwrap();
|
||||
ctx.link().send_future(async move {
|
||||
let users_response = post_graphql::<graphql::queries::users::Users, _>(
|
||||
&client,
|
||||
graphql::URL.as_str(),
|
||||
graphql::queries::users::users::Variables,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let students_response = post_graphql::<graphql::queries::users_by_role::Students, _>(
|
||||
&client,
|
||||
graphql::URL.as_str(),
|
||||
|
@ -56,20 +65,32 @@ impl Component for Users {
|
|||
.unwrap();
|
||||
|
||||
let x = [
|
||||
users_response.errors.map_or_else(|| vec![], |e| e),
|
||||
students_response.errors.map_or_else(|| vec![], |e| e),
|
||||
teachers_response.errors.map_or_else(|| vec![], |e| e),
|
||||
]
|
||||
.concat();
|
||||
|
||||
if x.is_empty() {
|
||||
let mut users = users_response.data.unwrap().users.unwrap();
|
||||
users.sort_by_key(|x| x.id);
|
||||
|
||||
let mut students = students_response.data.unwrap().students.unwrap();
|
||||
students.sort_by_key(|x| x.id);
|
||||
|
||||
let mut teachers = teachers_response.data.unwrap().teachers;
|
||||
teachers.sort_by_key(|x| x.id);
|
||||
|
||||
Msg::DoneFetching {
|
||||
errors: None,
|
||||
students: Some(students_response.data.unwrap().students.unwrap()),
|
||||
teachers: Some(teachers_response.data.unwrap().teachers),
|
||||
users: Some(users),
|
||||
students: Some(students),
|
||||
teachers: Some(teachers),
|
||||
}
|
||||
} else {
|
||||
Msg::DoneFetching {
|
||||
errors: graphql::convert(Some(x)),
|
||||
users: None,
|
||||
students: None,
|
||||
teachers: None,
|
||||
}
|
||||
|
@ -80,6 +101,7 @@ impl Component for Users {
|
|||
tab: UsersTab::All,
|
||||
fetching: true,
|
||||
errors: None,
|
||||
users: None,
|
||||
students: None,
|
||||
teachers: None,
|
||||
}
|
||||
|
@ -89,11 +111,14 @@ impl Component for Users {
|
|||
match msg {
|
||||
Msg::DoneFetching {
|
||||
errors,
|
||||
users,
|
||||
students,
|
||||
teachers,
|
||||
} => {
|
||||
self.fetching = false;
|
||||
self.errors = errors;
|
||||
|
||||
self.users = users;
|
||||
self.students = students;
|
||||
self.teachers = teachers;
|
||||
|
||||
|
@ -142,58 +167,39 @@ impl Component for Users {
|
|||
<th>{ "Vorname" }</th>
|
||||
<th>{ "Benutzername" }</th>
|
||||
<th>{ "Rolle" }</th>
|
||||
<th><abbr title="ID des externen Benutzerobjekts">{ "Externe Rollen-ID" }</abbr></th>
|
||||
<th><abbr title="ID des externen Benutzerobjekts">{ "Rollen-ID" }</abbr></th>
|
||||
<th>{ "Admin" }</th>
|
||||
<th><abbr title="Wenn Schüler, dann ob Lehrer gewählt wurde">{ "Gewählt" }</abbr></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
for self.students.as_ref().unwrap().iter().map(|s| html! {
|
||||
for self.users.as_ref().unwrap().iter().map(|u| html! {
|
||||
<tr>
|
||||
<td><code>{ &s.user.id }</code></td>
|
||||
<td>{ &s.user.last_name }</td>
|
||||
<td>{ &s.user.first_name }</td>
|
||||
<td><code>{ &s.user.username }</code></td>
|
||||
<td><code>{ &u.id }</code></td>
|
||||
<td>{ &u.last_name }</td>
|
||||
<td>{ &u.first_name }</td>
|
||||
<td><code>{ &u.username }</code></td>
|
||||
|
||||
<td>
|
||||
<code>
|
||||
{
|
||||
match &s.user.role {
|
||||
graphql::queries::users_by_role::students::UserRole::Student => "S",
|
||||
graphql::queries::users_by_role::students::UserRole::Teacher => "T",
|
||||
graphql::queries::users_by_role::students::UserRole::Other(_) => "N/A",
|
||||
match &u.role {
|
||||
graphql::queries::users::users::UserRole::Student => "S",
|
||||
graphql::queries::users::users::UserRole::Teacher => "T",
|
||||
graphql::queries::users::users::UserRole::Other(_) => "N/A",
|
||||
}
|
||||
}
|
||||
</code>
|
||||
</td>
|
||||
<td><code>{ &s.id }</code></td>
|
||||
<td><code>{ if s.user.admin { 1 } else { 0 } }</code></td>
|
||||
<td><code>{ if s.vote.is_some() { 1 } else { 0 } }</code></td>
|
||||
</tr>
|
||||
})
|
||||
}
|
||||
{
|
||||
for self.teachers.as_ref().unwrap().iter().map(|t| html! {
|
||||
<tr>
|
||||
<td><code>{ &t.user.id }</code></td>
|
||||
<td>{ &t.user.last_name }</td>
|
||||
<td>{ &t.user.first_name }</td>
|
||||
<td><code>{ &t.user.username }</code></td>
|
||||
// <td><code>{ if let Some(id) = u.external_id { id } else { "N/A" } }</code></td>
|
||||
<td>
|
||||
<code>
|
||||
{
|
||||
match &t.user.role {
|
||||
graphql::queries::users_by_role::teachers::UserRole::Student => "S",
|
||||
graphql::queries::users_by_role::teachers::UserRole::Teacher => "T",
|
||||
graphql::queries::users_by_role::teachers::UserRole::Other(_) => "N/A",
|
||||
}
|
||||
}
|
||||
</code>
|
||||
if let Some(id) = u.external_id {
|
||||
<code>{ id }</code>
|
||||
} else {
|
||||
<i>{ "N/A" }</i>
|
||||
}
|
||||
</td>
|
||||
<td><code>{ &t.id }</code></td>
|
||||
<td><code>{ if t.user.admin { 1 } else { 0 } }</code></td>
|
||||
<td><code>{ "N/A" }</code></td>
|
||||
<td><code>{ if u.admin { 1 } else { 0 } }</code></td>
|
||||
</tr>
|
||||
})
|
||||
}
|
||||
|
@ -238,7 +244,6 @@ impl Component for Users {
|
|||
<th>{ "Nachname" }</th>
|
||||
<th>{ "Vorname" }</th>
|
||||
<th>{ "Benutzername" }</th>
|
||||
<th>{ "Email" }</th>
|
||||
<th>{ "Benutzer-ID" }</th>
|
||||
<th>{ "Admin" }</th>
|
||||
</tr>
|
||||
|
@ -251,17 +256,6 @@ impl Component for Users {
|
|||
<td>{ &t.user.last_name }</td>
|
||||
<td>{ &t.user.first_name }</td>
|
||||
<td><code>{ &t.user.username }</code></td>
|
||||
<td>
|
||||
<code>
|
||||
{
|
||||
match &t.user.role {
|
||||
graphql::queries::users_by_role::teachers::UserRole::Student => "S",
|
||||
graphql::queries::users_by_role::teachers::UserRole::Teacher => "T",
|
||||
graphql::queries::users_by_role::teachers::UserRole::Other(_) => "N/A",
|
||||
}
|
||||
}
|
||||
</code>
|
||||
</td>
|
||||
<td><code>{ &t.user.id }</code></td>
|
||||
<td><code>{ if t.user.admin { 1 } else { 0 } }</code></td>
|
||||
</tr>
|
||||
|
|
Loading…
Reference in New Issue