170 Commits

Author SHA1 Message Date
b62dcb9c62 Merge pull request #21 from snow-jallen/main
looks good. I'll be doing some UX updates on the context menu, but I like the idea
2026-02-20 08:36:47 -07:00
Jonathan Allen
77fde8198e Add context menu for assignments on calendar view 2026-02-19 18:17:13 -07:00
Jonathan Allen
8172724a4f Merge pull request #1 from snow-jallen/fix-rubric-column-layout
Fix rubric layout: points left, description right
2026-02-19 17:32:20 -07:00
Jonathan Allen
07155991aa Fix rubric layout: points left, description right
Reorder rubric columns so points appear on the left and the description
is left-aligned on the right. Extra credit gets its own dedicated column.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 17:29:09 -07:00
9ce42c21f9 Merge pull request #20 from teichert/feature-and-fix-allow-custom-feedback-delims
Feature and fix allow custom feedback delims
2026-01-15 13:59:30 -07:00
Adam Teichert
55a9fffd54 merged 2026-01-15 13:55:59 -07:00
a4a8e3cbb6 Merge pull request #19 from teichert/feature-navigation-buttons
(feature) adds buttons to manager UI to navigate forward or backward between assignments/pages and lectures
2026-01-15 13:48:24 -07:00
dda46a3c49 Merge pull request #17 from teichert/table-scope-col
(ADA) markdown table headers cells are column scoped
2026-01-15 13:46:22 -07:00
5b202f25e6 Merge pull request #16 from teichert/mermaid-pako-images
mermaid diagrams are rendered as image tags with link to mermaid sever
2026-01-15 13:27:55 -07:00
558eb74fbc tooltip scrolling fixed 2026-01-12 12:04:33 -07:00
b6a84f2fbc more tooltips 2026-01-07 09:10:42 -07:00
fb5ee94b55 improved tooltips 2026-01-07 09:02:16 -07:00
678727c650 more permissive breadcrumbs 2026-01-05 14:57:32 -07:00
b5899c02e4 width fix 2026-01-05 14:46:18 -07:00
767528560c adding file name validation 2026-01-05 10:30:26 -07:00
8c01cb2422 adding breadcrumbs 2026-01-05 10:22:12 -07:00
076c0b1025 more subtle 2025-12-22 11:02:24 -07:00
20b14da180 updating monaco style 2025-12-22 11:02:05 -07:00
Adam Teichert
52b8967949 fix lint warning 2025-12-18 15:17:15 -07:00
Adam Teichert
eb661a3e59 inserting newline after feedback so as to keep separate from following content 2025-12-18 15:07:45 -07:00
Adam Teichert
712a3e5155 remove nesting options within options 2025-12-17 16:49:52 -07:00
Adam Teichert
7bb276d52a use custom feedback delims in editing as well as converting 2025-12-17 16:21:57 -07:00
Adam Teichert
44c42d1abc linting fixes 2025-12-17 15:28:28 -07:00
Adam Teichert
1e3ff085f8 (wip) fix earlier breaking change (feedback in quizzes) by allowing custom feedback delims so that - doesn't need to conflict with markdown list item 2025-12-17 15:10:59 -07:00
Adam Teichert
3c6ba35bce fix/squelch lint warnings so that image can build 2025-12-16 22:42:49 -07:00
Adam Teichert
cef2323886 add previous and next buttons (lectures are separate from assignments/quizzes/pages) 2025-12-16 22:27:04 -07:00
Adam Teichert
859bdf01f2 fix bug where links didn't happen in markdown tables 2025-12-16 21:20:16 -07:00
Adam Teichert
e9f33e0174 markdown tables headings have scope of col; raw html tables are not changed 2025-12-16 13:02:24 -07:00
Adam Teichert
e07d0a6e47 caption element before table is placed as first element of the table (allows markdown tables to have caption) 2025-12-15 23:01:35 -07:00
51f2be1988 fixed server side file editing 2025-12-15 09:18:06 -07:00
Adam Teichert
a203dc6e46 mermaid diagrams are rendered as image tags with link to mermaid server rendering; uses pako for compression as expected 2025-12-10 17:44:02 -07:00
f6b2427749 lectures do not publish images 2025-12-08 11:15:23 -07:00
cab8b881f2 updates 2025-12-05 16:20:53 -07:00
ae19b5a075 validation on changing assignment name 2025-11-24 16:06:25 -07:00
8aec682974 more colors 2025-11-18 13:58:42 -07:00
17bd460407 beter add button 2025-11-18 13:57:37 -07:00
53c8422a5b fixing expand icon 2025-11-18 13:37:11 -07:00
890e08d1b2 pointer 2025-11-11 14:19:03 -07:00
7fec0424d7 month buffer 2025-10-28 13:18:11 -06:00
11c2366f93 minor ui updates 2025-10-28 12:59:20 -06:00
5988639378 can remove courses from main page now 2025-10-28 10:15:06 -06:00
15b184ddc0 fixing new course config form 2025-10-27 14:22:13 -06:00
e35a5ffab6 log when not loading a file 2025-10-23 12:47:34 -06:00
b53948db72 can get exact answers 2025-10-22 13:18:18 -06:00
d9f7e7b3e9 can get exact answers 2025-10-22 12:26:28 -06:00
47c69251c8 fixing multiline feedback 2025-10-22 11:52:06 -06:00
4c978f392d fixed feedback, feedback only supported in descriptions, not in questions for now 2025-10-22 11:25:59 -06:00
d6584fd338 starting to handle feedback parsing bug 2025-10-22 10:55:05 -06:00
6a56036782 name validation 2025-10-22 09:21:40 -06:00
bd32599469 help scrolling on quiz 2025-10-22 08:50:40 -06:00
9638d7308e update canvas quiz service to send new feedback options to canvas 2025-10-10 14:21:20 -06:00
bf835caa37 adding feedback 2025-10-10 14:15:01 -06:00
e7e244222e linting fix 2025-09-29 12:01:53 -06:00
2e474cb43a more rate-limit aware posts and deletes 2025-09-29 11:58:11 -06:00
33120c40a5 simplify build process 2025-09-22 10:53:25 -06:00
2ec3d9349e merge 2025-09-22 10:51:13 -06:00
5e088fb4eb Merge pull request #13 from alexmickelson/copilot/fix-12
Add GitHub Actions workflow for automated Docker Hub deployment
2025-09-22 10:48:19 -06:00
copilot-swe-agent[bot]
aae9e7bba4 Revert README changes - remove deployment documentation
Co-authored-by: alexmickelson <43245625+alexmickelson@users.noreply.github.com>
2025-09-22 16:41:22 +00:00
58175c1426 Merge branch 'main' of github.com:alexmickelson/canvasManagement 2025-09-16 14:11:52 -06:00
03529f875a latex in help string 2025-09-16 14:11:51 -06:00
copilot-swe-agent[bot]
95c9d07592 Add GitHub Actions workflow for automated Docker Hub deployment
Co-authored-by: snow-jallen <42281341+snow-jallen@users.noreply.github.com>
2025-09-10 18:56:33 +00:00
copilot-swe-agent[bot]
9918b63a1e Initial plan 2025-09-10 18:49:50 +00:00
Jonathan Allen
f808a517d3 Merge pull request #9 from alexmickelson/copilot/fix-8
Add quiz question order verification after Canvas import
2025-09-10 12:35:49 -06:00
copilot-swe-agent[bot]
b47fa4cff5 Remove npm package-lock.json and use pnpm exclusively
Co-authored-by: alexmickelson <43245625+alexmickelson@users.noreply.github.com>
2025-09-10 18:20:04 +00:00
copilot-swe-agent[bot]
efe2060fcd Finalize quiz question order verification feature with comprehensive testing
Co-authored-by: snow-jallen <42281341+snow-jallen@users.noreply.github.com>
2025-09-10 18:02:52 +00:00
copilot-swe-agent[bot]
c60ba92f28 Add quiz question order verification after Canvas import
Co-authored-by: snow-jallen <42281341+snow-jallen@users.noreply.github.com>
2025-09-10 17:59:37 +00:00
copilot-swe-agent[bot]
b65cfa73d7 Initial exploration and understanding of quiz import order issue
Co-authored-by: snow-jallen <42281341+snow-jallen@users.noreply.github.com>
2025-09-10 17:56:25 +00:00
copilot-swe-agent[bot]
dbc7887d82 Initial plan 2025-09-10 17:51:41 +00:00
ecb5f6d70f error checking update 2025-09-01 08:51:32 -06:00
523a05d45e when creating assignments, verify the classroom url can be swapped 2025-08-29 11:05:50 -06:00
5f408749e4 updating matching logic 2025-08-25 11:13:11 -06:00
994d6e9a03 add graded answers for short answer questions to help text 2025-08-25 10:43:16 -06:00
d1a768393c improving replace url features 2025-08-21 08:55:29 -06:00
224cc9cd2a replacing text can work 2025-08-21 08:38:48 -06:00
e07a12f622 fix test 2025-08-21 08:28:52 -06:00
54e4d7b4a1 adding some prefetches, not sure if makes difference 2025-08-13 11:24:55 -06:00
e8de00a2b1 small refactors 2025-08-13 11:12:15 -06:00
762a51d6da sort module button 2025-08-11 14:06:59 -06:00
5715b081a9 adding readme instructions 2025-07-30 10:07:17 -06:00
c5759c0bec settings 2025-07-29 15:06:27 -06:00
f7357e4c08 better titles 2025-07-29 11:18:08 -06:00
60b2ad7959 sidebar collapsing is better 2025-07-23 11:56:56 -06:00
a94087dd98 one more folder change 2025-07-23 11:41:03 -06:00
99f491f16e refactoring canvas files 2025-07-23 11:40:18 -06:00
815f929c2d more code refactor to colocate feature code 2025-07-23 11:25:12 -06:00
c37ad0708e more refactor 2025-07-23 09:57:00 -06:00
aa15b2b335 more refactor 2025-07-23 09:55:30 -06:00
1885431574 more refactor 2025-07-23 09:54:11 -06:00
3e371247d6 more refactoring by feature 2025-07-23 09:46:35 -06:00
d5a40e52d9 fixing lint config 2025-07-23 09:29:19 -06:00
c95c40f9e7 refactoring files to be located by feature 2025-07-23 09:23:44 -06:00
46e0c36916 can add new courses, kinda janky 2025-07-22 15:09:10 -06:00
704a5ae404 can add existing courses 2025-07-22 14:23:40 -06:00
67b67100c1 path selecting element 2025-07-22 13:55:15 -06:00
01d137efcf moving to a global config 2025-07-22 10:05:55 -06:00
cea6aef453 adding github examples to help string 2025-07-21 14:22:29 -06:00
746253b6c2 importing course scrubs classroom links 2025-07-21 14:20:31 -06:00
5ab371334e can get classroom links based on settings 2025-07-21 14:18:22 -06:00
42ce579eee adding github classroom links to settings 2025-07-21 14:11:46 -06:00
9aec082467 working on mcp 2025-07-21 11:42:27 -06:00
d200c114d3 adding mcp tools 2025-07-17 18:19:38 -06:00
0efecad60e route edits 2025-07-16 16:27:57 -06:00
5f4417083a got one mcp endpoint 2025-07-16 15:52:30 -06:00
31ab49ed16 basic mcp working 2025-07-16 15:30:20 -06:00
bc2008f455 update run script 2025-07-16 14:38:36 -06:00
2432e0408f styling updates 2025-07-15 14:40:38 -06:00
c93c0b0e22 default expand past courses 2025-07-15 14:00:32 -06:00
2b11106f02 lecture styling 2025-07-15 13:39:29 -06:00
43ed57e558 mermaid ink image support 2025-07-15 12:47:53 -06:00
57b7d8ac1e consoladating layouts 2025-07-15 11:43:46 -06:00
c33b40b55e config updates 2025-07-15 11:19:03 -06:00
c39d7ca4d7 day of linting judgement 2025-07-14 11:53:13 -06:00
a128107094 canvas updates 2025-07-07 16:47:38 -06:00
abf6d5a9a2 run script updates 2025-07-07 12:24:30 -06:00
bc8b9ca0c4 updating packages and fixing linting 2025-07-07 11:58:17 -06:00
00cafeec0a improving navigation 2025-07-07 11:47:34 -06:00
5a56d26b4d can manage course navigation from settinss 2025-07-07 11:06:04 -06:00
d8f17faaae better assignment help, better layout for many modules 2025-04-22 08:28:40 -06:00
82ec0c0b28 improving readme 2025-04-16 09:01:44 -06:00
05f354ac9e fixing create item styling escaping 2025-04-16 08:53:32 -06:00
a9bc8ef390 if image cannot be downloaded, fall back to oringinal url 2025-04-16 08:44:18 -06:00
0bd55d3f67 more error refactoring 2025-04-09 12:29:41 -06:00
b35ba0f939 no errors when viewing a different course 2025-04-09 12:26:02 -06:00
0fef2a6b87 quiz fix 2025-03-25 10:44:40 -06:00
cda4be67fa bumping versions 2025-03-24 13:57:34 -06:00
32e77d5f4f refatoring image upload error 2025-03-24 13:38:35 -06:00
408246be7f updated some docs 2025-01-31 09:20:35 -07:00
54e071b053 added catches around markdown to html, might throw exception if image error 2025-01-31 09:14:16 -07:00
777d1e4659 versions 2025-01-29 12:07:39 -07:00
719106b6bb publishing images for assignments only 2025-01-29 08:57:25 -07:00
3ce0eff2f8 workign on file upload 2025-01-27 08:27:56 -07:00
f0c147cd6a can upload images 2025-01-24 12:03:16 -07:00
a60008c6d7 file support in progress 2025-01-24 09:20:07 -07:00
b2514bb356 transitions are back 2025-01-22 13:28:07 -07:00
4ed40bd24b fixed double scroll 2025-01-22 13:21:30 -07:00
3340dcd264 indicators for month collapsing 2025-01-22 13:16:12 -07:00
f96dcb070f can hide sidebar 2025-01-22 13:14:02 -07:00
da116abfae better matching 2025-01-22 12:09:09 -07:00
4005c85d60 added escape support on matching text 2025-01-22 08:59:53 -07:00
d581569c7a Merge pull request #4 from teichert/main
latex
2025-01-16 14:24:23 -07:00
Adam Teichert
90fcca7bbe short_answer= making it with answers to canvas (needed to include answer_text) 2025-01-15 18:59:21 -07:00
Adam Teichert
5f11fe76f1 short_answer= tested and implemented; local preview works, but canvas isn't getting the answers 2025-01-15 17:42:39 -07:00
Adam Teichert
ade3f4dca4 latex 2025-01-14 18:35:40 -07:00
ada36c143c more shallow links 2025-01-13 15:01:42 -07:00
6774624739 more shallow links 2025-01-13 15:00:29 -07:00
cc2001565e calendar scroll position... 2025-01-07 14:31:07 -07:00
a722e7291b larger hovers on lectures 2025-01-07 12:15:08 -07:00
a494e315d2 some papercuts 2025-01-06 15:52:28 -07:00
ad4b059a17 restoring page titles 2025-01-04 12:07:06 -07:00
f142b85424 fine tuning tooltip 2025-01-04 11:36:19 -07:00
b22d09da1a dont need old web anymore 2025-01-04 09:07:42 -07:00
8e825960f6 have old distributed 2025-01-02 15:25:36 -07:00
41c9cc7556 update settings to add clarity 2025-01-02 10:47:55 -07:00
f8ca0bca3c calendarweek 2025-01-02 10:05:16 -07:00
563fe01383 update 2024-12-23 14:03:25 -07:00
78c1e80380 update compose 2024-12-23 12:30:54 -07:00
30a8581587 fix quiz edit reload errror 2024-12-18 17:20:20 -07:00
c08b6857ed listen to all file system events 2024-12-17 17:02:31 -07:00
8547b99092 after rename, invalidate queries for calendar view 2024-12-17 16:54:50 -07:00
df57e93cf6 better build script 2024-12-17 14:54:14 -07:00
0f1d999e16 renaming pages also a thing 2024-12-17 14:49:34 -07:00
b020673282 deleting old quizzes and assignments correctly 2024-12-17 14:42:53 -07:00
7b1201c2ba quiz names not in markdown anymore 2024-12-17 14:40:10 -07:00
9a8c5bff91 assignment renaming is whole process now 2024-12-17 14:35:20 -07:00
c557bbcc28 moving name out of file, will mirror file system name 2024-12-17 14:09:41 -07:00
068c2b6983 who knows how long i have not been pushing... 2024-12-17 10:38:04 -07:00
7993342ee7 better moving between modles 2024-12-17 10:12:35 -07:00
5b4f5d3677 working on duplicate quiz when changing modules 2024-12-17 09:48:07 -07:00
2460936470 test env can go in repo 2024-12-17 09:20:39 -07:00
576ee02afb moving v2 to top level 2024-12-17 09:19:21 -07:00
592 changed files with 17143 additions and 25618 deletions

View File

@@ -1,18 +0,0 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-stryker": {
"version": "3.6.0",
"commands": [
"dotnet-stryker"
]
},
"csharpier": {
"version": "0.25.0",
"commands": [
"dotnet-csharpier"
]
}
}
}

View File

@@ -1,5 +0,0 @@
{
"printWidth": 100,
"useTabs": false,
"tabWidth": 2
}

View File

@@ -6,5 +6,9 @@ temp/
build.sh build.sh
run.sh run.sh
README.md README.md
docker-compose.yml
Dockerfile
.next/
.pnpm-store/

View File

@@ -1,348 +0,0 @@
root = true
# All files
[*]
indent_style = space
indent_size = 2
end_of_line = lf
# C# files
[*.cs]
tab_width = 2
insert_final_newline = false
[*.{cs,vb}]
# Organize usings
dotnet_separate_import_directive_groups = true
dotnet_sort_system_directives_first = true
file_header_template = unset
# this. and Me. preferences
dotnet_style_qualification_for_event = false:silent
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_method = false:silent
dotnet_style_qualification_for_property = false:silent
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
dotnet_style_predefined_type_for_member_access = true:silent
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
# Expression-level preferences
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_object_initializer = true:suggestion
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_return = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
# Field preferences
dotnet_style_readonly_field = true:warning
# Parameter preferences
dotnet_code_quality_unused_parameters = all:suggestion
# Suppression preferences
dotnet_remove_unnecessary_suppression_exclusions = none
#### C# Coding Conventions ####
[*.cs]
# var preferences
csharp_style_var_elsewhere = false:silent
csharp_style_var_for_built_in_types = false:silent
csharp_style_var_when_type_is_apparent = false:silent
# Expression-bodied members
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_lambdas = true:suggestion
csharp_style_expression_bodied_local_functions = false:silent
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_prefer_not_pattern = true:suggestion
csharp_style_prefer_pattern_matching = true:silent
csharp_style_prefer_switch_expression = true:suggestion
# Null-checking preferences
csharp_style_conditional_delegate_call = true:suggestion
# Modifier preferences
csharp_prefer_static_local_function = true:warning
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
# Code-block preferences
csharp_prefer_braces = true:silent
csharp_prefer_simple_using_statement = true:suggestion
# Expression-level preferences
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_pattern_local_over_anonymous_function = true:suggestion
csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_range_operator = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace:silent
#### C# Formatting Rules ####
# New line preferences
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### Naming styles ####
[*.{cs,vb}]
# Naming rules
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion
dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces
dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase
dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion
dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters
dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase
dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods
dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties
dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.events_should_be_pascalcase.symbols = events
dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion
dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables
dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase
dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion
dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants
dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase
dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion
dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters
dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase
dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields
dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.private_fields_should_be_s_camelcase.severity = suggestion
dotnet_naming_rule.private_fields_should_be_s_camelcase.symbols = private_fields
dotnet_naming_rule.private_fields_should_be_s_camelcase.style = camelcase
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums
dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions
dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase
# Symbol specifications
dotnet_naming_symbols.interfaces.applicable_kinds = interface
dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interfaces.required_modifiers =
dotnet_naming_symbols.enums.applicable_kinds = enum
dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.enums.required_modifiers =
dotnet_naming_symbols.events.applicable_kinds = event
dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.events.required_modifiers =
dotnet_naming_symbols.methods.applicable_kinds = method
dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.methods.required_modifiers =
dotnet_naming_symbols.properties.applicable_kinds = property
dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.properties.required_modifiers =
dotnet_naming_symbols.public_fields.applicable_kinds = field
dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_fields.required_modifiers =
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_fields.required_modifiers =
dotnet_naming_symbols.private_static_fields.applicable_kinds = field
dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_static_fields.required_modifiers = static
dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum
dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types_and_namespaces.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
dotnet_naming_symbols.type_parameters.applicable_kinds = namespace
dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
dotnet_naming_symbols.type_parameters.required_modifiers =
dotnet_naming_symbols.private_constant_fields.applicable_kinds = field
dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_constant_fields.required_modifiers = const
dotnet_naming_symbols.local_variables.applicable_kinds = local
dotnet_naming_symbols.local_variables.applicable_accessibilities = local
dotnet_naming_symbols.local_variables.required_modifiers =
dotnet_naming_symbols.local_constants.applicable_kinds = local
dotnet_naming_symbols.local_constants.applicable_accessibilities = local
dotnet_naming_symbols.local_constants.required_modifiers = const
dotnet_naming_symbols.parameters.applicable_kinds = parameter
dotnet_naming_symbols.parameters.applicable_accessibilities = *
dotnet_naming_symbols.parameters.required_modifiers =
dotnet_naming_symbols.public_constant_fields.applicable_kinds = field
dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_constant_fields.required_modifiers = const
dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field
dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static
dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field
dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static
dotnet_naming_symbols.local_functions.applicable_kinds = local_function
dotnet_naming_symbols.local_functions.applicable_accessibilities = *
dotnet_naming_symbols.local_functions.required_modifiers =
# Naming styles
dotnet_naming_style.pascalcase.required_prefix =
dotnet_naming_style.pascalcase.required_suffix =
dotnet_naming_style.pascalcase.word_separator =
dotnet_naming_style.pascalcase.capitalization = pascal_case
dotnet_naming_style.ipascalcase.required_prefix = I
dotnet_naming_style.ipascalcase.required_suffix =
dotnet_naming_style.ipascalcase.word_separator =
dotnet_naming_style.ipascalcase.capitalization = pascal_case
dotnet_naming_style.tpascalcase.required_prefix = T
dotnet_naming_style.tpascalcase.required_suffix =
dotnet_naming_style.tpascalcase.word_separator =
dotnet_naming_style.tpascalcase.capitalization = pascal_case
dotnet_naming_style.camelcase.required_prefix =
dotnet_naming_style.camelcase.required_suffix =
dotnet_naming_style.camelcase.word_separator =
dotnet_naming_style.camelcase.capitalization = camel_case
dotnet_naming_style.s_camelcase.required_prefix = s_
dotnet_naming_style.s_camelcase.required_suffix =
dotnet_naming_style.s_camelcase.word_separator =
dotnet_naming_style.s_camelcase.capitalization = camel_case

27
.github/workflows/docker-deploy.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Deploy to Docker Hub
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
run: |
chmod +x ./build.sh
./build.sh -t -p

52
.gitignore vendored
View File

