From d6dbd18090d38568c80298d4231f1c277646d952 Mon Sep 17 00:00:00 2001 From: Dominic Grimm Date: Sat, 4 Feb 2023 12:14:11 +0100 Subject: [PATCH] Add student vote enable toggle to admin panel --- backend/Dockerfile | 2 +- .../20220414171336_create_users.sql | 17 ++++ backend/micrate/shard.yml | 2 +- backend/shard.yml | 2 +- backend/src/backend/api/schema/mutation.cr | 12 +++ backend/src/backend/api/schema/query.cr | 5 +- backend/src/backend/api/schema/user.cr | 10 +- backend/src/backend/db/config.cr | 11 +++ backend/templates/html/users.html.ecr | 12 ++- frontend/graphql/mutations/set_voting.graphql | 3 + frontend/graphql/queries/users.graphql | 1 + frontend/graphql/schema.graphql | 4 +- frontend/src/graphql/mutations/mod.rs | 1 + frontend/src/graphql/mutations/set_voting.rs | 10 ++ frontend/src/routes/home/student_vote.rs | 10 +- .../src/routes/home/teacher_registration.rs | 13 ++- frontend/src/routes/settings/assignments.rs | 93 ++++++++++++++++-- frontend/src/routes/settings/users.rs | 94 +++++++++---------- 18 files changed, 218 insertions(+), 84 deletions(-) create mode 100644 backend/src/backend/db/config.cr create mode 100644 frontend/graphql/mutations/set_voting.graphql create mode 100644 frontend/src/graphql/mutations/set_voting.rs diff --git a/backend/Dockerfile b/backend/Dockerfile index c2cfe66..3399b3a 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -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 diff --git a/backend/db/migrations/20220414171336_create_users.sql b/backend/db/migrations/20220414171336_create_users.sql index 157537d..3346b9b 100644 --- a/backend/db/migrations/20220414171336_create_users.sql +++ b/backend/db/migrations/20220414171336_create_users.sql @@ -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; diff --git a/backend/micrate/shard.yml b/backend/micrate/shard.yml index dff0666..b99e29c 100644 --- a/backend/micrate/shard.yml +++ b/backend/micrate/shard.yml @@ -8,7 +8,7 @@ targets: micrate: main: src/micrate.cr -crystal: 1.7.1 +crystal: 1.7.2 dependencies: micrate: diff --git a/backend/shard.yml b/backend/shard.yml index f426886..d22466a 100644 --- a/backend/shard.yml +++ b/backend/shard.yml @@ -26,7 +26,7 @@ targets: backend: main: src/backend.cr -crystal: 1.6.2 +crystal: 1.7.2 dependencies: clear: diff --git a/backend/src/backend/api/schema/mutation.cr b/backend/src/backend/api/schema/mutation.cr index 7e6c3d8..c262017 100644 --- a/backend/src/backend/api/schema/mutation.cr +++ b/backend/src/backend/api/schema/mutation.cr @@ -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 diff --git a/backend/src/backend/api/schema/query.cr b/backend/src/backend/api/schema/query.cr index 0ca1e67..bf5d0e3 100644 --- a/backend/src/backend/api/schema/query.cr +++ b/backend/src/backend/api/schema/query.cr @@ -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] diff --git a/backend/src/backend/api/schema/user.cr b/backend/src/backend/api/schema/user.cr index e43baab..55ee76f 100644 --- a/backend/src/backend/api/schema/user.cr +++ b/backend/src/backend/api/schema/user.cr @@ -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] diff --git a/backend/src/backend/db/config.cr b/backend/src/backend/db/config.cr new file mode 100644 index 0000000..6de2f50 --- /dev/null +++ b/backend/src/backend/db/config.cr @@ -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 diff --git a/backend/templates/html/users.html.ecr b/backend/templates/html/users.html.ecr index d07443a..600a5fa 100644 --- a/backend/templates/html/users.html.ecr +++ b/backend/templates/html/users.html.ecr @@ -4,7 +4,7 @@ - Benutzeraccounts | Mentorenwahl + Benutzerexport @@ -68,6 +68,12 @@ height: 1px; background: black; } + + .link { + font-style: italic; + font-size: small; + font-weight: lighter; + } @@ -123,6 +129,7 @@ <%- group.each do |student| %> <%- if student -%> + mentorenwahl.dergrimm.net
@@ -170,10 +177,11 @@ <%- group.each do |teacher| %> <%- if teacher -%> + mentorenwahl.dergrimm.net diff --git a/frontend/graphql/mutations/set_voting.graphql b/frontend/graphql/mutations/set_voting.graphql new file mode 100644 index 0000000..6088bfb --- /dev/null +++ b/frontend/graphql/mutations/set_voting.graphql @@ -0,0 +1,3 @@ +mutation SetVoting($state: Boolean!) { + setVoting(state: $state) +} diff --git a/frontend/graphql/queries/users.graphql b/frontend/graphql/queries/users.graphql index ca8f466..99bb7ec 100644 --- a/frontend/graphql/queries/users.graphql +++ b/frontend/graphql/queries/users.graphql @@ -5,6 +5,7 @@ query Users { lastName username role + externalId admin } } diff --git a/frontend/graphql/schema.graphql b/frontend/graphql/schema.graphql index ae0154e..0f38cf2 100644 --- a/frontend/graphql/schema.graphql +++ b/frontend/graphql/schema.graphql @@ -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 } diff --git a/frontend/src/graphql/mutations/mod.rs b/frontend/src/graphql/mutations/mod.rs index 48cf68f..c8b2e1b 100644 --- a/frontend/src/graphql/mutations/mod.rs +++ b/frontend/src/graphql/mutations/mod.rs @@ -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; diff --git a/frontend/src/graphql/mutations/set_voting.rs b/frontend/src/graphql/mutations/set_voting.rs new file mode 100644 index 0000000..17c2b78 --- /dev/null +++ b/frontend/src/graphql/mutations/set_voting.rs @@ -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; diff --git a/frontend/src/routes/home/student_vote.rs b/frontend/src/routes/home/student_vote.rs index b489e40..2769e6b 100644 --- a/frontend/src/routes/home/student_vote.rs +++ b/frontend/src/routes/home/student_vote.rs @@ -7,15 +7,15 @@ use crate::graphql; pub enum Msg { DoneFetchingCanVote { - errors: Option>, + errors: graphql::Errors, can_vote: bool, }, DoneFetchingConfig { - errors: Option>, + errors: graphql::Errors, min: usize, }, DoneFetchingTeachers { - errors: Option>, + errors: graphql::Errors, teachers: Vec, }, RadioSelect { @@ -23,7 +23,7 @@ pub enum Msg { teacher: i64, }, Submit, - Vote(Option>), + Vote(graphql::Errors), AddSlot, RemoveSlot, Reset, @@ -38,7 +38,7 @@ pub struct StudentVoteProps { pub struct StudentVote { fetching: bool, can_vote: bool, - errors: Option>, + errors: graphql::Errors, min: usize, slots: usize, teachers: Vec, diff --git a/frontend/src/routes/home/teacher_registration.rs b/frontend/src/routes/home/teacher_registration.rs index f0d78f4..4b80289 100644 --- a/frontend/src/routes/home/teacher_registration.rs +++ b/frontend/src/routes/home/teacher_registration.rs @@ -83,16 +83,19 @@ impl Component for TeacherRegistration { html! { <>
-
+
+
+ +
-
- +
+
+ +
diff --git a/frontend/src/routes/settings/assignments.rs b/frontend/src/routes/settings/assignments.rs index cd3b0bb..0973088 100644 --- a/frontend/src/routes/settings/assignments.rs +++ b/frontend/src/routes/settings/assignments.rs @@ -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>), } @@ -16,18 +22,71 @@ pub struct AssignmentsProps { pub struct Assignments { errors: Option>, + 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 { errors: None } + fn create(ctx: &Context) -> Self { + let client = graphql::client(Some(&ctx.props().token)).unwrap(); + ctx.link().send_future(async move { + let response = post_graphql::( + &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, 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::( + &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 {
- + + + if self.fetching_students_can_vote { + + } else { + + + } + diff --git a/frontend/src/routes/settings/users.rs b/frontend/src/routes/settings/users.rs index 5a8582c..5019e30 100644 --- a/frontend/src/routes/settings/users.rs +++ b/frontend/src/routes/settings/users.rs @@ -7,6 +7,7 @@ use crate::graphql; pub enum Msg { DoneFetching { errors: graphql::Errors, + users: Option>, students: Option>, teachers: Option>, }, @@ -29,6 +30,7 @@ pub struct Users { tab: UsersTab, fetching: bool, errors: graphql::Errors, + users: Option>, students: Option>, teachers: Option>, } @@ -40,6 +42,13 @@ impl Component for Users { fn create(ctx: &Context) -> Self { let client = graphql::client(Some(&ctx.props().token)).unwrap(); ctx.link().send_future(async move { + let users_response = post_graphql::( + &client, + graphql::URL.as_str(), + graphql::queries::users::users::Variables, + ) + .await + .unwrap(); let students_response = post_graphql::( &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 { - + - { - for self.students.as_ref().unwrap().iter().map(|s| html! { + for self.users.as_ref().unwrap().iter().map(|u| html! { - - - - + + + + - - - - - }) - } - { - for self.teachers.as_ref().unwrap().iter().map(|t| html! { - - - - - + // - - - + }) } @@ -238,7 +244,6 @@ impl Component for Users { - @@ -251,17 +256,6 @@ impl Component for Users { -
- <%= teacher.last_name %>, <%= teacher.first_name %> + <%= (teacher.first_name ? "#{teacher.last_name}, #{teacher.first_name}" : teacher.last_name).rstrip.rchop(',') %>
{ "Aktion" }{ "Optionen" }{ "Option" }
{ "Wahl erlauben" } + +
{ "Zuweisung starten" } -
    -
  • - -
  • -
+
{ "Vorname" } { "Benutzername" } { "Rolle" }{ "Externe Rollen-ID" }{ "Rollen-ID" } { "Admin" }{ "Gewählt" }
{ &s.user.id }{ &s.user.last_name }{ &s.user.first_name }{ &s.user.username }{ &u.id }{ &u.last_name }{ &u.first_name }{ &u.username } { - 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", } } { &s.id }{ if s.user.admin { 1 } else { 0 } }{ if s.vote.is_some() { 1 } else { 0 } }
{ &t.user.id }{ &t.user.last_name }{ &t.user.first_name }{ &t.user.username }{ if let Some(id) = u.external_id { id } else { "N/A" } } - - { - 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", - } - } - + if let Some(id) = u.external_id { + { id } + } else { + { "N/A" } + } { &t.id }{ if t.user.admin { 1 } else { 0 } }{ "N/A" }{ if u.admin { 1 } else { 0 } }
{ "Nachname" } { "Vorname" } { "Benutzername" }{ "Email" } { "Benutzer-ID" } { "Admin" }
{ &t.user.last_name } { &t.user.first_name } { &t.user.username } - - { - 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", - } - } - - { &t.user.id } { if t.user.admin { 1 } else { 0 } }