104 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
e9c570a821 Add comprehensive GitHub Copilot instructions with validated build processes
Co-authored-by: snow-jallen <42281341+snow-jallen@users.noreply.github.com>
2025-09-10 18:09:06 +00:00
copilot-swe-agent[bot]
3f44e54c9b Initial plan 2025-09-10 17:54:50 +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
554 changed files with 13570 additions and 24424 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
run.sh
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

152
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,152 @@
# Canvas Management Application
Canvas Management is a Next.js web application that provides a user interface for managing Canvas LMS courses, modules, assignments, quizzes, and pages. It features real-time file synchronization through WebSocket connections and supports Docker containerization.
Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
## Working Effectively
### Bootstrap, Build, and Test the Repository
- Install pnpm globally: `npm install -g pnpm`
- Install dependencies: `pnpm install --config.confirmModulesPurge=false` -- takes 25 seconds. NEVER CANCEL. Set timeout to 60+ seconds.
- Build the application: `pnpm run build` -- takes 47 seconds. NEVER CANCEL. Set timeout to 90+ minutes for CI environments.
- Run tests: `pnpm run test` -- takes 8 seconds. NEVER CANCEL. Set timeout to 30+ seconds.
- Run linting: `pnpm run lint` -- takes 13 seconds. NEVER CANCEL. Set timeout to 30+ seconds.
### Environment Setup
- Copy `.env.test` to `.env.local` for local development
- Set required environment variables:
- `STORAGE_DIRECTORY="./temp/storage"` - Directory for course data storage
- `NEXT_PUBLIC_ENABLE_FILE_SYNC=true` - Enable file synchronization features
- `CANVAS_TOKEN="your_canvas_api_token"` - Canvas API token (optional for local development)
- Create storage directory: `mkdir -p temp/storage`
- Create simple globalSettings.yml: `echo "courses: []" > globalSettings.yml`
### Run the Development Server
- **Development server without WebSocket**: `pnpm run devNoSocket` -- starts in 1.5 seconds
- **Development server with WebSocket**: `pnpm run dev` -- runs `pnpm install` then starts WebSocket server with Next.js
- Access the application at: `http://localhost:3000`
- WebSocket server enables real-time file watching and synchronization
### Run Production Build
- **Production server**: `pnpm run start` -- starts production WebSocket server with Next.js
- Requires completing the build step first
### Docker Development
- **Local Docker build**: `./build.sh` -- takes 5+ minutes. NEVER CANCEL. Set timeout to 120+ minutes.
- **Development with Docker**: Use `run.sh` or `docker-compose.dev.yml`
- **Production with Docker**: Use `docker-compose.yml`
- Note: Docker builds may fail in sandboxed environments due to certificate issues
## Validation
### Manual Testing Scenarios
- ALWAYS run through complete end-to-end scenarios after making changes
- Start the development server and verify the main page loads with "Add New Course" and "Add Existing Course" buttons
- Test course creation workflow (will fail without valid Canvas API token, which is expected)
- Verify WebSocket connectivity shows "Socket connected successfully" in browser console
- Check that file system watching works when NEXT_PUBLIC_ENABLE_FILE_SYNC=true
### Required Validation Steps
- Always run `pnpm run lint` before committing changes
- Always run `pnpm run test` to ensure tests pass
- Always run `pnpm run build` to verify production build works
- Test both `devNoSocket` and `dev` modes to ensure WebSocket functionality works
## Common Tasks
### Build System Requirements
- **Node.js**: Version 20+ (validated with v20.19.5)
- **Package Manager**: pnpm v10+ (install with `npm install -g pnpm`)
- **Build Tool**: Next.js 15.3.5 with React 19
### Technology Stack
- **Frontend**: Next.js 15.3.5, React 19, TypeScript 5.8.3
- **Styling**: Tailwind CSS 4.1.11
- **Testing**: Vitest 3.2.4 with @testing-library/react
- **Linting**: ESLint 9.31.0 with Next.js and TypeScript configs
- **Real-time**: WebSocket with Socket.IO for file watching
- **API**: tRPC for type-safe API calls
- **Canvas Integration**: Axios for Canvas LMS API communication
### Key Project Structure
```
src/
├── app/ # Next.js app router pages
├── components/ # Reusable React components
├── features/ # Feature-specific code (local file handling, Canvas API)
├── services/ # Utility services and API helpers
└── websocket.js # WebSocket server for file watching
```
### Development Workflow Tips
- Use `pnpm dev` for full development with file watching
- Use `pnpm devNoSocket` for faster startup when WebSocket features not needed
- Monitor console for WebSocket connection status and Canvas API errors
- Canvas API errors are expected without valid CANVAS_TOKEN
- File sync requires NEXT_PUBLIC_ENABLE_FILE_SYNC=true
### Storage Configuration
- Course data stored in markdown files within storage directory
- `globalSettings.yml` controls which courses appear in UI
- Each course requires settings.yml file in its directory
- Images supported via volume mounts to `/app/public/images/`
### Frequent Command Outputs
#### Repository Root Structure
```
.
├── README.md
├── package.json
├── pnpm-lock.yaml
├── Dockerfile
├── docker-compose.yml
├── docker-compose.dev.yml
├── build.sh
├── run.sh
├── globalSettings.yml
├── eslint.config.mjs
├── vitest.config.ts
├── tsconfig.json
├── tailwind.config.ts
├── next.config.mjs
├── postcss.config.mjs
└── src/
```
#### Key Package Scripts
```json
{
"dev": "pnpm install --config.confirmModulesPurge=false && node src/websocket.js",
"devNoSocket": "next dev",
"build": "next build",
"start": "NODE_ENV=production node src/websocket.js",
"lint": "eslint . --config eslint.config.mjs && tsc && next lint",
"test": "vitest"
}
```
## Critical Timing Information
- **NEVER CANCEL** any build or test commands - wait for completion
- **Dependency Installation**: ~25 seconds (set 60+ second timeout)
- **Production Build**: ~47 seconds (set 90+ minute timeout for CI)
- **Test Suite**: ~8 seconds (set 30+ second timeout)
- **Linting**: ~13 seconds (set 30+ second timeout)
- **Development Server Startup**: ~1.5 seconds
- **Docker Build**: 5+ minutes locally (set 120+ minute timeout)
## Error Handling
### Expected Errors During Development
- Canvas API connection errors without valid CANVAS_TOKEN
- Socket.IO 404 errors when running devNoSocket mode
- Docker build certificate issues in sandboxed environments
- Course settings file not found errors with empty storage directory
### Troubleshooting
- If WebSocket connection fails, check NEXT_PUBLIC_ENABLE_FILE_SYNC environment variable
- If build fails, ensure pnpm is installed globally
- If tests fail with storage errors, check STORAGE_DIRECTORY environment variable
- For Canvas integration, set valid CANVAS_TOKEN environment variable

52
.gitignore vendored
View File

@@ -1,8 +1,46 @@
obj/
bin/
.env
*.env
storage/
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.pnpm-store/
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 rm -rf /app/storage/*
ENV NEXT_PUBLIC_ENABLE_FILE_SYNC=true
RUN pnpm run build
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