@@ -1,8 +1,46 @@
obj/ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
bin/
.env .pnpm-store/
*.env
storage/
tmp.json tmp.json
tmp*.json
.vs/ **/*.env
.env
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
storage/
temp/

View File

@@ -1,4 +0,0 @@
{
"recommendations": [
]
}

35
.vscode/launch.json vendored
View File

@@ -1,35 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md.
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/Management.Web/bin/Debug/net8.0/Management.Web.dll",
"args": [],
"cwd": "${workspaceFolder}/Management.Web",
"stopAtEntry": false,
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach"
}
]
}

View File

@@ -1,3 +0,0 @@
{
"dotnet.defaultSolution": "canvasManagement.sln"
}

41
.vscode/tasks.json vendored
View File

@@ -1,41 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/canvasManagement.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/canvasManagement.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/canvasManagement.sln"
],
"problemMatcher": "$msCompile"
}
]
}

View File

@@ -11,6 +11,7 @@ COPY . .
RUN mkdir -p storage RUN mkdir -p storage
RUN rm -rf /app/storage/* RUN rm -rf /app/storage/*
ENV NEXT_PUBLIC_ENABLE_FILE_SYNC=true
RUN pnpm run build RUN pnpm run build
FROM node:22-alpine AS production FROM node:22-alpine AS production

View File

@@ -1,29 +0,0 @@
using CanvasModel.EnrollmentTerms;
public class DeserializationTests
{
[Fact]
public void TestTerm()
{
var canvasContentResponse = @"{
""enrollment_terms"": [
{
""id"": 1,
""name"": ""one"",
""start_at"": ""2022-01-01T00:00:00Z"",
""end_at"": ""2022-02-01T00:00:00Z"",
""created_at"": ""2011-04-26T23:34:35Z"",
""workflow_state"": ""active"",
""grading_period_group_id"": null
}
]
}";
var result = JsonSerializer.Deserialize<RedundantEnrollmentTermsResponse>(canvasContentResponse);
result.Should().NotBeNull();
result?.EnrollmentTerms?.First().Id.Should().Be(1);
}
}

View File

@@ -1,45 +0,0 @@
public class CalendarMonthTests
{
[Fact]
public void TestCalendarMonthCanGetFirstWeek()
{
var month = new CalendarMonth(2023, 2);
int?[] expectedFirstWeek = new int?[] {
null, null, null, 1, 2, 3, 4
};
month.Weeks.First().Should().BeEquivalentTo(expectedFirstWeek);
}
[Fact]
public void TestCanGetAnotherMonthsFirstWeek()
{
var month = new CalendarMonth(2023, 4);
int?[] expectedFirstWeek = new int?[] {
null, null, null, null, null, null, 1
};
month.Weeks.First().Should().BeEquivalentTo(expectedFirstWeek);
}
[Fact]
public void TestCorrectNumberOfWeeks()
{
var month = new CalendarMonth(2023, 4);
month.Weeks.Count().Should().Be(6);
}
[Fact]
public void TestLastWeekIsCorrect()
{
var month = new CalendarMonth(2023, 4);
int?[] expectedLastWeek = new int?[] {
30, null, null, null, null, null, null,
};
month.Weeks.Last().Should().BeEquivalentTo(expectedLastWeek);
}
}

View File

@@ -1,26 +0,0 @@
// using CanvasModel.EnrollmentTerms;
// public class ConfigurationTests
// {
// [Fact]
// public void TestCanCreateConfigFromTermAndDays()
// {
// DateTime startAt = new DateTime(2022, 1, 1);
// DateTime endAt = new DateTime(2022, 1, 2);
// var canvasTerm = new EnrollmentTermModel(
// Id: 1,
// Name: "one",
// StartAt: startAt,
// EndAt: endAt
// );
// var daysOfWeek = new DayOfWeek[] { DayOfWeek.Monday };
// var management = new CoursePlanner();
// management.SetConfiguration(canvasTerm, daysOfWeek);
// var config = management.SemesterCalendar;
// if(config == null) Assert.Fail();
// config!.StartDate.Should().Be(startAt);
// config!.EndDate.Should().Be(endAt);
// config!.Days.Should().BeEquivalentTo(daysOfWeek);
// }
// }

View File

@@ -1,40 +0,0 @@
// public class ModuleTests
// {
// [Fact]
// public void CanAddModule()
// {
// var manager = new ModuleManager();
// var module = new CourseModule("First Module", new LocalAssignment[] { });
// manager.AddModule(module);
// manager.Modules.Count().Should().Be(1);
// manager.Modules.First().Should().Be(module);
// }
// [Fact]
// public void CanAddAssignmentToCorrectModule()
// {
// var manager = new ModuleManager();
// manager.AddModule(new CourseModule("First Module", new LocalAssignment[] { }));
// manager.AddModule(new CourseModule("Second Module", new LocalAssignment[] { }));
// manager.AddModule(new CourseModule("Third Module", new LocalAssignment[] { }));
// manager.AddModule(new CourseModule("Fourth Module", new LocalAssignment[] { }));
// var assignment = new LocalAssignment
// {
// name = "testname",
// description = "testDescription",
// published = false,
// lock_at_due_date = true,
// rubric = new RubricItem[] { },
// lock_at = null,
// due_at = DateTime.Now,
// points_possible = 10,
// submission_types = new SubmissionType[] { SubmissionType.online_text_entry }
// };
// manager.AddAssignment(3, assignment);
// manager.Modules.Count().Should().Be(4);
// manager.Modules.ElementAt(3).Assignments.Count().Should().Be(1);
// }
// }

View File

@@ -1,97 +0,0 @@
// using CanvasModel.EnrollmentTerms;
// namespace Management.Test;
// public class SemesterPlannerTests
// {
// [Fact]
// public void TestCanCreatePlanner()
// {
// var config = new SemesterCalendarConfig(
// StartDate: new DateTime(2022, 1, 1),
// EndDate: new DateTime(2022, 1, 2),
// new DayOfWeek[] { }
// );
// var semester = new SemesterPlanner(config);
// semester.Months.Count().Should().Be(1);
// }
// [Fact]
// public void TestNewPlannerHasCorrectNumberOfMonths()
// {
// var config = new SemesterCalendarConfig(
// StartDate: new DateTime(2022, 1, 1),
// EndDate: new DateTime(2022, 2, 1),
// new DayOfWeek[] { }
// );
// var semester = new SemesterPlanner(config);
// semester.Months.Count().Should().Be(2);
// }
// [Fact]
// public void TestNewPlannerHandlesTermsThatWrapYears()
// {
// var config = new SemesterCalendarConfig(
// StartDate: new DateTime(2022, 12, 1),
// EndDate: new DateTime(2023, 1, 1),
// new DayOfWeek[] { }
// );
// var semester = new SemesterPlanner(config);
// semester.Months.Count().Should().Be(2);
// }
// [Fact]
// public void TestSemesterGetsCorrectMonths()
// {
// var config = new SemesterCalendarConfig(
// StartDate: new DateTime(2022, 1, 1),
// EndDate: new DateTime(2022, 2, 1),
// new DayOfWeek[] { }
// );
// var semester = new SemesterPlanner(config);
// semester.Months.First().Month.Should().Be(1);
// semester.Months.Last().Month.Should().Be(2);
// }
// [Fact]
// public void TestMonthsCanWrapYears()
// {
// var config = new SemesterCalendarConfig(
// StartDate: new DateTime(2022, 12, 1),
// EndDate: new DateTime(2023, 1, 1),
// new DayOfWeek[] { }
// );
// var semester = new SemesterPlanner(config);
// semester.Months.First().Month.Should().Be(12);
// semester.Months.First().Year.Should().Be(2022);
// semester.Months.Last().Month.Should().Be(1);
// semester.Months.Last().Year.Should().Be(2023);
// }
// [Fact]
// public void TestSemesterTracksDaysOfWeek()
// {
// DayOfWeek[] days = new DayOfWeek[] { DayOfWeek.Monday };
// var config = new SemesterCalendarConfig(
// StartDate: new DateTime(2022, 12, 1),
// EndDate: new DateTime(2023, 1, 1),
// days
// );
// var semester = new SemesterPlanner(config);
// semester.Days.Should().BeEquivalentTo(days);
// }
// }

View File

@@ -1,515 +0,0 @@
using LocalModels;
public class CouresDifferencesChangesTests
{
[Fact]
public void CanDetectNewSettings()
{
LocalCourse oldCourse = new()
{
Settings = new() { Name = "Test Course" },
Modules = []
};
LocalCourse newCourse = new()
{
Settings = new() { Name = "new course name" },
Modules = []
};
var differences = CourseDifferences.GetNewChanges(newCourse, oldCourse);
differences.Modules.Should().BeEmpty();
differences.Settings.Should().NotBeNull();
differences.Settings?.Name.Should().Be("new course name");
}
[Fact]
public void CanDetectNewModule()
{
LocalCourse oldCourse = new()
{
Settings = new() { Name = "Test Course" },
Modules = []
};
LocalCourse newCourse = new()
{
Settings = new() { Name = "Test Course" },
Modules = [
new()
{
Name = "new module",
}
]
};
var differences = CourseDifferences.GetNewChanges(newCourse, oldCourse);
differences.Modules.Should().NotBeNull();
differences.Modules?.Count().Should().Be(1);
differences.Modules?.First().Name.Should().Be("new module");
}
[Fact]
public void CanDetectChangedAssignment()
{
LocalCourse oldCourse = new()
{
Settings = new() { Name = "Test Course" },
Modules = [
new()
{
Name = "new module",
Assignments = [
new()
{
Name = "test assignment",
Description = "",
DueAt = new DateTime(),
SubmissionTypes = [],
Rubric = []
}
]
}]
};
LocalCourse newCourse = new()
{
Settings = new() { Name = "Test Course" },
Modules = [
new()
{
Name = "new module",
Assignments = [
new()
{
Name = "test assignment",
Description = "new description",
DueAt = new DateTime(),
SubmissionTypes = [],
Rubric = []
}
]
}
]
};
var differences = CourseDifferences.GetNewChanges(newCourse, oldCourse);
differences.Modules.Should().NotBeNull();
differences.Modules?.Count().Should().Be(1);
differences.Modules?.First().Assignments.First().Description.Should().Be("new description");
}
[Fact]
public void CanProperlyIgnoreUnchangedModules()
{
var commonDate = new DateTime();
LocalCourse oldCourse = new()
{
Settings = new() { Name = "Test Course" },
Modules = [new()
{
Name = "new module",
Assignments = [
new()
{
Name = "test assignment",
Description = "",
DueAt = commonDate,
SubmissionTypes = [],
Rubric = []
}
]
}]
};
LocalCourse newCourse = new()
{
Settings = new() { Name = "Test Course" },
Modules = [new()
{
Name = "new module",
Assignments = [
new()
{
Name = "test assignment",
Description = "",
DueAt = commonDate,
SubmissionTypes = [],
Rubric = []
}
]
}]
};
var differences = CourseDifferences.GetNewChanges(newCourse, oldCourse);
differences.Modules.Should().BeEmpty();
}
[Fact]
public void OnlyChangedAssignmentRepresented()
{
var commonDate = new DateTime();
LocalCourse oldCourse = new()
{
Settings = new() { Name = "Test Course" },
Modules = [new()
{
Name = "new module",
Assignments = [
new()
{
Name = "test assignment",
Description = "",
DueAt = commonDate,
SubmissionTypes = [AssignmentSubmissionType.ONLINE_UPLOAD],
Rubric = [ new() {Points = 1, Label = "rubric"} ],
},
new()
{
Name = "test assignment 2",
Description = "",
DueAt = commonDate,
SubmissionTypes = [],
Rubric = [],
}
]
}]
};
LocalCourse newCourse = oldCourse with
{
Modules = [
new()
{
Name = "new module",
Assignments = [
new()
{
Name = "test assignment",
Description = "",
DueAt = commonDate,
SubmissionTypes = [AssignmentSubmissionType.ONLINE_UPLOAD],
Rubric = [ new() {Points = 1, Label = "rubric"} ],
},
new()
{
Name = "test assignment 2 with a new name",
Description = "",
DueAt = commonDate,
SubmissionTypes = [],
Rubric = []
}
]
}
]
};
var differences = CourseDifferences.GetNewChanges(newCourse, oldCourse);
differences.Modules.First().Assignments.Count().Should().Be(1);
differences.Modules.First().Assignments.First().Name.Should().Be("test assignment 2 with a new name");
}
[Fact]
public void IdenticalQuizzesIgnored()
{
var commonDate = new DateTime();
LocalCourse oldCourse = new()
{
Settings = new() { Name = "Test Course" },
Modules = [new(){
Name = "new module",
Assignments = [],
Quizzes = [
new()
{
Name = "Test Quiz",
Description = @"this is my description ",
LockAt = commonDate,
DueAt = commonDate,
ShuffleAnswers = true,
OneQuestionAtATime = false,
LocalAssignmentGroupName = "someId",
AllowedAttempts = -1,
Questions = []
}
]
}]
};
LocalCourse newCourse = oldCourse with
{
Modules = [new(){
Name = "new module",
Assignments = [],
Quizzes = [
new()
{
Name = "Test Quiz",
Description = @"this is my description ",
LockAt = commonDate,
DueAt = commonDate,
ShuffleAnswers = true,
OneQuestionAtATime = false,
LocalAssignmentGroupName = "someId",
AllowedAttempts = -1,
Questions = []
}
]
}]
};
var differences = CourseDifferences.GetNewChanges(newCourse, oldCourse);
differences.Modules.Count().Should().Be(0);
}
[Fact]
public void CanDetectDifferentQuiz()
{
var commonDate = new DateTime();
LocalCourse oldCourse = new()
{
Settings = new() { Name = "Test Course" },
Modules = [new(){
Name = "new module",
Assignments = [],
Quizzes = [
new()
{
Name = "Test Quiz",
Description = @"this is my description ",
LockAt = commonDate,
DueAt = commonDate,
ShuffleAnswers = true,
OneQuestionAtATime = false,
LocalAssignmentGroupName = "someId",
AllowedAttempts = -1,
Questions = []
}
]
}]
};
LocalCourse newCourse = oldCourse with
{
Modules = [new(){
Name = "new module",
Assignments = [],
Quizzes = [
new()
{
Name = "Test Quiz",
Description = @"this is my description ",
LockAt = DateTime.MaxValue,
DueAt = commonDate,
ShuffleAnswers = true,
OneQuestionAtATime = false,
LocalAssignmentGroupName = "someId",
AllowedAttempts = -1,
Questions = []
}
]
}]
};
var differences = CourseDifferences.GetNewChanges(newCourse, oldCourse);
differences.Modules.Count().Should().Be(1);
differences.Modules.First().Quizzes.Count().Should().Be(1);
differences.Modules.First().Quizzes.First().LockAt.Should().Be(DateTime.MaxValue);
}
[Fact]
public void CanDetectOnlyDifferentQuiz_WhenOtherQuizzesStay()
{
var commonDate = new DateTime();
LocalCourse oldCourse = new()
{
Settings = new() { Name = "Test Course" },
Modules = [new(){
Name = "new module",
Assignments = [],
Quizzes = [
new()
{
Name = "Test Quiz",
Description = @"this is my description ",
LockAt = commonDate,
DueAt = commonDate,
ShuffleAnswers = true,
OneQuestionAtATime = false,
LocalAssignmentGroupName = "someId",
AllowedAttempts = -1,
Questions = []
}
]
}]
};
LocalCourse newCourse = oldCourse with
{
Modules = [new(){
Name = "new module",
Assignments = [],
Quizzes = [
new()
{
Name = "Test Quiz",
Description = @"this is my description ",
LockAt = commonDate,
DueAt = commonDate,
ShuffleAnswers = true,
OneQuestionAtATime = false,
LocalAssignmentGroupName = "someId",
AllowedAttempts = -1,
Questions = []
},
new()
{
Name = "Test Quiz 2",
Description = @"this is my description ",
LockAt = commonDate,
DueAt = commonDate,
ShuffleAnswers = true,
OneQuestionAtATime = false,
LocalAssignmentGroupName = "someId",
AllowedAttempts = -1,
Questions = []
}
]
}]
};
var differences = CourseDifferences.GetNewChanges(newCourse, oldCourse);
differences.Modules.Count().Should().Be(1);
differences.Modules.First().Quizzes.Count().Should().Be(1);
differences.Modules.First().Quizzes.First().Name.Should().Be("Test Quiz 2");
}
[Fact]
public void SamePagesNotDetected()
{
var commonDate = new DateTime();
LocalCourse oldCourse = new()
{
Settings = new() { Name = "Test Course" },
Modules = [new(){
Name = "new module",
Pages = [
new()
{
Name= "test page",
Text = "test description",
DueAt = commonDate
}
]
}]
};
LocalCourse newCourse = oldCourse with
{
Modules = [
new(){
Name = "new module",
Pages = [
new()
{
Name= "test page",
Text = "test description",
DueAt = commonDate
}
]
}
]
};
var differences = CourseDifferences.GetNewChanges(newCourse, oldCourse);
differences.Modules.Count().Should().Be(0);
}
[Fact]
public void DifferentPageDetected()
{
var commonDate = new DateTime();
LocalCourse oldCourse = new()
{
Settings = new() { Name = "Test Course" },
Modules = [new(){
Name = "new module",
Pages = [
new()
{
Name= "test page",
Text = "test description",
DueAt = commonDate
}
]
}]
};
LocalCourse newCourse = oldCourse with
{
Modules = [
new(){
Name = "new module",
Pages = [
new()
{
Name= "test page",
Text = "test description changed",
DueAt = commonDate
}
]
}
]
};
var differences = CourseDifferences.GetNewChanges(newCourse, oldCourse);
differences.Modules.Count().Should().Be(1);
differences.Modules.First().Pages.Count().Should().Be(1);
differences.Modules.First().Pages.First().Text.Should().Be("test description changed");
}
[Fact]
public void DifferentPageDetected_ButNotSamePage()
{
var commonDate = new DateTime();
LocalCourse oldCourse = new()
{
Settings = new() { Name = "Test Course" },
Modules = [new(){
Name = "new module",
Pages = [
new()
{
Name= "test page",
Text = "test description",
DueAt = commonDate
}
]
}]
};
LocalCourse newCourse = oldCourse with
{
Modules = [
new(){
Name = "new module",
Pages = [
new()
{
Name= "test page",
Text = "test description",
DueAt = commonDate
},
new()
{
Name= "test page 2",
Text = "test description",
DueAt = commonDate
}
]
}
]
};
var differences = CourseDifferences.GetNewChanges(newCourse, oldCourse);
differences.Modules.Count().Should().Be(1);
differences.Modules.First().Pages.Count().Should().Be(1);
differences.Modules.First().Pages.First().Name.Should().Be("test page 2");
}
}

View File

@@ -1,297 +0,0 @@
using LocalModels;
public class CourseDifferencesDeletionsTests
{
[Fact]
public void SameModuleDoesNotGetDeleted()
{
LocalCourse oldCourse = new()
{
Settings = new() { },
Modules = [
new()
{
Name = "test module"
}]
};
LocalCourse newCourse = oldCourse with
{
Modules = [
new()
{
Name = "test module"
}]
};
var differences = CourseDifferences.GetDeletedChanges(newCourse, oldCourse);
differences.NamesOfModulesToDeleteCompletely.Should().BeEmpty();
}
[Fact]
public void ChangedModule_OldOneGetsDeleted()
{
LocalCourse oldCourse = new()
{
Settings = new() { },
Modules = [
new()
{
Name = "test module"
}
]
};
LocalCourse newCourse = oldCourse with
{
Modules = [
new()
{
Name = "test module 2"
}]
};
var differences = CourseDifferences.GetDeletedChanges(newCourse, oldCourse);
differences.NamesOfModulesToDeleteCompletely.Count().Should().Be(1);
differences.NamesOfModulesToDeleteCompletely.First().Should().Be("test module");
}
[Fact]
public void newAssignmentNameGetsDeleted()
{
LocalCourse oldCourse = new()
{
Settings = new() { },
Modules = [
new()
{
Name = "test module",
Assignments = [
new()
{
Name = "test assignment"
}
]
}
]
};
LocalCourse newCourse = oldCourse with
{
Modules = [
new()
{
Name = "test module",
Assignments = [
new()
{
Name = "test assignment changed name"
}
]
}]
};
var differences = CourseDifferences.GetDeletedChanges(newCourse, oldCourse);
differences.NamesOfModulesToDeleteCompletely.Should().BeEmpty();
differences.DeleteContentsOfModule.Count().Should().Be(1);
differences.DeleteContentsOfModule.First().Assignments.Count().Should().Be(1);
differences.DeleteContentsOfModule.First().Assignments.First().Name.Should().Be("test assignment");
}
[Fact]
public void AssignmentsWithChangedDescriptionsDoNotGetDeleted()
{
LocalCourse oldCourse = new()
{
Settings = new() { },
Modules = [
new()
{
Name = "test module",
Assignments = [
new()
{
Name = "test assignment",
Description = "test description",
}
]
}
]
};
LocalCourse newCourse = oldCourse with
{
Modules = [
new()
{
Name = "test module",
Assignments = [
new()
{
Name = "test assignment",
Description = "test description",
}
]
}]
};
var differences = CourseDifferences.GetDeletedChanges(newCourse, oldCourse);
differences.DeleteContentsOfModule.Should().BeEmpty();
}
[Fact]
public void CanDetectChangedAndUnchangedAssignments()
{
LocalCourse oldCourse = new()
{
Settings = new() { },
Modules = [
new()
{
Name = "test module",
Assignments = [
new()
{
Name = "test assignment",
Description = "test description",
},
new()
{
Name = "test assignment 2",
Description = "test description",
}
]
}
]
};
LocalCourse newCourse = oldCourse with
{
Modules = [
new()
{
Name = "test module",
Assignments = [
new()
{
Name = "test assignment",
Description = "test description",
},
new()
{
Name = "test assignment 2 changed",
Description = "test description",
}
]
}]
};
var differences = CourseDifferences.GetDeletedChanges(newCourse, oldCourse);
differences.DeleteContentsOfModule.Count().Should().Be(1);
differences.DeleteContentsOfModule.First().Assignments.Count().Should().Be(1);
differences.DeleteContentsOfModule.First().Assignments.First().Name.Should().Be("test assignment 2");
}
[Fact]
public void ChangedQuizzesGetDeleted()
{
LocalCourse oldCourse = new()
{
Settings = new() { },
Modules = [
new()
{
Name = "test module",
Quizzes = [
new()
{
Name = "Test Quiz",
Description = "test description"
},
new()
{
Name = "Test Quiz 2",
Description = "test description"
}
]
}
]
};
LocalCourse newCourse = oldCourse with
{
Modules = [
new()
{
Name = "test module",
Quizzes = [
new()
{
Name = "Test Quiz",
Description = "test description"
},
new()
{
Name = "Test Quiz 3",
Description = "test description"
}
]
}]
};
var differences = CourseDifferences.GetDeletedChanges(newCourse, oldCourse);
differences.DeleteContentsOfModule.Count().Should().Be(1);
differences.DeleteContentsOfModule.First().Quizzes.Count().Should().Be(1);
differences.DeleteContentsOfModule.First().Quizzes.First().Name.Should().Be("Test Quiz 2");
}
[Fact]
public void ChangedPagesGetDeleted()
{
LocalCourse oldCourse = new()
{
Settings = new() { },
Modules = [
new()
{
Name = "test module",
Pages = [
new()
{
Name = "Test Page",
Text = "test contents"
},
new()
{
Name = "Test Page 2",
Text = "test contents"
},
]
}
]
};
LocalCourse newCourse = oldCourse with
{
Modules = [
new()
{
Name = "test module",
Pages = [
new()
{
Name = "Test Page",
Text = "test contents"
},
new()
{
Name = "Test Page 3",
Text = "test contents"
},
]
}]
};
var differences = CourseDifferences.GetDeletedChanges(newCourse, oldCourse);
differences.DeleteContentsOfModule.Count().Should().Be(1);
differences.DeleteContentsOfModule.First().Pages.Count().Should().Be(1);
differences.DeleteContentsOfModule.First().Pages.First().Name.Should().Be("Test Page 2");
}
}

View File

@@ -1,301 +0,0 @@
using System.Configuration;
using LocalModels;
using Management.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
public class FileStorageTests
{
private FileStorageService fileManager { get; set; }
public FileStorageTests()
{
var tempDirectory = Path.GetTempPath();
var storageDirectory = tempDirectory + "fileStorageTests";
Console.WriteLine(storageDirectory);
if (!Directory.Exists(storageDirectory))
Directory.CreateDirectory(storageDirectory);
else
{
var dirInfo = new DirectoryInfo(storageDirectory);
foreach (var file in dirInfo.GetFiles())
file.Delete();
foreach (var dir in dirInfo.GetDirectories())
dir.Delete(true);
}
var fileManagerLogger = new MyLogger<FileStorageService>(NullLogger<FileStorageService>.Instance);
var markdownLoaderLogger = new MyLogger<CourseMarkdownLoader>(NullLogger<CourseMarkdownLoader>.Instance);
var markdownSaverLogger = new MyLogger<MarkdownCourseSaver>(NullLogger<MarkdownCourseSaver>.Instance);
var otherLogger = NullLoggerFactory.Instance.CreateLogger<FileStorageService>();
Environment.SetEnvironmentVariable("storageDirectory", storageDirectory);
var config = new ConfigurationBuilder()
.AddEnvironmentVariables()
.Build();
var fileConfiguration = new FileConfiguration(config);
var markdownLoader = new CourseMarkdownLoader(markdownLoaderLogger, fileConfiguration);
var markdownSaver = new MarkdownCourseSaver(markdownSaverLogger, fileConfiguration);
fileManager = new FileStorageService(fileManagerLogger, markdownLoader, markdownSaver, otherLogger, fileConfiguration);
}
[Fact]
public async Task EmptyCourse_CanBeSavedAndLoaded()
{
LocalCourse testCourse = new LocalCourse
{
Settings = new() { Name = "test empty course" },
Modules = []
};
await fileManager.SaveCourseAsync(testCourse, null);
var loadedCourses = await fileManager.LoadSavedCourses();
var loadedCourse = loadedCourses.First(c => c.Settings.Name == testCourse.Settings.Name);
loadedCourse.Should().BeEquivalentTo(testCourse);
}
[Fact]
public async Task CourseSettings_CanBeSavedAndLoaded()
{
LocalCourse testCourse = new()
{
Settings = new()
{
AssignmentGroups = [],
Name = "Test Course with settings",
DaysOfWeek = [DayOfWeek.Monday, DayOfWeek.Wednesday],
StartDate = new DateTime(),
EndDate = new DateTime(),
DefaultDueTime = new() { Hour = 1, Minute = 59 },
},
Modules = []
};
await fileManager.SaveCourseAsync(testCourse, null);
var loadedCourses = await fileManager.LoadSavedCourses();
var loadedCourse = loadedCourses.First(c => c.Settings.Name == testCourse.Settings.Name);
loadedCourse.Settings.Should().BeEquivalentTo(testCourse.Settings);
}
[Fact]
public async Task EmptyCourseModules_CanBeSavedAndLoaded()
{
LocalCourse testCourse = new()
{
Settings = new() { Name = "Test Course with modules" },
Modules = [
new()
{
Name = "test module 1",
Assignments = [],
Quizzes = []
}
]
};
await fileManager.SaveCourseAsync(testCourse, null);
var loadedCourses = await fileManager.LoadSavedCourses();
var loadedCourse = loadedCourses.First(c => c.Settings.Name == testCourse.Settings.Name);
loadedCourse.Modules.Should().BeEquivalentTo(testCourse.Modules);
}
[Fact]
public async Task CourseModules_WithAssignments_CanBeSavedAndLoaded()
{
LocalCourse testCourse = new()
{
Settings = new() { Name = "Test Course with modules and assignments" },
Modules = [
new()
{
Name = "test module 1 with assignments",
Assignments = [
new()
{
Name = "test assignment",
Description = "here is the description",
DueAt = new DateTime(),
LockAt = new DateTime(),
SubmissionTypes = [AssignmentSubmissionType.ONLINE_UPLOAD],
LocalAssignmentGroupName = "Final Project",
Rubric = [
new() { Points = 4, Label = "do task 1" },
new() { Points = 2, Label = "do task 2" },
]
}
],
Quizzes = []
}
]
};
await fileManager.SaveCourseAsync(testCourse, null);
var loadedCourses = await fileManager.LoadSavedCourses();
var loadedCourse = loadedCourses.First(c => c.Settings.Name == testCourse.Settings.Name);
var actualAssignments = loadedCourse.Modules.First().Assignments;
var expectedAssignments = testCourse.Modules.First().Assignments;
actualAssignments.Should().BeEquivalentTo(expectedAssignments);
}
[Fact]
public async Task CourseModules_WithQuizzes_CanBeSavedAndLoaded()
{
LocalCourse testCourse = new()
{
Settings = new() { Name = "Test Course with modules and quiz" },
Modules = [
new()
{
Name = "test module 1 with quiz",
Assignments = [],
Quizzes = [
new()
{
Name = "Test Quiz",
Description = "quiz description",
LockAt = new DateTime(2022, 10, 3, 12, 5, 0),
DueAt = new DateTime(2022, 10, 3, 12, 5, 0),
ShuffleAnswers = true,
OneQuestionAtATime = true,
LocalAssignmentGroupName = "Assignments",
Questions = [
new()
{
Text = "test essay",
QuestionType = QuestionType.ESSAY,
Points = 1
}
]
}
]
}
]
};
await fileManager.SaveCourseAsync(testCourse, null);
var loadedCourses = await fileManager.LoadSavedCourses();
var loadedCourse = loadedCourses.First(c => c.Settings.Name == testCourse.Settings.Name);
loadedCourse.Modules.First().Quizzes.Should().BeEquivalentTo(testCourse.Modules.First().Quizzes);
}
[Fact]
public async Task MarkdownStorage_FullyPopulated_DoesNotLoseData()
{
LocalCourse testCourse = new()
{
Settings = new()
{
AssignmentGroups = [],
Name = "Test Course with lots of data",
DaysOfWeek = [DayOfWeek.Monday, DayOfWeek.Wednesday],
StartDate = new DateTime(),
EndDate = new DateTime(),
DefaultDueTime = new() { Hour = 1, Minute = 59 },
},
Modules = [
new()
{
Name = "new test module",
Assignments = [
new()
{
Name = "test assignment",
Description = "here is the description",
DueAt = new DateTime(),
LockAt = new DateTime(),
SubmissionTypes = [AssignmentSubmissionType.ONLINE_UPLOAD],
LocalAssignmentGroupName = "Final Project",
Rubric = [
new() { Points = 4, Label = "do task 1" },
new() { Points = 2, Label = "do task 2" },
]
}
],
Quizzes = [
new()
{
Name = "Test Quiz",
Description = "quiz description",
LockAt = new DateTime(),
DueAt = new DateTime(),
ShuffleAnswers = true,
OneQuestionAtATime = false,
LocalAssignmentGroupName = "someId",
AllowedAttempts = -1,
Questions = [
new()
{
Text = "test short answer",
QuestionType = QuestionType.SHORT_ANSWER,
Points = 1
}
]
}
]
}
]
};
await fileManager.SaveCourseAsync(testCourse, null);
var loadedCourses = await fileManager.LoadSavedCourses();
var loadedCourse = loadedCourses.First(c => c.Settings.Name == testCourse.Settings.Name);
loadedCourse.Should().BeEquivalentTo(testCourse);
}
[Fact]
public async Task MarkdownStorage_CanPersistPages()
{
LocalCourse testCourse = new()
{
Settings = new()
{
AssignmentGroups = [],
Name = "Test Course with page",
DaysOfWeek = [DayOfWeek.Monday, DayOfWeek.Wednesday],
StartDate = new DateTime(),
EndDate = new DateTime(),
DefaultDueTime = new() { Hour = 1, Minute = 59 },
},
Modules = [
new()
{
Name = "page test module",
Pages = [
new()
{
Name = "test page persistence",
DueAt = new DateTime(),
Text = "this is some\n## markdown\n"
}
]
}
]
};
await fileManager.SaveCourseAsync(testCourse, null);
var loadedCourses = await fileManager.LoadSavedCourses();
var loadedCourse = loadedCourses.First(c => c.Settings.Name == testCourse.Settings.Name);
loadedCourse.Should().BeEquivalentTo(testCourse);
}
}

View File

@@ -1,38 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="akka.testkit" Version="1.5.27.1" />
<PackageReference Include="Akka.TestKit.Xunit2" Version="1.5.27.1" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="nsubstitute" Version="5.1.0" />
<PackageReference Include="NSubstitute.Analyzers.CSharp" Version="1.0.17">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Management\Management.csproj" />
<ProjectReference Include="..\Management.Web\Management.Web.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,156 +0,0 @@
using LocalModels;
public class AssignmentMarkdownTests
{
[Fact]
public void TestCanParseAssignmentSettings()
{
var assignment = new LocalAssignment()
{
Name = "test assignment",
Description = "here is the description",
DueAt = new DateTime(),
LockAt = new DateTime(),
SubmissionTypes = [AssignmentSubmissionType.ONLINE_UPLOAD],
LocalAssignmentGroupName = "Final Project",
Rubric = new List<RubricItem>() {
new RubricItem() {Points = 4, Label="do task 1"},
new RubricItem() {Points = 2, Label="do task 2"},
}
};
var assignmentMarkdown = assignment.ToMarkdown();
var parsedAssignment = LocalAssignment.ParseMarkdown(assignmentMarkdown);
parsedAssignment.Should().BeEquivalentTo(assignment);
}
[Fact]
public void AssignmentWithEmptyRubric_CanBeParsed()
{
var assignment = new LocalAssignment()
{
Name = "test assignment",
Description = "here is the description",
DueAt = new DateTime(),
LockAt = new DateTime(),
SubmissionTypes = [AssignmentSubmissionType.ONLINE_UPLOAD],
LocalAssignmentGroupName = "Final Project",
Rubric = new List<RubricItem>() { }
};
var assignmentMarkdown = assignment.ToMarkdown();
var parsedAssignment = LocalAssignment.ParseMarkdown(assignmentMarkdown);
parsedAssignment.Should().BeEquivalentTo(assignment);
}
[Fact]
public void AssignmentWithEmptySubmissionTypes_CanBeParsed()
{
var assignment = new LocalAssignment()
{
Name = "test assignment",
Description = "here is the description",
DueAt = new DateTime(),
LockAt = new DateTime(),
SubmissionTypes = [],
LocalAssignmentGroupName = "Final Project",
Rubric = new List<RubricItem>() {
new RubricItem() {Points = 4, Label="do task 1"},
new RubricItem() {Points = 2, Label="do task 2"},
}
};
var assignmentMarkdown = assignment.ToMarkdown();
var parsedAssignment = LocalAssignment.ParseMarkdown(assignmentMarkdown);
parsedAssignment.Should().BeEquivalentTo(assignment);
}
[Fact]
public void AssignmentWithoutLockAtDate_CanBeParsed()
{
var assignment = new LocalAssignment()
{
Name = "test assignment",
Description = "here is the description",
DueAt = new DateTime(),
LockAt = null,
SubmissionTypes = [],
LocalAssignmentGroupName = "Final Project",
Rubric = new List<RubricItem>() {
new RubricItem() {Points = 4, Label="do task 1"},
new RubricItem() {Points = 2, Label="do task 2"},
}
};
var assignmentMarkdown = assignment.ToMarkdown();
var parsedAssignment = LocalAssignment.ParseMarkdown(assignmentMarkdown);
parsedAssignment.Should().BeEquivalentTo(assignment);
}
[Fact]
public void AssignmentWithoutDescription_CanBeParsed()
{
var assignment = new LocalAssignment()
{
Name = "test assignment",
Description = "",
DueAt = new DateTime(),
LockAt = new DateTime(),
SubmissionTypes = [],
LocalAssignmentGroupName = "Final Project",
Rubric = new List<RubricItem>() {
new RubricItem() {Points = 4, Label="do task 1"},
new RubricItem() {Points = 2, Label="do task 2"},
}
};
var assignmentMarkdown = assignment.ToMarkdown();
var parsedAssignment = LocalAssignment.ParseMarkdown(assignmentMarkdown);
parsedAssignment.Should().BeEquivalentTo(assignment);
}
[Fact]
public void Assignments_CanHaveThreeDashes()
{
var assignment = new LocalAssignment()
{
Name = "test assignment",
Description = "test assignment\n---\nsomestuff",
DueAt = new DateTime(),
LockAt = new DateTime(),
SubmissionTypes = [],
LocalAssignmentGroupName = "Final Project",
Rubric = new List<RubricItem>()
{
}
};
var assignmentMarkdown = assignment.ToMarkdown();
var parsedAssignment = LocalAssignment.ParseMarkdown(assignmentMarkdown);
parsedAssignment.Should().BeEquivalentTo(assignment);
}
[Fact]
public void Assignments_CanRestrictUploadTypes()
{
var assignment = new LocalAssignment()
{
Name = "test assignment",
Description = "here is the description",
DueAt = new DateTime(),
LockAt = new DateTime(),
SubmissionTypes = [AssignmentSubmissionType.ONLINE_UPLOAD],
AllowedFileUploadExtensions = ["pdf", "txt"],
LocalAssignmentGroupName = "Final Project",
Rubric = new List<RubricItem>() {}
};
var assignmentMarkdown = assignment.ToMarkdown();
var parsedAssignment = LocalAssignment.ParseMarkdown(assignmentMarkdown);
parsedAssignment.Should().BeEquivalentTo(assignment);
}
}

View File

@@ -1,21 +0,0 @@
using LocalModels;
public class PageMarkdownTests
{
[Fact]
public void TestCanParsePage()
{
var page = new LocalCoursePage
{
Name = "test title",
Text = "test text content",
DueAt = new DateTime()
};
var pageMarkdown = page.ToMarkdown();
var parsedPage = LocalCoursePage.ParseMarkdown(pageMarkdown);
parsedPage.Should().BeEquivalentTo(page);
}
}

View File

@@ -1,159 +0,0 @@
using LocalModels;
public class MatchingTests
{
[Fact]
public void CanParseMatchingQuestion()
{
var rawMarkdownQuiz = @"
Name: Test Quiz
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 2023-08-21T23:59:00
LockAt: 2023-08-21T23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description:
---
Match the following terms & definitions
^ statement - a single command to be executed
^ identifier - name of a variable
^ keyword - reserved word that has special meaning in a program (e.g. class, void, static, etc.)
";
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz);
var firstQuestion = quiz.Questions.First();
firstQuestion.QuestionType.Should().Be(QuestionType.MATCHING);
firstQuestion.Text.Should().NotContain("statement");
firstQuestion.Answers.First().MatchedText.Should().Be("a single command to be executed");
}
[Fact]
public void CanCreateMarkdownForMatchingQuesiton()
{
var rawMarkdownQuiz = @"
Name: Test Quiz
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 2023-08-21T23:59:00
LockAt: 2023-08-21T23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description:
---
Match the following terms & definitions
^ statement - a single command to be executed
^ identifier - name of a variable
^ keyword - reserved word that has special meaning in a program (e.g. class, void, static, etc.)
";
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz);
var questionMarkdown = quiz.Questions.First().ToMarkdown();
var expectedMarkdown = @"Points: 1
Match the following terms & definitions
^ statement - a single command to be executed
^ identifier - name of a variable
^ keyword - reserved word that has special meaning in a program (e.g. class, void, static, etc.)";
questionMarkdown.Should().Contain(expectedMarkdown);
}
[Fact]
public void WhitespaceIsOptional()
{
var rawMarkdownQuiz = @"
Name: Test Quiz
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 2023-08-21T23:59:00
LockAt: 2023-08-21T23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description:
---
Match the following terms & definitions
^statement - a single command to be executed
";
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz);
quiz.Questions.First().Answers.First().Text.Should().Be("statement");
}
[Fact]
public void CanHaveDistractors()
{
var rawMarkdownQuiz = @"
Name: Test Quiz
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 2023-08-21T23:59:00
LockAt: 2023-08-21T23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description:
---
Match the following terms & definitions
^statement - a single command to be executed
^ - this is the distractor
";
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz);
quiz.Questions.First().MatchDistractors.Should().BeEquivalentTo(["this is the distractor"]);
}
[Fact]
public void CanHaveDistractorsAndBePersisted()
{
var rawMarkdownQuiz = @"
Name: Test Quiz
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 2023-08-21T23:59:00
LockAt: 2023-08-21T23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description:
---
Match the following terms & definitions
^ statement - a single command to be executed
^ - this is the distractor
";
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz);
var quizMarkdown = quiz.ToMarkdown();
quizMarkdown.Should().Contain("^ statement - a single command to be executed\n^ - this is the distractor");
}
[Fact]
public void DistractorsDoNotAddDelimiterOntheEnd()
{
var rawMarkdownQuiz = @"
Name: Test Quiz
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 2023-08-21T23:59:00
LockAt: 2023-08-21T23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description:
---
Points: 2
Match up the term with the best possible answer.
^ - a variable name
^ - A reserved word with special meaning to the compiler
";
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz);
var quizMarkdown = quiz.ToMarkdown();
quizMarkdown.Should().Contain(@"Match up the term with the best possible answer.
^ - a variable name
^ - A reserved word with special meaning to the compiler");
}
}

View File

@@ -1,125 +0,0 @@
using LocalModels;
public class MultipleAnswersTests
{
[Fact]
public void QuzMarkdownIncludesMultipleAnswerQuestion()
{
var quiz = new LocalQuiz()
{
Name = "Test Quiz",
Description = "desc",
LockAt = DateTime.MaxValue,
DueAt = DateTime.MaxValue,
ShuffleAnswers = true,
OneQuestionAtATime = false,
LocalAssignmentGroupName = "someId",
AllowedAttempts = -1,
Questions = new LocalQuizQuestion[]
{
new()
{
Text = "oneline question",
Points = 1,
QuestionType = QuestionType.MULTIPLE_ANSWERS,
Answers = new LocalQuizQuestionAnswer[]
{
new() { Correct = true, Text = "true" },
new() { Correct = true, Text = "false"},
new() { Correct = false, Text = "neither"},
}
}
}
};
var markdown = quiz.ToMarkdown();
var expectedQuestionString = @"
Points: 1
oneline question
[*] true
[*] false
[ ] neither
";
markdown.Should().Contain(expectedQuestionString);
}
[Fact]
public void CanParseQuestionWithMultipleAnswers()
{
var rawMarkdownQuiz = @"
Name: Test Quiz
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 2023-08-21T23:59:00
LockAt: 2023-08-21T23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Which events are triggered when the user clicks on an input field?
[*] click
[*] focus
[*] mousedown
[] submit
[] change
[] mouseout
[] keydown
---
";
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz);
var firstQuestion = quiz.Questions.First();
firstQuestion.Points.Should().Be(1);
firstQuestion.QuestionType.Should().Be(QuestionType.MULTIPLE_ANSWERS);
firstQuestion.Text.Should().Contain("Which events are triggered when the user clicks on an input field?");
firstQuestion.Answers.First().Text.Should().Be("click");
firstQuestion.Answers.First().Correct.Should().BeTrue();
firstQuestion.Answers.ElementAt(3).Correct.Should().BeFalse();
firstQuestion.Answers.ElementAt(3).Text.Should().Be("submit");
}
[Fact]
public void CanUseBracesInAnswerFormultipleAnswer()
{
var rawMarkdownQuestion = @"
Which events are triggered when the user clicks on an input field?
[*] `int[] theThing()`
[] keydown
";
var question = LocalQuizQuestion.ParseMarkdown(rawMarkdownQuestion, 0);
question.Answers.First().Text.Should().Be("`int[] theThing()`");
question.Answers.Count().Should().Be(2);
}
[Fact]
public void CanUseBracesInAnswerFormultipleAnswer_MultiLine()
{
var rawMarkdownQuestion = @"
Which events are triggered when the user clicks on an input field?
[*]
```
int[] myNumbers = new int[] { };
DoSomething(ref myNumbers);
static void DoSomething(ref int[] numbers)
{
// do something
}
```
";
var question = LocalQuizQuestion.ParseMarkdown(rawMarkdownQuestion, 0);
question.Answers.First().Text.Should().Be(@"```
int[] myNumbers = new int[] { };
DoSomething(ref myNumbers);
static void DoSomething(ref int[] numbers)
{
// do something
}
```");
question.Answers.Count().Should().Be(1);
}
}

View File

@@ -1,73 +0,0 @@
using LocalModels;
public class MultipleChoiceTests
{
[Fact]
public void QuzMarkdownIncludesMultipleChoiceQuestion()
{
var quiz = new LocalQuiz()
{
Name = "Test Quiz",
Description = "desc",
LockAt = DateTime.MaxValue,
DueAt = DateTime.MaxValue,
ShuffleAnswers = true,
OneQuestionAtATime = false,
LocalAssignmentGroupName = "someId",
AllowedAttempts = -1,
Questions = new LocalQuizQuestion[]
{
new LocalQuizQuestion()
{
Points = 2,
Text = @"`some type` of question
with many
```
lines
```
",
QuestionType = QuestionType.MULTIPLE_CHOICE,
Answers = new LocalQuizQuestionAnswer[]
{
new LocalQuizQuestionAnswer() { Correct = true, Text = "true" },
new LocalQuizQuestionAnswer() { Correct = false, Text = "false\n\nendline" },
}
}
}
};
var markdown = quiz.ToMarkdown();
var expectedQuestionString = @"
Points: 2
`some type` of question
with many
```
lines
```
*a) true
b) false
endline
";
markdown.Should().Contain(expectedQuestionString);
}
[Fact]
public void LetterOptionalForMultipleChoice()
{
var questionMarkdown = @"Points: 2
`some type` of question
*) true
) false
";
var question = LocalQuizQuestion.ParseMarkdown(questionMarkdown, 0);
question.Answers.Count().Should().Be(2);
}
}

View File

@@ -1,218 +0,0 @@
using System.Text;
using LocalModels;
public class QuizDeterministicChecks
{
[Fact]
public void SerializationIsDeterministic_EmptyQuiz()
{
var quiz = new LocalQuiz()
{
Name = "Test Quiz",
Description = "quiz description",
LockAt = new DateTime(2022, 10, 3, 12, 5, 0),
DueAt = new DateTime(2022, 10, 3, 12, 5, 0),
ShuffleAnswers = true,
OneQuestionAtATime = true,
LocalAssignmentGroupName = "Assignments"
};
var quizMarkdown = quiz.ToMarkdown();
var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown);
parsedQuiz.Should().BeEquivalentTo(quiz);
}
[Fact]
public void SerializationIsDeterministic_ShowCorrectAnswers()
{
var quiz = new LocalQuiz()
{
Name = "Test Quiz",
Description = "quiz description",
LockAt = new DateTime(2022, 10, 3, 12, 5, 0),
DueAt = new DateTime(2022, 10, 3, 12, 5, 0),
showCorrectAnswers = false,
ShuffleAnswers = true,
OneQuestionAtATime = true,
LocalAssignmentGroupName = "Assignments"
};
var quizMarkdown = quiz.ToMarkdown();
var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown);
parsedQuiz.Should().BeEquivalentTo(quiz);
}
[Fact]
public void SerializationIsDeterministic_ShortAnswer()
{
var quiz = new LocalQuiz()
{
Name = "Test Quiz",
Description = "quiz description",
LockAt = new DateTime(2022, 10, 3, 12, 5, 0),
DueAt = new DateTime(2022, 10, 3, 12, 5, 0),
ShuffleAnswers = true,
OneQuestionAtATime = true,
LocalAssignmentGroupName = "Assignments",
Questions = new LocalQuizQuestion[]
{
new ()
{
Text = "test short answer",
QuestionType = QuestionType.SHORT_ANSWER,
Points = 1
}
}
};
var quizMarkdown = quiz.ToMarkdown();
var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown);
parsedQuiz.Should().BeEquivalentTo(quiz);
}
[Fact]
public void SerializationIsDeterministic_Essay()
{
var quiz = new LocalQuiz()
{
Name = "Test Quiz",
Description = "quiz description",
LockAt = new DateTime(2022, 10, 3, 12, 5, 0),
DueAt = new DateTime(2022, 10, 3, 12, 5, 0),
ShuffleAnswers = true,
OneQuestionAtATime = true,
LocalAssignmentGroupName = "Assignments",
Questions = new LocalQuizQuestion[]
{
new ()
{
Text = "test essay",
QuestionType = QuestionType.ESSAY,
Points = 1
}
}
};
var quizMarkdown = quiz.ToMarkdown();
var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown);
parsedQuiz.Should().BeEquivalentTo(quiz);
}
[Fact]
public void SerializationIsDeterministic_MultipleAnswer()
{
var quiz = new LocalQuiz()
{
Name = "Test Quiz",
Description = "quiz description",
LockAt = new DateTime(2022, 10, 3, 12, 5, 0),
DueAt = new DateTime(2022, 10, 3, 12, 5, 0),
ShuffleAnswers = true,
OneQuestionAtATime = true,
LocalAssignmentGroupName = "Assignments",
Questions = new LocalQuizQuestion[]
{
new ()
{
Text = "test multiple answer",
QuestionType = QuestionType.MULTIPLE_ANSWERS,
Points = 1,
Answers = new LocalQuizQuestionAnswer[]
{
new() {
Correct = true,
Text="yes",
},
new() {
Correct = true,
Text="no",
}
}
}
}
};
var quizMarkdown = quiz.ToMarkdown();
var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown);
parsedQuiz.Should().BeEquivalentTo(quiz);
}
[Fact]
public void SerializationIsDeterministic_MultipleChoice()
{
var quiz = new LocalQuiz()
{
Name = "Test Quiz",
Description = "quiz description",
LockAt = new DateTime(2022, 10, 3, 12, 5, 0),
DueAt = new DateTime(2022, 10, 3, 12, 5, 0),
ShuffleAnswers = true,
OneQuestionAtATime = true,
LocalAssignmentGroupName = "Assignments",
Questions = new LocalQuizQuestion[]
{
new ()
{
Text = "test multiple choice",
QuestionType = QuestionType.MULTIPLE_CHOICE,
Points = 1,
Answers = new LocalQuizQuestionAnswer[]
{
new() {
Correct = true,
Text="yes",
},
new() {
Correct = false,
Text="no",
}
}
}
}
};
var quizMarkdown = quiz.ToMarkdown();
var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown);
parsedQuiz.Should().BeEquivalentTo(quiz);
}
[Fact]
public void SerializationIsDeterministic_Matching()
{
var quiz = new LocalQuiz()
{
Name = "Test Quiz",
Description = "quiz description",
LockAt = new DateTime(2022, 10, 3, 12, 5, 0),
DueAt = new DateTime(2022, 10, 3, 12, 5, 0),
ShuffleAnswers = true,
OneQuestionAtATime = true,
LocalAssignmentGroupName = "Assignments",
Questions = new LocalQuizQuestion[]
{
new ()
{
Text = "test matching",
QuestionType = QuestionType.MATCHING,
Points = 1,
Answers = [
new() {
Correct = true,
Text="yes",
MatchedText = "testing yes"
},
new() {
Correct = true,
Text="no",
MatchedText = "testing no"
}
]
}
}
};
var quizMarkdown = quiz.ToMarkdown();
var parsedQuiz = LocalQuiz.ParseMarkdown(quizMarkdown);
parsedQuiz.Should().BeEquivalentTo(quiz);
}
}

View File

@@ -1,271 +0,0 @@
using System.Text;
using LocalModels;
// try to follow syntax from https://github.com/gpoore/text2qti
public class QuizMarkdownTests
{
[Fact]
public void CanSerializeQuizToMarkdown()
{
var quiz = new LocalQuiz()
{
Name = "Test Quiz",
Description = @"
# quiz description
this is my description in markdown
`here is code`
",
LockAt = DateTime.MaxValue,
DueAt = DateTime.MaxValue,
ShuffleAnswers = true,
OneQuestionAtATime = false,
LocalAssignmentGroupName = "someId",
AllowedAttempts = -1,
Questions = []
};
var markdown = quiz.ToMarkdown();
markdown.Should().Contain("Name: Test Quiz");
markdown.Should().Contain(quiz.Description);
markdown.Should().Contain("ShuffleAnswers: true");
markdown.Should().Contain("OneQuestionAtATime: false");
markdown.Should().Contain("AssignmentGroup: someId");
markdown.Should().Contain("AllowedAttempts: -1");
}
[Fact]
public void TestCanParseMarkdownQuizWithNoQuestions()
{
var rawMarkdownQuiz = new StringBuilder();
rawMarkdownQuiz.Append("Name: Test Quiz\n");
rawMarkdownQuiz.Append("ShuffleAnswers: true\n");
rawMarkdownQuiz.Append("OneQuestionAtATime: false\n");
rawMarkdownQuiz.Append("DueAt: 2023-08-21T23:59:00\n");
rawMarkdownQuiz.Append("LockAt: 2023-08-21T23:59:00\n");
rawMarkdownQuiz.Append("AssignmentGroup: Assignments\n");
rawMarkdownQuiz.Append("AllowedAttempts: -1\n");
rawMarkdownQuiz.Append("Description: this is the\n");
rawMarkdownQuiz.Append("multi line\n");
rawMarkdownQuiz.Append("description\n");
rawMarkdownQuiz.Append("---\n");
rawMarkdownQuiz.Append('\n');
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz.ToString());
var expectedDescription = new StringBuilder();
expectedDescription.Append("this is the\n");
expectedDescription.Append("multi line\n");
expectedDescription.Append("description");
quiz.Name.Should().Be("Test Quiz");
quiz.ShuffleAnswers.Should().Be(true);
quiz.OneQuestionAtATime.Should().BeFalse();
quiz.AllowedAttempts.Should().Be(-1);
quiz.Description.Should().Be(expectedDescription.ToString());
}
[Fact]
public void TestCanParseMarkdownQuizPassword()
{
var password = "this-is-the-password";
var rawMarkdownQuiz = new StringBuilder();
rawMarkdownQuiz.Append("Name: Test Quiz\n");
rawMarkdownQuiz.Append($"Password: {password}\n");
rawMarkdownQuiz.Append("ShuffleAnswers: true\n");
rawMarkdownQuiz.Append("OneQuestionAtATime: false\n");
rawMarkdownQuiz.Append("DueAt: 2023-08-21T23:59:00\n");
rawMarkdownQuiz.Append("LockAt: 2023-08-21T23:59:00\n");
rawMarkdownQuiz.Append("AssignmentGroup: Assignments\n");
rawMarkdownQuiz.Append("AllowedAttempts: -1\n");
rawMarkdownQuiz.Append("Description: this is the\n");
rawMarkdownQuiz.Append("multi line\n");
rawMarkdownQuiz.Append("description\n");
rawMarkdownQuiz.Append("---\n");
rawMarkdownQuiz.Append('\n');
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz.ToString());
quiz.Password.Should().Be(password);
}
[Fact]
public void TestCanParseMarkdownQuiz_CanConfigureToShowCorrectAnswers()
{
var rawMarkdownQuiz = new StringBuilder();
rawMarkdownQuiz.Append("Name: Test Quiz\n");
rawMarkdownQuiz.Append("ShuffleAnswers: true\n");
rawMarkdownQuiz.Append("OneQuestionAtATime: false\n");
rawMarkdownQuiz.Append("ShowCorrectAnswers: false\n");
rawMarkdownQuiz.Append("DueAt: 2023-08-21T23:59:00\n");
rawMarkdownQuiz.Append("LockAt: 2023-08-21T23:59:00\n");
rawMarkdownQuiz.Append("AssignmentGroup: Assignments\n");
rawMarkdownQuiz.Append("AllowedAttempts: -1\n");
rawMarkdownQuiz.Append("Description: this is the\n");
rawMarkdownQuiz.Append("multi line\n");
rawMarkdownQuiz.Append("description\n");
rawMarkdownQuiz.Append("---\n");
rawMarkdownQuiz.Append('\n');
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz.ToString());
quiz.showCorrectAnswers.Should().BeFalse();
}
[Fact]
public void TestCanParseQuizWithQuestions()
{
var rawMarkdownQuiz = @"
Name: Test Quiz
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 2023-08-21T23:59:00
LockAt: 2023-08-21T23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Points: 2
`some type` of question
with many
```
lines
```
*a) true
b) false
endline";
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz);
var firstQuestion = quiz.Questions.First();
firstQuestion.QuestionType.Should().Be(QuestionType.MULTIPLE_CHOICE);
firstQuestion.Points.Should().Be(2);
firstQuestion.Text.Should().Contain("```");
firstQuestion.Text.Should().Contain("`some type` of question");
firstQuestion.Answers.First().Text.Should().Be("true");
firstQuestion.Answers.First().Correct.Should().BeTrue();
firstQuestion.Answers.ElementAt(1).Correct.Should().BeFalse();
firstQuestion.Answers.ElementAt(1).Text.Should().Contain("endline");
}
[Fact]
public void CanParseMultipleQuestions()
{
var rawMarkdownQuiz = @"
Name: Test Quiz
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 2023-08-21T23:59:00
LockAt: 2023-08-21T23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Which events are triggered when the user clicks on an input field?
[*] click
---
points: 2
`some type` of question
*a) true
b) false
";
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz);
var firstQuestion = quiz.Questions.First();
firstQuestion.Points.Should().Be(1);
firstQuestion.QuestionType.Should().Be(QuestionType.MULTIPLE_ANSWERS);
var secondQuestion = quiz.Questions.ElementAt(1);
secondQuestion.Points.Should().Be(2);
secondQuestion.QuestionType.Should().Be(QuestionType.MULTIPLE_CHOICE);
}
[Fact]
public void ShortAnswerToMarkdown_IsCorrect()
{
var rawMarkdownQuiz = @"
Name: Test Quiz
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 2023-08-21T23:59:00
LockAt: 2023-08-21T23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Which events are triggered when the user clicks on an input field?
short answer
";
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz);
var firstQuestion = quiz.Questions.First();
var questionMarkdown = firstQuestion.ToMarkdown();
var expectedMarkdown = @"Points: 1
Which events are triggered when the user clicks on an input field?
short_answer";
questionMarkdown.Should().Contain(expectedMarkdown);
}
[Fact]
public void NegativePoints_IsAllowed()
{
var rawMarkdownQuiz = @"
Name: Test Quiz
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 2023-08-21T23:59:00
LockAt: 2023-08-21T23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Points: -4
Which events are triggered when the user clicks on an input field?
short answer
";
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz);
var firstQuestion = quiz.Questions.First();
firstQuestion.Points.Should().Be(-4);
}
[Fact]
public void FloatingPointPoints_IsAllowed()
{
var rawMarkdownQuiz = @"
Name: Test Quiz
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 2023-08-21T23:59:00
LockAt: 2023-08-21T23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Points: 4.56
Which events are triggered when the user clicks on an input field?
short answer
";
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz);
var firstQuestion = quiz.Questions.First();
firstQuestion.Points.Should().Be(4.56);
}
}

View File

@@ -1,114 +0,0 @@
using LocalModels;
public class TextAnswerTests
{
[Fact]
public void CanParseEssay()
{
var rawMarkdownQuiz = @"
Name: Test Quiz
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 2023-08-21T23:59:00
LockAt: 2023-08-21T23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Which events are triggered when the user clicks on an input field?
essay
";
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz);
var firstQuestion = quiz.Questions.First();
firstQuestion.Points.Should().Be(1);
firstQuestion.QuestionType.Should().Be(QuestionType.ESSAY);
firstQuestion.Text.Should().NotContain("essay");
}
[Fact]
public void CanParseShortAnswer()
{
var rawMarkdownQuiz = @"
Name: Test Quiz
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 2023-08-21T23:59:00
LockAt: 2023-08-21T23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Which events are triggered when the user clicks on an input field?
short answer
";
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz);
var firstQuestion = quiz.Questions.First();
firstQuestion.Points.Should().Be(1);
firstQuestion.QuestionType.Should().Be(QuestionType.SHORT_ANSWER);
firstQuestion.Text.Should().NotContain("short answer");
}
[Fact]
public void ShortAnswerToMarkdown_IsCorrect()
{
var rawMarkdownQuiz = @"
Name: Test Quiz
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 2023-08-21T23:59:00
LockAt: 2023-08-21T23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Which events are triggered when the user clicks on an input field?
short answer
";
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz);
var firstQuestion = quiz.Questions.First();
var questionMarkdown = firstQuestion.ToMarkdown();
var expectedMarkdown = @"Points: 1
Which events are triggered when the user clicks on an input field?
short_answer";
questionMarkdown.Should().Contain(expectedMarkdown);
}
[Fact]
public void EssayQuestionToMarkdown_IsCorrect()
{
var rawMarkdownQuiz = @"
Name: Test Quiz
ShuffleAnswers: true
OneQuestionAtATime: false
DueAt: 2023-08-21T23:59:00
LockAt: 2023-08-21T23:59:00
AssignmentGroup: Assignments
AllowedAttempts: -1
Description: this is the
multi line
description
---
Which events are triggered when the user clicks on an input field?
essay
";
var quiz = LocalQuiz.ParseMarkdown(rawMarkdownQuiz);
var firstQuestion = quiz.Questions.First();
var questionMarkdown = firstQuestion.ToMarkdown();
var expectedMarkdown = @"Points: 1
Which events are triggered when the user clicks on an input field?
essay";
questionMarkdown.Should().Contain(expectedMarkdown);
}
}

View File

@@ -1,102 +0,0 @@
using LocalModels;
public class RubricMarkdownTests
{
[Fact]
public void TestCanParseOneItem()
{
var rawRubric = @"
- 2pts: this is the task
";
var rubric = LocalAssignment.ParseRubricMarkdown(rawRubric);
rubric.Count().Should().Be(1);
rubric.First().IsExtraCredit.Should().BeFalse();
rubric.First().Label.Should().Be("this is the task");
rubric.First().Points.Should().Be(2);
}
[Fact]
public void TestCanParseMultipleItems()
{
var rawRubric = @"
- 2pts: this is the task
- 3pts: this is the other task
";
var rubric = LocalAssignment.ParseRubricMarkdown(rawRubric);
rubric.Count().Should().Be(2);
rubric.ElementAt(1).IsExtraCredit.Should().BeFalse();
rubric.ElementAt(1).Label.Should().Be("this is the other task");
rubric.ElementAt(1).Points.Should().Be(3);
}
[Fact]
public void TestCanParseSinglePoint()
{
var rawRubric = @"
- 1pt: this is the task
";
var rubric = LocalAssignment.ParseRubricMarkdown(rawRubric);
rubric.First().IsExtraCredit.Should().BeFalse();
rubric.First().Label.Should().Be("this is the task");
rubric.First().Points.Should().Be(1);
}
[Fact]
public void TestCanParseSingleExtraCredit_LowerCase()
{
var rawRubric = @"
- 1pt: (extra credit) this is the task
";
var rubric = LocalAssignment.ParseRubricMarkdown(rawRubric);
rubric.First().IsExtraCredit.Should().BeTrue();
rubric.First().Label.Should().Be("(extra credit) this is the task");
}
[Fact]
public void TestCanParseSingleExtraCredit_UpperCase()
{
var rawRubric = @"
- 1pt: (Extra Credit) this is the task
";
var rubric = LocalAssignment.ParseRubricMarkdown(rawRubric);
rubric.First().IsExtraCredit.Should().BeTrue();
rubric.First().Label.Should().Be("(Extra Credit) this is the task");
}
[Fact]
public void TestCanParseFloatingPointNubmers()
{
var rawRubric = @"
- 1.5pt: this is the task
";
var rubric = LocalAssignment.ParseRubricMarkdown(rawRubric);
rubric.First().Points.Should().Be(1.5);
}
[Fact]
public void TestCanParseNegativeNubmers()
{
var rawRubric = @"
- -2pt: this is the task
";
var rubric = LocalAssignment.ParseRubricMarkdown(rawRubric);
rubric.First().Points.Should().Be(-2.0);
}
[Fact]
public void TestCanParseNegativeFloatingPointNubmers()
{
var rawRubric = @"
- -2895.00053pt: this is the task
";
var rubric = LocalAssignment.ParseRubricMarkdown(rawRubric);
rubric.First().Points.Should().Be(-2895.00053);
}
}

View File

@@ -1,82 +0,0 @@
// using CanvasModel.Courses;
// using CanvasModel.EnrollmentTerms;
// using FluentAssertions;
// using Moq;
// using RestSharp;
// using System.Net;
// namespace Management.Test;
// public class ICanvasServiceTests
// {
// [Fact]
// public async Task CanReadCanvasSemesters()
// {
// var expectedTerms = new EnrollmentTermModel[] {
// new EnrollmentTermModel(
// Id: 1,
// Name: "one",
// StartAt: new DateTime(2022, 1, 1),
// EndAt: new DateTime(2022, 2, 1)
// ),
// };
// Mock<IWebRequestor> mockRequestor = getTermsMock(expectedTerms);
// var service = new ICanvasService(mockRequestor.Object);
// var canvasTerms = await service.GetTerms();
// canvasTerms.Should().BeEquivalentTo(expectedTerms);
// }
// [Fact]
// public async Task CanGetActiveTerms()
// {
// var expectedTerms = new EnrollmentTermModel[] {
// new EnrollmentTermModel(
// Id: 1,
// Name: "one",
// StartAt: new DateTime(2022, 5, 1),
// EndAt: new DateTime(2022, 7, 1)
// ),
// new EnrollmentTermModel(
// Id: 2,
// Name: "two",
// StartAt: new DateTime(2022, 7, 1),
// EndAt: new DateTime(2022, 9, 1)
// ),
// new EnrollmentTermModel(
// Id: 3,
// Name: "three",
// StartAt: new DateTime(2022, 9, 1),
// EndAt: new DateTime(2022, 10, 1)
// ),
// new EnrollmentTermModel(
// Id: 4,
// Name: "four",
// StartAt: new DateTime(2022, 10, 1),
// EndAt: new DateTime(2022, 11, 1)
// ),
// };
// Mock<IWebRequestor> mockRequestor = getTermsMock(expectedTerms);
// var service = new ICanvasService(mockRequestor.Object);
// var queryDate = new DateTime(2022, 6, 1);
// var canvasTerms = await service.GetCurrentTermsFor(queryDate);
// canvasTerms.Count().Should().Be(3);
// var termIds = canvasTerms.Select(t => t.Id);
// var expectedIds = new int[] { 1, 2, 3 };
// termIds.Should().BeEquivalentTo(expectedIds);
// }
// private static Mock<IWebRequestor> getTermsMock(EnrollmentTermModel[] expectedTerms)
// {
// var data = new RedundantEnrollmentTermsResponse(EnrollmentTerms: expectedTerms);
// var response = new RestResponse<RedundantEnrollmentTermsResponse>();
// response.Data = data;
// var mockRequestor = new Mock<IWebRequestor>();
// mockRequestor
// .Setup(s => s.GetAsync<RedundantEnrollmentTermsResponse>(It.IsAny<RestRequest>()))
// .ReturnsAsync(response);
// return mockRequestor;
// }
// }

View File

@@ -1,3 +0,0 @@
global using System.Text.Json;
global using FluentAssertions;
global using Xunit;

View File

@@ -1,20 +0,0 @@
using Management.Web.Pages.Course.CourseCalendar;
public class MonthDetailTests
{
[Fact]
public void TestCanGetMonthName()
{
var calendarMonth = new CalendarMonth(2022, 2);
#pragma warning disable BL0005 // Component parameter should not be set outside of its component.
var detail = new MonthDetail()
{
Month = calendarMonth
};
#pragma warning restore BL0005 // Component parameter should not be set outside of its component.
detail.MonthName.Should().Be("February");
}
}

View File

@@ -1,12 +0,0 @@
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View File

@@ -1,14 +0,0 @@
public static class ConfigurationSetup
{
public static void Canvas(WebApplicationBuilder builder)
{
var canvas_token = builder.Configuration["CANVAS_TOKEN"] ?? throw new Exception("CANVAS_TOKEN is null");
var canvas_url = builder.Configuration["CANVAS_URL"];
if (canvas_url == null)
{
Console.WriteLine("CANVAS_URL is null, defaulting to https://snow.instructure.com");
builder.Configuration["CANVAS_URL"] = "https://snow.instructure.com";
}
}
}

View File

@@ -1,20 +0,0 @@
using System.Diagnostics;
using OpenTelemetry;
public class CustomConsoleExporter : BaseExporter<Activity>
{
public override ExportResult Export(in Batch<Activity> batch)
{
using var scope = SuppressInstrumentationScope.Begin();
foreach (var activity in batch)
{
string[] ignoreOperations = [
"Microsoft.AspNetCore.Hosting.HttpRequestIn",
];
if (!ignoreOperations.Contains(activity.OperationName))
Console.WriteLine($"{activity.OperationName}: {activity.DisplayName}");
}
return ExportResult.Success;
}
}

View File

@@ -1,26 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<ProjectReference Include="..\Management\Management.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Akka" Version="1.5.27.1" />
<PackageReference Include="Akka.DependencyInjection" Version="1.5.27.1" />
<PackageReference Include="BlazorMonaco" Version="3.2.0" />
<PackageReference Include="dotenv.net" Version="3.2.0" />
<PackageReference Include="Markdig" Version="0.37.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.8" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>6dc43700-9593-43ca-bda7-4fa2c4e7abc7</UserSecretsId>
</PropertyGroup>
</Project>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ActiveDebugProfile>https</ActiveDebugProfile>
</PropertyGroup>
</Project>

View File

@@ -1,229 +0,0 @@
@using Management.Web.Shared.Components
@using Management.Web.Shared.Components.Forms
@using CanvasModel.Assignments
@inject CoursePlanner planner
@inject ICanvasService canvas
@inject NavigationManager Navigation
@inject AssignmentEditorContext assignmentContext
@code {
protected override void OnInitialized()
{
assignmentContext.StateHasChanged += reload;
reload();
}
private void reload()
{
if (assignmentContext.Assignment != null)
{
name = assignmentContext.Assignment.Name;
}
this.InvokeAsync(this.StateHasChanged);
}
public void Dispose()
{
assignmentContext.StateHasChanged -= reload;
}
private void OnHide()
{
assignmentContext.Assignment = null;
name = "";
}
private string name { get; set; } = String.Empty;
private bool addingAssignmentToCanvas = false;
private bool deletingAssignmentFromCanvas = false;
private bool showHelp = false;
private void toggleHelp() => showHelp = !showHelp;
private void submitHandler()
{
if (assignmentContext.Assignment != null)
{
var newAssignment = assignmentContext.Assignment with
{
Name = name,
};
assignmentContext.SaveAssignment(newAssignment);
}
assignmentContext.Assignment = null;
}
private async Task HandleDelete()
{
if (planner.LocalCourse != null && assignmentContext.Assignment != null)
{
var assignment = assignmentContext.Assignment;
var currentModule = planner
.LocalCourse
.Modules
.First(m =>
m.Assignments.Contains(assignment)
) ?? throw new Exception("handling assignment delete, could not find module");
var newModules = planner.LocalCourse.Modules.Select(m =>
m.Name == currentModule.Name
? m with
{
Assignments = m.Assignments.Where(a => a != assignment).ToArray()
}
: m
)
.ToArray();
planner.LocalCourse = planner.LocalCourse with
{
Modules = newModules
};
if (assignmentInCanvas != null && planner.LocalCourse.Settings.CanvasId != null)
{
ulong courseId = planner.LocalCourse.Settings.CanvasId ?? throw new Exception("cannot delete if no course id");
await canvas.Assignments.Delete(courseId, assignmentInCanvas.Id, assignment.Name);
}
Navigation.NavigateTo("/course/" + planner.LocalCourse?.Settings.Name);
}
}
private void handleNameChange(ChangeEventArgs e)
{
if (assignmentContext.Assignment != null)
{
var newAssignment = assignmentContext.Assignment with { Name = e.Value?.ToString() ?? "" };
assignmentContext.SaveAssignment(newAssignment);
}
}
private void setAssignmentGroup(LocalAssignmentGroup? group)
{
if (assignmentContext.Assignment == null)
return;
var newAssignment = assignmentContext.Assignment with
{
LocalAssignmentGroupName = group?.Name
};
assignmentContext.SaveAssignment(newAssignment);
}
private LocalAssignmentGroup? selectedAssignmentGroup =>
planner
.LocalCourse?
.Settings
.AssignmentGroups
.FirstOrDefault(g => g.Name == assignmentContext.Assignment?.LocalAssignmentGroupName);
private async Task addToCanvas()
{
addingAssignmentToCanvas = true;
await assignmentContext.AddAssignmentToCanvas();
await planner.LoadCanvasData();
addingAssignmentToCanvas = false;
}
private async Task updateInCanvas()
{
if(assignmentInCanvas != null)
{
addingAssignmentToCanvas = true;
await assignmentContext.UpdateInCanvas(assignmentInCanvas.Id);
await planner.LoadCanvasData();
addingAssignmentToCanvas = false;
}
}
private CanvasAssignment? assignmentInCanvas =>
planner.CanvasData?.Assignments.FirstOrDefault(a => a.Name == assignmentContext.Assignment?.Name);
private string canvasAssignmentUrl =>
$"https://snow.instructure.com/courses/{planner.LocalCourse?.Settings.CanvasId}/assignments/{assignmentInCanvas?.Id}";
private async Task deleteFromCanvas()
{
if (assignmentInCanvas == null
|| planner?.LocalCourse?.Settings.CanvasId == null
|| assignmentContext.Assignment == null
)
return;
deletingAssignmentFromCanvas = true;
await canvas.Assignments.Delete(
(ulong)planner.LocalCourse.Settings.CanvasId,
assignmentInCanvas.Id,
assignmentContext.Assignment.Name
);
await planner.LoadCanvasData();
deletingAssignmentFromCanvas = false;
StateHasChanged();
}
}
<div class="d-flex flex-column p-2 h-100 w-100" style="height: 100%;" >
<div>
@assignmentContext.Assignment?.Name
</div>
<section class="flex-grow-1 p-1 border rounded-4 bg-dark-subtle" style="min-height: 0;">
@if (assignmentContext.Assignment != null)
{
<AssignmentMarkdownEditor ShowHelp=@showHelp />
}
</section>
<div class="d-flex justify-content-end p-3">
@if (addingAssignmentToCanvas || deletingAssignmentFromCanvas)
{
<div>
<Spinner />
</div>
}
<button class="btn btn-outline-secondary mx-3" @onclick=toggleHelp>
Toggle Help
</button>
<ConfirmationModal Label="Delete" Class="btn btn-danger" OnConfirmAsync="HandleDelete" />
<button
class="btn btn-outline-secondary mx-3"
disabled="@(addingAssignmentToCanvas || deletingAssignmentFromCanvas)"
@onclick="addToCanvas"
>
Add To Canvas
</button>
@if (assignmentInCanvas != null)
{
<a
class="btn btn-outline-secondary me-1"
href="@canvasAssignmentUrl"
target="_blank"
disabled="@(addingAssignmentToCanvas || deletingAssignmentFromCanvas)"
>
View in Canvas
</a>
<button
class="btn btn-outline-secondary mx-3"
disabled="@(addingAssignmentToCanvas || deletingAssignmentFromCanvas)"
@onclick="updateInCanvas"
>
Update In Canvas
</button>
<ConfirmationModal
Disabled="@(addingAssignmentToCanvas || deletingAssignmentFromCanvas)"
Label="Delete from Canvas"
Class="btn btn-outline-danger mx-3"
OnConfirmAsync="deleteFromCanvas"
/>
}
<button class="btn btn-primary mx-2" @onclick="@(() => {
assignmentContext.Assignment = null;
Navigation.NavigateTo("/course/" + planner.LocalCourse?.Settings.Name);
})">
Done
</button>
</div>
</div>

View File

@@ -1,67 +0,0 @@
@page "/course/{CourseName}/assignment/{AssignmentName}"
@using CanvasModel.EnrollmentTerms
@using CanvasModel.Courses
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@using LocalModels
@using Management.Web.Pages.Course.Module.ModuleItems
@using Management.Web.Shared.Components
@inject IFileStorageManager fileStorageManager
@inject ICanvasService canvas
@inject CoursePlanner planner
@inject AssignmentEditorContext assignmentContext
@inject ILogger<AssignmentFormPage> logger
@code {
[Parameter]
public string? CourseName { get; set; } = default!;
[Parameter]
public string? AssignmentName { get; set; } = default!;
private bool loading { get; set; } = true;
protected override async Task OnInitializedAsync()
{
if (loading)
{
loading = false;
logger.LogInformation($"loading assignment {CourseName} {AssignmentName}");
if (planner.LocalCourse == null)
{
var courses = await fileStorageManager.LoadSavedCourses();
planner.LocalCourse = courses.First(c => c.Settings.Name == CourseName);
logger.LogInformation($"set course to '{planner.LocalCourse?.Settings.Name}'");
}
if (assignmentContext.Assignment == null)
{
var assignment = planner
.LocalCourse?
.Modules
.SelectMany(m => m.Assignments)
.FirstOrDefault(a => a.Name == AssignmentName);
assignmentContext.Assignment = assignment;
logger.LogInformation($"set assignment to '{assignmentContext.Assignment?.Name}'");
}
await planner.LoadCanvasData();
base.OnInitialized();
StateHasChanged();
}
}
}
<PageTitle>@CourseName - @AssignmentName</PageTitle>
<div style="height: 100vh;" class="m-0 p-1 d-flex flex-row">
@if (loading)
{
<Spinner />
}
@if (planner.LocalCourse != null && assignmentContext.Assignment != null)
{
<AssignmentForm />
}
</div>

View File

@@ -1,130 +0,0 @@
@using Management.Web.Shared.Components
@inject CoursePlanner planner
@inject AssignmentEditorContext assignmentContext
@code
{
[Parameter, EditorRequired]
public bool ShowHelp { get; set; } = false;
protected override void OnInitialized()
{
assignmentContext.StateHasChanged += reload;
reload();
}
private void reload()
{
if (assignmentContext.Assignment != null)
{
if(rawText == string.Empty)
{
rawText = assignmentContext.Assignment.ToMarkdown();
this.InvokeAsync(this.StateHasChanged);
}
}
}
public void Dispose()
{
assignmentContext.StateHasChanged -= reload;
}
private string rawText { get; set; } = string.Empty;
private string? error = null;
private void handleChange(string newRawAssignment)
{
rawText = newRawAssignment;
if (newRawAssignment != string.Empty)
{
try
{
var parsed = LocalAssignment.ParseMarkdown(newRawAssignment);
error = null;
assignmentContext.SaveAssignment(parsed);
}
catch(AssignmentMarkdownParseException e)
{
error = e.Message;
}
catch(RubricMarkdownParseException e)
{
error = e.Message;
}
finally
{
StateHasChanged();
}
}
StateHasChanged();
}
private MarkupString preview { get
{
return (MarkupString)MarkdownService.Render(assignmentContext?.Assignment?.Description ?? "");
}
}
private string HelpText()
{
var groupNames = string.Join("\n- " , planner.LocalCourse?.Settings.AssignmentGroups.Select(g => g.Name) ?? []);
return $@"
SubmissionTypes:
- {AssignmentSubmissionType.ONLINE_TEXT_ENTRY}
- {AssignmentSubmissionType.ONLINE_UPLOAD}
- {AssignmentSubmissionType.DISCUSSION_TOPIC}
AllowedFileUploadExtensions:
- pdf
- jpg
- jpeg
- png
Assignment Group Names:
- {groupNames}
";
}
}
<div class="d-flex w-100 h-100 flex-row">
@if(ShowHelp)
{
<div class=" rounded rounded-3 bg-black" >
<pre class=" me-3 pe-5 ps-3 rounded rounded-3">
@HelpText()
</pre>
</div>
}
@if(assignmentContext.Assignment != null && planner.LocalCourse != null)
{
<div class="row h-100 w-100">
<div class="col-6">
<MonacoTextArea Value=@rawText OnChange=@handleChange />
</div>
<div class="col-6 overflow-y-auto h-100" >
@if (error != null)
{
<p class="text-danger text-truncate">Error: @error</p>
}
<div>Due At: @assignmentContext.Assignment.DueAt</div>
<div>Lock At: @assignmentContext.Assignment.LockAt</div>
<div>Assignment Group Name @assignmentContext.Assignment.LocalAssignmentGroupName</div>
<div>Submission Types</div>
<ul>
@foreach(var t in assignmentContext.Assignment.SubmissionTypes)
{
<li>@t</li>
}
</ul>
<hr>
<div>
@(preview)
</div>
<hr>
<RubricDisplay />
</div>
</div>
}
</div>

View File

@@ -1,61 +0,0 @@
@using Management.Web.Shared.Components
@inject CoursePlanner planner
@inject AssignmentEditorContext assignmentContext
@code
{
private string? error { get; set; } = null;
protected override void OnInitialized()
{
assignmentContext.StateHasChanged += reload;
reload();
}
private void reload()
{
this.InvokeAsync(this.StateHasChanged);
}
public void Dispose()
{
assignmentContext.StateHasChanged -= reload;
}
private double requiredPoints => assignmentContext?.Assignment?.Rubric.Where(r => !r.IsExtraCredit).Select(r => r.Points).Sum() ?? 0;
private double extraCreditPoints => assignmentContext?.Assignment?.Rubric.Where(r => r.IsExtraCredit).Select(r => r.Points).Sum() ?? 0;
}
@if(assignmentContext != null)
{
<div class="row">
<h4 class="text-center">Rubric</h4>
</div>
@if (error != null)
{
<p class="text-danger text-truncate">Error: @error</p>
}
<div class="row border-bottom">
<div class="col-6 text-end">Label</div>
<div class="col-3 text-center">Points</div>
<div class="col-3 text-center">Extra Credit</div>
</div>
@foreach (var item in assignmentContext?.Assignment?.Rubric ?? [])
{
<div class="row border-bottom">
<div class="col-6 text-end">@item.Label</div>
<div class="col-3 text-center">@item.Points</div>
<div class="col-3 text-center">@item.IsExtraCredit</div>
</div>
}
<div class="text-end">
<div>
Required Points: @requiredPoints
</div>
<div>
Extra Credit Points @extraCreditPoints
</div>
</div>
}

View File

@@ -1,85 +0,0 @@
@using System.Reflection
@inject AssignmentEditorContext assignmentContext
@code
{
protected override void OnInitialized()
{
assignmentContext.StateHasChanged += reload;
reload();
}
private void reload()
{
if (assignmentContext.Assignment != null)
{
types = assignmentContext.Assignment.SubmissionTypes;
}
this.InvokeAsync(this.StateHasChanged);
}
public void Dispose()
{
assignmentContext.StateHasChanged -= reload;
}
private IEnumerable<string> types { get; set; } = Enumerable.Empty<string>();
private string getLabel(string type)
{
return type.ToString().Replace("_", "") + "switch";
}
private bool discussionIsSelected
{
get => types.FirstOrDefault(
t => t == AssignmentSubmissionType.DISCUSSION_TOPIC
) != null;
}
private void saveTypes(IEnumerable<string> newTypes)
{
if(assignmentContext.Assignment != null)
{
types = newTypes;
assignmentContext.SaveAssignment(assignmentContext.Assignment with
{
SubmissionTypes = types
});
}
}
}
<h5>Submission Types</h5>
<div class="row" @key="types">
@foreach (var submissionType in AssignmentSubmissionType.AllTypes)
{
var isDiscussion = submissionType == AssignmentSubmissionType.DISCUSSION_TOPIC;
var allowedToBeChecked = !discussionIsSelected || isDiscussion;
<div class="col-3">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="@getLabel(submissionType)"
checked="@(types.Contains(submissionType) && allowedToBeChecked)"
@onchange="(e) => {
var isChecked = (bool)(e.Value ?? false);
if(isChecked)
saveTypes(types.Append(submissionType));
else
saveTypes(types.Where(t => t != submissionType));
}"
disabled="@(discussionIsSelected && !isDiscussion)"
>
<label
class="form-check-label"
for="@getLabel(submissionType)"
>
@submissionType
</label>
</div>
</div>
}
</div>

View File

@@ -1,11 +0,0 @@
@page "/test"
@rendermode InteractiveServer
@inject ICanvasService canvas
@inject CoursePlanner planner
@inject IFileStorageManager fileStorageManager
@inject NavigationManager Navigation
@code {
}

View File

@@ -1,142 +0,0 @@
@using Management.Web.Shared.Components
@inject ICanvasService canvas
@inject CoursePlanner planner
@code {
protected override void OnInitialized()
{
planner.StateHasChanged += reload;
}
private void reload()
{
this.InvokeAsync(this.StateHasChanged);
}
public void Dispose()
{
planner.StateHasChanged -= reload;
}
private bool syncingAssignmentGroups { get; set; } = false;
private void AddAssignmentGroup()
{
if(planner.LocalCourse != null)
{
var newGroup = new LocalAssignmentGroup
{
Name = "",
Weight = 0,
Id = Guid.NewGuid().ToString()
};
var updatedGroups = planner.LocalCourse.Settings.AssignmentGroups.Append(newGroup);
planner.LocalCourse = planner.LocalCourse with
{
Settings = planner.LocalCourse.Settings with
{
AssignmentGroups = updatedGroups
}
};
}
}
private Action<ChangeEventArgs> saveGroupName(string groupId)
{
return (e) =>
{
if(planner.LocalCourse != null)
{
var newName = e.Value?.ToString() ?? "";
var newGroups = planner.LocalCourse.Settings.AssignmentGroups.Select(
g => g.Id == groupId
? g with { Name = newName }
: g
);
planner.LocalCourse = planner.LocalCourse with
{
Settings = planner.LocalCourse.Settings with
{
AssignmentGroups = newGroups
}
};
}
};
}
private Action<ChangeEventArgs> saveGroupWeight(string groupId)
{
return (e) =>
{
if(planner.LocalCourse != null)
{
var newWeight = double.Parse(e.Value?.ToString() ?? "0");
var newGroups = planner.LocalCourse.Settings.AssignmentGroups.Select(
g => g.Id == groupId
? g with { Weight = newWeight }
: g
);
planner.LocalCourse = planner.LocalCourse with
{
Settings = planner.LocalCourse.Settings with
{
AssignmentGroups = newGroups
}
};
}
};
}
private async Task SyncAssignmentGroupsWithCanvas()
{
syncingAssignmentGroups = true;
await planner.SyncAssignmentGroups();
syncingAssignmentGroups = false;
}
}
@if(planner.LocalCourse != null)
{
<h4 class="text-center">Assignment Groups</h4>
@foreach (var group in planner.LocalCourse.Settings.AssignmentGroups)
{
var groupName = group.Name;
var nameInputCallback = saveGroupName(group.Id);
var weight = group.Weight;
var weightInputCallback = saveGroupWeight(group.Id);
<div class="row">
<div class="col-auto">
<label class="form-label">Group Name</label>
<input
class="form-control"
@bind="groupName" @oninput="nameInputCallback">
</div>
<div class="col-auto">
<label class="form-label">Weight</label>
<input
class="form-control"
@bind="weight"
@oninput="weightInputCallback"
>
</div>
</div>
}
<div class="d-flex justify-content-end">
<button
class="btn btn-outline-primary"
@onclick="AddAssignmentGroup"
>
+ Assignment Group
</button>
</div>
<button
class="btn btn-outline-secondary"
@onclick="SyncAssignmentGroupsWithCanvas"
disabled="@syncingAssignmentGroups"
>
Sync Assignment Groups With Canvas
</button>
@if(syncingAssignmentGroups)
{
<Spinner />
}
}

View File

@@ -1,81 +0,0 @@
@page "/course/{CourseName}"
@using CanvasModel.EnrollmentTerms
@using CanvasModel.Courses
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@using LocalModels
@using Management.Web.Pages.Course.Module.ModuleItems
@using Management.Web.Shared.Components
@inject IFileStorageManager fileStorageManager
@inject ICanvasService canvas
@inject CoursePlanner planner
@inject NavigationManager navigtion
@inject IConfiguration config
@code {
[Parameter]
public string? CourseName { get; set; }
private bool loading = true;
protected override async Task OnInitializedAsync()
{
if (planner.LocalCourse == null)
{
System.Diagnostics.Activity.Current = null;
using var activity = DiagnosticsConfig.Source?.StartActivity("Loading Course");
activity?.AddTag("CourseName", CourseName);
var courses = await fileStorageManager.LoadSavedCourses();
planner.LocalCourse = courses.First(c => c.Settings.Name == CourseName);
}
base.OnInitialized();
loading = false;
}
private void selectNewCourse()
{
planner.Clear();
navigtion.NavigateTo("/");
}
}
<PageTitle>@CourseName</PageTitle>
<div style="height: 100vh;">
@if (loading)
{
<Spinner />
}
@if (planner.LocalCourse != null)
{
<div class="pb-3 d-flex justify-content-between" style="height: 4em;">
<div class="my-auto">
<button @onclick="selectNewCourse" class="btn btn-primary">
Select New Course
</button>
<CourseSettings />
<a class="btn btn-outline-secondary" target="_blank"
href="@($"{config["CANVAS_URL"]}/courses/{planner.LocalCourse.Settings.CanvasId}")">
View In Canvas
</a>
<div class="my-auto ms-2 d-inline">
@planner.LocalCourse.Settings.Name
</div>
</div>
@if (planner.LoadingCanvasData)
{
<Spinner />
}
</div>
<CourseDetails />
}
</div>

View File

@@ -1,54 +0,0 @@
@using Management.Web.Course.Module.ModuleItems
@inject DragContainer dragContainer
@inject NavigationManager Navigation
@inject AssignmentEditorContext assignmentContext
@inject MyLogger<AssignmentInDay> logger
@inherits DroppableAssignment
@code {
protected override void OnInitialized()
{
planner.StateHasChanged += reload;
}
private void reload()
{
this.InvokeAsync(this.StateHasChanged);
}
public void Dispose()
{
planner.StateHasChanged -= reload;
}
private void HandleDragStart()
{
dragContainer.DropCallback = DropCallback;
}
private void HandleDragEnd()
{
dragContainer.DropCallback = null;
}
private void OnClick()
{
if(planner.LocalCourse != null)
{
assignmentContext.Assignment = Assignment;
Navigation.NavigateTo("/course/" + planner.LocalCourse.Settings.Name + "/assignment/" + Assignment.Name);
logger.Log("navigating to assignment page");
}
}
}
<li
draggable="true"
@ondragstart="HandleDragStart"
@ondragend="HandleDragEnd"
@onclick="OnClick"
role="button"
>
@Assignment.Name
</li>

View File

@@ -1,148 +0,0 @@
@inject DragContainer dragContainer
@inject CoursePlanner configurationManagement
@inject CoursePlanner planner
@code
{
[Parameter, EditorRequired]
public DateTime? date { get; set; } =
default!;
private bool isWeekDay {
get => date?.DayOfWeek != null;
}
private bool dragging {get; set;} = false;
protected override void OnInitialized()
{
planner.StateHasChanged += reload;
}
private void reload()
{
this.InvokeAsync(this.StateHasChanged);
}
public void Dispose()
{
planner.StateHasChanged -= reload;
}
private IEnumerable<LocalAssignment> TodaysAssignments
{
get
{
if(planner.LocalCourse == null || date == null)
return Enumerable.Empty<LocalAssignment>();
else
return planner.LocalCourse.Modules
.SelectMany(m => m.Assignments)
.Where(a => a.DueAt.Date == date?.Date);
}
}
private IEnumerable<LocalQuiz> todaysQuizzes
{
get
{
if(planner.LocalCourse == null || date == null)
return Enumerable.Empty<LocalQuiz>();
else
return planner.LocalCourse.Modules
.SelectMany(m => m.Quizzes)
.Where(q => q.DueAt.Date == date?.Date);
}
}
private IEnumerable<LocalCoursePage> todaysPages
{
get
{
if(planner.LocalCourse == null || date == null)
return Enumerable.Empty<LocalCoursePage>();
else
return planner.LocalCourse.Modules
.SelectMany(m => m.Pages)
.Where(q => q.DueAt.Date == date?.Date);
}
}
private string calculatedClass
{
get
{
var baseClasses = "col border rounded rounded-3 p-2 pb-4 m-1 ";
if(dragging)
return baseClasses + " bg-secondary text-light ";
if(date?.Date == DateTime.Today)
baseClasses += " border-1 border-primary-subtle ";
if (isWeekDay)
{
DayOfWeek? weekDay = date?.DayOfWeek;
DayOfWeek notNullDay = weekDay ?? default;
var isClassDay = planner.LocalCourse?.Settings.DaysOfWeek.Contains(notNullDay) ?? false;
var dayInSemester =
isClassDay
&& date <= planner.LocalCourse?.Settings.EndDate
&& date >= planner.LocalCourse?.Settings.StartDate;
var totalClasses = dayInSemester
? "bg-light-subtle text-light " + baseClasses
: " " + baseClasses;
return totalClasses;
}
else
{
return baseClasses;
}
}
}
void OnDragEnter() {
dragging = true;
}
void OnDragLeave() {
dragging = false;
}
void OnDrop()
{
dragging = false;
if(dragContainer.DropCallback == null){
System.Console.WriteLine("no drop callback set");
return;
}
if(date != null)
{
DateTime d = date ?? throw new Exception("should not get here, error converting date from nullable");
dragContainer.DropCallback?.Invoke(d, null);
}
}
}
<div
class="@calculatedClass"
@ondrop="@(() => OnDrop())"
@ondragenter="OnDragEnter"
@ondragleave="OnDragLeave"
ondragover="event.preventDefault();"
>
@(isWeekDay ? date?.Day : "")
<ul class="m-0 ps-3">
@foreach (var assignment in TodaysAssignments)
{
<AssignmentInDay Assignment="assignment" @key="@assignment" />
}
@foreach(var quiz in todaysQuizzes)
{
<QuizInDay Quiz="quiz" @key="@quiz" />
}
@foreach(var page in todaysPages)
{
<PageInDay Page="page" @key="page" />
}
</ul>
</div>

View File

@@ -1,55 +0,0 @@
@using Management.Web.Course.Module.ModuleItems
@inject DragContainer dragContainer
@inject NavigationManager Navigation
@inject PageEditorContext pageContext
@inject MyLogger<PageInDay> logger
@inherits DroppablePage
@code {
protected override void OnInitialized()
{
planner.StateHasChanged += reload;
}
private void reload()
{
this.InvokeAsync(this.StateHasChanged);
}
public void Dispose()
{
planner.StateHasChanged -= reload;
}
private void HandleDragStart()
{
dragContainer.DropCallback = dropCallback;
}
private void HandleDragEnd()
{
dragContainer.DropCallback = null;
}
private void OnClick()
{
if(planner.LocalCourse != null)
{
pageContext.Page = Page;
Navigation.NavigateTo("/course/" + planner.LocalCourse.Settings.Name + "/page/" + Page.Name);
logger.Log("navigating to coursePage page");
}
}
}
<li
draggable="true"
@ondragstart="HandleDragStart"
@ondragend="HandleDragEnd"
@onclick="OnClick"
role="button"
>
@Page.Name
</li>

View File

@@ -1,34 +0,0 @@
@using Management.Web.Shared.Components.Quiz
@inject DragContainer dragContainer
@inject QuizEditorContext quizContext
@inject NavigationManager Navigation
@inherits DroppableQuiz
@code {
private void HandleDragStart()
{
dragContainer.DropCallback = dropCallback;
}
private void HandleDragEnd()
{
dragContainer.DropCallback = null;
}
private void OnClick()
{
quizContext.Quiz = Quiz;
Navigation.NavigateTo("/course/" + planner.LocalCourse?.Settings.Name + "/quiz/" + Quiz.Name);
}
}
<li
draggable="true"
@ondragstart="HandleDragStart"
@ondragend="HandleDragEnd"
@onclick="OnClick"
role="button"
>
@Quiz.Name
</li>

View File

@@ -1,55 +0,0 @@
@using System.Linq
@using Management.Web.Pages.Course.CourseCalendar.Day
@inject CoursePlanner planner
@code
{
[Parameter, EditorRequired]
public CalendarMonth Month { get; set; } = default!;
public DayOfWeek[] WeekDaysList { get => (DayOfWeek[])Enum.GetValues(typeof(DayOfWeek)); }
public string MonthName { get => Month?.DaysByWeek.First().FirstOrDefault(d => d != null)?.ToString("MMMM") ?? ""; }
private string htmlLabel => "collapse"+MonthName;
private bool isInPast =>
new DateTime(Month.Year, Month.Month, 1) < new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
private string collapseClass => " collapse " + (isInPast ? "hide" : "show");
}
<h3 class="text-center">
<a
role="button"
data-bs-toggle="collapse"
data-bs-target="@("#" + htmlLabel)"
aria-expanded="@( isInPast ? "false" : "true")"
aria-controls="@htmlLabel"
>
@MonthName
</a>
</h3>
<div class="@collapseClass" id="@htmlLabel">
<div class="row text-center fw-bold">
@foreach (DayOfWeek day in WeekDaysList)
{
<div class="@(
planner.LocalCourse?.Settings.DaysOfWeek.Contains(day) ?? false
? "col"
: "col text-secondary"
)">
@day
</div>
}
</div>
@foreach (var week in Month.DaysByWeek)
{
<div class="row m-3">
@foreach (var day in week)
{
<Day date="day"></Day>
}
</div>
}
</div>

View File

@@ -1,57 +0,0 @@
@using CanvasModel.EnrollmentTerms
@using Management.Web.Pages.Course.Module
@using Management.Web.Pages.Course.CourseCalendar
@inject ICanvasService canvas
@inject CoursePlanner planner
@code
{
protected override void OnInitialized()
{
planner.StateHasChanged += reload;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if(firstRender)
{
if(
planner.CanvasData == null
&& planner.LocalCourse != null
&& planner.LocalCourse.Settings.CanvasId != null
)
{
await planner.LoadCanvasData();
}
}
}
private void reload()
{
this.InvokeAsync(this.StateHasChanged);
}
public void Dispose()
{
planner.StateHasChanged -= reload;
}
}
<div class="row">
<div class="col overflow-y-auto border rounded " style="max-height: 95vh;">
@if (planner.LocalCourse != null)
{
<div class="py-2">
@foreach (var month in SemesterPlanner.GetMonthsBetweenDates(planner.LocalCourse.Settings.StartDate, planner.LocalCourse.Settings.EndDate))
{
<MonthDetail Month="month" />
<hr />
}
</div>
}
</div>
<div class="col-4 overflow-y-auto" style="max-height: 95vh;">
<Modules />
</div>
</div>

View File

@@ -1,186 +0,0 @@
@using CanvasModel.Enrollments
@using Management.Web.Shared.Components
@inject ICanvasService canvas
@inject CoursePlanner planner
@code
{
private Modal modal { get; set; } = default!;
protected override void OnInitialized()
{
planner.StateHasChanged += reload;
}
private void reload()
{
this.InvokeAsync(this.StateHasChanged);
}
public void Dispose()
{
planner.StateHasChanged -= reload;
}
private IEnumerable<EnrollmentTermModel>? terms { get; set; } = null;
private IEnumerable<EnrollmentModel>? studentEnrollments { get; set; } = null;
private ulong? _selectedTermId {get; set;}
private ulong? selectedTermId {
get => _selectedTermId;
set
{
_selectedTermId = value;
if(selectedTerm != null && planner.LocalCourse != null)
{
planner.LocalCourse = planner.LocalCourse with
{
Settings = planner.LocalCourse.Settings with
{
StartDate=selectedTerm.StartAt ?? new DateTime(),
EndDate=selectedTerm.EndAt ?? new DateTime(),
}
};
}
}
}
private EnrollmentTermModel? selectedTerm
{
get => terms?.FirstOrDefault(t => t.Id == selectedTermId);
}
private bool loading = false;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
if(planner.LocalCourse != null && planner.LocalCourse.Settings.CanvasId != null)
{
loading = true;
ulong id = planner.LocalCourse?.Settings.CanvasId ?? throw new Exception("wtf how did i get here");
var enrollmentsTask = canvas.GetEnrolledStudents(id);
var canvasCourse = await canvas.GetCourse(id);
terms = await canvas.GetCurrentTermsFor(canvasCourse.StartAt);
studentEnrollments = await enrollmentsTask;
loading = false;
}
}
}
}
<button
class="btn btn-outline-secondary"
@onclick="@(() => modal.Show())"
>
Edit Course Settings
</button>
<Modal @ref="modal">
<Title>
<h1>Course Settings</h1>
</Title>
<Body>
<h5 class="text-center">Select Days Of Week</h5>
<div class="row m-3">
@foreach (DayOfWeek day in (DayOfWeek[])Enum.GetValues(typeof(DayOfWeek)))
{
<div class="col">
<button
class="@(
planner.LocalCourse?.Settings.DaysOfWeek.Contains(day) ?? false
? "btn btn-secondary"
: "btn btn-outline-secondary"
)"
@onclick="() =>
{
if(planner.LocalCourse?.Settings.DaysOfWeek.Contains(day) ?? false)
{
planner.LocalCourse = planner.LocalCourse with
{
Settings = planner.LocalCourse.Settings with
{
DaysOfWeek = planner.LocalCourse.Settings.DaysOfWeek.Where((d) => d != day)
}
};
}
else
{
if (planner.LocalCourse != null)
{
planner.LocalCourse = planner.LocalCourse with
{
Settings = planner.LocalCourse.Settings with
{
DaysOfWeek = planner.LocalCourse.Settings.DaysOfWeek.Append(day)
}
};
}
}
}"
>
@day
</button>
</div>
}
</div>
@if(loading)
{
<Spinner />
}
@if (terms != null)
{
<div class="row justify-content-center">
<div class="col-auto">
<form @onsubmit:preventDefault="true">
<label for="termselect">Select Term for Start and End Date:</label>
<select id="termselect" class="form-select" @bind="selectedTermId">
@foreach (var term in terms)
{
<option value="@term.Id">@term.Name</option>
}
</select>
</form>
</div>
</div>
}
@if(planner.LocalCourse != null)
{
<div class="row justify-content-center m-3 text-center">
<div class="col-auto">
<div>Default Assignment Due Time</div>
<TimePicker Time="planner.LocalCourse.Settings.DefaultDueTime" UpdateTime="@((newTime) =>
planner.LocalCourse =
planner.LocalCourse with
{ Settings = planner.LocalCourse.Settings with { DefaultDueTime=newTime } }
)"
/>
</div>
</div>
}
<AssignmentGroups />
@if(studentEnrollments != null)
{
<div>
Students to import to github classroom:
@foreach(var enrollment in studentEnrollments)
{
<div class="ps-3">
@(enrollment.User.DisplayName ?? enrollment.User.ShortName)
</div>
}
</div>
}
</Body>
<Footer>
<button
class="btn btn-outline-secondary"
@onclick="@(() => modal.Hide())"
>
Done Editing Course Settings
</button>
</Footer>
</Modal>

View File

@@ -1,185 +0,0 @@
@using Management.Web.Shared.Components
@using Management.Web.Shared.Components.Quiz
@using Management.Web.Pages.Course.Module
@using Management.Web.Pages.Course.Module.ModuleItems
@using Management.Web.Pages.Course.Module.NewItemsButtons
@using LocalModels
@using BlazorMonaco
@using BlazorMonaco.Editor
@inject CoursePlanner configurationManagement
@inject CoursePlanner planner
@inject DragContainer dragContainer
@code {
[Parameter, EditorRequired]
public LocalModule Module { get; set; } = default!;
private bool dragging { get; set; } = false;
private bool publishing = false;
private string _notes { get; set; } = "";
private string notes
{
get => _notes;
set
{
if (value != _notes)
{
_notes = value;
if (planner.LocalCourse != null)
{
var newModule = Module with { Notes = _notes };
var newModules = planner.LocalCourse.Modules.Select(
m => m.Name == newModule.Name
? newModule
: m
);
planner.LocalCourse = planner.LocalCourse with
{
Modules = newModules
};
}
}
}
}
protected override void OnInitialized()
{
if (_notes == string.Empty)
{
_notes = Module.Notes;
}
planner.StateHasChanged += reload;
}
private void reload()
{
this.InvokeAsync(this.StateHasChanged);
}
public void Dispose()
{
planner.StateHasChanged -= reload;
}
private string accordionId
{
get => Module.Name.Replace(" ", "").Replace("#", "") + "-AccordionItem";
}
void OnDragEnter()
{
dragging = true;
}
void OnDragLeave()
{
dragging = false;
}
void OnDrop()
{
dragging = false;
if (dragContainer.DropCallback == null)
{
System.Console.WriteLine("no drop callback set");
return;
}
dragContainer.DropCallback?.Invoke(null, Module);
}
private bool isSyncedWithCanvas => planner
.CanvasData?
.Modules
.FirstOrDefault(
cm => cm.Name == Module.Name
) != null;
private async Task Publish()
{
publishing = true;
await planner.CreateModule(Module);
publishing = false;
}
}
<div class="@("accordion-item " + (dragging ? "" : ""))" @ondrop="@(() => OnDrop())" @ondragenter="OnDragEnter"
@ondragleave="OnDragLeave" ondragover="event.preventDefault();">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="@("#" + accordionId)" aria-controls="@accordionId">
<div class="w-100 d-flex justify-content-between pe-3">
<div>
@Module.Name
</div>
@if (isSyncedWithCanvas)
{
<CheckIcon />
}
else
{
<SyncIcon />
}
</div>
</button>
</h2>
<div id="@accordionId" class="accordion-collapse collapse">
<div class="accordion-body pt-1">
<div class="row m-1">
<div class="col my-auto">
<RenameModule Module="Module" />
</div>
<div class="col my-auto">
@if(publishing)
{
<Spinner />
}
else
{
if(!isSyncedWithCanvas)
{
<button
class="btn btn-outline-primary"
@onclick="Publish"
disabled="@publishing"
>
Add to Canvas
</button>
}
}
</div>
<div class="col-auto my-auto">
<NewPage Module=Module />
<NewQuiz Module="Module" />
<NewAssignment Module="Module" />
</div>
</div>
<h5>Assignments</h5>
<div class="row">
@* @foreach(var p in Module.Pages)
{
<PageListItem Page=p />
}
@foreach (var a in Module.Assignments)
{
<AssignmentListItem Assignment="a" Module="Module" />
}
<br>
@foreach (var quiz in Module.Quizzes)
{
<QuizListItem Quiz="quiz" />
} *@
@foreach(var item in Module.GetSortedModuleItems())
{
@(item switch
{
LocalAssignment assignment => (@<AssignmentListItem Assignment="assignment" Module="Module" />),
LocalQuiz quiz => (@<QuizListItem Quiz="quiz" />),
LocalCoursePage page => (@<PageListItem Page=page />),
_ => (@<div></div>)
})
}
</div>
</div>
</div>
</div>

View File

@@ -1,137 +0,0 @@
@using Management.Web.Shared.Components
@using Management.Web.Course.Module.ModuleItems
@using CanvasModel.Assignments
@inject DragContainer dragContainer
@inject NavigationManager Navigation
@inject AssignmentEditorContext assignmentContext
@inherits DroppableAssignment
@code {
[Parameter]
[EditorRequired]
public LocalModule Module { get; set; } = new();
protected override void OnInitialized()
{
planner.StateHasChanged += reload;
}
private void reload()
{
this.InvokeAsync(this.StateHasChanged);
}
public void Dispose()
{
planner.StateHasChanged -= reload;
}
private bool showAll { get; set; } = false;
private void HandleDragStart()
{
dragContainer.DropCallback = DropCallback;
}
private void HandleDragEnd()
{
dragContainer.DropCallback = null;
}
private CanvasAssignment? assignmentInCanvas => planner
.CanvasData?
.Assignments
.FirstOrDefault(
a => a.Name == Assignment.Name
);
private bool existsInCanvas =>
assignmentInCanvas != null;
private void OnClick()
{
assignmentContext.Assignment = Assignment;
Navigation.NavigateTo("/course/" + planner.LocalCourse?.Settings.Name + "/assignment/" + Assignment.Name);
}
private bool NeedsToBeUpdatedInCanvas => planner.LocalCourse != null
&& planner.LocalCourse.Settings.CanvasId != null
&& planner.CanvasData != null
&& assignmentInCanvas != null
&& Assignment.NeedsUpdates(
(CanvasAssignment)assignmentInCanvas,
Assignment.GetCanvasAssignmentGroupId(planner.LocalCourse.Settings.AssignmentGroups)
);
}
<div
draggable="true"
@ondragstart="HandleDragStart"
@ondragend="HandleDragEnd"
@onclick="OnClick"
role="button"
>
<ModuleItemLayout Name=@Assignment.Name IsSyncedWithCanvas=@(existsInCanvas && !NeedsToBeUpdatedInCanvas)>
@if(
planner.LocalCourse != null
&& existsInCanvas
&& NeedsToBeUpdatedInCanvas
&& assignmentInCanvas != null
)
{
<div class="mx-3 text-body-tertiary">
@Assignment.GetUpdateReason(
(CanvasAssignment)assignmentInCanvas,
Assignment.GetCanvasAssignmentGroupId(planner.LocalCourse.Settings.AssignmentGroups))
</div>
}
@if(!existsInCanvas)
{
<div class="mx-3 text-body-tertiary">
no assignment with same name in canvas
</div>
}
@if(!showAll)
{
<div class="card-text overflow-hidden p-2" style="max-height: 5rem;">
<div>Points: @Assignment.PointsPossible</div>
<div>Due At: @Assignment.DueAt</div>
</div>
}
else
{
<div class="card-text">
<div class="px-3 py-1 bg-dark-subtle my-1">
@((MarkupString) @Assignment.GetDescriptionHtml())
</div>
<section class="px-3">
<div>Points: @Assignment.PointsPossible</div>
<div>Due At: @Assignment.DueAt</div>
<div>Lock At: @Assignment.LockAt</div>
<div>Submission Types:</div>
<ul>
@foreach(var type in Assignment.SubmissionTypes)
{
<li>
@type
</li>
}
</ul>
</section>
</div>
}
<div
class="text-center fs-3 fw-bold lh-1 text-primary"
role="button"
@onclick:preventDefault="true"
@onclick:stopPropagation="true"
@onclick="() => showAll = !showAll"
>
<MeatballsIcon />
</div>
</ModuleItemLayout>
</div>

View File

@@ -1,89 +0,0 @@
using Microsoft.AspNetCore.Components;
namespace Management.Web.Course.Module.ModuleItems;
public class DroppableAssignment : ComponentBase
{
[Inject]
protected CoursePlanner planner { get; set; } = default!;
[Parameter, EditorRequired]
public LocalAssignment Assignment { get; set; } = default!;
private void dropOnDate(DateTime dropDate)
{
if (planner.LocalCourse == null) return;
var currentModule = planner
.LocalCourse
.Modules
.First(m =>
m.Assignments.Contains(Assignment)
) ?? throw new Exception("in day callback, could not find module");
var defaultDueTimeDate = new DateTime(
year: dropDate.Year,
month: dropDate.Month,
day: dropDate.Day,
hour: planner.LocalCourse.Settings.DefaultDueTime.Hour,
minute: planner.LocalCourse.Settings.DefaultDueTime.Minute,
second: 0
);
var moduleWithUpdatedAssignment = currentModule with
{
Assignments = currentModule.Assignments.Select(a =>
a.Name != Assignment.Name // we are only changing the due date, so the name should be the same
? a
: a with
{
DueAt = defaultDueTimeDate,
LockAt = a.LockAt > defaultDueTimeDate ? a.LockAt : defaultDueTimeDate
}
)
};
var updatedModules = planner.LocalCourse.Modules
.Select(m =>
m.Name == moduleWithUpdatedAssignment.Name
? moduleWithUpdatedAssignment
: m
);
var newCourse = planner.LocalCourse with
{
Modules = updatedModules
};
planner.LocalCourse = newCourse;
}
private void dropOnModule(LocalModule module)
{
if (planner.LocalCourse == null) return;
var newModules = planner.LocalCourse.Modules.Select(m =>
m.Name != module.Name
? m with
{
Assignments = m.Assignments.Where(a => a.Name != Assignment.Name).DistinctBy(a => a.Name)
}
: m with
{
Assignments = m.Assignments.Append(Assignment).DistinctBy(a => a.Name)
}
);
var newCourse = planner.LocalCourse with
{
Modules = newModules
};
planner.LocalCourse = newCourse;
}
protected void DropCallback(DateTime? dropDate, LocalModule? module)
{
if (module == null)
{
dropOnDate(dropDate ?? Assignment.DueAt);
}
else
{
dropOnModule(module);
}
}
}

View File

@@ -1,85 +0,0 @@
using Microsoft.AspNetCore.Components;
namespace Management.Web.Course.Module.ModuleItems;
public class DroppablePage : ComponentBase
{
[Inject]
protected CoursePlanner planner { get; set; } = default!;
[Parameter, EditorRequired]
public LocalCoursePage Page { get; set; } = default!;
private void dropOnDate(DateTime dropDate)
{
if (planner.LocalCourse == null) return;
var currentModule = planner
.LocalCourse
.Modules
.First(m =>
m.Pages.Contains(Page)
) ?? throw new Exception("in drop page callback, could not find module");
var defaultDueTimeDate = new DateTime(
year: dropDate.Year,
month: dropDate.Month,
day: dropDate.Day,
hour: planner.LocalCourse.Settings.DefaultDueTime.Hour,
minute: planner.LocalCourse.Settings.DefaultDueTime.Minute,
second: 0
);
var moduleWithUpdatedPage = currentModule with
{
Pages = currentModule.Pages.Select(p =>
p.Name != Page.Name // we are only changing the due date, so the name should be the same
? p
: p with { DueAt = defaultDueTimeDate }
)
};
var updatedModules = planner.LocalCourse.Modules
.Select(m =>
m.Name == moduleWithUpdatedPage.Name
? moduleWithUpdatedPage
: m
);
var newCourse = planner.LocalCourse with
{
Modules = updatedModules
};
planner.LocalCourse = newCourse;
}
private void dropOnModule(LocalModule module)
{
if (planner.LocalCourse == null) return;
var newModules = planner.LocalCourse.Modules.Select(m =>
m.Name != module.Name
? m with
{
Pages = m.Pages.Where(p => p.Name != Page.Name).DistinctBy(p => p.Name)
}
: m with
{
Pages = m.Pages.Append(Page).DistinctBy(a => a.Name)
}
);
var newCourse = planner.LocalCourse with
{
Modules = newModules
};
planner.LocalCourse = newCourse;
}
protected void dropCallback(DateTime? dropDate, LocalModule? module)
{
if (module == null)
{
dropOnDate(dropDate ?? Page.DueAt);
}
else
{
dropOnModule(module);
}
}
}

View File

@@ -1,86 +0,0 @@
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Components;
namespace Management.Web.Shared.Components.Quiz;
public class DroppableQuiz : ComponentBase
{
[Inject]
protected CoursePlanner planner { get; set; } = default!;
[Parameter, EditorRequired]
public LocalQuiz Quiz { get; set; } = default!;
internal void dropCallback(DateTime? dropDate, LocalModule? dropModule)
{
if (dropDate != null)
{
dropOnDate(dropDate ?? throw new Exception("drop date for quiz is null"));
}
else if (dropModule != null)
{
dropOnModule(dropModule);
}
}
private void dropOnDate(DateTime dropDate)
{
if (planner.LocalCourse == null)
return;
var currentModule =
planner.LocalCourse.Modules.First(m => m.Quizzes.Select(q => q.Name + q.Description).Contains(Quiz.Name + Quiz.Description))
?? throw new Exception("in quiz callback, could not find module");
var defaultDueTimeDate = new DateTime(
year: dropDate.Year,
month: dropDate.Month,
day: dropDate.Day,
hour: planner.LocalCourse.Settings.DefaultDueTime.Hour,
minute: planner.LocalCourse.Settings.DefaultDueTime.Minute,
second: 0
);
var NewQuizList = currentModule.Quizzes
.Select(q =>
q.Name + q.Description != Quiz.Name + Quiz.Description
? q :
q with
{
DueAt = defaultDueTimeDate,
LockAt = q.LockAt > defaultDueTimeDate ? q.LockAt : defaultDueTimeDate
}
)
.ToArray();
var updatedModule = currentModule with { Quizzes = NewQuizList };
var updatedModules = planner.LocalCourse.Modules
.Select(m => m.Name == updatedModule.Name ? updatedModule : m)
.ToArray();
planner.LocalCourse = planner.LocalCourse with { Modules = updatedModules };
}
private void dropOnModule(LocalModule dropModule)
{
if (planner.LocalCourse == null)
return;
var newModules = planner.LocalCourse.Modules
.Select(
m =>
m.Name != dropModule.Name
? m with
{
Quizzes = m.Quizzes.Where(q => q.Name + q.Description != Quiz.Name + Quiz.Description).DistinctBy(q => q.Name + q.Description)
}
: m with
{
Quizzes = m.Quizzes.Append(Quiz).DistinctBy(q => q.Name + q.Description)
}
)
.ToArray();
var newCourse = planner.LocalCourse with { Modules = newModules };
planner.LocalCourse = newCourse;
}
}

View File

@@ -1,32 +0,0 @@
@using Management.Web.Shared.Components
@code {
[Parameter]
public RenderFragment ChildContent { get; set; } = default!;
[Parameter, EditorRequired]
public string Name { get; set; } = default!;
[Parameter, EditorRequired]
public bool IsSyncedWithCanvas { get; set; } = default!;
}
<div class="card">
<div class="card-body p-0">
<div class="card-title pt-2 px-2 m-0 d-flex justify-content-between">
<h4>@Name</h4>
@if(IsSyncedWithCanvas)
{
<CheckIcon />
}
else
{
<SyncIcon />
}
</div>
@ChildContent
</div>
</div>

View File

@@ -1,48 +0,0 @@
@using Management.Web.Shared.Components
@using Management.Web.Course.Module.ModuleItems
@inject DragContainer dragContainer
@inject NavigationManager Navigation
@inject PageEditorContext pageContext
@inherits DroppablePage
@code {
private void HandleDragStart()
{
dragContainer.DropCallback = dropCallback;
}
private void HandleDragEnd()
{
dragContainer.DropCallback = null;
}
private bool existsInCanvas => false;
private void OnClick()
{
pageContext.Page = Page;
Navigation.NavigateTo("/course/" + planner.LocalCourse?.Settings.Name + "/page/" + Page.Name);
}
}
<div
draggable="true"
@ondragstart="HandleDragStart"
@ondragend="HandleDragEnd"
@onclick="OnClick"
role="button"
>
<ModuleItemLayout Name=@Page.Name IsSyncedWithCanvas=existsInCanvas>
@if(!existsInCanvas)
{
<div class="mx-3 text-body-tertiary">
no page with same name in canvas
</div>
}
<div class="card-text overflow-hidden p-2">
<div>Due At: @Page.DueAt</div>
</div>
</ModuleItemLayout>
</div>

View File

@@ -1,54 +0,0 @@
@using Management.Web.Shared.Components
@using Management.Web.Shared.Components.Quiz
@inject DragContainer dragContainer
@inject QuizEditorContext quizContext
@inject NavigationManager Navigation
@inherits DroppableQuiz
@code {
private void HandleDragStart()
{
dragContainer.DropCallback = dropCallback;
}
private void HandleDragEnd()
{
dragContainer.DropCallback = null;
}
private bool existsInCanvas =>
planner.CanvasData != null
? Quiz.QuizIsCreated(planner.CanvasData.Quizzes)
: false;
private void OnClick()
{
quizContext.Quiz = Quiz;
Navigation.NavigateTo("/course/" + planner.LocalCourse?.Settings.Name + "/quiz/" + Quiz.Name);
}
}
<div
draggable="true"
@ondragstart="HandleDragStart"
@ondragend="HandleDragEnd"
@onclick="OnClick"
role="button"
>
<ModuleItemLayout Name=@Quiz.Name IsSyncedWithCanvas=@existsInCanvas>
@if(!existsInCanvas)
{
<div class="mx-3 text-body-tertiary">
no quiz with same name in canvas
</div>
}
<div class="card-text overflow-hidden p-2">
<div>Due At: @Quiz.DueAt</div>
</div>
</ModuleItemLayout>
</div>

View File

@@ -1,51 +0,0 @@
@using Management.Web.Pages.Course.Module
@using System.Linq
@using Management.Web.Pages.Course.Module.NewItemsButtons
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject CoursePlanner planner
@code {
private bool showNewModule { get; set; } = false;
protected override void OnInitialized()
{
planner.StateHasChanged += reload;
}
private void reload()
{
this.InvokeAsync(this.StateHasChanged);
}
public void Dispose()
{
planner.StateHasChanged -= reload;
}
}
<div class="row justify-content-end mb-1">
<div class="col-auto">
@if (!showNewModule)
{
<button class="btn btn-outline-secondary" @onclick="() => showNewModule = true">New Module</button>
}
else
{
<button class="btn btn-outline-secondary" @onclick="() => showNewModule = false">Hide New Module</button>
}
</div>
</div>
@if (showNewModule)
{
<NewModule OnSubmit="() => showNewModule = false" />
}
@if (planner.LocalCourse != null)
{
<div class="accordion" id="modulesAccordion">
@foreach (var module in planner.LocalCourse.Modules)
{
<ModuleDetail Module="module" />
}
</div>
}

View File

@@ -1,95 +0,0 @@
@using Management.Web.Shared.Components
@using Management.Web.Shared.Components.Forms
@inject CoursePlanner planner
@code {
[Parameter]
[EditorRequired]
public LocalModule Module { get; set; } = default!;
[Required]
[StringLength(50, ErrorMessage = "Name too long (50 character limit).")]
private string Name { get; set; } = "";
private Modal? modal { get; set; } = null;
private void submitHandler()
{
System.Console.WriteLine("new assignment");
var newAssignment = new LocalAssignment
{
Name = Name,
Description = "",
@* LockAtDueDate = true, *@
Rubric = new RubricItem[] { },
LockAt = null,
DueAt = DateTime.Now,
SubmissionTypes = new string[] { AssignmentSubmissionType.ONLINE_TEXT_ENTRY },
LocalAssignmentGroupName = selectedAssignmentGroup?.Name,
};
if(planner.LocalCourse != null)
{
var newModules =planner.LocalCourse.Modules.Select(m =>
m.Name != Module.Name
? m
: Module with
{
Assignments=Module.Assignments.Append(newAssignment)
}
);
planner.LocalCourse = planner.LocalCourse with
{
Modules=newModules
};
}
Name = "";
modal?.Hide();
}
private void setAssignmentGroup(LocalAssignmentGroup? group)
{
selectedAssignmentGroup = group;
}
private LocalAssignmentGroup? selectedAssignmentGroup { get; set; }
}
<button
class="btn btn-outline-secondary"
@onclick="() => modal?.Show()"
>
+ Assignment
</button>
<Modal @ref="modal">
<Title>New Assignment</Title>
<Body>
<form @onsubmit:preventDefault="true" @onsubmit="submitHandler">
<label for="Assignment Name">Name</label>
<input id="moduleName" class="form-control" @bind="Name" />
</form>
<br>
<label class="form-label">Assignment Group</label>
@if(planner != null)
{
<ButtonSelect
Label="Assignment Group"
Options="planner.LocalCourse?.Settings.AssignmentGroups ?? []"
GetName="(g) => g?.Name"
OnSelect="(g) => setAssignmentGroup(g)"
/>
}
</Body>
<Footer>
<button
type="button"
class="btn btn-primary"
@onclick="submitHandler"
>
Create Assignment
</button>
</Footer>
</Modal>

View File

@@ -1,38 +0,0 @@
@inject CoursePlanner planner
@inject ICanvasService canvas
@code {
[Required]
[StringLength(50, ErrorMessage = "Name too long (50 character limit).")]
private string Name { get; set; } = "";
[Parameter]
public EventCallback OnSubmit { get; set; }
private async Task submitHandler()
{
if(planner.LocalCourse != null && Name != "")
{
var newModule = new LocalModule
{
Name=Name
};
planner.LocalCourse = planner.LocalCourse with
{
Modules = planner.LocalCourse.Modules.Append(newModule)
};
}
Name = "";
await OnSubmit.InvokeAsync();
}
}
<h1>New Module</h1>
<form @onsubmit:preventDefault="true" @onsubmit="submitHandler">
<label for="moduleName">Name:</label>
<input id="moduleName" class="form-control" @bind="Name" />
<button class="btn btn-primary">Save</button>
</form>

View File

@@ -1,76 +0,0 @@
@using Management.Web.Shared.Components
@using Management.Web.Shared.Components.Forms
@inject CoursePlanner planner
@code {
[Parameter]
[EditorRequired]
public LocalModule Module { get; set; } = default!;
[Required]
[StringLength(50, ErrorMessage = "Name too long (50 character limit).")]
private string Name { get; set; } = "";
private Modal? modal { get; set; } = null;
private void submitHandler()
{
DiagnosticsConfig.Source?.StartActivity("Creating Page");
if(planner.LocalCourse != null)
{
var newPage = new LocalCoursePage
{
Name = Name,
Text = "",
DueAt = DateTime.Now
};
var newModules =planner.LocalCourse.Modules.Select(m =>
m.Name != Module.Name
? m
: Module with
{
Pages=Module.Pages.Append(newPage)
}
);
planner.LocalCourse = planner.LocalCourse with
{
Modules=newModules
};
}
Name = "";
modal?.Hide();
}
}
<button
class="btn btn-outline-secondary"
@onclick="() => modal?.Show()"
>
+ Page
</button>
<Modal @ref="modal">
<Title>New Page</Title>
<Body>
<form @onsubmit:preventDefault="true" @onsubmit="submitHandler">
<label for="Page Name">Name</label>
<input id="moduleName" class="form-control" @bind="Name" />
</form>
<br>
</Body>
<Footer>
<button
type="button"
class="btn btn-primary"
@onclick="submitHandler"
>
Create Page
</button>
</Footer>
</Modal>

View File

@@ -1,92 +0,0 @@
@using Management.Web.Shared.Components
@using Management.Web.Shared.Components.Forms
@inject CoursePlanner planner
@code {
[Parameter]
[EditorRequired]
public LocalModule Module { get; set; } = default!;
[Required]
[StringLength(50, ErrorMessage = "Name too long (50 character limit).")]
private string Name { get; set; } = "";
private Modal? modal { get; set; } = null;
private void submitHandler()
{
Console.WriteLine("new quiz");
Console.WriteLine(selectedAssignmentGroup);
if(Name.Trim() == string.Empty)
{
return;
}
var newQuiz = new LocalQuiz
{
Name=Name,
Description = "",
LocalAssignmentGroupName = selectedAssignmentGroup?.Name,
};
if(planner.LocalCourse != null)
{
var newModules = planner.LocalCourse.Modules.Select(m =>
m.Name != Module.Name
? m
: Module with
{
Quizzes=Module.Quizzes.Append(newQuiz)
}
);
planner.LocalCourse = planner.LocalCourse with
{
Modules=newModules
};
}
modal?.Hide();
}
private void setAssignmentGroup(LocalAssignmentGroup? group)
{
selectedAssignmentGroup = group;
}
private LocalAssignmentGroup? selectedAssignmentGroup { get; set; }
}
<button
class="btn btn-outline-secondary"
@onclick="() => modal?.Show()"
>
+ Quiz
</button>
<Modal @ref="modal">
<Title>New Quiz</Title>
<Body>
<form @onsubmit:preventDefault="true" @onsubmit="submitHandler">
<label for="Assignment Name">Name</label>
<input id="moduleName" class="form-control" @bind="Name" />
</form>
<br>
<label class="form-label">Assignment Group</label>
@if(planner != null && planner.LocalCourse != null)
{
<ButtonSelect
Label="Assignment Group"
Options="planner.LocalCourse.Settings.AssignmentGroups"
GetName="(g) => g?.Name"
OnSelect="(g) => setAssignmentGroup(g)"
/>
}
</Body>
<Footer>
<button
class="btn btn-primary"
@onclick="submitHandler"
>
CreateQuiz
</button>
</Footer>
</Modal>

View File

@@ -1,66 +0,0 @@
@using Management.Web.Shared.Components
@inject CoursePlanner planner
@code {
[Parameter]
[EditorRequired]
public LocalModule Module { get; set; } = default!;
private Modal? modal { get; set; } = null;
private string Name { get; set; } = string.Empty;
protected override void OnParametersSet()
{
if (Name == string.Empty)
Name = Module.Name;
}
private void submitHandler()
{
if (planner.LocalCourse == null)
return;
var newModule = Module with
{
Name = Name
};
// Module is the not renamed version
var newModules = planner.LocalCourse.Modules.Select(
m => m.Name == Module.Name
? newModule
: m
).ToArray();
planner.LocalCourse = planner.LocalCourse with
{
Modules = newModules
};
Name = "";
modal?.Hide();
}
}
<button
class="btn btn-outline-secondary"
@onclick="() => modal?.Show()"
>
Rename
</button>
<Modal @ref="modal">
<Title>Rename Module</Title>
<Body>
<form @onsubmit:preventDefault="true" @onsubmit="submitHandler">
<label for="moduleName">Name</label>
<input id="moduleName" class="form-control" @bind="Name" />
</form>
</Body>
<Footer>
<button type="button" class="btn btn-primary" @onclick="submitHandler">
Rename
</button>
</Footer>
</Modal>

View File

@@ -1,201 +0,0 @@
@using Management.Web.Shared.Components
@using CanvasModel.Pages
@inject CoursePlanner planner
@inject ICanvasService canvas
@inject NavigationManager Navigation
@inject PageEditorContext pageContext
@code {
protected override void OnInitialized()
{
pageContext.StateHasChanged += reload;
reload();
}
private void reload()
{
if (pageContext.Page != null)
{
name = pageContext.Page.Name;
}
this.InvokeAsync(this.StateHasChanged);
}
public void Dispose()
{
pageContext.StateHasChanged -= reload;
}
private string name { get; set; } = String.Empty;
private bool addingPageToCanvas = false;
private bool deletingPageFromCanvas = false;
private CanvasPage? pageInCanvas =>
planner.CanvasData?.Pages.FirstOrDefault(a => a.Title == pageContext.Page?.Name);
private string canvasPageUrl =>
$"https://snow.instructure.com/courses/{planner.LocalCourse?.Settings.CanvasId}/assignments/{pageInCanvas?.PageId}";
private void submitHandler()
{
if (pageContext.Page != null)
{
var newPage = pageContext.Page with
{
Name = name,
};
pageContext.SavePage(newPage);
}
pageContext.Page = null;
}
private async Task HandleDelete()
{
if (planner.LocalCourse != null && pageContext.Page != null)
{
var page = pageContext.Page;
var currentModule = planner
.LocalCourse
.Modules
.First(m =>
m.Pages.Contains(page)
) ?? throw new Exception("handling page delete, could not find module");
var newModules = planner.LocalCourse.Modules.Select(m =>
m.Name == currentModule.Name
? m with
{
Pages = m.Pages.Where(p => p != page).ToArray()
}
: m
)
.ToArray();
planner.LocalCourse = planner.LocalCourse with
{
Modules = newModules
};
if (pageInCanvas != null && planner.LocalCourse.Settings.CanvasId != null)
{
ulong courseId = planner.LocalCourse.Settings.CanvasId ?? throw new Exception("cannot delete if no course id");
await canvas.Pages.Delete(courseId, pageInCanvas.PageId);
}
Navigation.NavigateTo("/course/" + planner.LocalCourse?.Settings.Name);
}
}
private void handleNameChange(ChangeEventArgs e)
{
if (pageContext.Page != null)
{
var newPage = pageContext.Page with { Name = e.Value?.ToString() ?? "" };
pageContext.SavePage(newPage);
}
}
private async Task addToCanvas()
{
addingPageToCanvas = true;
await pageContext.AddPageToCanvas();
await planner.LoadCanvasData();
addingPageToCanvas = false;
}
private async Task updateInCanvas()
{
if(pageInCanvas != null)
{
addingPageToCanvas = true;
await pageContext.UpdateInCanvas(pageInCanvas.PageId);
await planner.LoadCanvasData();
addingPageToCanvas = false;
}
}
private async Task deleteFromCanvas()
{
if (pageInCanvas == null
|| planner?.LocalCourse?.Settings.CanvasId == null
|| pageContext.Page == null
)
return;
deletingPageFromCanvas = true;
await canvas.Pages.Delete(
(ulong)planner.LocalCourse.Settings.CanvasId,
pageInCanvas.PageId
);
await planner.LoadCanvasData();
deletingPageFromCanvas = false;
StateHasChanged();
}
}
<div class="d-flex flex-column p-2 h-100 w-100" style="height: 100%;" >
<div>
@pageContext.Page?.Name
</div>
<section class="flex-grow-1 p-1 border rounded-4 bg-dark-subtle" style="min-height: 0;">
@if (pageContext.Page != null)
{
<CoursePageMarkdownEditor />
}
</section>
<div class="d-flex justify-content-end p-3">
@if (addingPageToCanvas || deletingPageFromCanvas)
{
<div>
<Spinner />
</div>
}
<ConfirmationModal Label="Delete" Class="btn btn-danger" OnConfirmAsync="HandleDelete" />
<button
class="btn btn-outline-secondary mx-3"
disabled="@(addingPageToCanvas || deletingPageFromCanvas)"
@onclick="addToCanvas"
>
Add To Canvas
</button>
@if (pageInCanvas != null)
{
<a
class="btn btn-outline-secondary me-1"
href="@canvasPageUrl"
target="_blank"
disabled="@(addingPageToCanvas || deletingPageFromCanvas)"
>
View in Canvas
</a>
<button
class="btn btn-outline-secondary mx-3"
disabled="@(addingPageToCanvas || deletingPageFromCanvas)"
@onclick="updateInCanvas"
>
Update In Canvas
</button>
<ConfirmationModal
Disabled="@(addingPageToCanvas || deletingPageFromCanvas)"
Label="Delete from Canvas"
Class="btn btn-outline-danger mx-3"
OnConfirmAsync="deleteFromCanvas"
/>
}
<button class="btn btn-primary mx-2" @onclick="@(() => {
pageContext.Page = null;
Navigation.NavigateTo("/course/" + planner.LocalCourse?.Settings.Name);
})">
Done
</button>
</div>
</div>

View File

@@ -1,71 +0,0 @@
@page "/course/{CourseName}/page/{PageName}"
@using CanvasModel.EnrollmentTerms
@using CanvasModel.Courses
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@using LocalModels
@using Management.Web.Pages.Course.Module.ModuleItems
@using Management.Web.Shared.Components
@inject IFileStorageManager fileStorageManager
@inject ICanvasService canvas
@inject CoursePlanner planner
@inject PageEditorContext pageContext
@inject ILogger<CoursePageFormPage> logger
@code {
[Parameter]
public string? CourseName { get; set; } = default!;
[Parameter]
public string? PageName { get; set; } = default!;
private bool loading { get; set; } = true;
protected override async Task OnInitializedAsync()
{
if (loading)
{
loading = false;
logger.LogInformation($"loading page {CourseName} {PageName}");
if (planner.LocalCourse == null)
{
var courses = await fileStorageManager.LoadSavedCourses();
planner.LocalCourse = courses.First(c => c.Settings.Name == CourseName);
logger.LogInformation($"set course to '{planner.LocalCourse?.Settings.Name}'");
}
if (pageContext.Page == null)
{
var page = planner
.LocalCourse?
.Modules
.SelectMany(m => m.Pages)
.FirstOrDefault(a => a.Name == PageName);
pageContext.Page = page;
logger.LogInformation($"set page to '{pageContext.Page?.Name}'");
}
await planner.LoadCanvasData();
base.OnInitialized();
StateHasChanged();
}
}
}
<PageTitle>@CourseName - @PageName</PageTitle>
<div style="height: 100vh;" class="m-0 p-1 d-flex flex-row">
@if (loading)
{
<Spinner />
}
@if (planner.LocalCourse != null && pageContext.Page != null)
{
<CoursePageForm />
}
</div>

View File

@@ -1,78 +0,0 @@
@using Management.Web.Shared.Components
@inject CoursePlanner planner
@inject PageEditorContext pageContext
@code {
protected override void OnInitialized()
{
pageContext.StateHasChanged += reload;
reload();
}
private void reload()
{
if (pageContext.Page != null)
{
if(rawText == string.Empty)
{
rawText = pageContext.Page.ToMarkdown();
this.InvokeAsync(this.StateHasChanged);
}
}
}
public void Dispose()
{
pageContext.StateHasChanged -= reload;
}
private string rawText { get; set; } = string.Empty;
private string? error = null;
private MarkupString preview { get => (MarkupString)MarkdownService.Render(pageContext?.Page?.Text ?? ""); }
private void handleChange(string newRawPage)
{
rawText = newRawPage;
if (newRawPage != string.Empty)
{
try
{
var parsed = LocalCoursePage.ParseMarkdown(newRawPage);
error = null;
pageContext.SavePage(parsed);
}
catch(LocalPageMarkdownParseException e)
{
error = e.Message;
}
finally
{
StateHasChanged();
}
}
StateHasChanged();
}
}
<div class="d-flex w-100 h-100 flex-row">
@if(pageContext.Page != null && planner.LocalCourse != null)
{
<div class="row h-100 w-100">
<div class="col-6">
<MonacoTextArea Value=@rawText OnChange=@handleChange />
</div>
<div class="col-6 overflow-y-auto h-100" >
@if (error != null)
{
<p class="text-danger text-truncate">Error: @error</p>
}
<div>Due At: @pageContext.Page.DueAt</div>
<hr>
<div>
@(preview)
</div>
</div>
</div>
}
</div>

View File

@@ -1,42 +0,0 @@
@page
@model Management.Web.Pages.ErrorModel
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Error</title>
<link href="~/css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="~/css/site.css" rel="stylesheet" asp-append-version="true" />
</head>
<body>
<div class="main">
<div class="content px-4">
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
</div>
</div>
</body>
</html>

View File

@@ -1,26 +0,0 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Management.Web.Pages;
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
public string? RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
private readonly ILogger<ErrorModel> _logger;
public ErrorModel(ILogger<ErrorModel> logger)
{
_logger = logger;
}
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
}
}

View File

@@ -1,81 +0,0 @@
@page "/"
@using CanvasModel.EnrollmentTerms
@using CanvasModel.Courses
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@using LocalModels
@using Management.Web.Pages.Course.Module.ModuleItems
@using Management.Web.Shared.Components
@inject ICanvasService canvas
@inject CoursePlanner planner
@code {
private bool showNewFile { get; set; } = false;
protected override void OnInitialized()
{
planner.LocalCourse = null;
planner.StateHasChanged += reload;
}
private void reload()
{
this.InvokeAsync(this.StateHasChanged);
}
public void Dispose()
{
planner.StateHasChanged -= reload;
}
private void NewFileCreated()
{
showNewFile = false;
refreshKey++;
StateHasChanged();
}
private int refreshKey;
}
<PageTitle>Index</PageTitle>
<br>
@if(planner.LocalCourse == null)
{
<div class="row justify-content-center">
<div class="col-auto">
<CurrentFiles RefreshKey="refreshKey" />
</div>
</div>
@if(!showNewFile)
{
<div class="text-center">
<button
@onclick="@(()=>showNewFile = true)"
class="btn btn-primary"
>
Mange New Course
</button>
</div>
}
@if(showNewFile)
{
<div class="text-center">
<button
@onclick="@(()=>showNewFile = false)"
class="btn btn-primary"
>
Hide File Initialization
</button>
</div>
<div class="border rounded bg-dark-subtle p-3 my-3">
<InitializeNewCourse NewFileCreated="NewFileCreated" />
</div>
}
}
<br>

View File

@@ -1,81 +0,0 @@
@code {
[Parameter, EditorRequired]
public LocalQuizQuestion Question { get; set; } = default!;
}
<div class="row justify-content-between text-secondary">
<div class="col">
points: @Question.Points
</div>
<div class="col-auto">
@Question.QuestionType
</div>
</div>
@((MarkupString)Question.HtmlText)
@if(Question.QuestionType == QuestionType.MATCHING)
{
@foreach(var answer in Question.Answers)
{
<div class="mx-3 mb-1 bg-dark px-2 rounded rounded-2 border row">
<div
class="col text-end my-auto p-1"
>
@answer.Text
</div>
<div
class="col my-auto"
>
@answer.MatchedText
</div>
</div>
}
}
else
{
@foreach(var answer in Question.Answers)
{
string answerPreview = answer.HtmlText.StartsWith("<p>")
? answer.HtmlText.Replace("<p>", "<p class='m-0'>")
: answer.HtmlText;
<div class="mx-3 mb-1 bg-dark px-2 rounded rounded-2 d-flex flex-row border">
@if(answer.Correct)
{
<svg
style="width: 1em;"
class="me-1 my-auto"
viewBox="0 0 24 24"
fill="none"
>
<path
d="M4 12.6111L8.92308 17.5L20 6.5"
stroke="var(--bs-success)"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
}
else
{
<div
class="me-1 my-auto"
style="width: 1em;"
>
@if(Question.QuestionType == QuestionType.MULTIPLE_ANSWERS)
{
<span>[ ]</span>
}
</div>
}
<div class="markdownQuizAnswerPreview p-1">
@((MarkupString)answerPreview)
</div>
</div>
}
}

View File

@@ -1,79 +0,0 @@
@using Management.Web.Shared.Components
@inject QuizEditorContext quizContext
@code {
private Modal? modal { get; set; }
private LocalQuiz? testQuiz;
private string? error { get; set; } = null;
private string _quizMarkdownInput { get; set; } = "";
private string quizMarkdownInput
{
get => _quizMarkdownInput;
set
{
_quizMarkdownInput = value;
try
{
var newQuiz = LocalQuiz.ParseMarkdown(_quizMarkdownInput);
error = null;
testQuiz = newQuiz;
quizContext.SaveQuiz(newQuiz);
}
catch (QuizMarkdownParseException e)
{
error = e.Message;
StateHasChanged();
}
}
}
protected override void OnInitialized()
{
reload();
quizContext.StateHasChanged += reload;
}
private void reload()
{
if (quizContext.Quiz != null)
{
if (quizMarkdownInput == "")
{
quizMarkdownInput = quizContext.Quiz.ToMarkdown();
}
this.InvokeAsync(this.StateHasChanged);
}
}
public void Dispose()
{
quizContext.StateHasChanged -= reload;
}
}
<div class="d-flex flex-column h-100">
<div class="d-flex flex-row h-100 p-2">
<div class="row flex-grow-1">
<div class="col-6">
<MonacoTextArea
Value="@quizMarkdownInput"
OnChange="@((v) => quizMarkdownInput = v)"
/>
</div>
<div class="col-6 h-100 overflow-y-auto">
@if (error != null)
{
<p class="text-danger text-truncate">Error: @error</p>
}
@if(testQuiz != null)
{
<QuizPreview Quiz="testQuiz" />
}
</div>
</div>
</div>
</div>

View File

@@ -1,249 +0,0 @@
@page "/course/{CourseName}/quiz/{QuizName}"
@using CanvasModel.EnrollmentTerms
@using CanvasModel.Quizzes
@using Management.Web.Shared.Components
@using CanvasModel.Courses
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@using LocalModels
@using Management.Web.Pages.Course.Module.ModuleItems
@inject IFileStorageManager fileStorageManager
@inject ICanvasService canvas
@inject CoursePlanner planner
@inject QuizEditorContext quizContext
@inject MyLogger<QuizFormPage> logger
@inject NavigationManager Navigation
@code {
[Parameter]
public string? CourseName { get; set; } = default!;
[Parameter]
public string? QuizName { get; set; } = default!;
private bool loading { get; set; } = true;
private bool addingQuizToCanvas = false;
protected override void OnInitialized()
{
quizContext.StateHasChanged += reload;
}
private void reload()
{
this.InvokeAsync(this.StateHasChanged);
}
public void Dispose()
{
quizContext.StateHasChanged -= reload;
}
protected override async Task OnInitializedAsync()
{
if (loading)
{
loading = false;
logger.Log($"loading quiz {CourseName} {QuizName}");
if (planner.LocalCourse == null)
{
var courses = await fileStorageManager.LoadSavedCourses();
planner.LocalCourse = courses.First(c => c.Settings.Name == CourseName);
logger.Log($"set course to '{planner.LocalCourse?.Settings.Name}'");
}
if (quizContext.Quiz == null)
{
var quiz = planner
.LocalCourse?
.Modules
.SelectMany(m => m.Quizzes)
.FirstOrDefault(q => q.Name == QuizName);
quizContext.Quiz = quiz;
logger.Log($"set quiz to '{quizContext.Quiz?.Name}'");
}
StateHasChanged();
if (planner.CanvasData == null)
{
await planner.LoadCanvasData();
}
base.OnInitialized();
StateHasChanged();
}
}
private void deleteQuiz()
{
quizContext.DeleteQuiz();
Navigation.NavigateTo("/course/" + planner.LocalCourse?.Settings.Name);
}
private async Task addToCanvas()
{
addingQuizToCanvas = true;
await quizContext.AddQuizToCanvas();
await planner.LoadCanvasData();
addingQuizToCanvas = false;
}
private void done()
{
quizContext.Quiz = null;
Navigation.NavigateTo("/course/" + planner.LocalCourse?.Settings.Name);
}
private CanvasQuiz? quizInCanvas => planner.CanvasData?.Quizzes.FirstOrDefault(q => q.Title == quizContext.Quiz?.Name);
private string canvasQuizUrl =>
$"https://snow.instructure.com/courses/{planner.LocalCourse?.Settings.CanvasId}/quizzes/{quizInCanvas?.Id}";
private double? quizPoints => quizContext.Quiz?.Questions.Sum(q => q.Points);
private bool showHelp = false;
private readonly static string exampleMarkdownQuestion = @"QUESTION REFERENCE
---
Points: 2
this is a question?
*a) correct
b) not correct
---
points: 1
question goes here
[*] correct
[ ] not correct
[] not correct
---
the points default to 1?
*a) true
b) false
---
Markdown is supported
- like
- this
- list
[*] true
[ ] false
---
This is a one point essay question
essay
---
points: 4
this is a short answer question
short_answer
---
points: 4
the underscore is optional
short answer
---
this is a matching question
^ left answer - right dropdown
^ other thing - another option
";
}
<div class="d-flex flex-column py-3" style="height: 100vh;">
<section>
<div class="row justify-content-between">
<div class="col-auto my-auto">
<button class="btn btn-outline-secondary" @onclick="done">
← go back
</button>
</div>
<div class="col-auto my-auto">
<h2>
@quizContext.Quiz?.Name
</h2>
</div>
@if (quizContext.Quiz == null)
{
<div class="col-auto">
<Spinner />
</div>
}
<div class="col-auto me-3">
<h3>
Questions: @quizContext.Quiz?.Questions.Count() - Points: @quizPoints
</h3>
@if (quizInCanvas != null)
{
@if (quizInCanvas?.Published == true)
{
<div class="text-success">
Published!
</div>
}
else
{
<div class="text-danger">
Not Published
</div>
}
}
</div>
</div>
</section>
<section
class="flex-grow-1 w-100 d-flex justify-content-center border rounded-4 bg-dark-subtle"
style="min-height: 10%; max-width: 100%;"
>
@if(showHelp)
{
<pre class="bg-dark-subtle me-3 pe-5 ps-3 rounded rounded-3">
@exampleMarkdownQuestion
</pre>
}
<div class="w-100" style="max-width: 120em; max-height: 100%;">
@if (quizContext.Quiz != null)
{
<MarkdownQuizForm />
}
</div>
</section>
<div>
<button
class="btn btn-outline-secondary mt-3"
@onclick="@(() => showHelp = !showHelp)"
>
toggle help
</button>
</div>
<section class="p-2">
@if (quizContext.Quiz != null)
{
<div class="row justify-content-end">
<div class="col-auto">
<ConfirmationModal
Label="Delete"
Class="btn btn-danger"
OnConfirm="deleteQuiz"
Disabled="@addingQuizToCanvas"
/>
<button class="btn btn-outline-secondary me-1" @onclick="addToCanvas" disabled="@addingQuizToCanvas">
Add to Canvas
</button>
@if (quizInCanvas != null)
{
<a class="btn btn-outline-secondary me-1" href="@canvasQuizUrl" target="_blank">
View in Canvas
</a>
}
<button class="btn btn-primary" @onclick="done" disabled="@addingQuizToCanvas">
Done
</button>
</div>
</div>
}
@if (addingQuizToCanvas)
{
<Spinner />
}
</section>
</div>

View File

@@ -1,71 +0,0 @@
@using Management.Web.Shared.Components
@inject QuizEditorContext quizContext
@code {
[Parameter, EditorRequired]
public LocalQuiz Quiz { get; set; } = default!;
protected override void OnInitialized()
{
quizContext.StateHasChanged += reload;
}
private void reload()
{
this.InvokeAsync(this.StateHasChanged);
}
public void Dispose()
{
quizContext.StateHasChanged -= reload;
}
}
@if(Quiz != null)
{
<div class="row justify-content-start">
<div class="col-auto" style="min-width: 35em;">
<div class="row">
<div class="col-6 text-end">Name: </div>
<div class="col-6">@Quiz.Name</div>
</div>
<div class="row">
<div class="col-6 text-end">Due At: </div>
<div class="col-6">@Quiz.DueAt</div>
</div>
<div class="row">
<div class="col-6 text-end">Lock At: </div>
<div class="col-6">@Quiz.LockAt</div>
</div>
<div class="row">
<div class="col-6 text-end">Shuffle Answers: </div>
<div class="col-6">@Quiz.ShuffleAnswers</div>
</div>
<div class="row">
<div class="col-6 text-end">Allowed Attempts: </div>
<div class="col-6">@Quiz.AllowedAttempts</div>
</div>
<div class="row">
<div class="col-6 text-end">One question at a time: </div>
<div class="col-6">@Quiz.OneQuestionAtATime</div>
</div>
<div class="row">
<div class="col-6 text-end">Assignment Group: </div>
<div class="col-6">@Quiz.LocalAssignmentGroupName</div>
</div>
</div>
</div>
<div class="p-3" style="white-space: pre-wrap;">@Quiz.Description</div>
@foreach(var question in Quiz.Questions)
{
<div class="bg-dark-subtle mt-1 p-1 ps-2 rounded rounded-2">
<MarkdownQuestionPreview
Question="question"
@key="question"
/>
</div>
}
}

View File

@@ -1,55 +0,0 @@
@page "/"
@using Microsoft.AspNetCore.Components.Web
@namespace Management.Web.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="~/" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
crossorigin="anonymous"></script>
<link href="css/site.css" rel="stylesheet" />
<link href="Management.Web.styles.css" rel="stylesheet" />
<link
href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Kanit&family=Mukta&family=Roboto&family=Sofia+Sans+Condensed:wght@400;500&display=swap"
rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,600&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
<link rel="icon" type="image/png" href="favicon.png" />
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
</head>
<body data-bs-theme="dark">
<component type="typeof(App)" render-mode="ServerPrerendered" />
<div id="blazor-error-ui" class="p-0 m-0">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_content/BlazorMonaco/jsInterop.js"></script>
<script src="_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js"></script>
<script src="_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.js"></script>
<script src="_framework/blazor.server.js"></script>
</body>
</html>

View File

@@ -1,164 +0,0 @@
global using System.ComponentModel.DataAnnotations;
global using System.Text.Json;
global using System.Text.Json.Serialization;
global using CanvasModel;
global using CanvasModel.Courses;
global using CanvasModel.EnrollmentTerms;
global using LocalModels;
global using Management.Planner;
global using Management.Services;
global using Management.Services.Canvas;
global using Management.Web.Shared;
global using Management.Web.Shared.Components;
using dotenv.net;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.ResponseCompression;
using OpenTelemetry;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
DotEnv.Load();
var builder = WebApplication.CreateBuilder(args);
ConfigurationSetup.Canvas(builder);
const string serviceName = "canvas-management";
builder.Logging.AddOpenTelemetry(options =>
{
options
.SetResourceBuilder(
ResourceBuilder
.CreateDefault()
.AddService(serviceName)
)
.AddOtlpExporter(o =>
{
o.Endpoint = new Uri("http://localhost:4317/");
});
// .AddConsoleExporter();
});
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService(serviceName))
.WithTracing(tracing => tracing
.AddSource(DiagnosticsConfig.SourceName)
.AddOtlpExporter(o =>
{
o.Endpoint = new Uri("http://localhost:4317/");
})
.AddAspNetCoreInstrumentation()
.AddProcessor(new BatchActivityExportProcessor(new CustomConsoleExporter()))
)
.WithMetrics(metrics => metrics
.AddOtlpExporter(o =>
{
o.Endpoint = new Uri("http://localhost:4317/");
}
)
.AddAspNetCoreInstrumentation()
);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddLogging();
builder.Services.AddSingleton(typeof(MyLogger<>));
// stateless services
builder.Services.AddSingleton<IWebRequestor, WebRequestor>();
builder.Services.AddSingleton<CanvasServiceUtils>();
builder.Services.AddSingleton<ICanvasAssignmentService, CanvasAssignmentService>();
builder.Services.AddSingleton<ICanvasCoursePageService, CanvasCoursePageService>();
builder.Services.AddSingleton<ICanvasAssignmentGroupService, CanvasAssignmentGroupService>();
builder.Services.AddSingleton<ICanvasQuizService, CanvasQuizService>();
builder.Services.AddSingleton<ICanvasModuleService, CanvasModuleService>();
builder.Services.AddSingleton<ICanvasService, CanvasService>();
builder.Services.AddSingleton<MarkdownCourseSaver>();
builder.Services.AddSingleton<CourseMarkdownLoader>();
builder.Services.AddSingleton<FileStorageService>();
// one actor system, maybe different actor for different pages?
builder.Services.AddSingleton<AkkaService>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<AkkaService>());
// TODO: need to handle scoped requirements
// builder.Services.AddSingleton(sp =>
// {
// var akka = sp.GetRequiredService<AkkaService>();
// return new CanvasQueueActorWrapper(akka.CoursePlannerActor ?? throw new Exception("Canvas queue actor not properly created"));
// });
builder.Services.AddSingleton<IFileStorageManager>(sp =>
{
var akka = sp.GetRequiredService<AkkaService>();
return new LocalStorageActorWrapper(akka.StorageActor ?? throw new Exception("Canvas queue actor not properly created"));
});
builder.Services.AddScoped<CoursePlanner>();
builder.Services.AddScoped<AssignmentEditorContext>();
builder.Services.AddScoped<PageEditorContext>();
builder.Services.AddScoped<QuizEditorContext>();
builder.Services.AddScoped<DragContainer>();
builder.Services.AddSingleton<FileConfiguration>();
builder.Services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "application/octet-stream" });
});
builder.Services.AddSignalR(e =>
{
e.MaximumReceiveMessageSize = 102400000;
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// app.UseResponseCompression();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Start();
var addresses = app.Services.GetService<IServer>()?.Features.Get<IServerAddressesFeature>()?.Addresses ?? [];
foreach (var address in addresses)
{
Console.WriteLine("Running at: " + address);
}
app.WaitForShutdown();

View File

@@ -1,37 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:25470",
"sslPort": 44349
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5087",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7055;http://localhost:5087",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -1,20 +0,0 @@
<svg
width="24px"
height="24px"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke="var(--bs-success-border-subtle)"
d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
stroke-width="2"
/>
<path
stroke="var(--bs-success-border-subtle)"
d="M9 12L10.6828 13.6828V13.6828C10.858 13.858 11.142 13.858 11.3172 13.6828V13.6828L15 10"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>

Before

Width:  |  Height:  |  Size: 561 B

View File

@@ -1,85 +0,0 @@
@namespace Management.Web.Shared.Components
@code {
[Parameter]
public Action? OnConfirm { get; init; }
[Parameter]
public Action? OnDeny { get; init; }
[Parameter]
public Func<Task>? OnConfirmAsync { get; init; }
[Parameter]
public Func<Task>? OnDenyAsync { get; init; }
[Parameter]
[EditorRequired]
public string Label { get; set; } = "";
[Parameter]
[EditorRequired]
public string Class { get; set; } = "";
[Parameter]
public bool Disabled { get; set; } = false;
private Modal? modal { get; set; } = null;
private bool doingAsyncThings { get; set; } = false;
private async Task HandleDeny()
{
if(OnDeny != null)
OnDeny();
if(OnDenyAsync != null)
{
doingAsyncThings = true;
await OnDenyAsync();
doingAsyncThings = false;
}
modal?.Hide();
}
private async Task HandleConfirm()
{
if(OnConfirm != null)
OnConfirm();
if(OnConfirmAsync != null)
{
doingAsyncThings = true;
await OnConfirmAsync();
doingAsyncThings = false;
}
modal?.Hide();
}
}
<button
class="@(Class != "" ? Class : "btn btn-danger ")"
@onclick="() => modal?.Show()"
disabled="@Disabled"
>
@Label
</button>
<Modal @ref="modal">
<Title>Are you sure you want to @Label?</Title>
<Body>
<div class="text-center">
<button
class="btn btn-secondary"
@onclick="HandleDeny"
disabled="@Disabled"
>
no
</button>
<button
class="btn btn-primary"
@onclick="HandleConfirm"
disabled="@Disabled"
>
yes
</button>
</div>
</Body>
<Footer>
@if(doingAsyncThings)
{
<Spinner />
}
</Footer>
</Modal>

View File

@@ -1,43 +0,0 @@
@typeparam T
@code {
[Parameter, EditorRequired]
public string Label { get; set; } = string.Empty;
[Parameter, EditorRequired]
public IEnumerable<T> Options { get; set; } = default!;
[Parameter, EditorRequired]
public Func<T?, string?> GetName { get; set; } = default!;
[Parameter, EditorRequired]
public Action<T?> OnSelect { get; set; } = default!;
[Parameter]
public T? SelectedOption { get; set; }
private string htmlLabel => Label.Replace("-", "");
private void onSelect(T option)
{
SelectedOption = option;
OnSelect(SelectedOption);
}
private string getButtonClass(T option)
{
var partClass = GetName(option) == GetName(SelectedOption) ? "primary" : "outline-primary";
return $"mx-1 btn btn-{partClass}";
}
}
<div key="@GetName(SelectedOption)">
@foreach(var option in Options)
{
<button
class="@getButtonClass(option)"
@onclick="() => onSelect(option)"
>
@GetName(option)
</button>
}
</div>

View File

@@ -1,45 +0,0 @@
@typeparam T
@code {
[Parameter, EditorRequired]
public string Label { get; set; } = string.Empty;
[Parameter, EditorRequired]
public IEnumerable<T> Options { get; set; } = default!;
[Parameter, EditorRequired]
public Func<T, string> GetId { get; set; } = default!;
[Parameter, EditorRequired]
public Func<T, string> GetName { get; set; } = default!;
[Parameter, EditorRequired]
public Action<T?> OnSelect { get; set; } = default!;
private string htmlLabel => Label.Replace("-", "");
private void onSelect(ChangeEventArgs e)
{
var newId = e.Value?.ToString();
var selectedOption = Options.FirstOrDefault(o => GetId(o) == newId);
OnSelect(selectedOption);
}
}
<div>
<label for="@htmlLabel">@Label</label>
<select
id="@htmlLabel"
name="@htmlLabel"
@oninput="onSelect"
>
@foreach(var option in Options)
{
<option
value="@(GetId(option))"
>
@GetName(option)
</option>
}
</select>
</div>

View File

@@ -1,11 +0,0 @@
<svg
width="30"
height="30"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 10a2 2 0 11-4.001-.001A2 2 0 016 10zm6 0a2 2 0 11-4.001-.001A2 2 0 0112 10zm6 0a2 2 0 11-4.001-.001A2 2 0 0118 10z"
fill="var(--bs-primary)"
/>
</svg>

Before

Width:  |  Height:  |  Size: 268 B

View File

@@ -1,56 +0,0 @@
@code {
[Parameter, EditorRequired]
public RenderFragment? Title { get; set; }
[Parameter, EditorRequired]
public RenderFragment? Body { get; set; }
[Parameter, EditorRequired]
public RenderFragment? Footer { get; set; }
[Parameter]
public Action OnShow { get; set; } = () => { };
[Parameter]
public Action OnHide { get; set; } = () => { };
[Parameter]
public string Size { get; set; } = "xl"; //sm, lg, xl, xxl
private string modalClass = "hide-modal";
private bool showBackdrop = false;
public void Show()
{
modalClass = "show-modal";
showBackdrop = true;
OnShow();
}
public void Hide()
{
modalClass = "hide-modal";
showBackdrop = false;
OnHide();
}
}
<div class="modal @modalClass" @onmousedown="Hide">
<div class="@($"modal-dialog modal-{Size}")" role="document" @onmousedown:stopPropagation="true">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title text-center w-100">@Title</h4>
<button type="button" class="btn-close" @onclick="Hide"></button>
</div>
<div class="modal-body">@Body</div>
<div class="modal-footer">@Footer</div>
</div>
</div>
</div>
@if (showBackdrop)
{
<div
class="modal-backdrop fade show"
></div>
}

View File

@@ -1,16 +0,0 @@
.show-modal {
animation: enter 250ms;
display: block;
opacity: 1;
}
@keyframes enter {
from {
display: block;
opacity: 0;
}
to {
opacity: 1;
}
}

View File

@@ -1,84 +0,0 @@
@using BlazorMonaco
@using BlazorMonaco.Editor
<h3>Code Editor</h3>
<div>
<div style="margin:10px 0;">
New Value: <input type="text" @bind="_valueToSet" style="width: 400px;" /> <button @onclick="SetValue">Set Value</button>
</div>
<div style="margin:10px 0;">
<button @onclick="GetValue">Get Value</button>
</div>
<div style="margin:10px 0;">
See the console for results.
</div>
</div>
<div
style="height: 300px"
>
<StandaloneCodeEditor
@ref="_editor"
Id="sample-code-editor-123"
ConstructionOptions="EditorConstructionOptions"
OnDidInit="EditorOnDidInit"
/>
</div>
@code {
private StandaloneCodeEditor _editor = null!;
private string _valueToSet = "";
private StandaloneEditorConstructionOptions EditorConstructionOptions(StandaloneCodeEditor editor)
{
return new StandaloneEditorConstructionOptions
{
Language = "markdown",
Theme = "vs-dark",
TabSize = 2,
Value = "this is the default \n value",
Minimap = new EditorMinimapOptions { Enabled = false }
};
}
private async Task EditorOnDidInit()
{
await _editor.AddCommand((int)KeyMod.CtrlCmd | (int)KeyCode.KeyH, (args) =>
{
Console.WriteLine("Ctrl+H : Initial editor command is triggered.");
});
var newDecorations = new ModelDeltaDecoration[]
{
new ModelDeltaDecoration
{
Range = new BlazorMonaco.Range(3,1,3,1),
Options = new ModelDecorationOptions
{
IsWholeLine = true,
ClassName = "decorationContentClass",
GlyphMarginClassName = "decorationGlyphMarginClass"
}
}
};
decorationIds = await _editor.DeltaDecorations(null, newDecorations);
// You can now use 'decorationIds' to change or remove the decorations
}
private string[] decorationIds = new string[0];
private async Task SetValue()
{
Console.WriteLine($"setting value to: {_valueToSet}");
await _editor.SetValue(_valueToSet);
}
private async Task GetValue()
{
var val = await _editor.GetValue();
Console.WriteLine($"value is: {val}");
}
}

View File

@@ -1,59 +0,0 @@
@* @rendermode @(new InteractiveServerRenderMode(prerender: false)) *@
@implements IDisposable
@using BlazorMonaco
@using BlazorMonaco.Editor
@code {
[Parameter, EditorRequired]
public string Value { get; set; } = default!;
[Parameter, EditorRequired]
public Action<string> OnChange { get; set; } = default!;
private string randomId = "monaco-editor-" + BitConverter.ToString(new byte[16].Select(b => (byte)new
Random().Next(256)).ToArray()).Replace("-", "");
private StandaloneCodeEditor? _editor = null;
private StandaloneEditorConstructionOptions EditorConstructionOptions(StandaloneCodeEditor editor)
{
return new StandaloneEditorConstructionOptions
{
Language = "markdown",
Theme = "vs-dark",
TabSize = 2,
Value = Value,
Minimap = new EditorMinimapOptions { Enabled = false },
LineNumbers = "off",
LineDecorationsWidth = 0,
WordWrap = "on",
AutomaticLayout = true,
FontFamily = "Roboto-mono",
FontSize = 16,
Padding = new EditorPaddingOptions()
{
Top = 10
}
};
}
private async Task OnDidChangeModelContent()
{
if (_editor == null) return;
var newValue = await _editor.GetValue();
OnChange(newValue);
}
void IDisposable.Dispose()
{
_editor?.Dispose();
_editor = null;
}
}
<StandaloneCodeEditor @ref="_editor" Id="@randomId" ConstructionOptions="EditorConstructionOptions"
OnDidChangeModelContent="OnDidChangeModelContent" />

View File

@@ -1,4 +0,0 @@
<div class="text-center m-3">
<span class="loader"></span>
</div>

View File

@@ -1,56 +0,0 @@
.loader {
width: 48px;
height: 48px;
border-radius: 50%;
display: inline-block;
position: relative;
border: 3px solid;
border-color: #6c757d #6c757d transparent transparent;
box-sizing: border-box;
animation: rotation 2s linear infinite;
}
.loader::after,
.loader::before {
content: '';
box-sizing: border-box;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
border: 3px solid;
border-color: transparent transparent #092565 #092565;
width: 40px;
height: 40px;
border-radius: 50%;
box-sizing: border-box;
animation: rotationBack 1s linear infinite;
transform-origin: center center;
}
/* #092565 */
/* #3a0647 */
.loader::before {
width: 32px;
height: 32px;
border-color: #6c757d #6c757d transparent transparent;
animation: rotation 3s linear infinite;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes rotationBack {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(-360deg);
}
}

View File

@@ -1,40 +0,0 @@
<svg
class="d-inline"
width="24px"
height="24px"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_429_11068)">
<path
stroke="var(--bs-danger-border-subtle)"
d="M3 11.9998C3 7.02925 7.02944 2.99982 12 2.99982C14.8273 2.99982 17.35 4.30348 19 6.34248"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"/>
<path
stroke="var(--bs-danger-border-subtle)"
d="M19.5 2.99982L19.5 6.99982L15.5 6.99982"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"/>
<path
stroke="var(--bs-danger-border-subtle)"
d="M21 11.9998C21 16.9704 16.9706 20.9998 12 20.9998C9.17273 20.9998 6.64996 19.6962 5 17.6572"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"/>
<path
stroke="var(--bs-danger-border-subtle)"
d="M4.5 20.9998L4.5 16.9998L8.5 16.9998"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_429_11068">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,127 +0,0 @@
@inject CoursePlanner planner
@code {
[Parameter]
[EditorRequired]
public SimpleTimeOnly Time { get; set; } = default!;
[Parameter]
[EditorRequired]
public Action<SimpleTimeOnly> UpdateTime { get; set; }= default!;
protected override void OnInitialized()
{
planner.StateHasChanged += reload;
}
private void reload()
{
this.InvokeAsync(this.StateHasChanged);
}
public void Dispose()
{
planner.StateHasChanged -= reload;
}
private string AmPm
{
get => Time.Hour < 12 ? "AM" : "PM";
}
private int AdjustedHour
{
get
{
var time = Time.Hour % 12;
if (time == 0) return 12;
return time;
}
}
private int convertTo24Hour(int hour, string? amPm)
{
if(amPm == "AM")
{
return hour % 12;
}
else
{
if (hour == 12)
return 12;
else
return hour + 12;
}
}
}
<div>
<select
@onchange="async (e) =>
UpdateTime(
new SimpleTimeOnly
{
Hour=convertTo24Hour(Convert.ToInt32(e.Value), AmPm),
Minute=Time.Minute
}
)"
class="form-control w-auto d-inline"
>
<option
value="12"
selected="@(12 == AdjustedHour)"
>
12
</option>
@foreach (var hour in Enumerable.Range(1, 11))
{
<option
value="@hour"
selected="@(hour == AdjustedHour)"
>
@hour.ToString("00")
</option>
}
</select>
<span class="pl-0">:</span>
<select
@onchange="async (e) =>
UpdateTime(
new SimpleTimeOnly
{
Hour=Time.Hour,
Minute=Convert.ToInt32(e.Value)
}
)"
class="form-control w-auto d-inline"
>
@foreach (var minute in new int[] {0, 15, 30, 45, 59})
{
<option
value="@minute"
selected="@(minute == Time.Minute)"
>
@(minute.ToString("00"))
</option>
}
</select>
<select
@onchange="(e) =>
UpdateTime(
new SimpleTimeOnly
{
Hour=convertTo24Hour(Time.Hour, e.Value?.ToString()),
Minute=Time.Minute
}
)"
class="form-control w-auto d-inline"
>
@foreach (var amPm in new string[] {"AM", "PM"})
{
<option
value="@amPm"
selected="@(amPm == AmPm)"
>
@amPm
</option>
}
</select>
</div>

View File

@@ -1,49 +0,0 @@
@code
{
[Parameter, EditorRequired]
public Func<string,Task> SetToken { get; set; } = default!;
private Modal modal { get; set; } = default!;
private string tokenInput { get; set; } = "";
protected override void OnAfterRender(bool firstRender)
{
if(firstRender)
modal.Show();
}
}
<Modal @ref="modal">
<Title>
<h3>Canvas Token</h3>
</Title>
<Body>
<div>
<p>
Please input your canvas token to enable canvas integration
</p>
<p>
We only store the token encrypted in your browser. We do not store the token on our servers.
</p>
<p>
You can get your canvas token <a href="https://snow.instructure.com/profile/settings">here</a>
</p>
<form
onsubmit:preventDefault="true"
@onsubmit="async () => await SetToken(tokenInput)"
>
<input
type="text"
class="form-control"
@bind="tokenInput"
@bind:event="oninput"
/>
</form>
</div>
</Body>
<Footer>
</Footer>
</Modal>

View File

@@ -1,39 +0,0 @@
@using LocalModels
@inject IFileStorageManager fileStorageManager
@inject CoursePlanner planner
@inject NavigationManager Navigation
@inject MyLogger<CurrentFiles> logger
@code
{
[Parameter]
public int RefreshKey { get; set; }
public IEnumerable<LocalCourse>? localCourses { get; set; }
protected override async Task OnParametersSetAsync()
{
localCourses = await fileStorageManager.LoadSavedCourses();
}
void handleClick(MouseEventArgs e, LocalCourse course)
{
planner.LocalCourse = course;
Navigation.NavigateTo("/course/" + course.Settings.Name);
}
}
<div class="">
@if (localCourses != null)
{
<h3 class="text-center mb-3">Stored Courses</h3>
@foreach (var course in localCourses)
{
var location = "/course/" + course.Settings.Name;
<div>
<div class=" fs-4 text-start mb-3 hover-underline-animation" @onclick="(e) => handleClick(e, course)" role='button'>
@course.Settings.Name
</div>
</div>
}
}
</div>

