Update ci
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
parent
55afb91ece
commit
100f7c8ad6
|
@ -39,31 +39,35 @@ RUN shards install --production
|
|||
FROM crystallang/crystal:1.6-alpine as builder
|
||||
WORKDIR /usr/src/mentorenwahl
|
||||
RUN apk add --no-cache pcre2-dev
|
||||
RUN mkdir deps
|
||||
COPY --from=deps /usr/src/mentorenwahl/shard.yml /usr/src/mentorenwahl/shard.lock ./
|
||||
COPY --from=deps /usr/src/mentorenwahl/lib ./lib
|
||||
COPY ./LICENSE .
|
||||
COPY ./Makefile .
|
||||
COPY ./db ./db
|
||||
COPY ./src ./src
|
||||
ARG BUILD_ENV
|
||||
COPY ./Makefile .
|
||||
COPY ./LICENSE .
|
||||
COPY ./db ./db
|
||||
COPY --from=public /usr/src/public/dist ./public
|
||||
COPY ./src ./src
|
||||
RUN if [ "${BUILD_ENV}" = "development" ]; then \
|
||||
make dev; \
|
||||
else \
|
||||
make; \
|
||||
fi
|
||||
RUN mkdir deps
|
||||
RUN if [ "${BUILD_ENV}" = "development" ]; then \
|
||||
ldd bin/backend | tr -s '[:blank:]' '\n' | grep '^/' | \
|
||||
xargs -I % sh -c 'mkdir -p $(dirname deps%); cp % deps%;'; \
|
||||
fi
|
||||
|
||||
FROM scratch as runner
|
||||
LABEL maintainer="Dominic Grimm <dominic@dergrimm.net>" \
|
||||
org.opencontainers.image.description="Backend of Mentorenwahl" \
|
||||
org.opencontainers.image.licenses="GNU GPLv3" \
|
||||
org.opencontainers.image.source="https://git.dergrimm.net/mentorenwahl/mentorenwahl" \
|
||||
org.opencontainers.image.url="https://git.dergrimm.net/mentorenwahl/mentorenwahl"
|
||||
WORKDIR /
|
||||
COPY --from=micrate-builder /usr/src/micrate/bin/micrate ./bin/micrate
|
||||
COPY --from=builder /usr/src/mentorenwahl/db ./db
|
||||
COPY --from=builder /usr/src/mentorenwahl/deps /
|
||||
COPY --from=builder /usr/src/mentorenwahl/bin/backend ./bin/backend
|
||||
COPY --from=builder /usr/src/mentorenwahl/db ./db
|
||||
EXPOSE 80
|
||||
ENTRYPOINT [ "./bin/backend" ]
|
||||
CMD [ "run" ]
|
||||
CMD [ "./bin/backend", "run" ]
|
||||
|
|
|
@ -30,6 +30,7 @@ CREATE TABLE tokens(
|
|||
id uuid PRIMARY KEY,
|
||||
iat timestamp NOT NULL,
|
||||
exp timestamp NOT NULL,
|
||||
active boolean NOT NULL,
|
||||
user_id int NOT NULL REFERENCES users(id)
|
||||
);
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ require "graphql"
|
|||
require "http/headers"
|
||||
require "jwt/errors"
|
||||
require "json"
|
||||
require "uuid"
|
||||
|
||||
module Backend
|
||||
module Api
|
||||
|
@ -41,13 +42,17 @@ module Backend
|
|||
# User's external object
|
||||
getter external
|
||||
|
||||
# JTI of request token
|
||||
getter jti
|
||||
|
||||
def initialize(
|
||||
@development : Bool,
|
||||
@status : Status,
|
||||
@user : Db::User?,
|
||||
@admin : Bool?,
|
||||
@role : Schema::UserRole?,
|
||||
@external : (Db::Teacher | Db::Student)?
|
||||
@external : (Db::Teacher | Db::Student)?,
|
||||
@jti : UUID?
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -63,15 +68,20 @@ module Backend
|
|||
@status = Status::JWTError
|
||||
else
|
||||
if @user = Db::User.find(payload.context.user)
|
||||
@admin = user.not_nil!.admin
|
||||
@role = user.not_nil!.role.to_api
|
||||
@external =
|
||||
case @role.not_nil!
|
||||
when .teacher?
|
||||
@user.not_nil!.teacher
|
||||
when .student?
|
||||
@user.not_nil!.student
|
||||
end
|
||||
@jti = payload.jti
|
||||
if Db::Token.query.where { (id == payload.jti) & active }.first.nil?
|
||||
@status = Status::SessionExpired
|
||||
else
|
||||
@admin = user.not_nil!.admin
|
||||
@role = user.not_nil!.role.to_api
|
||||
@external =
|
||||
case @role.not_nil!
|
||||
when .teacher?
|
||||
@user.not_nil!.teacher
|
||||
when .student?
|
||||
@user.not_nil!.student
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,7 +20,7 @@ module Backend
|
|||
# Schema helper macros
|
||||
module Helpers
|
||||
# Defines DB model field helper functions
|
||||
macro db_object(type)
|
||||
macro db_object(type, id_type)
|
||||
def initialize(@model : {{ type }})
|
||||
end
|
||||
|
||||
|
@ -32,7 +32,7 @@ module Backend
|
|||
|
||||
@[GraphQL::Field]
|
||||
# {{ space_name }}'s ID
|
||||
def id : Int32
|
||||
def id : {{ id_type }}
|
||||
@model.id
|
||||
end
|
||||
end
|
||||
|
|
|
@ -36,9 +36,9 @@ module Backend
|
|||
id: UUID.random(Random::Secure),
|
||||
iat: Time.utc,
|
||||
exp: Time.utc + Backend.config.api.jwt_expiration.minutes,
|
||||
active: true,
|
||||
user_id: user.id
|
||||
)
|
||||
pp! token, typeof(token.id)
|
||||
|
||||
LoginPayload.new(
|
||||
user: User.new(user),
|
||||
|
@ -51,6 +51,29 @@ module Backend
|
|||
)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
# Logs out of account by revoking token
|
||||
def logout(context : Context) : Scalars::UUID?
|
||||
context.authenticated!
|
||||
|
||||
jti = context.jti.not_nil!
|
||||
|
||||
token = Db::Token.find!(jti)
|
||||
token.active = false
|
||||
token.save!
|
||||
|
||||
Scalars::UUID.new(jti)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def revoke_token(context : Context, token : Scalars::UUID) : Scalars::UUID
|
||||
context.authenticated!
|
||||
|
||||
Db::Token.query.find!(token.value).delete
|
||||
|
||||
Scalars::UUID.new(token.value)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
# Creates user
|
||||
def create_user(context : Context, input : UserCreateInput, check_ldap : Bool = true) : User?
|
||||
|
|
|
@ -39,6 +39,16 @@ module Backend
|
|||
User.new(context.user.not_nil!)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
# Active tokens of the current user
|
||||
def tokens(context : Context) : Array(Token)?
|
||||
context.authenticated!
|
||||
|
||||
now = Time.utc
|
||||
|
||||
Db::Token.query.where { (user_id == context.user.not_nil!.id) & (exp > now) & active }.map { |t| Token.new(t) }
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
# User by ID
|
||||
def user(context : Context, id : Int32) : User?
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
require "./scalars/*"
|
|
@ -0,0 +1,17 @@
|
|||
module Backend::Api::Schema::Scalars
|
||||
@[GraphQL::Scalar]
|
||||
class Time < GraphQL::BaseScalar
|
||||
property value
|
||||
|
||||
def initialize(@value : ::Time)
|
||||
end
|
||||
|
||||
def self.from_json(string_or_io)
|
||||
self.new(::Time.parse_iso8601(string_or_io))
|
||||
end
|
||||
|
||||
def to_json(builder : JSON::Builder)
|
||||
builder.scalar(@value.to_rfc3339)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
require "uuid"
|
||||
|
||||
module Backend::Api::Schema::Scalars
|
||||
@[GraphQL::Scalar]
|
||||
class UUID < GraphQL::BaseScalar
|
||||
property value
|
||||
|
||||
def initialize(@value : ::UUID)
|
||||
end
|
||||
|
||||
def self.from_json(string_or_io)
|
||||
self.new(::UUID.from_json(string_or_io))
|
||||
end
|
||||
|
||||
def to_json(builder : JSON::Builder)
|
||||
builder.scalar(@value.to_s)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -22,7 +22,7 @@ module Backend
|
|||
class Student < GraphQL::BaseObject
|
||||
include Helpers
|
||||
|
||||
db_object Db::Student
|
||||
db_object Db::Student, Int32
|
||||
|
||||
@[GraphQL::Field]
|
||||
# Student's user
|
||||
|
|
|
@ -22,7 +22,7 @@ module Backend
|
|||
class Teacher < GraphQL::BaseObject
|
||||
include Helpers
|
||||
|
||||
db_object Db::Teacher
|
||||
db_object Db::Teacher, Int32
|
||||
|
||||
@[GraphQL::Field]
|
||||
# Teacher's user
|
||||
|
|
|
@ -22,7 +22,7 @@ module Backend
|
|||
class TeacherVote < GraphQL::BaseObject
|
||||
include Helpers
|
||||
|
||||
db_object Db::TeacherVote
|
||||
db_object Db::TeacherVote, Int32
|
||||
|
||||
@[GraphQL::Field]
|
||||
# Voted teacher
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
module Backend::Api::Schema
|
||||
@[GraphQL::Object]
|
||||
# Token model
|
||||
class Token < GraphQL::BaseObject
|
||||
include Helpers
|
||||
|
||||
def initialize(@model : Db::Token)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
# Token ID / JTI
|
||||
def id : Scalars::UUID
|
||||
Scalars::UUID.new(@model.id)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
# Time the token was issued at
|
||||
def iat : Scalars::Time
|
||||
Scalars::Time.new(@model.iat)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
# Time for the token to expire
|
||||
def exp : Scalars::Time
|
||||
Scalars::Time.new(@model.exp)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
# Token is expired
|
||||
def expired : Bool
|
||||
@model.exp > Time.utc
|
||||
end
|
||||
end
|
||||
end
|
|
@ -44,7 +44,7 @@ module Backend
|
|||
class User < GraphQL::BaseObject
|
||||
include Helpers
|
||||
|
||||
db_object Db::User
|
||||
db_object Db::User, Int32
|
||||
|
||||
@ldap : Ldap::User?
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ module Backend
|
|||
class Vote < GraphQL::BaseObject
|
||||
include Helpers
|
||||
|
||||
db_object Db::Vote
|
||||
db_object Db::Vote, Int32
|
||||
|
||||
@[GraphQL::Field]
|
||||
# Student who voted
|
||||
|
|
|
@ -14,16 +14,14 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
module Backend
|
||||
module Db
|
||||
class Assignment
|
||||
include Clear::Model
|
||||
self.table = :assignments
|
||||
module Backend::Db
|
||||
class Assignment
|
||||
include Clear::Model
|
||||
self.table = :assignments
|
||||
|
||||
primary_key type: :serial
|
||||
primary_key type: :serial
|
||||
|
||||
belongs_to student : Student
|
||||
belongs_to teacher : Teacher
|
||||
end
|
||||
belongs_to student : Student
|
||||
belongs_to teacher : Teacher
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
module Backend
|
||||
module Db
|
||||
class MicrateDbVersion
|
||||
include Clear::Model
|
||||
self.table = :micrate_db_version
|
||||
module Backend::Db
|
||||
class MicrateDbVersion
|
||||
include Clear::Model
|
||||
self.table = :micrate_db_version
|
||||
|
||||
primary_key type: :serial
|
||||
primary_key type: :serial
|
||||
|
||||
column version_id : Int64
|
||||
column is_applied : Bool
|
||||
column tstamp : Time?
|
||||
end
|
||||
column version_id : Int64
|
||||
column is_applied : Bool
|
||||
column tstamp : Time?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
module Backend
|
||||
module Db
|
||||
class Student
|
||||
include Clear::Model
|
||||
self.table = :students
|
||||
module Backend::Db
|
||||
class Student
|
||||
include Clear::Model
|
||||
self.table = :students
|
||||
|
||||
primary_key type: :serial
|
||||
primary_key type: :serial
|
||||
|
||||
belongs_to user : User
|
||||
belongs_to user : User
|
||||
|
||||
has_one vote : Vote?, foreign_key: :student_id
|
||||
has_one assignment : Assignment?, foreign_key: :student_id
|
||||
end
|
||||
has_one vote : Vote?, foreign_key: :student_id
|
||||
has_one assignment : Assignment?, foreign_key: :student_id
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
module Backend
|
||||
module Db
|
||||
class Teacher
|
||||
include Clear::Model
|
||||
self.table = :teachers
|
||||
module Backend::Db
|
||||
class Teacher
|
||||
include Clear::Model
|
||||
self.table = :teachers
|
||||
|
||||
primary_key type: :serial
|
||||
primary_key type: :serial
|
||||
|
||||
belongs_to user : User
|
||||
belongs_to user : User
|
||||
|
||||
column max_students : Int32
|
||||
column max_students : Int32
|
||||
|
||||
has_many teacher_votes : TeacherVote, foreign_key: :teacher_id
|
||||
has_many assignments : Assignment, foreign_key: :teacher_id
|
||||
end
|
||||
has_many teacher_votes : TeacherVote, foreign_key: :teacher_id
|
||||
has_many assignments : Assignment, foreign_key: :teacher_id
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
module Backend
|
||||
module Db
|
||||
class TeacherVote
|
||||
include Clear::Model
|
||||
self.table = :teacher_votes
|
||||
module Backend::Db
|
||||
class TeacherVote
|
||||
include Clear::Model
|
||||
self.table = :teacher_votes
|
||||
|
||||
primary_key type: :serial
|
||||
primary_key type: :serial
|
||||
|
||||
belongs_to vote : Vote
|
||||
belongs_to teacher : Teacher
|
||||
belongs_to vote : Vote
|
||||
belongs_to teacher : Teacher
|
||||
|
||||
column priority : Int32
|
||||
end
|
||||
column priority : Int32
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,6 +7,7 @@ module Backend::Db
|
|||
|
||||
column iat : Time
|
||||
column exp : Time
|
||||
column active : Bool
|
||||
|
||||
belongs_to user : User
|
||||
end
|
||||
|
|
|
@ -1,40 +1,38 @@
|
|||
module Backend
|
||||
module Db
|
||||
Clear.enum UserRole, :teacher, :student
|
||||
module Backend::Db
|
||||
Clear.enum UserRole, :teacher, :student
|
||||
|
||||
struct UserRole
|
||||
# API representation of the enum
|
||||
def to_api : Api::Schema::UserRole
|
||||
case self
|
||||
when Student
|
||||
Api::Schema::UserRole::Student
|
||||
when Teacher
|
||||
Api::Schema::UserRole::Teacher
|
||||
else
|
||||
raise "Invalid enum value for UserRole"
|
||||
end
|
||||
end
|
||||
|
||||
# DB representation of the enum
|
||||
def self.from_api(role : Api::Schema::UserRole) : self
|
||||
role.to_db
|
||||
struct UserRole
|
||||
# API representation of the enum
|
||||
def to_api : Api::Schema::UserRole
|
||||
case self
|
||||
when Student
|
||||
Api::Schema::UserRole::Student
|
||||
when Teacher
|
||||
Api::Schema::UserRole::Teacher
|
||||
else
|
||||
raise "Invalid enum value for UserRole"
|
||||
end
|
||||
end
|
||||
|
||||
class User
|
||||
include Clear::Model
|
||||
self.table = :users
|
||||
|
||||
primary_key type: :serial
|
||||
|
||||
column username : String
|
||||
column role : UserRole
|
||||
column admin : Bool = false
|
||||
|
||||
has_one student : Student?, foreign_key: :user_id
|
||||
has_one teacher : Teacher?, foreign_key: :user_id
|
||||
|
||||
has_many tokens : Token, foreign_key: :user_id
|
||||
# DB representation of the enum
|
||||
def self.from_api(role : Api::Schema::UserRole) : self
|
||||
role.to_db
|
||||
end
|
||||
end
|
||||
|
||||
class User
|
||||
include Clear::Model
|
||||
self.table = :users
|
||||
|
||||
primary_key type: :serial
|
||||
|
||||
column username : String
|
||||
column role : UserRole
|
||||
column admin : Bool = false
|
||||
|
||||
has_one student : Student?, foreign_key: :user_id
|
||||
has_one teacher : Teacher?, foreign_key: :user_id
|
||||
|
||||
has_many tokens : Token, foreign_key: :user_id
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
module Backend
|
||||
module Db
|
||||
class Vote
|
||||
include Clear::Model
|
||||
self.table = :votes
|
||||
module Backend::Db
|
||||
class Vote
|
||||
include Clear::Model
|
||||
self.table = :votes
|
||||
|
||||
primary_key type: :serial
|
||||
primary_key type: :serial
|
||||
|
||||
belongs_to student : Student
|
||||
belongs_to student : Student
|
||||
|
||||
has_many teacher_votes : TeacherVote, foreign_key: :vote_id
|
||||
end
|
||||
has_many teacher_votes : TeacherVote, foreign_key: :vote_id
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,28 +14,27 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
require "random/secure"
|
||||
|
||||
module Backend
|
||||
module Worker
|
||||
module Jobs
|
||||
# Assigns students to teachers when all students voted
|
||||
class AssignStudentsJob < Mosquito::QueuedJob
|
||||
def rescheduleable?
|
||||
# run_every 1.minute
|
||||
|
||||
def rescheduleable? : Bool
|
||||
false
|
||||
end
|
||||
|
||||
alias TeacherVote = {student: Int32, priority: Int32}
|
||||
alias Assignment = {teacher: Int32, priority: Int32}
|
||||
|
||||
# :ditto:
|
||||
def perform : Nil
|
||||
user_count = Db::User.query.count
|
||||
teacher_count = Db::Teacher.query.count
|
||||
student_count = Db::Student.query.count
|
||||
vote_count = Db::Vote.query.count
|
||||
|
||||
if user_count == 0
|
||||
log "No users found, skipping assignment"
|
||||
fail
|
||||
elsif teacher_count == 0
|
||||
if teacher_count == 0
|
||||
log "No teachers found, skipping assignment"
|
||||
fail
|
||||
elsif student_count == 0
|
||||
|
@ -47,67 +46,123 @@ module Backend
|
|||
elsif vote_count < student_count
|
||||
log "Not all students voted, skipping assignment"
|
||||
fail
|
||||
elsif Db::Assignment.query.count > 0
|
||||
log "Assignment has already run, skipping another assignment"
|
||||
fail
|
||||
end
|
||||
|
||||
# possibilities = [] of Possibility
|
||||
# teachers = Db::Teacher.query.to_a.select! { |t| t.assignments.count < t.max_students }
|
||||
# empty_assignment = Hash.zip(teachers.map(&.id), [[] of StudentAssignment] * teachers.size)
|
||||
# random_votes = Db::Student.query.to_a.select!(&.vote).map do |s|
|
||||
# {
|
||||
# student: s.id,
|
||||
# teachers: s.vote.not_nil!.teacher_votes.to_a
|
||||
# .sort_by!(&.priority)
|
||||
# .reverse!
|
||||
# .map(&.teacher.id),
|
||||
# }
|
||||
# end
|
||||
|
||||
# Backend.config.assignment_retry_count.times do
|
||||
# pp! random_votes.shuffle!(Random::Secure)
|
||||
# a = empty_assignment.clone
|
||||
# random_votes.
|
||||
|
||||
# possibilities << {assignment: a, weighting: 0}
|
||||
# end
|
||||
|
||||
# pp! possibilities
|
||||
|
||||
# teacher_ids = Db::Teacher.query
|
||||
# .select("id")
|
||||
# .where { raw("(SELECT COUNT(*) FROM assignments WHERE teacher_id = teachers.id)") < max_students }
|
||||
# .map(&.id)
|
||||
students = Db::Student.query
|
||||
teachers = Db::Teacher.query
|
||||
.where do
|
||||
raw("NOT EXISTS (SELECT 1 FROM assignments WHERE student_id = students.id)") &
|
||||
raw("EXISTS (SELECT 1 FROM votes WHERE student_id = students.id)")
|
||||
raw("EXISTS (SELECT 1 FROM teacher_votes WHERE teacher_id = teachers.id)") &
|
||||
(max_students > 0)
|
||||
end
|
||||
.with_vote(&.with_teacher_votes(&.order_by(:priority, :asc)))
|
||||
# student_ids = students.map(&.id)
|
||||
.with_teacher_votes
|
||||
.to_a
|
||||
vote_index = Hash.zip(teachers.map(&.id), [0] * teachers.size)
|
||||
teacher_votes : Hash(Int32, Array(TeacherVote)) = Hash.zip(
|
||||
teachers.map(&.id),
|
||||
teachers.map do |t|
|
||||
t.teacher_votes.map do |tv|
|
||||
vote_index[t.id] += 1
|
||||
|
||||
# votes = Hash.zip(
|
||||
# student_ids,
|
||||
# students.map do |s|
|
||||
# s.vote.not_nil!.teacher_votes.map(&.teacher_id)
|
||||
# end,
|
||||
# )
|
||||
# pp! votes, student_ids
|
||||
{
|
||||
student: tv.vote.student.id,
|
||||
priority: tv.priority,
|
||||
}
|
||||
end
|
||||
end
|
||||
)
|
||||
teachers.sort_by! { |t| vote_index[t.id] }
|
||||
|
||||
random_votes = students.map do |s|
|
||||
{
|
||||
student: s.id,
|
||||
teachers: s.vote.not_nil!.teacher_votes.map(&.teacher_id),
|
||||
}
|
||||
students = Db::Student.query
|
||||
.with_vote(&.with_teacher_votes(&.order_by(priority: :asc)))
|
||||
.to_a
|
||||
student_ids = students.map(&.id)
|
||||
votes = Hash.zip(
|
||||
student_ids,
|
||||
students.map do |s|
|
||||
s.vote.not_nil!.teacher_votes
|
||||
.to_a
|
||||
.select { |tv| teacher_votes.has_key?(tv.teacher.id) }
|
||||
.map do |tv|
|
||||
{
|
||||
teacher: tv.teacher.id,
|
||||
teacher_max_students: tv.teacher.max_students,
|
||||
}
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
best_assignment = {
|
||||
assignment: {} of Int32 => Assignment,
|
||||
score: Float32::INFINITY,
|
||||
}
|
||||
|
||||
Backend.config.assignment_possibility_count.times.each do
|
||||
assignment = {} of Int32 => Assignment
|
||||
assignment_count = Hash.zip(teachers.map(&.id), [0] * teachers.size)
|
||||
# teachers.each do |t|
|
||||
# queue = Deque.new(teacher_votes[t.id].shuffle)
|
||||
|
||||
# count = 1
|
||||
# while count < t.max_students
|
||||
# break unless x = queue.shift?
|
||||
# tv = x.not_nil!
|
||||
|
||||
# if assignment[tv[:student]]?.nil? || assignment[tv[:student]][:priority] <= tv[:priority]
|
||||
# assignment[tv[:student]] = {teacher: t.id, priority: tv[:priority]}
|
||||
# count += 1
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
votes.to_a.shuffle.each do |s, tvs|
|
||||
tvs.each_with_index do |tv, i|
|
||||
if assignment[s]?.nil?
|
||||
assignment_count[tv[:teacher]] += 1
|
||||
assignment[s] = {teacher: tv[:teacher], priority: i}
|
||||
elsif assignment_count[tv[:teacher]] < tv[:teacher_max_students]
|
||||
assignment_count[assignment[s][:teacher]] -= 1
|
||||
assignment_count[tv[:teacher]] += 1
|
||||
assignment[s] = {teacher: tv[:teacher], priority: i}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
pp! assignment, assignment_count
|
||||
|
||||
score = 0_f32
|
||||
# positivity = 0
|
||||
# assignment.each do |s, a|
|
||||
# ratio = (vote_sizes[s] - a[:priority]) / vote_sizes[s]
|
||||
# score += 2 ** ratio
|
||||
# positivity += ratio > 0.5 ? 1 : -1
|
||||
# end
|
||||
assignment.each do |s, a|
|
||||
size = votes[s].size
|
||||
p! a[:priority], (votes[s].size - a[:priority]) / size
|
||||
# score += 1 if ((votes[s].size - a[:priority]) / size) >= 0.5
|
||||
score += a[:priority]
|
||||
end
|
||||
|
||||
# full_score = score ** positivity
|
||||
if score < best_assignment[:score]
|
||||
best_assignment = {
|
||||
assignment: assignment,
|
||||
score: score,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
assignments = [] of {assignment: Hash(Int32, Array(Int32)), weighting: Int32}
|
||||
# empty_assignment = Hash.zip(teacher_ids, [[] of {assignment: Hash(Int32, Array(Int32)), weighting: Int32}] * teacher_ids.size)
|
||||
Backend.config.assignment_possibility_count.times do
|
||||
random_votes.shuffle!(Random::Secure)
|
||||
pp! best_assignment
|
||||
|
||||
# votes = random_votes.dup
|
||||
# a = empty_assignment.clone
|
||||
str = String.build do |str|
|
||||
str << "===========================\n"
|
||||
best_assignment[:assignment].each do |s, a|
|
||||
str << "#{Db::Student.query.find!(s).user.username} : #{Db::Teacher.query.find!(a[:teacher]).user.username} (#{a[:priority]} / #{votes[s].size})\n"
|
||||
end
|
||||
str << "===========================\n"
|
||||
end
|
||||
pp! assignments
|
||||
print str
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,9 +15,13 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
server_tokens off;
|
||||
more_clear_headers Server;
|
||||
|
||||
server {
|
||||
location / {
|
||||
proxy_pass http://frontend/;
|
||||
|
|
|
@ -18,7 +18,7 @@ version: "3"
|
|||
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
image: byjg/nginx-extras
|
||||
restart: always
|
||||
volumes:
|
||||
- ./config/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
|
@ -58,7 +58,7 @@ services:
|
|||
- redis:/data
|
||||
|
||||
backend:
|
||||
image: mentorenwahl/backend
|
||||
image: git.dergrimm.net/mentorenwahl/backend:latest
|
||||
build:
|
||||
context: ./backend
|
||||
args:
|
||||
|
@ -96,7 +96,7 @@ services:
|
|||
BACKEND_LDAP_CACHE_REFRESH_INTERVAL: ${BACKEND_LDAP_CACHE_REFRESH_INTERVAL}
|
||||
|
||||
frontend:
|
||||
image: mentorenwahl/frontend
|
||||
image: git.dergrimm.net/mentorenwahl/frontend:latest
|
||||
build:
|
||||
context: ./frontend
|
||||
restart: always
|
||||
|
|
|
@ -36,6 +36,11 @@ WORKDIR /usr/src/public
|
|||
COPY --from=public /usr/src/public .
|
||||
RUN find . -name "*.wasm" -type f | xargs -I % wasm-opt % -o % -O --intrinsic-lowering -Oz
|
||||
|
||||
FROM nginx:alpine as runner
|
||||
FROM byjg/nginx-extras as runner
|
||||
LABEL maintainer="Dominic Grimm <dominic@dergrimm.net>" \
|
||||
org.opencontainers.image.description="Frontend of Mentorenwahl" \
|
||||
org.opencontainers.image.licenses="GNU GPLv3" \
|
||||
org.opencontainers.image.source="https://git.dergrimm.net/mentorenwahl/mentorenwahl" \
|
||||
org.opencontainers.image.url="https://git.dergrimm.net/mentorenwahl/mentorenwahl"
|
||||
COPY ./nginx.conf /etc/nginx/nginx.conf
|
||||
COPY --from=binaryen /usr/src/public /var/www/html
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
mutation Logout {
|
||||
logout
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
mutation RevokeToken($token: UUID!) {
|
||||
revokeToken(token: $token)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
query Tokens {
|
||||
tokens {
|
||||
id
|
||||
iat
|
||||
exp
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ type Query {
|
|||
teacherVote(id: Int!): TeacherVote
|
||||
teacherVotes: [TeacherVote!]
|
||||
teachers: [Teacher!]!
|
||||
tokens: [Token!]
|
||||
user(id: Int!): User
|
||||
userByUsername(username: String!): User
|
||||
users: [User!]
|
||||
|
@ -42,6 +43,17 @@ type TeacherVote {
|
|||
vote: Vote!
|
||||
}
|
||||
|
||||
scalar Time
|
||||
|
||||
type Token {
|
||||
exp: Time!
|
||||
expired: Boolean!
|
||||
iat: Time!
|
||||
id: UUID!
|
||||
}
|
||||
|
||||
scalar UUID
|
||||
|
||||
type User {
|
||||
admin: Boolean!
|
||||
email: String!
|
||||
|
@ -79,7 +91,9 @@ 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!
|
||||
}
|
||||
|
||||
input TeacherInput {
|
||||
|
|
|
@ -1,29 +1,10 @@
|
|||
user nginx;
|
||||
worker_processes auto;
|
||||
|
||||
error_log /var/log/nginx/error.log notice;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
#gzip on;
|
||||
server_tokens off;
|
||||
more_clear_headers Server;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
|
|
@ -4,6 +4,7 @@ use graphql_client::GraphQLQuery;
|
|||
#[graphql(
|
||||
schema_path = "graphql/schema.graphql",
|
||||
query_path = "graphql/mutations/login.graphql",
|
||||
response_derives = "Debug"
|
||||
response_derives = "Debug",
|
||||
skip_serializing_none
|
||||
)]
|
||||
pub struct Login;
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
use graphql_client::GraphQLQuery;
|
||||
|
||||
type UUID = String;
|
||||
|
||||
#[derive(GraphQLQuery)]
|
||||
#[graphql(
|
||||
schema_path = "graphql/schema.graphql",
|
||||
query_path = "graphql/mutations/logout.graphql",
|
||||
response_derives = "Debug",
|
||||
skip_serializing_none
|
||||
)]
|
||||
pub struct Logout;
|
|
@ -1,3 +1,5 @@
|
|||
pub mod login;
|
||||
pub mod logout;
|
||||
pub mod register_teacher;
|
||||
pub mod revoke_token;
|
||||
pub mod vote;
|
||||
|
|
|
@ -4,6 +4,7 @@ use graphql_client::GraphQLQuery;
|
|||
#[graphql(
|
||||
schema_path = "graphql/schema.graphql",
|
||||
query_path = "graphql/mutations/register_teacher.graphql",
|
||||
response_derives = "Debug"
|
||||
response_derives = "Debug",
|
||||
skip_serializing_none
|
||||
)]
|
||||
pub struct RegisterTeacher;
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
use graphql_client::GraphQLQuery;
|
||||
|
||||
type UUID = String;
|
||||
|
||||
#[derive(GraphQLQuery)]
|
||||
#[graphql(
|
||||
schema_path = "graphql/schema.graphql",
|
||||
query_path = "graphql/mutations/revoke_token.graphql",
|
||||
response_derives = "Debug",
|
||||
skip_serializing_none
|
||||
)]
|
||||
pub struct RevokeToken;
|
|
@ -4,6 +4,7 @@ use graphql_client::GraphQLQuery;
|
|||
#[graphql(
|
||||
schema_path = "graphql/schema.graphql",
|
||||
query_path = "graphql/mutations/vote.graphql",
|
||||
response_derives = "Debug"
|
||||
response_derives = "Debug",
|
||||
skip_serializing_none
|
||||
)]
|
||||
pub struct Vote;
|
||||
|
|
|
@ -4,6 +4,7 @@ use graphql_client::GraphQLQuery;
|
|||
#[graphql(
|
||||
schema_path = "graphql/schema.graphql",
|
||||
query_path = "graphql/queries/config.graphql",
|
||||
response_derives = "Debug"
|
||||
response_derives = "Debug",
|
||||
skip_serializing_none
|
||||
)]
|
||||
pub struct Config;
|
||||
|
|
|
@ -4,6 +4,7 @@ use graphql_client::GraphQLQuery;
|
|||
#[graphql(
|
||||
schema_path = "graphql/schema.graphql",
|
||||
query_path = "graphql/queries/me.graphql",
|
||||
response_derives = "Debug"
|
||||
response_derives = "Debug",
|
||||
skip_serializing_none
|
||||
)]
|
||||
pub struct Me;
|
||||
|
|
|
@ -3,3 +3,4 @@ pub mod me;
|
|||
pub mod ok;
|
||||
pub mod students_can_vote;
|
||||
pub mod teachers;
|
||||
pub mod tokens;
|
||||
|
|
|
@ -4,6 +4,7 @@ use graphql_client::GraphQLQuery;
|
|||
#[graphql(
|
||||
schema_path = "graphql/schema.graphql",
|
||||
query_path = "graphql/queries/ok.graphql",
|
||||
response_derives = "Debug"
|
||||
response_derives = "Debug",
|
||||
skip_serializing_none
|
||||
)]
|
||||
pub struct Ok;
|
||||
pub struct Ok;
|
||||
|
|
|
@ -4,6 +4,7 @@ use graphql_client::GraphQLQuery;
|
|||
#[graphql(
|
||||
schema_path = "graphql/schema.graphql",
|
||||
query_path = "graphql/queries/students_can_vote.graphql",
|
||||
response_derives = "Debug"
|
||||
response_derives = "Debug",
|
||||
skip_serializing_none
|
||||
)]
|
||||
pub struct StudentsCanVote;
|
||||
|
|
|
@ -4,6 +4,7 @@ use graphql_client::GraphQLQuery;
|
|||
#[graphql(
|
||||
schema_path = "graphql/schema.graphql",
|
||||
query_path = "graphql/queries/teachers.graphql",
|
||||
response_derives = "Debug"
|
||||
response_derives = "Debug",
|
||||
skip_serializing_none
|
||||
)]
|
||||
pub struct Teachers;
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
use graphql_client::GraphQLQuery;
|
||||
|
||||
type UUID = String;
|
||||
type Time = String;
|
||||
|
||||
#[derive(GraphQLQuery)]
|
||||
#[graphql(
|
||||
schema_path = "graphql/schema.graphql",
|
||||
query_path = "graphql/queries/tokens.graphql",
|
||||
response_derives = "Debug"
|
||||
)]
|
||||
pub struct Tokens;
|
|
@ -1,17 +1,21 @@
|
|||
use graphql_client::reqwest::post_graphql;
|
||||
use yew::prelude::*;
|
||||
use yew_agent::{Dispatched, Dispatcher};
|
||||
use yew_router::prelude::*;
|
||||
|
||||
use crate::agents;
|
||||
use crate::components;
|
||||
use crate::graphql;
|
||||
use crate::routes;
|
||||
|
||||
pub enum Msg {
|
||||
LogOut,
|
||||
LogOutClicked,
|
||||
LogOut(Option<Vec<String>>),
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct MainProps {
|
||||
pub token: Option<String>,
|
||||
pub logged_in: bool,
|
||||
#[prop_or_default]
|
||||
pub children: Children,
|
||||
|
@ -20,6 +24,7 @@ pub struct MainProps {
|
|||
pub struct Main {
|
||||
logged_in: bool,
|
||||
logged_in_event_bus: Dispatcher<agents::logged_in::EventBus>,
|
||||
errors: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Component for Main {
|
||||
|
@ -30,50 +35,75 @@ impl Component for Main {
|
|||
Self {
|
||||
logged_in: ctx.props().logged_in,
|
||||
logged_in_event_bus: agents::logged_in::EventBus::dispatcher(),
|
||||
errors: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::LogOut => {
|
||||
self.logged_in_event_bus
|
||||
.send(agents::logged_in::Request::LoggedIn(false));
|
||||
Msg::LogOutClicked => {
|
||||
let client = graphql::client(ctx.props().token.as_ref()).unwrap();
|
||||
ctx.link().send_future(async move {
|
||||
let response = post_graphql::<graphql::mutations::logout::Logout, _>(
|
||||
&client,
|
||||
graphql::URL.as_str(),
|
||||
graphql::mutations::logout::logout::Variables,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Msg::LogOut(components::graphql_errors::convert(response.errors))
|
||||
});
|
||||
|
||||
false
|
||||
}
|
||||
Msg::LogOut(errors) => {
|
||||
if self.errors.is_none() {
|
||||
self.logged_in_event_bus
|
||||
.send(agents::logged_in::Request::LoggedIn(false));
|
||||
false
|
||||
} else {
|
||||
self.errors = errors;
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let logout_onclick = ctx.link().callback(move |_| Msg::LogOut);
|
||||
|
||||
html! {
|
||||
<>
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<Link<routes::Route> to={routes::Route::Home}>
|
||||
<button>{ "Home" }</button>
|
||||
</Link<routes::Route>>
|
||||
</li>
|
||||
<li>
|
||||
if self.logged_in {
|
||||
<button onclick={logout_onclick}>{ "Logout" }</button>
|
||||
} else {
|
||||
<Link<routes::Route> to={routes::Route::Login}>
|
||||
<button>{ "Login" }</button>
|
||||
</Link<routes::Route>>
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<>
|
||||
<components::logged_in_handler::LoggedInHandler logged_in={self.logged_in} />
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<Link<routes::Route> to={routes::Route::Home}>
|
||||
<button>{ "Home" }</button>
|
||||
</Link<routes::Route>>
|
||||
</li>
|
||||
<li>
|
||||
<Link<routes::Route> to={routes::Route::Settings}>
|
||||
<button>{ "Settings" }</button>
|
||||
</Link<routes::Route>>
|
||||
</li>
|
||||
<li>
|
||||
if self.logged_in {
|
||||
<button onclick={ctx.link().callback(move |_| Msg::LogOutClicked)}>{ "Logout" }</button>
|
||||
} else {
|
||||
<Link<routes::Route> to={routes::Route::Login}>
|
||||
<button>{ "Login" }</button>
|
||||
</Link<routes::Route>>
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<hr />
|
||||
|
||||
<components::logged_in_handler::LoggedInHandler logged_in={self.logged_in} />
|
||||
<main>
|
||||
{ for ctx.props().children.iter() }
|
||||
</main>
|
||||
</>
|
||||
<components::graphql_errors::GraphQLErrors errors={self.errors.clone()} />
|
||||
</nav>
|
||||
<hr />
|
||||
<main>
|
||||
{ for ctx.props().children.iter() }
|
||||
</main>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,13 +22,8 @@ pub struct HomeProps {
|
|||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
enum State {
|
||||
Fetching,
|
||||
Done,
|
||||
}
|
||||
|
||||
pub struct Home {
|
||||
state: State,
|
||||
fetching: bool,
|
||||
errors: Option<Vec<String>>,
|
||||
me: Option<graphql::queries::me::me::MeMe>,
|
||||
}
|
||||
|
@ -37,9 +32,25 @@ impl Component for Home {
|
|||
type Message = Msg;
|
||||
type Properties = HomeProps;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let client = graphql::client(ctx.props().token.as_ref()).unwrap();
|
||||
ctx.link().send_future(async move {
|
||||
let response = post_graphql::<graphql::queries::me::Me, _>(
|
||||
&client,
|
||||
graphql::URL.as_str(),
|
||||
graphql::queries::me::me::Variables,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Msg::DoneFetching {
|
||||
errors: components::graphql_errors::convert(response.errors),
|
||||
me: response.data.unwrap().me.unwrap(),
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
state: State::Fetching,
|
||||
fetching: true,
|
||||
errors: None,
|
||||
me: None,
|
||||
}
|
||||
|
@ -48,7 +59,7 @@ impl Component for Home {
|
|||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::DoneFetching { errors, me } => {
|
||||
self.state = State::Done;
|
||||
self.fetching = false;
|
||||
self.errors = errors;
|
||||
self.me = Some(me);
|
||||
true
|
||||
|
@ -60,62 +71,39 @@ impl Component for Home {
|
|||
html! {
|
||||
<>
|
||||
<Title value="Home" />
|
||||
{
|
||||
match self.state {
|
||||
State::Fetching => {
|
||||
let client = graphql::client(ctx.props().token.as_ref()).unwrap();
|
||||
ctx.link().send_future(async move {
|
||||
let response = post_graphql::<graphql::queries::me::Me, _>(
|
||||
&client,
|
||||
graphql::URL.as_str(),
|
||||
graphql::queries::me::me::Variables,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Msg::DoneFetching {
|
||||
errors: components::graphql_errors::convert(response.errors),
|
||||
me: response.data.unwrap().me.unwrap(),
|
||||
}
|
||||
});
|
||||
if self.fetching {
|
||||
<p>{ "Fetching..." }</p>
|
||||
} else {
|
||||
if let Some(errors) = &self.errors {
|
||||
<components::graphql_errors::GraphQLErrors errors={errors.clone()} />
|
||||
} else {
|
||||
{{
|
||||
let me = self.me.as_ref().unwrap();
|
||||
|
||||
html! {
|
||||
<p>{ "Fetching..." }</p>
|
||||
}
|
||||
}
|
||||
State::Done => {
|
||||
if let Some(errors) = &self.errors {
|
||||
html! {
|
||||
<components::graphql_errors::GraphQLErrors errors={errors.clone()} />
|
||||
}
|
||||
} else {
|
||||
let me = self.me.as_ref().unwrap();
|
||||
|
||||
html! {
|
||||
<>
|
||||
<h1>{ format!("Hey, {}!", me.first_name) }</h1>
|
||||
<hr />
|
||||
{
|
||||
match me.role {
|
||||
graphql::queries::me::me::UserRole::Student => html! {
|
||||
<student_home::StudentHome
|
||||
token={ctx.props().token.as_ref().unwrap().to_owned()}
|
||||
voted={me.student.as_ref().unwrap().vote.is_some()}
|
||||
/>
|
||||
},
|
||||
graphql::queries::me::me::UserRole::Teacher => html! {
|
||||
<teacher_home::TeacherHome
|
||||
token={ctx.props().token.as_ref().unwrap().to_owned()}
|
||||
registered={me.teacher.is_some()}
|
||||
/>
|
||||
},
|
||||
graphql::queries::me::me::UserRole::Other(_) => html! {}
|
||||
}
|
||||
<>
|
||||
<h1>{ format!("Hey, {}!", me.first_name) }</h1>
|
||||
<hr />
|
||||
{
|
||||
match me.role {
|
||||
graphql::queries::me::me::UserRole::Student => html! {
|
||||
<student_home::StudentHome
|
||||
token={ctx.props().token.as_ref().unwrap().to_owned()}
|
||||
voted={me.student.as_ref().unwrap().vote.is_some()}
|
||||
/>
|
||||
},
|
||||
graphql::queries::me::me::UserRole::Teacher => html! {
|
||||
<teacher_home::TeacherHome
|
||||
token={ctx.props().token.as_ref().unwrap().to_owned()}
|
||||
registered={me.teacher.is_some()}
|
||||
/>
|
||||
},
|
||||
graphql::queries::me::me::UserRole::Other(_) => html! {}
|
||||
}
|
||||
</>
|
||||
}
|
||||
}
|
||||
</>
|
||||
}
|
||||
}
|
||||
}}
|
||||
}
|
||||
}
|
||||
</>
|
||||
|
|
|
@ -231,10 +231,8 @@ impl Component for StudentVote {
|
|||
<form {onsubmit}>
|
||||
{ for (0..self.slots).map(|i| {
|
||||
let curr_t = self.votes.get(&i);
|
||||
if let Some(te) = curr_t {
|
||||
if let Some(t) = te {
|
||||
teachers.push(*t);
|
||||
}
|
||||
if let Some(Some(t)) = curr_t {
|
||||
teachers.push(*t);
|
||||
}
|
||||
let name = format!("mentors_{}", i);
|
||||
|
||||
|
|
|
@ -87,7 +87,7 @@ impl Component for TeacherRegistration {
|
|||
<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=1 />
|
||||
<input ref={self.max_students.clone()} type="number" id="max_students" name="max_students" min=0 />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -7,11 +7,14 @@ use crate::layouts;
|
|||
pub mod home;
|
||||
pub mod login;
|
||||
pub mod not_found;
|
||||
pub mod settings;
|
||||
|
||||
#[derive(Clone, Routable, PartialEq, Eq)]
|
||||
pub enum Route {
|
||||
#[at("/")]
|
||||
Home,
|
||||
#[at("/settings")]
|
||||
Settings,
|
||||
#[at("/login")]
|
||||
Login,
|
||||
#[not_found]
|
||||
|
@ -39,19 +42,28 @@ pub fn switch(routes: &Route) -> Html {
|
|||
Route::Home => {
|
||||
html! {
|
||||
<layouts::logged_in::LoggedIn {logged_in}>
|
||||
<layouts::main::Main {logged_in}>
|
||||
<layouts::main::Main token={token.to_owned()} {logged_in}>
|
||||
<home::Home {token} />
|
||||
</layouts::main::Main>
|
||||
</layouts::logged_in::LoggedIn>
|
||||
}
|
||||
}
|
||||
Route::Settings => {
|
||||
html! {
|
||||
<layouts::logged_in::LoggedIn {logged_in}>
|
||||
<layouts::main::Main token={token.to_owned()} {logged_in}>
|
||||
<settings::Settings {token} />
|
||||
</layouts::main::Main>
|
||||
</layouts::logged_in::LoggedIn>
|
||||
}
|
||||
}
|
||||
Route::Login => html! {
|
||||
<layouts::main::Main {logged_in}>
|
||||
<layouts::main::Main {token} {logged_in}>
|
||||
<login::Login />
|
||||
</layouts::main::Main>
|
||||
},
|
||||
Route::NotFound => html! {
|
||||
<layouts::main::Main {logged_in}>
|
||||
<layouts::main::Main {token} {logged_in}>
|
||||
<not_found::NotFound />
|
||||
</layouts::main::Main>
|
||||
},
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
use yew::prelude::*;
|
||||
use yew_side_effect::title::Title;
|
||||
|
||||
pub mod tokens;
|
||||
|
||||
#[derive(Properties, PartialEq, Eq)]
|
||||
pub struct SettingsProps {
|
||||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
pub struct Settings;
|
||||
|
||||
impl Component for Settings {
|
||||
type Message = ();
|
||||
type Properties = SettingsProps;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<>
|
||||
<Title value="Settings" />
|
||||
<section>
|
||||
<tokens::Tokens token={ctx.props().token.as_ref().unwrap().to_owned()} />
|
||||
</section>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
use graphql_client::reqwest::post_graphql;
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components;
|
||||
use crate::graphql;
|
||||
|
||||
pub enum Msg {
|
||||
DoneFetching {
|
||||
errors: Option<Vec<String>>,
|
||||
tokens: Vec<graphql::queries::tokens::tokens::TokensTokens>,
|
||||
},
|
||||
Revoke(usize),
|
||||
RevokeDone {
|
||||
errors: Option<Vec<String>>,
|
||||
id: usize,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Properties, PartialEq, Eq)]
|
||||
pub struct TokensProps {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
pub struct Tokens {
|
||||
fetching: bool,
|
||||
errors: Option<Vec<String>>,
|
||||
tokens: Option<Vec<graphql::queries::tokens::tokens::TokensTokens>>,
|
||||
}
|
||||
|
||||
impl Component for Tokens {
|
||||
type Message = Msg;
|
||||
type Properties = TokensProps;
|
||||
|
||||
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::tokens::Tokens, _>(
|
||||
&client,
|
||||
graphql::URL.as_str(),
|
||||
graphql::queries::tokens::tokens::Variables,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Msg::DoneFetching {
|
||||
errors: None,
|
||||
tokens: response.data.unwrap().tokens.unwrap(),
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
fetching: true,
|
||||
errors: None,
|
||||
tokens: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::DoneFetching { errors, tokens } => {
|
||||
self.fetching = false;
|
||||
self.errors = errors;
|
||||
self.tokens = Some(tokens);
|
||||
true
|
||||
}
|
||||
Msg::Revoke(id) => {
|
||||
let client = graphql::client(Some(&ctx.props().token)).unwrap();
|
||||
let token = self.tokens.as_ref().unwrap()[id].id.to_owned();
|
||||
ctx.link().send_future(async move {
|
||||
let response =
|
||||
post_graphql::<graphql::mutations::revoke_token::RevokeToken, _>(
|
||||
&client,
|
||||
graphql::URL.as_str(),
|
||||
graphql::mutations::revoke_token::revoke_token::Variables { token },
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Msg::RevokeDone {
|
||||
errors: components::graphql_errors::convert(response.errors),
|
||||
id,
|
||||
}
|
||||
});
|
||||
|
||||
true
|
||||
}
|
||||
Msg::RevokeDone { errors, id } => {
|
||||
self.errors = errors;
|
||||
self.tokens.as_mut().unwrap().remove(id);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<fieldset>
|
||||
<legend>{ "Tokens" }</legend>
|
||||
if self.fetching {
|
||||
<p>{ "Fetching..." }</p>
|
||||
} else {
|
||||
<table>
|
||||
<tr>
|
||||
<th>{ "ID" }</th>
|
||||
<th>{ "Issued at" }</th>
|
||||
<th>{ "Expires at" }</th>
|
||||
<th>{ "Revoke" }</th>
|
||||
</tr>
|
||||
{ for self.tokens.as_ref().unwrap().iter().enumerate().map(|(i, t)| {
|
||||
html! {
|
||||
<tr>
|
||||
<td><code>{ &t.id }</code></td>
|
||||
<td><code>{ &t.iat }</code></td>
|
||||
<td><code>{ &t.exp }</code></td>
|
||||
<td>
|
||||
<button onclick={ctx.link().callback(move |_| Msg::Revoke(i))}>
|
||||
{ "Revoke" }
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}) }
|
||||
</table>
|
||||
|
||||
<components::graphql_errors::GraphQLErrors errors={self.errors.clone()} />
|
||||
}
|
||||
</fieldset>
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue