This commit is contained in:
parent
6620dea812
commit
8055a5e4db
4
Makefile
4
Makefile
|
@ -19,10 +19,10 @@
|
||||||
all: prod
|
all: prod
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
docker-compose build --build-arg BUILD_ENV=development
|
docker compose build --build-arg BUILD_ENV=development
|
||||||
|
|
||||||
prod:
|
prod:
|
||||||
docker-compose build
|
docker compose build
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
cd docs && mdbook build
|
cd docs && mdbook build
|
||||||
|
|
|
@ -54,12 +54,13 @@ CREATE TABLE teacher_votes(
|
||||||
id serial PRIMARY KEY,
|
id serial PRIMARY KEY,
|
||||||
vote_id int NOT NULL REFERENCES votes(id),
|
vote_id int NOT NULL REFERENCES votes(id),
|
||||||
teacher_id int NOT NULL REFERENCES teachers(id),
|
teacher_id int NOT NULL REFERENCES teachers(id),
|
||||||
priority int NOT NULL
|
priority int NOT NULL,
|
||||||
|
UNIQUE (vote_id, teacher_id, priority)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE assignments(
|
CREATE TABLE assignments(
|
||||||
id serial PRIMARY KEY,
|
id serial PRIMARY KEY,
|
||||||
student_id int NOT NULL REFERENCES students(id),
|
student_id int NOT NULL REFERENCES students(id) UNIQUE,
|
||||||
teacher_id int NOT NULL REFERENCES teachers(id)
|
teacher_id int NOT NULL REFERENCES teachers(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,12 @@ module Backend
|
||||||
return ATH::Exceptions::BadRequest.new("No request body given") unless request.body
|
return ATH::Exceptions::BadRequest.new("No request body given") unless request.body
|
||||||
query = GraphQLQuery.from_json(request.body.not_nil!)
|
query = GraphQLQuery.from_json(request.body.not_nil!)
|
||||||
|
|
||||||
ATH::StreamedResponse.new(headers: HTTP::Headers{"content-type" => "application/json"}) do |io|
|
ATH::StreamedResponse.new(
|
||||||
|
headers: HTTP::Headers{
|
||||||
|
"content-type" => "application/json",
|
||||||
|
"cache-control" => ["no-cache", "no-store", "max-age=0", "must-revalidate"],
|
||||||
|
}
|
||||||
|
) do |io|
|
||||||
Api::Schema::SCHEMA.execute(
|
Api::Schema::SCHEMA.execute(
|
||||||
io,
|
io,
|
||||||
query.query,
|
query.query,
|
||||||
|
|
|
@ -19,11 +19,13 @@ module Backend
|
||||||
module Jobs
|
module Jobs
|
||||||
# Assigns students to teachers when all students voted
|
# Assigns students to teachers when all students voted
|
||||||
class AssignmentJob < Mosquito::QueuedJob
|
class AssignmentJob < Mosquito::QueuedJob
|
||||||
# run_every 1.minute
|
|
||||||
|
|
||||||
alias TeacherVote = {student: Int32, priority: Int32}
|
alias TeacherVote = {student: Int32, priority: Int32}
|
||||||
alias Assignment = {teacher: Int32, priority: Int32}
|
alias Assignment = {teacher: Int32, priority: Int32}
|
||||||
|
|
||||||
|
def rescheduleable?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
# :ditto:
|
# :ditto:
|
||||||
def perform : Nil
|
def perform : Nil
|
||||||
teacher_count = Db::Teacher.query.count
|
teacher_count = Db::Teacher.query.count
|
||||||
|
|
|
@ -30,7 +30,7 @@ services:
|
||||||
- frontend
|
- frontend
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:alpine
|
image: docker.io/postgres:alpine
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
- db
|
- db
|
||||||
|
@ -41,7 +41,7 @@ services:
|
||||||
- db:/var/lib/postgresql/data
|
- db:/var/lib/postgresql/data
|
||||||
|
|
||||||
adminer:
|
adminer:
|
||||||
image: adminer:standalone
|
image: docker.io/adminer:standalone
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
|
|
|
@ -10,10 +10,10 @@ opt-level = "z"
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
yew = "0.19.3"
|
yew = { version = "0.20.0", features = ["csr"] }
|
||||||
wasm-logger = "0.2.0"
|
wasm-logger = "0.2.0"
|
||||||
log = "0.4.6"
|
log = "0.4.6"
|
||||||
yew-router = "0.16.0"
|
yew-router = "0.17.0"
|
||||||
wee_alloc = "0.4.5"
|
wee_alloc = "0.4.5"
|
||||||
graphql_client = { git = "https://github.com/graphql-rust/graphql-client.git", branch = "main", features = ["reqwest"] }
|
graphql_client = { git = "https://github.com/graphql-rust/graphql-client.git", branch = "main", features = ["reqwest"] }
|
||||||
reqwest = "0.11.12"
|
reqwest = "0.11.12"
|
||||||
|
@ -22,7 +22,7 @@ web-sys = { version = "0.3.60", features = ["Window", "Location"] }
|
||||||
wasm-cookies = "0.1.0"
|
wasm-cookies = "0.1.0"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
const_format = "0.2.30"
|
const_format = "0.2.30"
|
||||||
yew-agent = "0.1.0"
|
|
||||||
yew-side-effect = "0.2.0"
|
|
||||||
uuid = { version = "1.2.2", features = ["serde"] }
|
uuid = { version = "1.2.2", features = ["serde"] }
|
||||||
chrono = { version = "0.4.23", features = ["serde"] }
|
chrono = { version = "0.4.23", features = ["serde"] }
|
||||||
|
yewdux = "0.9.0"
|
||||||
|
bounce = { version = "0.6.0", features = ["helmet"] }
|
||||||
|
|
17
frontend/graphql/queries/users_by_role_students.graphql
Normal file
17
frontend/graphql/queries/users_by_role_students.graphql
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
query Students {
|
||||||
|
students {
|
||||||
|
id
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
username
|
||||||
|
email
|
||||||
|
role
|
||||||
|
admin
|
||||||
|
}
|
||||||
|
vote {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
frontend/graphql/queries/users_by_role_teachers.graphql
Normal file
14
frontend/graphql/queries/users_by_role_teachers.graphql
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
query Teachers {
|
||||||
|
teachers {
|
||||||
|
id
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
username
|
||||||
|
email
|
||||||
|
role
|
||||||
|
admin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,19 @@
|
||||||
|
"The `Boolean` scalar type represents `true` or `false`."
|
||||||
|
scalar Boolean
|
||||||
|
|
||||||
type Config {
|
type Config {
|
||||||
minimumTeacherSelectionCount: Int!
|
minimumTeacherSelectionCount: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point)."
|
||||||
|
scalar Float
|
||||||
|
|
||||||
|
"The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID."
|
||||||
|
scalar ID
|
||||||
|
|
||||||
|
"The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1."
|
||||||
|
scalar Int
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
admins: [User!]
|
admins: [User!]
|
||||||
allStudentsVoted: Boolean
|
allStudentsVoted: Boolean
|
||||||
|
@ -23,6 +35,9 @@ type Query {
|
||||||
votes: [Vote!]
|
votes: [Vote!]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text."
|
||||||
|
scalar String
|
||||||
|
|
||||||
type Student {
|
type Student {
|
||||||
id: Int!
|
id: Int!
|
||||||
user: User!
|
user: User!
|
||||||
|
@ -79,6 +94,67 @@ type Vote {
|
||||||
teacherVotes: [TeacherVote!]!
|
teacherVotes: [TeacherVote!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type __Directive {
|
||||||
|
args: [__InputValue!]!
|
||||||
|
description: String
|
||||||
|
locations: [String!]!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type __EnumValue {
|
||||||
|
deprecationReason: String
|
||||||
|
description: String
|
||||||
|
isDeprecated: Boolean!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type __Field {
|
||||||
|
args: [__InputValue!]!
|
||||||
|
deprecationReason: String
|
||||||
|
description: String
|
||||||
|
isDeprecated: Boolean!
|
||||||
|
name: String!
|
||||||
|
type: __Type!
|
||||||
|
}
|
||||||
|
|
||||||
|
type __InputValue {
|
||||||
|
defaultValue: String
|
||||||
|
description: String
|
||||||
|
name: String!
|
||||||
|
type: __Type!
|
||||||
|
}
|
||||||
|
|
||||||
|
type __Schema {
|
||||||
|
directives: [__Directive!]!
|
||||||
|
mutationType: __Type
|
||||||
|
queryType: __Type!
|
||||||
|
subscriptionType: __Type
|
||||||
|
types: [__Type!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type __Type {
|
||||||
|
description: String
|
||||||
|
enumValues(includeDeprecated: Boolean! = false): [__EnumValue!]
|
||||||
|
fields(includeDeprecated: Boolean! = false): [__Field!]
|
||||||
|
inputFields: [__InputValue!]
|
||||||
|
interfaces: [__Type!]
|
||||||
|
kind: __TypeKind!
|
||||||
|
name: String
|
||||||
|
ofType: __Type
|
||||||
|
possibleTypes: [__Type!]
|
||||||
|
}
|
||||||
|
|
||||||
|
enum __TypeKind {
|
||||||
|
ENUM
|
||||||
|
INPUT_OBJECT
|
||||||
|
INTERFACE
|
||||||
|
LIST
|
||||||
|
NON_NULL
|
||||||
|
OBJECT
|
||||||
|
SCALAR
|
||||||
|
UNION
|
||||||
|
}
|
||||||
|
|
||||||
type LoginPayload {
|
type LoginPayload {
|
||||||
bearer: String!
|
bearer: String!
|
||||||
token: String!
|
token: String!
|
||||||
|
@ -108,4 +184,4 @@ input UserCreateInput {
|
||||||
|
|
||||||
input VoteCreateInput {
|
input VoteCreateInput {
|
||||||
teacherIds: [Int!]!
|
teacherIds: [Int!]!
|
||||||
}
|
}
|
|
@ -1,47 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use yew_agent::{Agent, AgentLink, Context, HandlerId};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub enum Request {
|
|
||||||
LoggedIn(bool),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EventBus {
|
|
||||||
link: AgentLink<Self>,
|
|
||||||
subscribers: HashSet<HandlerId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Agent for EventBus {
|
|
||||||
type Reach = Context<Self>;
|
|
||||||
type Message = ();
|
|
||||||
type Input = Request;
|
|
||||||
type Output = bool;
|
|
||||||
|
|
||||||
fn create(link: AgentLink<Self>) -> Self {
|
|
||||||
Self {
|
|
||||||
link,
|
|
||||||
subscribers: HashSet::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self, _msg: Self::Message) {}
|
|
||||||
|
|
||||||
fn handle_input(&mut self, msg: Self::Input, _id: HandlerId) {
|
|
||||||
match msg {
|
|
||||||
Request::LoggedIn(x) => {
|
|
||||||
for sub in self.subscribers.iter() {
|
|
||||||
self.link.respond(*sub, x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn connected(&mut self, id: HandlerId) {
|
|
||||||
self.subscribers.insert(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn disconnected(&mut self, id: HandlerId) {
|
|
||||||
self.subscribers.remove(&id);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
pub mod logged_in;
|
|
|
@ -15,7 +15,7 @@ impl Component for Footer {
|
||||||
<footer class={classes!("footer")}>
|
<footer class={classes!("footer")}>
|
||||||
<div class={classes!("columns")}>
|
<div class={classes!("columns")}>
|
||||||
<div class={classes!("column", "is-three-quarters")}>
|
<div class={classes!("column", "is-three-quarters")}>
|
||||||
<h3 class={classes!("title", "is-3")}>{ "dergrimm.net" }</h3>
|
<h3 class={classes!("title", "is-3")}>{ "mentorenwahl.dergrimm.net" }</h3>
|
||||||
<p>
|
<p>
|
||||||
<a href="https://git.dergrimm.net/mentorenwahl/mentorenwahl"><strong>{ "Mentorenwahl" }</strong></a> { " von " }
|
<a href="https://git.dergrimm.net/mentorenwahl/mentorenwahl"><strong>{ "Mentorenwahl" }</strong></a> { " von " }
|
||||||
<a href="https://dergrimm.net">{ "Dominic Grimm" }</a> { ". " }
|
<a href="https://dergrimm.net">{ "Dominic Grimm" }</a> { ". " }
|
||||||
|
@ -24,9 +24,11 @@ impl Component for Footer {
|
||||||
</div>
|
</div>
|
||||||
<div class={classes!("column")}>
|
<div class={classes!("column")}>
|
||||||
<p>
|
<p>
|
||||||
<figure class={classes!("image")}>
|
<a href="https://www.rust-lang.org/">
|
||||||
<img src="https://forthebadge.com/images/badges/made-with-rust.svg" />
|
<figure class={classes!("image")}>
|
||||||
</figure>
|
<img src="https://forthebadge.com/images/badges/made-with-rust.svg" />
|
||||||
|
</figure>
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<figure class={classes!("image")}>
|
<figure class={classes!("image")}>
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
use yew::prelude::*;
|
|
||||||
use yew_agent::{Bridge, Bridged};
|
|
||||||
use yew_router::prelude::*;
|
|
||||||
|
|
||||||
use crate::agents;
|
|
||||||
use crate::cookies;
|
|
||||||
use crate::routes;
|
|
||||||
|
|
||||||
pub enum Msg {
|
|
||||||
LoggedIn(bool),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
|
||||||
pub struct LoggedInHandlerProps {
|
|
||||||
pub logged_in: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LoggedInHandler {
|
|
||||||
logged_in: bool,
|
|
||||||
_logged_in_producer: Box<dyn Bridge<agents::logged_in::EventBus>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for LoggedInHandler {
|
|
||||||
type Message = Msg;
|
|
||||||
type Properties = LoggedInHandlerProps;
|
|
||||||
|
|
||||||
fn create(ctx: &Context<Self>) -> Self {
|
|
||||||
Self {
|
|
||||||
logged_in: ctx.props().logged_in,
|
|
||||||
_logged_in_producer: agents::logged_in::EventBus::bridge(
|
|
||||||
ctx.link().callback(Msg::LoggedIn),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
|
||||||
match msg {
|
|
||||||
Msg::LoggedIn(x) => {
|
|
||||||
if self.logged_in && !x {
|
|
||||||
log::info!("Global logout!");
|
|
||||||
cookies::logout_clear();
|
|
||||||
ctx.link().history().unwrap().push(routes::Route::Login);
|
|
||||||
}
|
|
||||||
self.logged_in = x;
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
|
||||||
html! {}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
pub mod fetching;
|
pub mod fetching;
|
||||||
pub mod footer;
|
pub mod footer;
|
||||||
pub mod graphql_errors;
|
pub mod graphql_errors;
|
||||||
pub mod logged_in_handler;
|
|
||||||
pub mod navbar;
|
pub mod navbar;
|
||||||
|
pub mod title;
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
use graphql_client::reqwest::post_graphql;
|
use graphql_client::reqwest::post_graphql;
|
||||||
|
use std::rc::Rc;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_agent::{Dispatched, Dispatcher};
|
|
||||||
use yew_router::prelude::*;
|
use yew_router::prelude::*;
|
||||||
|
use yewdux::prelude::*;
|
||||||
|
|
||||||
use crate::agents;
|
use crate::cookies;
|
||||||
use crate::graphql;
|
use crate::graphql;
|
||||||
use crate::routes;
|
use crate::routes;
|
||||||
|
use crate::stores;
|
||||||
|
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
|
UpdateLoggedIn(Rc<stores::LoggedIn>),
|
||||||
LogoutClicked,
|
LogoutClicked,
|
||||||
Logout,
|
Logout,
|
||||||
BurgerClicked,
|
BurgerClicked,
|
||||||
|
@ -16,12 +19,11 @@ pub enum Msg {
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct NavbarProps {
|
pub struct NavbarProps {
|
||||||
pub token: Option<String>,
|
pub token: Option<String>,
|
||||||
pub logged_in: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Navbar {
|
pub struct Navbar {
|
||||||
logged_in: bool,
|
logged_in: Rc<stores::LoggedIn>,
|
||||||
logged_in_event_bus: Dispatcher<agents::logged_in::EventBus>,
|
logged_in_dispatch: Dispatch<stores::LoggedIn>,
|
||||||
burger_active: bool,
|
burger_active: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,15 +32,23 @@ impl Component for Navbar {
|
||||||
type Properties = NavbarProps;
|
type Properties = NavbarProps;
|
||||||
|
|
||||||
fn create(ctx: &Context<Self>) -> Self {
|
fn create(ctx: &Context<Self>) -> Self {
|
||||||
|
let dispatch = Dispatch::subscribe(ctx.link().callback(Msg::UpdateLoggedIn));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
logged_in: ctx.props().logged_in,
|
logged_in: dispatch.get(),
|
||||||
logged_in_event_bus: agents::logged_in::EventBus::dispatcher(),
|
logged_in_dispatch: dispatch,
|
||||||
burger_active: false,
|
burger_active: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
match msg {
|
match msg {
|
||||||
|
Msg::UpdateLoggedIn(x) => {
|
||||||
|
let tmp = self.logged_in.0;
|
||||||
|
self.logged_in = x;
|
||||||
|
|
||||||
|
tmp != self.logged_in.0
|
||||||
|
}
|
||||||
Msg::LogoutClicked => {
|
Msg::LogoutClicked => {
|
||||||
let client = graphql::client(ctx.props().token.as_ref()).unwrap();
|
let client = graphql::client(ctx.props().token.as_ref()).unwrap();
|
||||||
ctx.link().send_future(async move {
|
ctx.link().send_future(async move {
|
||||||
|
@ -56,8 +66,9 @@ impl Component for Navbar {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
Msg::Logout => {
|
Msg::Logout => {
|
||||||
self.logged_in_event_bus
|
cookies::logout_clear();
|
||||||
.send(agents::logged_in::Request::LoggedIn(false));
|
self.logged_in_dispatch.reduce_mut(|x| x.0 = false);
|
||||||
|
ctx.link().navigator().unwrap().push(&routes::Route::Login);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
Msg::BurgerClicked => {
|
Msg::BurgerClicked => {
|
||||||
|
@ -114,7 +125,7 @@ impl Component for Navbar {
|
||||||
|
|
||||||
<div class={classes!("navbar-item")}>
|
<div class={classes!("navbar-item")}>
|
||||||
<div class={classes!("buttons")}>
|
<div class={classes!("buttons")}>
|
||||||
if self.logged_in {
|
if self.logged_in.0 {
|
||||||
<a onclick={ctx.link().callback(|_| Msg::LogoutClicked)} class={classes!("button")}>
|
<a onclick={ctx.link().callback(|_| Msg::LogoutClicked)} class={classes!("button")}>
|
||||||
{ "Logout" }
|
{ "Logout" }
|
||||||
</a>
|
</a>
|
||||||
|
|
26
frontend/src/components/title.rs
Normal file
26
frontend/src/components/title.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
use bounce::helmet::Helmet;
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct TitleProps {
|
||||||
|
pub title: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Title;
|
||||||
|
|
||||||
|
impl Component for Title {
|
||||||
|
type Message = ();
|
||||||
|
type Properties = TitleProps;
|
||||||
|
|
||||||
|
fn create(_ctx: &Context<Self>) -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
|
html! {
|
||||||
|
<Helmet>
|
||||||
|
<title>{ format!("{} | Mentorenwahl", ctx.props().title) }</title>
|
||||||
|
</Helmet>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,21 @@
|
||||||
use const_format::concatcp;
|
pub const DEFAULT_COOKIE_OPTIONS: wasm_cookies::CookieOptions = wasm_cookies::CookieOptions {
|
||||||
|
domain: None,
|
||||||
|
expires: None,
|
||||||
|
path: None,
|
||||||
|
same_site: wasm_cookies::SameSite::Lax,
|
||||||
|
secure: false,
|
||||||
|
};
|
||||||
|
|
||||||
const BASE: &str = "mentorenwahl_";
|
pub mod names {
|
||||||
|
use const_format::concatcp;
|
||||||
|
|
||||||
pub const TOKEN: &str = concatcp!(BASE, "token");
|
const BASE: &str = "mentorenwahl_";
|
||||||
pub const ADMIN: &str = concatcp!(BASE, "admin");
|
|
||||||
|
|
||||||
pub const DELETE_ON_LOGOUT: [&str; 2] = [TOKEN, ADMIN];
|
pub const TOKEN: &str = concatcp!(BASE, "token");
|
||||||
|
pub const ADMIN: &str = concatcp!(BASE, "admin");
|
||||||
|
}
|
||||||
|
|
||||||
|
const DELETE_ON_LOGOUT: [&str; 2] = [names::TOKEN, names::ADMIN];
|
||||||
|
|
||||||
pub fn logout_clear() {
|
pub fn logout_clear() {
|
||||||
for x in DELETE_ON_LOGOUT {
|
for x in DELETE_ON_LOGOUT {
|
||||||
|
|
|
@ -4,3 +4,4 @@ pub mod ok;
|
||||||
pub mod students_can_vote;
|
pub mod students_can_vote;
|
||||||
pub mod teachers;
|
pub mod teachers;
|
||||||
pub mod tokens;
|
pub mod tokens;
|
||||||
|
pub mod users_by_role;
|
||||||
|
|
|
@ -2,7 +2,6 @@ use graphql_client::GraphQLQuery;
|
||||||
|
|
||||||
use crate::graphql::scalars;
|
use crate::graphql::scalars;
|
||||||
|
|
||||||
// type UUID = String;
|
|
||||||
type UUID = scalars::UUID;
|
type UUID = scalars::UUID;
|
||||||
type Time = String;
|
type Time = String;
|
||||||
|
|
||||||
|
|
19
frontend/src/graphql/queries/users_by_role.rs
Normal file
19
frontend/src/graphql/queries/users_by_role.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
use graphql_client::GraphQLQuery;
|
||||||
|
|
||||||
|
#[derive(GraphQLQuery)]
|
||||||
|
#[graphql(
|
||||||
|
schema_path = "graphql/schema.graphql",
|
||||||
|
query_path = "graphql/queries/users_by_role_students.graphql",
|
||||||
|
response_derives = "Debug",
|
||||||
|
skip_serializing_none
|
||||||
|
)]
|
||||||
|
pub struct Students;
|
||||||
|
|
||||||
|
#[derive(GraphQLQuery)]
|
||||||
|
#[graphql(
|
||||||
|
schema_path = "graphql/schema.graphql",
|
||||||
|
query_path = "graphql/queries/users_by_role_teachers.graphql",
|
||||||
|
response_derives = "Debug",
|
||||||
|
skip_serializing_none
|
||||||
|
)]
|
||||||
|
pub struct Teachers;
|
|
@ -5,7 +5,6 @@ use crate::components;
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct BaseProps {
|
pub struct BaseProps {
|
||||||
pub token: Option<String>,
|
pub token: Option<String>,
|
||||||
pub logged_in: bool,
|
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub children: Children,
|
pub children: Children,
|
||||||
}
|
}
|
||||||
|
@ -23,10 +22,8 @@ impl Component for Base {
|
||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<components::logged_in_handler::LoggedInHandler logged_in={ctx.props().logged_in} />
|
|
||||||
|
|
||||||
<div id="wrapper">
|
<div id="wrapper">
|
||||||
<components::navbar::Navbar token={ctx.props().token.to_owned()} logged_in={ctx.props().logged_in} />
|
<components::navbar::Navbar token={ctx.props().token.to_owned()} />
|
||||||
{ for ctx.props().children.iter() }
|
{ for ctx.props().children.iter() }
|
||||||
</div>
|
</div>
|
||||||
<components::footer::Footer />
|
<components::footer::Footer />
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
|
use std::rc::Rc;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_agent::{Bridge, Bridged};
|
|
||||||
use yew_router::prelude::*;
|
use yew_router::prelude::*;
|
||||||
|
use yewdux::prelude::*;
|
||||||
|
|
||||||
use crate::agents;
|
|
||||||
use crate::routes;
|
use crate::routes;
|
||||||
|
use crate::stores;
|
||||||
|
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
LoggedIn(bool),
|
UpdateLoggedIn(Rc<stores::LoggedIn>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct LoggedInProps {
|
pub struct LoggedInProps {
|
||||||
pub logged_in: bool,
|
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub children: Children,
|
pub children: Children,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LoggedIn {
|
pub struct LoggedIn {
|
||||||
logged_in: bool,
|
logged_in: Rc<stores::LoggedIn>,
|
||||||
_logged_in_producer: Box<dyn Bridge<agents::logged_in::EventBus>>,
|
_logged_in_dispatch: Dispatch<stores::LoggedIn>, // _logged_in_producer: Box<dyn Bridge<agents::logged_in::EventBus>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for LoggedIn {
|
impl Component for LoggedIn {
|
||||||
|
@ -26,33 +26,37 @@ impl Component for LoggedIn {
|
||||||
type Properties = LoggedInProps;
|
type Properties = LoggedInProps;
|
||||||
|
|
||||||
fn create(ctx: &Context<Self>) -> Self {
|
fn create(ctx: &Context<Self>) -> Self {
|
||||||
|
let dispatch = Dispatch::subscribe(ctx.link().callback(Msg::UpdateLoggedIn));
|
||||||
|
let logged_in = dispatch.get();
|
||||||
|
|
||||||
|
if !logged_in.0 {
|
||||||
|
ctx.link().navigator().unwrap().push(&routes::Route::Login)
|
||||||
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
logged_in: ctx.props().logged_in,
|
logged_in,
|
||||||
_logged_in_producer: agents::logged_in::EventBus::bridge(
|
_logged_in_dispatch: dispatch,
|
||||||
ctx.link().callback(Msg::LoggedIn),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
match msg {
|
match msg {
|
||||||
Msg::LoggedIn(x) => {
|
Msg::UpdateLoggedIn(x) => {
|
||||||
let prev = self.logged_in;
|
|
||||||
self.logged_in = x;
|
self.logged_in = x;
|
||||||
prev != self.logged_in
|
if !self.logged_in.0 {
|
||||||
|
ctx.link().navigator().unwrap().push(&routes::Route::Login);
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
if !self.logged_in {
|
|
||||||
ctx.link().history().unwrap().push(routes::Route::Login);
|
|
||||||
}
|
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
if self.logged_in {
|
if self.logged_in.0 {
|
||||||
{ for ctx.props().children.iter() }
|
{ for ctx.props().children.iter() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ use crate::layouts;
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct MainProps {
|
pub struct MainProps {
|
||||||
pub token: Option<String>,
|
pub token: Option<String>,
|
||||||
pub logged_in: bool,
|
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub children: Children,
|
pub children: Children,
|
||||||
}
|
}
|
||||||
|
@ -23,9 +22,9 @@ impl Component for Main {
|
||||||
|
|
||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
html! {
|
html! {
|
||||||
<layouts::base::Base token={ctx.props().token.to_owned()} logged_in={ctx.props().logged_in}>
|
<layouts::base::Base token={ctx.props().token.to_owned()}>
|
||||||
<div class={classes!("columns")}>
|
<div class={classes!("columns", "is-centered")}>
|
||||||
<main class={classes!("column", "is-four-fifths", "mx-auto")}>
|
<main class={classes!("column", "is-four-fifths")}>
|
||||||
{ for ctx.props().children.iter() }
|
{ for ctx.props().children.iter() }
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
pub mod agents;
|
|
||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod cookies;
|
pub mod cookies;
|
||||||
pub mod graphql;
|
pub mod graphql;
|
||||||
pub mod layouts;
|
pub mod layouts;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
|
pub mod stores;
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
use std::rc::Rc;
|
use bounce::helmet::HelmetBridge;
|
||||||
|
use bounce::BounceRoot;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_router::prelude::*;
|
use yew_router::prelude::*;
|
||||||
use yew_side_effect::title::TitleProvider;
|
|
||||||
|
|
||||||
use frontend::routes;
|
|
||||||
|
|
||||||
#[global_allocator]
|
#[global_allocator]
|
||||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||||
|
@ -19,15 +17,13 @@ impl Component for App {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||||
let format_title =
|
|
||||||
Rc::new(|m: &str| format!("{} | mentorenwahl.de", m)) as Rc<dyn Fn(&str) -> String>;
|
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<BrowserRouter>
|
<BounceRoot>
|
||||||
<TitleProvider default_title="mentorenwahl.de" {format_title}>
|
<HelmetBridge default_title="Mentorenwahl" />
|
||||||
<Switch<routes::Route> render={Switch::render(routes::switch)} />
|
<BrowserRouter>
|
||||||
</TitleProvider>
|
<Switch<frontend::routes::Route> render={frontend::routes::switch} />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
</BounceRoot>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,5 +32,5 @@ fn main() {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
wasm_logger::init(wasm_logger::Config::default());
|
wasm_logger::init(wasm_logger::Config::default());
|
||||||
|
|
||||||
yew::start_app::<App>();
|
yew::Renderer::<App>::new().render();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use graphql_client::reqwest::post_graphql;
|
use graphql_client::reqwest::post_graphql;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_side_effect::title::Title;
|
|
||||||
|
|
||||||
use crate::components;
|
use crate::components;
|
||||||
use crate::graphql;
|
use crate::graphql;
|
||||||
|
@ -70,7 +69,7 @@ impl Component for Home {
|
||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<Title value="Home" />
|
<components::title::Title title="Home" />
|
||||||
if self.fetching {
|
if self.fetching {
|
||||||
<components::fetching::Fetching />
|
<components::fetching::Fetching />
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -218,7 +218,7 @@ impl Component for StudentVote {
|
||||||
<p>{ "Noch nicht erlaubt zu wählen." }</p>
|
<p>{ "Noch nicht erlaubt zu wählen." }</p>
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let onsubmit = ctx.link().callback(|e: FocusEvent| {
|
let onsubmit = ctx.link().callback(|e: SubmitEvent| {
|
||||||
e.prevent_default();
|
e.prevent_default();
|
||||||
|
|
||||||
Msg::Submit
|
Msg::Submit
|
||||||
|
@ -241,32 +241,34 @@ impl Component for StudentVote {
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{ format!("{}. Wahl", i + 1) }</legend>
|
<legend>{ format!("{}. Wahl", i + 1) }</legend>
|
||||||
|
|
||||||
{ for self.teachers.iter().enumerate().filter_map(|(j, t)| {
|
{
|
||||||
let checked = curr_t == Some(&Some(t.id));
|
for self.teachers.iter().enumerate().filter_map(|(j, t)| {
|
||||||
|
let checked = curr_t == Some(&Some(t.id));
|
||||||
|
|
||||||
if teachers.contains(&t.id) && !checked {
|
if teachers.contains(&t.id) && !checked {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let id = format!("{}_{}", name, j);
|
let id = format!("{}_{}", name, j);
|
||||||
let teacher_id = t.id;
|
let teacher_id = t.id;
|
||||||
let onchange = ctx.link().callback(move |_| Msg::RadioSelect { priority: i, teacher: teacher_id });
|
let onchange = ctx.link().callback(move |_| Msg::RadioSelect { priority: i, teacher: teacher_id });
|
||||||
|
|
||||||
Some(html! {
|
Some(html! {
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
id={id.to_owned()}
|
id={id.to_owned()}
|
||||||
name={name.to_owned()}
|
name={name.to_owned()}
|
||||||
value={t.user.name.to_owned()}
|
value={t.user.name.to_owned()}
|
||||||
required=true
|
required=true
|
||||||
{onchange}
|
{onchange}
|
||||||
{checked}
|
{checked}
|
||||||
/>
|
/>
|
||||||
<label for={id}>{ &t.user.name }</label>
|
<label for={id}>{ &t.user.name }</label>
|
||||||
</div>
|
</div>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}) }
|
})
|
||||||
|
}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
if i < self.slots - 1 {
|
if i < self.slots - 1 {
|
||||||
<br />
|
<br />
|
||||||
|
|
|
@ -74,7 +74,7 @@ impl Component for TeacherRegistration {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
let onsubmit = ctx.link().callback(|e: FocusEvent| {
|
let onsubmit = ctx.link().callback(|e: SubmitEvent| {
|
||||||
e.prevent_default();
|
e.prevent_default();
|
||||||
|
|
||||||
Msg::Submit
|
Msg::Submit
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_side_effect::title::Title;
|
|
||||||
|
|
||||||
use crate::components;
|
use crate::components;
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct IndexProps {
|
pub struct IndexProps {
|
||||||
pub token: Option<String>,
|
pub token: Option<String>,
|
||||||
pub logged_in: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Index;
|
pub struct Index;
|
||||||
|
@ -22,13 +20,10 @@ impl Component for Index {
|
||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<components::logged_in_handler::LoggedInHandler logged_in={ctx.props().logged_in} />
|
|
||||||
|
|
||||||
<Title value="Mentorenwahl" />
|
|
||||||
<div id="wrapper">
|
<div id="wrapper">
|
||||||
<section class={classes!("hero", "is-success", "is-fullheight")}>
|
<section class={classes!("hero", "is-success", "is-fullheight")}>
|
||||||
<div class={classes!("hero-head")}>
|
<div class={classes!("hero-head")}>
|
||||||
<components::navbar::Navbar token={ctx.props().token.to_owned()} logged_in={ctx.props().logged_in} />
|
<components::navbar::Navbar token={ctx.props().token.to_owned()} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class={classes!("hero-body")}>
|
<div class={classes!("hero-body")}>
|
||||||
|
|
23
frontend/src/routes/info.rs
Normal file
23
frontend/src/routes/info.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::components;
|
||||||
|
|
||||||
|
pub struct Info;
|
||||||
|
|
||||||
|
impl Component for Info {
|
||||||
|
type Message = ();
|
||||||
|
type Properties = ();
|
||||||
|
|
||||||
|
fn create(_ctx: &Context<Self>) -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<components::title::Title title="Informationen" />
|
||||||
|
<h1>{ "Informationen" }</h1>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,27 +1,30 @@
|
||||||
use graphql_client::reqwest::post_graphql;
|
use graphql_client::reqwest::post_graphql;
|
||||||
|
use std::rc::Rc;
|
||||||
use web_sys::HtmlInputElement;
|
use web_sys::HtmlInputElement;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_router::prelude::*;
|
use yew_router::prelude::*;
|
||||||
use yew_side_effect::title::Title;
|
use yewdux::prelude::*;
|
||||||
|
|
||||||
use crate::components;
|
use crate::components;
|
||||||
use crate::cookies;
|
use crate::cookies;
|
||||||
use crate::graphql;
|
use crate::graphql;
|
||||||
use crate::layouts;
|
use crate::layouts;
|
||||||
use crate::routes;
|
use crate::routes;
|
||||||
|
use crate::stores;
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct LoginProps {
|
pub struct LoginProps {
|
||||||
pub token: Option<String>,
|
pub token: Option<String>,
|
||||||
pub logged_in: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
|
UpdateLoggedIn(Rc<stores::LoggedIn>),
|
||||||
Submit,
|
Submit,
|
||||||
Login(Option<Vec<String>>),
|
Login(Option<Vec<String>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Login {
|
pub struct Login {
|
||||||
|
logged_in_dispatch: Dispatch<stores::LoggedIn>,
|
||||||
username: NodeRef,
|
username: NodeRef,
|
||||||
password: NodeRef,
|
password: NodeRef,
|
||||||
fetching: bool,
|
fetching: bool,
|
||||||
|
@ -32,8 +35,9 @@ impl Component for Login {
|
||||||
type Message = Msg;
|
type Message = Msg;
|
||||||
type Properties = LoginProps;
|
type Properties = LoginProps;
|
||||||
|
|
||||||
fn create(_ctx: &Context<Self>) -> Self {
|
fn create(ctx: &Context<Self>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
logged_in_dispatch: Dispatch::subscribe(ctx.link().callback(Msg::UpdateLoggedIn)),
|
||||||
username: NodeRef::default(),
|
username: NodeRef::default(),
|
||||||
password: NodeRef::default(),
|
password: NodeRef::default(),
|
||||||
fetching: false,
|
fetching: false,
|
||||||
|
@ -43,6 +47,7 @@ impl Component for Login {
|
||||||
|
|
||||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
match msg {
|
match msg {
|
||||||
|
Msg::UpdateLoggedIn(_) => false,
|
||||||
Msg::Submit => {
|
Msg::Submit => {
|
||||||
if let (Some(username), Some(password)) = (
|
if let (Some(username), Some(password)) = (
|
||||||
self.username.cast::<HtmlInputElement>().map(|x| x.value()),
|
self.username.cast::<HtmlInputElement>().map(|x| x.value()),
|
||||||
|
@ -66,18 +71,18 @@ impl Component for Login {
|
||||||
let data = response.data.unwrap().login.unwrap();
|
let data = response.data.unwrap().login.unwrap();
|
||||||
|
|
||||||
wasm_cookies::set(
|
wasm_cookies::set(
|
||||||
cookies::TOKEN,
|
cookies::names::TOKEN,
|
||||||
&data.token,
|
&data.token,
|
||||||
&wasm_cookies::CookieOptions::default(),
|
&cookies::DEFAULT_COOKIE_OPTIONS,
|
||||||
);
|
);
|
||||||
if data.user.admin {
|
if data.user.admin {
|
||||||
wasm_cookies::set(
|
wasm_cookies::set(
|
||||||
cookies::ADMIN,
|
cookies::names::ADMIN,
|
||||||
"1",
|
"1",
|
||||||
&wasm_cookies::CookieOptions::default(),
|
&cookies::DEFAULT_COOKIE_OPTIONS,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
wasm_cookies::delete(cookies::ADMIN);
|
wasm_cookies::delete(cookies::names::ADMIN);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +101,8 @@ impl Component for Login {
|
||||||
self.fetching = false;
|
self.fetching = false;
|
||||||
self.errors = errors;
|
self.errors = errors;
|
||||||
if self.errors.is_none() {
|
if self.errors.is_none() {
|
||||||
ctx.link().history().unwrap().push(routes::Route::Home);
|
self.logged_in_dispatch.reduce_mut(|x| x.0 = true);
|
||||||
|
ctx.link().navigator().unwrap().push(&routes::Route::Home);
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
|
@ -106,17 +112,13 @@ impl Component for Login {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
if ctx.props().logged_in {
|
|
||||||
ctx.link().history().unwrap().push(routes::Route::Home);
|
|
||||||
}
|
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<Title value="Login" />
|
<components::title::Title title="Login" />
|
||||||
<layouts::main::Main token={ctx.props().token.to_owned()} logged_in={false}>
|
<layouts::main::Main token={ctx.props().token.to_owned()}>
|
||||||
<div class={classes!("columns")}>
|
<div class={classes!("columns", "is-centered")}>
|
||||||
<div class={classes!("column", "is-one-third", "mx-auto")}>
|
<div class={classes!("column", "is-half")}>
|
||||||
<form onsubmit={ctx.link().callback(|e: FocusEvent| { e.prevent_default(); Msg::Submit })} class={classes!("box")}>
|
<form onsubmit={ctx.link().callback(|e: SubmitEvent| { e.prevent_default(); Msg::Submit })} class={classes!("box")}>
|
||||||
<div class={classes!("field")}>
|
<div class={classes!("field")}>
|
||||||
<p class={classes!("control", "has-icons-left")}>
|
<p class={classes!("control", "has-icons-left")}>
|
||||||
<input ref={self.username.clone()}
|
<input ref={self.username.clone()}
|
||||||
|
|
|
@ -6,6 +6,7 @@ use crate::layouts;
|
||||||
|
|
||||||
pub mod home;
|
pub mod home;
|
||||||
pub mod index;
|
pub mod index;
|
||||||
|
pub mod info;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
pub mod not_found;
|
pub mod not_found;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
|
@ -27,28 +28,27 @@ pub enum Route {
|
||||||
NotFound,
|
NotFound,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn switch(routes: &Route) -> Html {
|
pub fn switch(routes: Route) -> Html {
|
||||||
let token = {
|
let token = {
|
||||||
let tmp = wasm_cookies::get(cookies::TOKEN);
|
let tmp = wasm_cookies::get(cookies::names::TOKEN);
|
||||||
if let Some(x) = tmp {
|
if let Some(x) = tmp {
|
||||||
if let Ok(y) = x {
|
if let Ok(y) = x {
|
||||||
Some(y)
|
Some(y)
|
||||||
} else {
|
} else {
|
||||||
wasm_cookies::delete(cookies::TOKEN);
|
wasm_cookies::delete(cookies::names::TOKEN);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let logged_in = token.is_some();
|
|
||||||
let admin = {
|
let admin = {
|
||||||
let tmp = wasm_cookies::get(cookies::ADMIN);
|
let tmp = wasm_cookies::get(cookies::names::ADMIN);
|
||||||
if let Some(x) = tmp {
|
if let Some(x) = tmp {
|
||||||
if let Ok(_) = x {
|
if let Ok(_) = x {
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
wasm_cookies::delete(cookies::ADMIN);
|
wasm_cookies::delete(cookies::names::ADMIN);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -59,13 +59,13 @@ pub fn switch(routes: &Route) -> Html {
|
||||||
match routes {
|
match routes {
|
||||||
Route::Index => {
|
Route::Index => {
|
||||||
html! {
|
html! {
|
||||||
<index::Index token={token.to_owned()} {logged_in} />
|
<index::Index token={token.to_owned()}/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Route::Home => {
|
Route::Home => {
|
||||||
html! {
|
html! {
|
||||||
<layouts::logged_in::LoggedIn {logged_in}>
|
<layouts::logged_in::LoggedIn>
|
||||||
<layouts::main::Main token={token.to_owned()} {logged_in}>
|
<layouts::main::Main token={token.to_owned()}>
|
||||||
<home::Home {token} />
|
<home::Home {token} />
|
||||||
</layouts::main::Main>
|
</layouts::main::Main>
|
||||||
</layouts::logged_in::LoggedIn>
|
</layouts::logged_in::LoggedIn>
|
||||||
|
@ -73,8 +73,8 @@ pub fn switch(routes: &Route) -> Html {
|
||||||
}
|
}
|
||||||
Route::Settings => {
|
Route::Settings => {
|
||||||
html! {
|
html! {
|
||||||
<layouts::logged_in::LoggedIn {logged_in}>
|
<layouts::logged_in::LoggedIn>
|
||||||
<layouts::main::Main token={token.to_owned()} {logged_in}>
|
<layouts::main::Main token={token.to_owned()}>
|
||||||
<settings::Settings {token} {admin} />
|
<settings::Settings {token} {admin} />
|
||||||
</layouts::main::Main>
|
</layouts::main::Main>
|
||||||
</layouts::logged_in::LoggedIn>
|
</layouts::logged_in::LoggedIn>
|
||||||
|
@ -82,16 +82,16 @@ pub fn switch(routes: &Route) -> Html {
|
||||||
}
|
}
|
||||||
Route::Info => {
|
Route::Info => {
|
||||||
html! {
|
html! {
|
||||||
<layouts::main::Main token={token.to_owned()} {logged_in}>
|
<layouts::main::Main token={token.to_owned()}>
|
||||||
<h1>{ "Informationen" }</h1>
|
<info::Info />
|
||||||
</layouts::main::Main>
|
</layouts::main::Main>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Route::Login => html! {
|
Route::Login => html! {
|
||||||
<login::Login {token} {logged_in} />
|
<login::Login {token} />
|
||||||
},
|
},
|
||||||
Route::NotFound => html! {
|
Route::NotFound => html! {
|
||||||
<not_found::NotFound {token} {logged_in} />
|
<not_found::NotFound {token} />
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_side_effect::title::Title;
|
|
||||||
|
|
||||||
|
use crate::components;
|
||||||
use crate::layouts;
|
use crate::layouts;
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct NotFoundProps {
|
pub struct NotFoundProps {
|
||||||
pub token: Option<String>,
|
pub token: Option<String>,
|
||||||
pub logged_in: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct NotFound;
|
pub struct NotFound;
|
||||||
|
@ -22,8 +21,8 @@ impl Component for NotFound {
|
||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<Title value="404 Not found" />
|
<components::title::Title title="404 Not found" />
|
||||||
<layouts::main::Main token={ctx.props().token.to_owned()} logged_in={ctx.props().logged_in}>
|
<layouts::main::Main token={ctx.props().token.to_owned()}>
|
||||||
<h1>{ "404" }</h1>
|
<h1>{ "404" }</h1>
|
||||||
</layouts::main::Main>
|
</layouts::main::Main>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -54,25 +54,29 @@ impl Component for Assignments {
|
||||||
|
|
||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
html! {
|
html! {
|
||||||
<fieldset>
|
<fieldset class={classes!("fieldset")}>
|
||||||
<legend>{ "Zuweisungen" }</legend>
|
<legend>{ "Zuweisungen" }</legend>
|
||||||
<table>
|
<table class={classes!("table")}>
|
||||||
<tr>
|
<thead>
|
||||||
<th>{ "Aktion" }</th>
|
<tr>
|
||||||
<th>{ "Optionen" }</th>
|
<th>{ "Aktion" }</th>
|
||||||
</tr>
|
<th>{ "Optionen" }</th>
|
||||||
<tr>
|
</tr>
|
||||||
<td>{ "Zuweisung starten" }</td>
|
</thead>
|
||||||
<td>
|
<tbody>
|
||||||
<ul>
|
<tr>
|
||||||
<li>
|
<td>{ "Zuweisung starten" }</td>
|
||||||
<button onclick={ctx.link().callback(|_| Msg::StartAssignment)}>
|
<td>
|
||||||
{ "Ausführen" }
|
<ul>
|
||||||
</button>
|
<li>
|
||||||
</li>
|
<button class={classes!("button")} onclick={ctx.link().callback(|_| Msg::StartAssignment)}>
|
||||||
</ul>
|
{ "Ausführen" }
|
||||||
</td>
|
</button>
|
||||||
</tr>
|
</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<components::graphql_errors::GraphQLErrors errors={self.errors.to_owned()} />
|
<components::graphql_errors::GraphQLErrors errors={self.errors.to_owned()} />
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_side_effect::title::Title;
|
|
||||||
|
use crate::components;
|
||||||
|
|
||||||
pub mod assignments;
|
pub mod assignments;
|
||||||
|
pub mod new_user_modal;
|
||||||
pub mod tokens;
|
pub mod tokens;
|
||||||
|
pub mod users;
|
||||||
|
|
||||||
#[derive(Properties, PartialEq, Eq)]
|
#[derive(Properties, PartialEq, Eq)]
|
||||||
pub struct SettingsProps {
|
pub struct SettingsProps {
|
||||||
|
@ -23,15 +26,18 @@ impl Component for Settings {
|
||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<Title value="Settings" />
|
<components::title::Title title="Settings" />
|
||||||
<section>
|
<section class={classes!("block")}>
|
||||||
<tokens::Tokens token={ctx.props().token.as_ref().unwrap().to_owned()} />
|
<tokens::Tokens token={ctx.props().token.as_ref().unwrap().to_owned()} />
|
||||||
|
</section>
|
||||||
|
if ctx.props().admin {
|
||||||
|
<section class={classes!("block")}>
|
||||||
|
<assignments::Assignments token={ctx.props().token.as_ref().unwrap().to_owned()} />
|
||||||
</section>
|
</section>
|
||||||
if ctx.props().admin {
|
<section class={classes!("block")}>
|
||||||
<section>
|
<users::Users token={ctx.props().token.as_ref().unwrap().to_owned()} />
|
||||||
<assignments::Assignments token={ctx.props().token.as_ref().unwrap().to_owned()} />
|
</section>
|
||||||
</section>
|
}
|
||||||
}
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
74
frontend/src/routes/settings/new_user_modal.rs
Normal file
74
frontend/src/routes/settings/new_user_modal.rs
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
pub enum Msg {
|
||||||
|
CloseModal,
|
||||||
|
Save,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct NewUserModalProps {
|
||||||
|
pub close: Callback<()>,
|
||||||
|
pub active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NewUserModal {
|
||||||
|
username: NodeRef,
|
||||||
|
admin: NodeRef,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for NewUserModal {
|
||||||
|
type Message = Msg;
|
||||||
|
type Properties = NewUserModalProps;
|
||||||
|
|
||||||
|
fn create(ctx: &Context<Self>) -> Self {
|
||||||
|
Self {
|
||||||
|
username: NodeRef::default(),
|
||||||
|
admin: NodeRef::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
|
match msg {
|
||||||
|
Msg::CloseModal => {
|
||||||
|
ctx.props().close.emit(());
|
||||||
|
false
|
||||||
|
}
|
||||||
|
Msg::Save => {
|
||||||
|
log::debug!("self.username = {:?}", self.username);
|
||||||
|
log::debug!("self.admin = {:?}", self.admin);
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
|
let close = ctx.link().callback(|_| Msg::CloseModal);
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class={classes!("modal", if ctx.props().active { Some("is-active") } else { None })}>
|
||||||
|
<div onclick={close.clone()}
|
||||||
|
class={classes!("modal-background")}
|
||||||
|
/>
|
||||||
|
<div class={classes!("modal-card")}>
|
||||||
|
<header class={classes!("modal-card-head")}>
|
||||||
|
<p class={classes!("modal-card-title")}>{ "Neuer Benutzer" }</p>
|
||||||
|
<button onclick={close.clone()} class={classes!("delete")} aria-label="close" />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class={classes!("modal-card-body")}>
|
||||||
|
<form onsubmit={ctx.link().callback(|e: SubmitEvent| { e.prevent_default(); Msg::Save })}>
|
||||||
|
<div class={classes!("field")}>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class={classes!("modal-card-foot")}>
|
||||||
|
<button onclick={close} class={classes!("button")}>{ "Abbrechen" }</button>
|
||||||
|
<button class={classes!("button", "is-success")}>{ "Speichern" }</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,11 +6,11 @@ use crate::graphql;
|
||||||
|
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
DoneFetching {
|
DoneFetching {
|
||||||
errors: Option<Vec<String>>,
|
errors: graphql::Errors,
|
||||||
tokens: Vec<graphql::queries::tokens::tokens::TokensTokens>,
|
tokens: Vec<graphql::queries::tokens::tokens::TokensTokens>,
|
||||||
},
|
},
|
||||||
Revoke(usize),
|
Revoke(usize),
|
||||||
RevokeDone(Option<Vec<String>>),
|
RevokeDone(graphql::Errors),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Properties, PartialEq, Eq)]
|
#[derive(Properties, PartialEq, Eq)]
|
||||||
|
@ -20,7 +20,7 @@ pub struct TokensProps {
|
||||||
|
|
||||||
pub struct Tokens {
|
pub struct Tokens {
|
||||||
fetching: bool,
|
fetching: bool,
|
||||||
errors: Option<Vec<String>>,
|
errors: graphql::Errors,
|
||||||
tokens: Option<Vec<graphql::queries::tokens::tokens::TokensTokens>>,
|
tokens: Option<Vec<graphql::queries::tokens::tokens::TokensTokens>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ impl Component for Tokens {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
Msg::DoneFetching {
|
Msg::DoneFetching {
|
||||||
errors: None,
|
errors: graphql::convert(response.errors),
|
||||||
tokens: response.data.unwrap().tokens.unwrap(),
|
tokens: response.data.unwrap().tokens.unwrap(),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -92,7 +92,7 @@ impl Component for Tokens {
|
||||||
<fieldset class={classes!("fieldset")}>
|
<fieldset class={classes!("fieldset")}>
|
||||||
<legend>{ "Tokens" }</legend>
|
<legend>{ "Tokens" }</legend>
|
||||||
if self.fetching {
|
if self.fetching {
|
||||||
<p>{ "Fetching..." }</p>
|
<components::fetching::Fetching />
|
||||||
} else {
|
} else {
|
||||||
<table class={classes!("table")}>
|
<table class={classes!("table")}>
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -104,20 +104,20 @@ impl Component for Tokens {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{ for self.tokens.as_ref().unwrap().iter().enumerate().map(|(i, t)| {
|
{
|
||||||
html! {
|
for self.tokens.as_ref().unwrap().iter().enumerate().map(|(i, t)| html! {
|
||||||
<tr>
|
<tr>
|
||||||
<th><code>{ &t.id }</code></th>
|
<th><code>{ &t.id }</code></th>
|
||||||
<td><code>{ &t.iat }</code></td>
|
<td><code>{ &t.iat }</code></td>
|
||||||
<td><code>{ &t.exp }</code></td>
|
<td><code>{ &t.exp }</code></td>
|
||||||
<td>
|
<td>
|
||||||
<button onclick={ctx.link().callback(move |_| Msg::Revoke(i))}>
|
<button class={classes!("button")} onclick={ctx.link().callback(move |_| Msg::Revoke(i))}>
|
||||||
{ "Revoke" }
|
{ "Revoke" }
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
})
|
||||||
}) }
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
329
frontend/src/routes/settings/users.rs
Normal file
329
frontend/src/routes/settings/users.rs
Normal file
|
@ -0,0 +1,329 @@
|
||||||
|
use graphql_client::reqwest::post_graphql;
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::components;
|
||||||
|
use crate::graphql;
|
||||||
|
use crate::routes::settings::new_user_modal;
|
||||||
|
|
||||||
|
pub enum Msg {
|
||||||
|
DoneFetching {
|
||||||
|
errors: graphql::Errors,
|
||||||
|
students: Option<Vec<graphql::queries::users_by_role::students::StudentsStudents>>,
|
||||||
|
teachers: Option<Vec<graphql::queries::users_by_role::teachers::TeachersTeachers>>,
|
||||||
|
},
|
||||||
|
SwitchTab(UsersTab),
|
||||||
|
OpenModal,
|
||||||
|
CloseModal,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq, Eq)]
|
||||||
|
pub struct UsersProps {
|
||||||
|
pub token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone, Debug)]
|
||||||
|
pub enum UsersTab {
|
||||||
|
All,
|
||||||
|
Students,
|
||||||
|
Teachers,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Users {
|
||||||
|
tab: UsersTab,
|
||||||
|
fetching: bool,
|
||||||
|
errors: graphql::Errors,
|
||||||
|
students: Option<Vec<graphql::queries::users_by_role::students::StudentsStudents>>,
|
||||||
|
teachers: Option<Vec<graphql::queries::users_by_role::teachers::TeachersTeachers>>,
|
||||||
|
modal_active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for Users {
|
||||||
|
type Message = Msg;
|
||||||
|
type Properties = UsersProps;
|
||||||
|
|
||||||
|
fn create(ctx: &Context<Self>) -> Self {
|
||||||
|
let client = graphql::client(Some(&ctx.props().token)).unwrap();
|
||||||
|
ctx.link().send_future(async move {
|
||||||
|
let students_response = post_graphql::<graphql::queries::users_by_role::Students, _>(
|
||||||
|
&client,
|
||||||
|
graphql::URL.as_str(),
|
||||||
|
graphql::queries::users_by_role::students::Variables,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let teachers_response = post_graphql::<graphql::queries::users_by_role::Teachers, _>(
|
||||||
|
&client,
|
||||||
|
graphql::URL.as_str(),
|
||||||
|
graphql::queries::users_by_role::teachers::Variables,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let x = [
|
||||||
|
students_response.errors.map_or_else(|| vec![], |e| e),
|
||||||
|
teachers_response.errors.map_or_else(|| vec![], |e| e),
|
||||||
|
]
|
||||||
|
.concat();
|
||||||
|
|
||||||
|
if x.is_empty() {
|
||||||
|
Msg::DoneFetching {
|
||||||
|
errors: None,
|
||||||
|
students: Some(students_response.data.unwrap().students.unwrap()),
|
||||||
|
teachers: Some(teachers_response.data.unwrap().teachers),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Msg::DoneFetching {
|
||||||
|
errors: graphql::convert(Some(x)),
|
||||||
|
students: None,
|
||||||
|
teachers: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
tab: UsersTab::All,
|
||||||
|
fetching: true,
|
||||||
|
errors: None,
|
||||||
|
students: None,
|
||||||
|
teachers: None,
|
||||||
|
modal_active: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
|
match msg {
|
||||||
|
Msg::DoneFetching {
|
||||||
|
errors,
|
||||||
|
students,
|
||||||
|
teachers,
|
||||||
|
} => {
|
||||||
|
self.fetching = false;
|
||||||
|
self.errors = errors;
|
||||||
|
self.students = students;
|
||||||
|
self.teachers = teachers;
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Msg::SwitchTab(tab) => {
|
||||||
|
if self.tab == tab {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
self.tab = tab;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Msg::OpenModal => {
|
||||||
|
self.modal_active = true;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Msg::CloseModal => {
|
||||||
|
self.modal_active = false;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
|
html! {
|
||||||
|
<fieldset class={classes!("fieldset")}>
|
||||||
|
<legend>{ "Benutzer" }</legend>
|
||||||
|
<div class={classes!("tabs")}>
|
||||||
|
<ul>
|
||||||
|
<li class={classes!(if self.tab == UsersTab::All { Some("is-active") } else { None })}>
|
||||||
|
<a onclick={ctx.link().callback(|_| Msg::SwitchTab(UsersTab::All))}>{ "Alle" }</a>
|
||||||
|
</li>
|
||||||
|
<li class={classes!(if self.tab == UsersTab::Students { Some("is-active") } else { None })}>
|
||||||
|
<a onclick={ctx.link().callback(|_| Msg::SwitchTab(UsersTab::Students))}>{ "Schüler" }</a>
|
||||||
|
</li>
|
||||||
|
<li class={classes!(if self.tab == UsersTab::Teachers { Some("is-active") } else { None })}>
|
||||||
|
<a onclick={ctx.link().callback(|_| Msg::SwitchTab(UsersTab::Teachers))}>{ "Lehrer" }</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
if self.fetching {
|
||||||
|
<components::fetching::Fetching />
|
||||||
|
} else {
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
match self.tab {
|
||||||
|
UsersTab::All => html! {
|
||||||
|
<table class={classes!("table")}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><abbr title="ID des Benutzers in der Datenbank / API">{ "ID" }</abbr></th>
|
||||||
|
<th>{ "Nachname" }</th>
|
||||||
|
<th>{ "Vorname" }</th>
|
||||||
|
<th><abbr title="LDAP Benutzername">{ "Benutzername" }</abbr></th>
|
||||||
|
<th>{ "Email" }</th>
|
||||||
|
<th>{ "Rolle" }</th>
|
||||||
|
<th><abbr title="ID des externen Benutzerobjekts">{ "Externe 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! {
|
||||||
|
<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>
|
||||||
|
<a href={format!("mailto:{}", s.user.email)}>
|
||||||
|
<code>{ &s.user.email }</code>
|
||||||
|
</a>
|
||||||
|
</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",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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>
|
||||||
|
<a href={format!("mailto:{}", t.user.email)}>
|
||||||
|
<code>{ &t.user.email }</code>
|
||||||
|
</a>
|
||||||
|
</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.id }</code></td>
|
||||||
|
<td><code>{ if t.user.admin { 1 } else { 0 } }</code></td>
|
||||||
|
<td><code>{ "N/A" }</code></td>
|
||||||
|
</tr>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
},
|
||||||
|
UsersTab::Students => html! {
|
||||||
|
<table class={classes!("table")}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><abbr title="ID des Schülers in der Datenbank / API">{ "ID" }</abbr></th>
|
||||||
|
<th>{ "Nachname" }</th>
|
||||||
|
<th>{ "Vorname" }</th>
|
||||||
|
<th><abbr title="LDAP Benutzername">{ "Benutzername" }</abbr></th>
|
||||||
|
<th>{ "Email" }</th>
|
||||||
|
<th>{ "Benutzer-ID" }</th>
|
||||||
|
<th>{ "Admin" }</th>
|
||||||
|
<th>{ "Gewählt" }</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
for self.students.as_ref().unwrap().iter().map(|s| html! {
|
||||||
|
<tr>
|
||||||
|
<td><code>{ &s.id }</code></td>
|
||||||
|
<td>{ &s.user.last_name }</td>
|
||||||
|
<td>{ &s.user.first_name }</td>
|
||||||
|
<td><code>{ &s.user.username }</code></td>
|
||||||
|
<td>
|
||||||
|
<a href={format!("mailto:{}", s.user.email)}>
|
||||||
|
<code>{ &s.user.email }</code>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td><code>{ &s.user.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>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
},
|
||||||
|
UsersTab::Teachers => html! {
|
||||||
|
<table class={classes!("table")}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><abbr title="ID des Lehrers in der Datenbank / API">{ "ID" }</abbr></th>
|
||||||
|
<th>{ "Nachname" }</th>
|
||||||
|
<th>{ "Vorname" }</th>
|
||||||
|
<th><abbr title="LDAP Benutzername">{ "Benutzername" }</abbr></th>
|
||||||
|
<th>{ "Email" }</th>
|
||||||
|
<th>{ "Benutzer-ID" }</th>
|
||||||
|
<th>{ "Admin" }</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
for self.teachers.as_ref().unwrap().iter().map(|t| html! {
|
||||||
|
<tr>
|
||||||
|
<td><code>{ &t.id }</code></td>
|
||||||
|
<td>{ &t.user.last_name }</td>
|
||||||
|
<td>{ &t.user.first_name }</td>
|
||||||
|
<td><code>{ &t.user.username }</code></td>
|
||||||
|
<td>
|
||||||
|
<a href={format!("mailto:{}", t.user.email)}>
|
||||||
|
<code>{ &t.user.email }</code>
|
||||||
|
</a>
|
||||||
|
</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>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<button onclick={ctx.link().callback(|_| Msg::OpenModal)}
|
||||||
|
class={classes!("button", "is-success", "is-fullwidth")}
|
||||||
|
>
|
||||||
|
<span class={classes!("icon")}>
|
||||||
|
<i class={classes!("fas", "fa-user-plus")} />
|
||||||
|
</span>
|
||||||
|
<span>{ "Neuer Benutzer" }</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<new_user_modal::NewUserModal close={ctx.link().callback(|_| Msg::CloseModal)}
|
||||||
|
active={self.modal_active}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<components::graphql_errors::GraphQLErrors errors={self.errors.to_owned()} />
|
||||||
|
}
|
||||||
|
</fieldset>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
frontend/src/stores.rs
Normal file
29
frontend/src/stores.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
use yewdux::prelude::*;
|
||||||
|
|
||||||
|
use crate::cookies;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Store)]
|
||||||
|
pub struct LoggedIn(pub bool);
|
||||||
|
|
||||||
|
impl Default for LoggedIn {
|
||||||
|
fn default() -> Self {
|
||||||
|
let token = {
|
||||||
|
let tmp = wasm_cookies::get(cookies::names::TOKEN);
|
||||||
|
if let Some(x) = tmp {
|
||||||
|
if let Ok(y) = x {
|
||||||
|
Some(y)
|
||||||
|
} else {
|
||||||
|
wasm_cookies::delete(cookies::names::TOKEN);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Self(token.is_some())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Store)]
|
||||||
|
pub struct Token(pub String);
|
|
@ -16,4 +16,4 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
docker-compose exec backend backend "$@"
|
docker compose exec backend backend "$@"
|
||||||
|
|
|
@ -16,4 +16,4 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
docker-compose exec backend micrate "$@"
|
docker compose exec backend micrate "$@"
|
||||||
|
|
Loading…
Reference in a new issue