View File

@@ -1,30 +0,0 @@
.hover-underline-animation {
display: inline-block;
position: relative;
color: var(--bs-primary-text-emphasis);
transition: all 500ms;
}
.hover-underline-animation:hover {
/* text-shadow: 10px 10px #092565; */
/* text-shadow: 10px 10px 40px #092565; */
transform: scale(1.05);
}
.hover-underline-animation::after {
content: '';
position: absolute;
width: 100%;
transform: scaleX(0);
height: 2px;
bottom: 0;
left: 0;
background-color: var(--bs-primary-text-emphasis);
transform-origin: bottom right;
transition: transform 500ms ease-out;
}
.hover-underline-animation:hover::after {
transform: scaleX(1);
transform-origin: bottom left;
}

View File

@@ -1,196 +0,0 @@
@using CanvasModel.EnrollmentTerms
@using Management.Web.Shared.Components
@using CanvasModel.Courses
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@using LocalModels
@inject ICanvasService canvas
@inject IFileStorageManager fileStorageManager
@code {
[Parameter, EditorRequired]
public Action NewFileCreated { get; set; } = default!;
private bool loadingTerms = false;
private bool loadingCourses = false;
public IEnumerable<LocalCourse>? localCourses { get; set; }
private IEnumerable<EnrollmentTermModel>? terms { get; set; } = null;
private IEnumerable<CourseModel>? courses { get; set; } = null;
private ulong? _selectedTermId { get; set; }
private ulong? selectedTermId
{
get => _selectedTermId;
set
{
_selectedTermId = value;
this.InvokeAsync(updateCourses);
}
}
private EnrollmentTermModel? selectedTerm
{
get => terms?.FirstOrDefault(t => t.Id == selectedTermId);
}
private ulong? _selectedCourseId { get; set; }
private ulong? selectedCourseId
{
get => _selectedCourseId;
set
{
_selectedCourseId = value;
}
}
private CourseModel? selectedCourse
{
get => courses?.First(c => c.Id == selectedCourseId);
}
private List<DayOfWeek> days { get; set; } = new();
private IEnumerable<string> directoriesNotUsed { get; set; } = [];
private string? selectedStorageDirectory { get; set; } = null;
protected override async Task OnInitializedAsync()
{
loadingTerms = true;
terms = await canvas.GetCurrentTermsFor();
loadingTerms = false;
directoriesNotUsed = await fileStorageManager.GetEmptyDirectories();
}
private async Task SaveNewCourse()
{
if (selectedCourse != null && selectedStorageDirectory != null && selectedStorageDirectory != string.Empty)
{
var course = new LocalCourse
{
Modules = new LocalModule[] { },
Settings = new LocalCourseSettings()
{
Name = Path.GetFileName(selectedStorageDirectory),
CanvasId = selectedCourse.Id,
StartDate = selectedTerm?.StartAt ?? new DateTime(),
EndDate = selectedTerm?.EndAt ?? new DateTime(),
DaysOfWeek = days,
}
};
await fileStorageManager.SaveCourseAsync(course, null);
NewFileCreated();
}
await updateCourses();
}
private async Task updateCourses()
{
if (selectedTermId != null)
{
loadingCourses = true;
localCourses = await fileStorageManager.LoadSavedCourses();
var storedCourseIds = localCourses.Select(c => c.Settings.CanvasId);
var allCourses = await canvas.GetCourses((ulong)selectedTermId);
courses = allCourses.Where(c => !storedCourseIds.Contains(c.Id));
loadingCourses = false;
}
else
courses = null;
StateHasChanged();
}
}
@if (loadingTerms)
{
<Spinner />
}
@if (terms != null)
{
<div class="row justify-content-center">
<div class="col-auto">
<label for="termselect">Select Term:</label>
<select
id="termselect"
class="form-select"
@bind="selectedTermId"
>
@foreach (var term in terms)
{
<option value="@term.Id">@term.Name</option>
}
</select>
</div>
</div>
}
@if (selectedTerm is not null)
{
@if (loadingCourses)
{
<Spinner />
}
@if (courses != null)
{
<div class="row justify-content-center m-3">
<div class="col-auto">
<label for="courseselect">Select Course:</label>
<select id="courseselect" class="form-select" @bind="selectedCourseId">
@foreach (var course in courses)
{
<option value="@course.Id">@course.Name</option>
}
</select>
</div>
</div>
<div class="row justify-content-center m-3">
<div class="col-auto">
<label for="directorySelect">Select Storage Directory:</label>
<select
id="directorySelect"
class="form-select"
@bind="selectedStorageDirectory"
>
<option></option>
@foreach (var folder in directoriesNotUsed)
{
<option value="@folder">@folder</option>
}
</select>
</div>
</div>
}
<h5 class="text-center mt-3">Select Days Of Week</h5>
<div class="row m-3">
@foreach (DayOfWeek day in (DayOfWeek[])Enum.GetValues(typeof(DayOfWeek)))
{
<div class="col">
<button
class="@(
days.Contains(day)
? "btn btn-secondary"
: "btn btn-outline-secondary"
)"
@onclick="() => {
if(days.Contains(day))
days.Remove(day);
else
days.Add(day);
}"
>
@day
</button>
</div>
}
</div>
<div class="text-center">
<button @onclick="SaveNewCourse" class="btn btn-primary">
Create Course Files
</button>
</div>
}

View File

@@ -1,10 +0,0 @@
@inherits LayoutComponentBase
<PageTitle>Management.Web</PageTitle>
<main class="d-flex justify-content-center">
<div class="w-100 px-3">
@Body
</div>
</main>

View File

@@ -1,4 +0,0 @@
public class DragContainer
{
public Action<DateTime?, LocalModule?>? DropCallback { get; set; }
}

View File

@@ -1,11 +0,0 @@
@using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using Management.Web
@using Management.Web.Shared
@using static Microsoft.AspNetCore.Components.Web.RenderMode

Some files were not shown because too many files have changed in this diff Show More