Merge pull request 'Passage du jalon 1 au jalon 2' (#136) from dev into main

Reviewed-on: https://10.4.0.131/gitea/DI1-P4-E1/Webzine/pulls/136
This commit is contained in:
j.vetu
2026-03-28 11:27:40 +01:00
103 changed files with 3613 additions and 7926 deletions

View File

@@ -18,7 +18,7 @@
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE

403
.editorconfig Normal file
View File

@@ -0,0 +1,403 @@
root = true
# All files
[*]
indent_style = space
# Xml files
[*.xml]
indent_size = 2
# Xml project files
[*.{csproj,fsproj,vbproj,proj,slnx}]
indent_size = 2
# Xml config files
[*.{props,targets,config,nuspec}]
indent_size = 2
[*.json]
indent_size = 2
# C# files
[*.cs]
#### Core EditorConfig Options ####
# Indentation and spacing
indent_size = 4
tab_width = 4
# New line preferences
insert_final_newline = false
#### .NET Coding Conventions ####
[*.{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_namespace_match_folder = 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_collection_expression = when_types_loosely_match: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_foreach_explicit_cast_in_source = when_strongly_typed: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_extended_property_pattern = 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_anonymous_function = true:suggestion
csharp_prefer_static_local_function = true:warning
csharp_preferred_modifier_order = public,private,protected,internal,file,const,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion
csharp_style_prefer_readonly_struct = true:suggestion
csharp_style_prefer_readonly_struct_member = true:suggestion
# Code-block preferences
csharp_prefer_braces = true:silent
csharp_prefer_simple_using_statement = true:suggestion
csharp_style_namespace_declarations = file_scoped:suggestion
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_prefer_primary_constructors = true:suggestion
csharp_style_prefer_top_level_statements = true:silent
# Expression-level preferences
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_local_over_anonymous_function = true:suggestion
csharp_style_prefer_null_check_over_type_check = true:suggestion
csharp_style_prefer_range_operator = true:suggestion
csharp_style_prefer_tuple_swap = true:suggestion
csharp_style_prefer_utf8_string_literals = 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__camelcase.severity = suggestion
dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields
dotnet_naming_rule.private_fields_should_be__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.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
dotnet_diagnostic.SA1623.severity = none
dotnet_diagnostic.SA1624.severity = none
dotnet_diagnostic.SA1600.severity = suggestion
[*.cs]
# 1. Tell the .NET Formatter to stop injecting a header
file_header_template = unset
# 2. Tell StyleCop to stop requiring a file header (SA1633)
dotnet_diagnostic.SA1633.severity = none
# 3. Tell the IDE to stop requiring a file header (IDE0073)
dotnet_diagnostic.IDE0073.severity = none

View File

@@ -0,0 +1,150 @@
name: PR Endpoint Performance Check
on: [pull_request]
jobs:
endpoint-performance-check:
name: Test All Endpoints (< 1s)
runs-on: ubuntu-latest
steps:
- name: Checkout PR branch
uses: actions/checkout@v4
- name: Configure appsettings for CI
run: |
# Find the appsettings.json file
APPSETTINGS_PATH="Webzine.WebApplication/appsettings.json"
# Backup original file
cp $APPSETTINGS_PATH $APPSETTINGS_PATH.bak
# Use jq to modify the JSON
jq '.UseDatabase = true | .IsSQLite = true' $APPSETTINGS_PATH > $APPSETTINGS_PATH.tmp
mv $APPSETTINGS_PATH.tmp $APPSETTINGS_PATH
echo "Updated appsettings.json:"
cat $APPSETTINGS_PATH
- name: Setup .NET 10
uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"
- name: Restore dependencies
run: dotnet restore Webzine.sln
- name: Build solution
run: dotnet build Webzine.sln --no-restore --configuration Release
- name: Run unit tests
run: |
dotnet test Webzine.Entity.Tests/Webzine.Entity.Tests.csproj \
--no-build \
--configuration Release
- name: Start Webzine application
run: |
dotnet run \
--project Webzine.WebApplication/Webzine.WebApplication.csproj \
--configuration Release \
--no-build \
-- --urls "http://localhost:5038" &
echo "Attente du démarrage de l'application..."
timeout 60 bash -c '
until curl -sf http://localhost:5038 > /dev/null 2>&1; do
sleep 1
done
'
echo "Application prête!"
- name: Test endpoint response times
id: perf_test
run: |
chmod +x scripts/test-endpoints.sh
bash scripts/test-endpoints.sh http://localhost:5038 1000 2>&1 | tee /tmp/webzine_endpoint_output.txt
EXIT_CODE=${PIPESTATUS[0]}
# Count failures
FAIL_COUNT=$(grep -cE "^\[ÉCHEC\]" /tmp/webzine_endpoint_output.txt 2>/dev/null || echo 0)
SLOW_COUNT=$(grep -cE "^\[LENT\]" /tmp/webzine_endpoint_output.txt 2>/dev/null || echo 0)
echo "failed=$FAIL_COUNT" >> "$GITHUB_OUTPUT"
echo "slow=$SLOW_COUNT" >> "$GITHUB_OUTPUT"
# Échoue sil y a DES problèmes (échecs OU lents)
if [ $FAIL_COUNT -gt 0 ] || [ $SLOW_COUNT -gt 0 ]; then
echo "❌ Performance check failed: $FAIL_COUNT endpoint(s) failed, $SLOW_COUNT endpoint(s) exceeded threshold (1000ms)"
exit 1
fi
echo "✅ All endpoints passed performance check (< 1000ms)"
- name: Post performance report as PR comment
if: always() # Always post comment, even on failure
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_SERVER_URL: ${{ gitea.server_url }}
REPO: ${{ gitea.repository }}
PR_NUMBER: ${{ gitea.event.pull_request.number }}
run: |
RAW_REPORT=$(cat /tmp/webzine_endpoint_output.txt 2>/dev/null || echo "Aucune sortie capturée.")
FAILED_COUNT="${{ steps.perf_test.outputs.failed }}"
SLOW_COUNT="${{ steps.perf_test.outputs.slow }}"
# Determine if the check passed or failed
if [ "$FAILED_COUNT" -gt 0 ] || [ "$SLOW_COUNT" -gt 0 ]; then
STATUS_HEADER="❌ **PERFORMANCE CHECK FAILED**"
STATUS_ICON="❌"
else
STATUS_HEADER="✅ **PERFORMANCE CHECK PASSED**"
STATUS_ICON="✅"
fi
# Convert report to Markdown with colors
CLEAN_REPORT=$(echo "$RAW_REPORT" | sed 's/\x1b\[[0-9;]*m//g')
# Generate formatted report
FORMATTED_REPORT=$(echo "$CLEAN_REPORT" | sed \
-e 's/^\[OK\] /✅ /' \
-e 's/^\[LENT\] /⚠️ /' \
-e 's/^\[ÉCHEC\] /❌ /' \
-e 's/^→ \[LENT\] / • ⚠️ /' \
-e 's/^→ \[ÉCHEC\] / • ❌ /' \
-e 's/^→ / • /' \
-e 's/^── \(.*\)$/\n### ── \1/' \
-e 's/^\(Total.*\)$/\n**\1**/' \
-e 's/^\(Réussis.*\)$/**✅ \1**/' \
-e 's/^\(Échecs.*\)$/**❌ \1**/' \
-e 's/^\(⚠️ ENDPOINTS LENTS.*\)$/\n### \1/' \
-e 's/^\(❌ ENDPOINTS EN ÉCHEC.*\)$/\n### \1/' \
-e 's/^\(La PR doit.*\)$/\n**❌ \1**/')
BODY=$(cat <<EOF
$STATUS_HEADER
$FORMATTED_REPORT
---
**Seuil**: 1000ms
**Statistiques**:
- ✅ Endpoints rapides: \$(grep -c "^\[OK\]" /tmp/webzine_endpoint_output.txt 2>/dev/null || echo 0)
- ⚠️ Endpoints lents (>1000ms): $SLOW_COUNT
- ❌ Endpoints en échec: $FAILED_COUNT
**Vérifié par**: Workflow PR Endpoint Performance
EOF
)
curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg body "$BODY" '{body: $body}')" \
"$GITEA_SERVER_URL/api/v1/repos/$REPO/issues/$PR_NUMBER/comments"
- name: Fail job if performance issues detected
if: steps.perf_test.outputs.failed > 0 || steps.perf_test.outputs.slow > 0
run: |
echo "❌ Job failed due to performance issues"
exit 1

View File

@@ -2,8 +2,14 @@
"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
"settings": {
"documentationRules": {
"companyName": " Equipe 1 - ",
"documentationCulture": "fr-FR"
}
"documentInterfaces": false,
"documentInternalElements": false,
"documentExposedElements": false
},
"maintainabilityRules": {
"settings": {
"commonWords": [ "Obtient", "définit" ]
}
}
}
}

View File

@@ -8,7 +8,10 @@
<ItemGroup>
<PackageReference Include="Faker.Net" Version="2.0.163" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" />
<PackageReference Include="NLog" Version="6.1.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -21,4 +24,8 @@
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Webzine.Entity\Webzine.Entity.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,111 @@
// <copyright file="WebzineDbContext.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
/// <summary>
/// Définit le contexte de la base de données.
/// </summary>
namespace Webzine.EntitiesContext
{
using Microsoft.EntityFrameworkCore;
using Webzine.Entity;
public class WebzineDbContext : DbContext
{
/// <summary>
/// Initializes a new instance of the <see cref="WebzineDbContext"/> class.
/// </summary>
/// <param name="options">Options.</param>
public WebzineDbContext(DbContextOptions<WebzineDbContext> options)
: base(options)
{
}
/// <summary>
/// Obtient ou définit les artistes de la base.
/// </summary>
public DbSet<Artiste> Artistes => this.Set<Artiste>();
/// <summary>
/// Obtient ou définit les styles de la base.
/// </summary>
public DbSet<Style> Styles => this.Set<Style>();
/// <summary>
/// Obtient ou définit les titres de la base.
/// </summary>
public DbSet<Titre> Titres => this.Set<Titre>();
/// <summary>
/// Obtient ou définit les commentaires de la base.
/// </summary>
public DbSet<Commentaire> Commentaires => this.Set<Commentaire>();
/// <inheritdoc/>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Artiste>(entity =>
{
entity.ToTable("Artistes");
entity.HasKey(a => a.IdArtiste)
.HasName("PK_Artiste");
entity.HasMany(a => a.Titres)
.WithOne(t => t.Artiste)
.HasForeignKey(t => t.IdArtiste)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<Style>(entity =>
{
entity.ToTable("Styles");
entity.HasKey(s => s.IdStyle)
.HasName("PK_Style");
});
modelBuilder.Entity<Commentaire>(entity =>
{
entity.ToTable("Commentaires");
entity.HasKey(c => c.IdCommentaire)
.HasName("PK_Commentaire");
entity.HasOne(c => c.Titre)
.WithMany(t => t.Commentaires)
.HasForeignKey(c => c.IdTitre)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<Titre>(entity =>
{
entity.ToTable("Titres");
entity.HasKey(t => t.IdTitre)
.HasName("PK_Titre");
entity.HasMany(t => t.Styles)
.WithMany(s => s.Titres)
.UsingEntity<Dictionary<string, object>>(
"TitreStyle",
right => right.HasOne<Style>()
.WithMany()
.HasForeignKey("IdStyle")
.OnDelete(DeleteBehavior.Cascade),
left => left.HasOne<Titre>()
.WithMany()
.HasForeignKey("IdTitre")
.OnDelete(DeleteBehavior.Cascade),
join =>
{
join.HasKey("IdTitre", "IdStyle");
join.ToTable("TitreStyle");
});
});
}
}
}

View File

@@ -106,4 +106,4 @@ namespace Webzine.Entity.Tests
Common.HasProperty(typeof(Commentaire), nameof(Commentaire.Titre));
}
}
}
}

View File

@@ -3,6 +3,7 @@ namespace Webzine.Entity.Tests
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
/// <summary>
@@ -13,8 +14,8 @@ namespace Webzine.Entity.Tests
/// <summary>
/// Vérifie que l'entité possède bien la propriété passée en paramètre.
/// </summary>
/// <param name="typeObjet">type de l'entité</param>
/// <param name="nomPropriete">nom de la propriété de l'entité</param>
/// <param name="typeObjet">type de l'entité.</param>
/// <param name="nomPropriete">nom de la propriété de l'entité.</param>
public static void HasProperty(Type typeObjet, string nomPropriete)
{
var property = typeObjet.GetProperty(nomPropriete);
@@ -24,9 +25,9 @@ namespace Webzine.Entity.Tests
/// <summary>
/// Vérifie que l'attribut de l'entité a l'annotation [Display(Name = "xxx")] avec la valeur attendue.
/// </summary>
/// <param name="typeObjet">type de l'entité</param>
/// <param name="nomPropriete">nom de la propriété de l'entité</param>
/// <param name="chaineAttendue">valeur attendue pour l'affichage de cette propriété</param>
/// <param name="typeObjet">type de l'entité.</param>
/// <param name="nomPropriete">nom de la propriété de l'entité.</param>
/// <param name="chaineAttendue">valeur attendue pour l'affichage de cette propriété.</param>
public static void AttributDisplay(Type typeObjet, string nomPropriete, string chaineAttendue)
{
var property = typeObjet.GetProperty(nomPropriete);
@@ -38,9 +39,9 @@ namespace Webzine.Entity.Tests
/// <summary>
/// Vérifie que l'attribut de l'entité a l'annotation [MinLength(xx)] avec la longueur attendue.
/// </summary>
/// <param name="typeObjet">type de l'entité</param>
/// <param name="nomPropriete">nom de la propriété de l'entité</param>
/// <param name="max">longueur maximum</param>
/// <param name="typeObjet">type de l'entité.</param>
/// <param name="nomPropriete">nom de la propriété de l'entité.</param>
/// <param name="max">longueur maximum.</param>
public static void AttributLongueurMax(Type typeObjet, string nomPropriete, int max)
{
var property = typeObjet.GetProperty(nomPropriete);
@@ -52,9 +53,9 @@ namespace Webzine.Entity.Tests
/// <summary>
/// Vérifie que l'attribut de l'entité a l'annotation [MinLength(xx)] avec la longueur attendue.
/// </summary>
/// <param name="typeObjet">type de l'entité</param>
/// <param name="nomPropriete">nom de la propriété de l'entité</param>
/// <param name="min">longueur minimum</param>
/// <param name="typeObjet">type de l'entité.</param>
/// <param name="nomPropriete">nom de la propriété de l'entité.</param>
/// <param name="min">longueur minimum.</param>
public static void AttributLongueurMin(Type typeObjet, string nomPropriete, int min)
{
var property = typeObjet.GetProperty(nomPropriete);
@@ -66,8 +67,8 @@ namespace Webzine.Entity.Tests
/// <summary>
/// Vérifie que l'attribut de l'entité a l'annotation [Required].
/// </summary>
/// <param name="typeObjet">type de l'entité</param>
/// <param name="nomPropriete">nom de la propriété de l'entité</param>
/// <param name="typeObjet">type de l'entité.</param>
/// <param name="nomPropriete">nom de la propriété de l'entité.</param>
public static void AttributRequis(Type typeObjet, string nomPropriete)
{
var property = typeObjet.GetProperty(nomPropriete);
@@ -78,8 +79,8 @@ namespace Webzine.Entity.Tests
/// <summary>
/// Vérifie que l'attribut de l'entité n'a pas l'annotation [Url].
/// </summary>
/// <param name="typeObjet">type de l'entité</param>
/// <param name="nomPropriete">nom de la propriété de l'entité</param>
/// <param name="typeObjet">type de l'entité.</param>
/// <param name="nomPropriete">nom de la propriété de l'entité.</param>
public static void AttributHasNotUrlValidation(Type typeObjet, string nomPropriete)
{
var property = typeObjet.GetProperty(nomPropriete);
@@ -87,4 +88,4 @@ namespace Webzine.Entity.Tests
Assert.IsNull(annotation, "La propriété '" + nomPropriete + "' ne doit pas être une URL obligatoirement. Retirez l'annotation Url.");
}
}
}
}

View File

@@ -238,4 +238,4 @@ namespace Webzine.Entity.Tests
Common.AttributHasNotUrlValidation(typeof(Titre), nameof(Titre.UrlJaquette));
}
}
}
}

View File

@@ -1,11 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
using System.Timers;
namespace Webzine.Entity
namespace Webzine.Entity
{
using System.ComponentModel.DataAnnotations;
/// <summary>
/// Classe représentant un artiste.
/// Lien avec l'entité <see cref="Titre"/> : un artiste peut avoir plusieurs titres, mais un titre n'a qu'un seul artiste.
@@ -36,4 +32,4 @@ namespace Webzine.Entity
/// </summary>
public List<Titre> Titres { get; set; }
}
}
}

View File

@@ -1,10 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
namespace Webzine.Entity
namespace Webzine.Entity
{
using System.ComponentModel.DataAnnotations;
/// <summary>
/// Classe représentant un commentaire laissé par un utilisateur sur un titre.
/// Lien avec l'entité <see cref="Titre"/> : un titre peut avoir plusieurs commentaires, mais un commentaire n'a qu'un seul titre.
@@ -51,4 +48,4 @@ namespace Webzine.Entity
/// </summary>
public Titre Titre { get; set; }
}
}
}

View File

@@ -1,53 +0,0 @@
using Bogus;
namespace Webzine.Entity.Fixtures
{
/// <summary>
/// Factory pour générer des artistes avec des titres associés, à l'aide de la bibliothèque Bogus.
/// </summary>
public class ArtisteFactory
{
/// <summary>
/// Récupère un artiste par son nom, en générant des données fictives pour ses titres associés.
/// </summary>
/// <param name="nom">Le nom de l'artiste à générer.</param>
/// <returns>Un objet Artiste avec des titres associés générés de manière aléatoire.</returns>
public static Artiste SeedArtisteByName(string nom)
{
// On définit nos albums "bouchonnés"
var albumsData = new[]
{
new { Nom = "Bohemian Rhapsody", Image = "https://upload.wikimedia.org/wikipedia/en/9/9f/Bohemian_Rhapsody.png" },
new { Nom = "Born This Way", Image = "https://static.wikia.nocookie.net/ladygaga/images/2/2d/BornThisWay-DeluxeEdition.jpg/revision/latest/scale-to-width-down/3500?cb=20111120030308" }
};
var faker = new Bogus.Faker("fr");
var tousLesTitres = new List<Titre>();
// Pour chaque album, on génère un paquet de titres
foreach (var album in albumsData)
{
var nombreDeTitres = faker.Random.Number(3, 6);
var titresDeLalbum = new Faker<Titre>("fr")
.RuleFor(t => t.IdTitre, f => f.IndexFaker + 1 + tousLesTitres.Count)
.RuleFor(t => t.UrlJaquette, _ => album.Image)
.RuleFor(t => t.Album, _ => album.Nom)
.RuleFor(t => t.Duree, f => f.Random.Number(90, 180))
.RuleFor(t => t.Libelle, f => f.Music.Genre() + " - " + f.Commerce.ProductName())
.Generate(nombreDeTitres);
tousLesTitres.AddRange(titresDeLalbum);
}
// On crée l'artiste final
var artisteFaker = new Faker<Artiste>("fr")
.RuleFor(a => a.IdArtiste, f => f.IndexFaker + 1)
.RuleFor(a => a.Nom, _ => nom)
.RuleFor(a => a.Biographie, f => f.Lorem.Paragraphs(2))
.RuleFor(a => a.Titres, _ => tousLesTitres);
return artisteFaker.Generate();
}
}
}

View File

@@ -1,192 +0,0 @@
namespace Webzine.Entity.Fixtures;
using Entity;
using Faker;
using System;
using System.Collections.Generic;
using System.Linq;
public class DataFactory
{
/// <summary>
/// Dictionnaire contenant des données de musique réelles pour les titres générés.
/// </summary>
public Dictionary<string, (string TrackUrl, string ImageUrl)> RealMusicData { get; set; }
= new Dictionary<string, (string TrackUrl, string ImageUrl)>
{
{ "juliana_chahayed_1", ("https://open.spotify.com/intl-fr/track/0qYLUdJQMhrCFA9dNZGcnm?si=b4fd45727a354a31",
"https://i.scdn.co/image/ab67616d0000b2738e8e7b8f8f8f8f8f8f8f8f8") },
{ "mister_v_1", ("https://youtu.be/JeqUw7sGUK8?si=FnmFR2EgkVY6MhqQ",
"https://img.youtube.com/vi/JeqUw7sGUK8/maxresdefault.jpg") },
{ "compagnie_creole_1", ("https://youtu.be/wfxt1SGWAI8",
"https://img.youtube.com/vi/wfxt1SGWAI8/maxresdefault.jpg") },
{ "femto_1", ("https://open.spotify.com/intl-fr/track/0qYLUdJQMhrCFA9dNZGcnm?si=b4fd45727a354a31",
"https://i.scdn.co/image/ab67616d0000b2738e8e7b8f8f8f8f8f8f8f8f8") },
{ "chat_noir_1", ("https://youtu.be/OTi4-q-_Tj0?si=SNnLd-6Y893nL5Au",
"https://img.youtube.com/vi/OTi4-q-_Tj0/maxresdefault.jpg") },
{ "chat_noir_2", ("https://youtu.be/X-rJ01EyiAI?si=gH9m_U8oXI35OgWu",
"https://img.youtube.com/vi/X-rJ01EyiAI/maxresdefault.jpg") },
{ "chat_noir_3", ("https://youtu.be/7lIM0wSx7kQ?si=S-RAsLzd4SiCQhE4",
"https://img.youtube.com/vi/7lIM0wSx7kQ/maxresdefault.jpg") },
{ "chat_noir_4", ("https://youtu.be/dbxyKR1P8vA?si=aRzdYBhwvKptV8Ff",
"https://img.youtube.com/vi/dbxyKR1P8vA/maxresdefault.jpg") },
{ "chat_noir_5", ("https://youtu.be/DDHvKo5NnII?si=NFRkdVQL2mELP0yn",
"https://img.youtube.com/vi/DDHvKo5NnII/maxresdefault.jpg") },
{ "chat_noir_6", ("https://youtu.be/J9LgHNf2Qy0?si=YGO1ggiLkefa9901",
"https://img.youtube.com/vi/J9LgHNf2Qy0/maxresdefault.jpg") },
{ "chat_noir_7", ("https://youtu.be/oadhHk2xs6c?si=mbnJCA6SGsoYXnUK",
"https://img.youtube.com/vi/oadhHk2xs6c/maxresdefault.jpg") },
{ "chat_noir_8", ("https://youtu.be/6K1zCgkBaoE?si=quq9vQLJ-AmzjRJJ",
"https://img.youtube.com/vi/6K1zCgkBaoE/maxresdefault.jpg") },
{ "german_rapper_1", ("https://www.youtube.com/watch?v=DWpg71HJt24",
"https://img.youtube.com/vi/DWpg71HJt24/maxresdefault.jpg") },
{ "nizard_1", ("https://youtu.be/1fjA68k8DAU?si=2PuZSquVQGvfmQkZ",
"https://img.youtube.com/vi/1fjA68k8DAU/maxresdefault.jpg") },
};
/// <summary>
/// Génère une liste d'artistes de musique en utilisant la bibliothèque Faker pour créer des noms d'artistes et des biographies réalistes.
/// </summary>
/// <param name="count">Nombre d'artistes à générer</param>
/// <returns>Liste d'artistes de musique générés</returns>
public List<Artiste> GenerateArtists(int count)
{
var artists = new List<Artiste>();
for (int i = 0; i < count; i++)
{
artists.Add(new Artiste
{
IdArtiste = i + 1,
Nom = Name.FullName(),
Biographie = Lorem.Paragraph(),
Titres = new List<Titre>()
});
}
return artists;
}
/// <summary>
/// Génère une liste de styles de musique à partir d'une liste prédéfinie de noms de styles.
/// </summary>
/// <param name="count">Nombre de styles à générer (maximum 15, car il y a 15 styles prédéfinis)</param>
/// <returns>Liste de styles de musique générés</returns>
public List<Style> GenerateStyles(int count)
{
var styleNames = new[] { "Rock", "Pop", "Rap", "Électro", "Jazz", "Classique", "Reggae", "Blues", "Metal", "Folk", "Chanson française", "Hip-Hop", "R&B", "Soul", "Punk" };
var styles = new List<Style>();
for (int i = 0; i < Math.Min(count, styleNames.Length); i++)
{
styles.Add(new Style
{
IdStyle = i + 1,
Libelle = styleNames[i],
Titres = new List<Titre>(),
});
}
return styles;
}
/// <summary>
/// Génère une liste de titres de musique en utilisant des données de musique réelles pour les URL d'écoute et les jaquettes.
/// </summary>
/// <param name="count">Nombre de titres à générer</param>
/// <param name="artists">Liste des artistes à associer aux titres générés</param>
/// <param name="styles">Liste des styles à associer aux titres générés</param>
/// <returns>Liste de titres de musique générés</returns>
public List<Titre> GenerateTitres(int count, List<Artiste> artists, List<Style> styles)
{
var titres = new List<Titre>();
var random = new Random();
var musicItems = RealMusicData.Values.ToList();
for (int i = 0; i < count; i++)
{
var randomMusic = musicItems[random.Next(musicItems.Count)];
var randomArtist = artists[random.Next(artists.Count)];
int numberOfStyles = random.Next(1, 4);
var selectedStyles = styles.OrderBy(x => random.Next()).Take(numberOfStyles).ToList();
var titre = new Titre
{
IdTitre = i + 1,
IdArtiste = randomArtist.IdArtiste,
Artiste = randomArtist,
Libelle = string.Join(" ", Lorem.Words(3)),
Chronique = Lorem.Paragraph(),
DateCreation = DateTime.Now.AddDays(-random.Next(1, 365)),
Duree = random.Next(120, 360), // 2 à 6 minutes en secondes
DateSortie = DateTime.Now.AddYears(-random.Next(0, 5)).AddDays(-random.Next(1, 365)),
UrlJaquette = randomMusic.ImageUrl,
UrlEcoute = randomMusic.TrackUrl,
NbLectures = random.Next(1000, 1000000),
NbLikes = random.Next(100, 100000), // Likes entre 100 et 100 000
Album = $"Album {random.Next(1, 10)}",
Commentaires = new List<Commentaire>(),
Styles = new List<Style>(),
};
foreach (var style in selectedStyles)
{
titre.Styles.Add(style);
if (!style.Titres.Contains(titre))
{
style.Titres.Add(titre);
}
}
titres.Add(titre);
randomArtist.Titres.Add(titres.Last());
}
return titres;
}
/// <summary>
/// Génère une liste de commentaires pour les titres de musique générés, en associant chaque commentaire à un titre aléatoire.
/// </summary>
/// <param name="count">Nombre de commentaires à générer</param>
/// <param name="titres">Liste des titres de musique à associer aux commentaires générés</param>
/// <returns>Liste de commentaires générés</returns>
public List<Commentaire> GenerateCommentaires(int count, List<Titre> titres)
{
var commentaires = new List<Commentaire>();
var random = new Random();
for (int i = 0; i < count; i++)
{
var randomTitre = titres[random.Next(titres.Count)];
commentaires.Add(new Commentaire
{
IdCommentaire = i + 1,
Contenu = Lorem.Paragraph(),
Auteur = Name.FullName(),
DateCreation = DateTime.Now.AddDays(-random.Next(1, 30)), // Commentaires créés dans les 30 derniers jours
IdTitre = randomTitre.IdTitre,
Titre = randomTitre
});
randomTitre.Commentaires.Add(commentaires.Last());
}
return commentaires;
}
}

View File

@@ -1,6 +1,169 @@
namespace Webzine.Entity.Fixtures;
// <copyright file="SeedDataLocal.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Webzine.Entity.Fixtures;
using Bogus;
/// <summary>
/// Classe pour générer des données de test locales, telles que des listes d'artistes, de
/// titres, de styles, de commentaires et d'albums, afin de faciliter le peuplement de la base de données
/// et les données lcoales.
/// </summary>
public class SeedDataLocal
{
/// <summary>
/// Génére une liste d'artiste.
/// </summary>
/// <param name="nombre">Nombre d'artiste.</param>
/// <returns>Liste d'artiste.</returns>
public static List<Artiste> GenererListeArtiste(int nombre)
{
int idStart = 1;
Faker<Artiste> artistes = new Faker<Artiste>("fr")
.RuleFor(a => a.IdArtiste, f => f.IndexFaker + idStart) // Créé les id des artistes de manière incrémentale
.RuleFor(a => a.Nom, f => f.Person.FullName)
.RuleFor(a => a.Biographie, f => f.Lorem.Paragraph(2));
return artistes.Generate(nombre);
}
/// <summary>
/// Génére une liste de titres.
/// </summary>
/// <param name="count">Nombre de titres à créer.</param>
/// <param name="artistes">Liste d'artistes.</param>
/// <param name="styles">Liste de styles.</param>
/// <param name="albums">Liste d'albums.</param>
/// <returns>Liste de titres.</returns>
public static List<Titre> GenererListeTitre(
int count,
List<Artiste> artistes,
List<Style> styles,
List<string> albums)
{
Random random = new Random();
int idStart = 1;
Faker<Titre> faker = new Faker<Titre>("fr")
.RuleFor(a => a.IdTitre, f => f.IndexFaker + idStart)
.RuleFor(t => t.Libelle, f => f.Lorem.Sentence(3).Replace(".", string.Empty))
.RuleFor(t => t.Chronique, f => f.Lorem.Paragraphs(3))
.RuleFor(t => t.DateCreation, f => DateTime.SpecifyKind(f.Date.Recent(120), DateTimeKind.Utc))
.RuleFor(t => t.DateSortie, (f, t) => DateTime.SpecifyKind(f.Date.Past(10, t.DateCreation), DateTimeKind.Utc))
.RuleFor(t => t.Duree, f => f.Random.Int(120, 420))
.RuleFor(t => t.UrlJaquette, f => $"https://picsum.photos/seed/{Guid.NewGuid():N}/640/640")
.RuleFor(t => t.UrlEcoute, f => $"https://example.com/listen/{Guid.NewGuid():N}")
.RuleFor(t => t.NbLectures, f => f.Random.Int(0, 5000))
.RuleFor(t => t.NbLikes, f => f.Random.Int(0, 1000))
.RuleFor(t => t.Album, f => f.PickRandom(albums))
.RuleFor(t => t.Artiste, f => f.PickRandom(artistes))
.RuleFor(t => t.Commentaires, f => new List<Commentaire>());
List<Titre> titres = faker.Generate(count);
foreach (Titre titre in titres)
{
int nbStyles = random.Next(1, 4);
titre.Styles = styles
.OrderBy(_ => Guid.NewGuid())
.Take(nbStyles)
.ToList();
titre.IdArtiste = titre.Artiste.IdArtiste;
}
return titres;
}
/// <summary>
/// Génére une liste de styles pour seeder la base
/// de données.
/// </summary>
/// <param name="minCount">Le nombre minimum de styles pouvant être créés.</params>
/// <param name="maxCount">Le nombre maximun de styles pouvant être créés.</params>
/// <returns>Liste de styles.</returns>
public static List<Style> GenererListeStyle(int minCount = 15, int maxCount = 20)
{
List<string> libelles = new List<string>
{
"Pop",
"Rock",
"Jazz",
"Blues",
"Hip-Hop",
"Rap",
"Electro",
"Techno",
"House",
"Metal",
"Funk",
"Soul",
"R&B",
"Classique",
"Reggae",
"Punk",
"Folk",
"Disco",
"Ambient",
"Indie",
};
Random random = new Random();
int count = random.Next(minCount, maxCount + 1);
return libelles
.Take(count)
.Select((libelle, index) => new Style
{
IdStyle = index + 1,
Libelle = libelle,
})
.ToList();
}
/// <summary>
/// Génére une liste de commentaires pour seeder la base
/// de données.
/// </summary>
/// <param name="titre">Titre.</param>
/// <param name="min">Le nombre minimum de commentaires pouvant être créés. La valeur par défaut est 0.</params>
/// <param name="max">Le nombre maximun de commentaires pouvant être créés. La valeur par défaut est 5.</params>
/// <returns>Liste de commentaire.</returns>
public static List<Commentaire> GenererListeCommentaire(Titre titre, int min = 0, int max = 5, int idStart = 1)
{
Random random = new Random();
int count = random.Next(min, max + 1);
Faker<Commentaire> faker = new Faker<Commentaire>("fr")
.RuleFor(a => a.IdCommentaire, f => f.IndexFaker + idStart)
.RuleFor(c => c.Auteur, f => f.Internet.UserName())
.RuleFor(c => c.Contenu, f => f.Lorem.Sentences(2))
.RuleFor(c => c.DateCreation, f => DateTime.SpecifyKind(f.Date.Recent(60), DateTimeKind.Utc))
.RuleFor(c => c.Titre, _ => titre)
.RuleFor(c => c.IdTitre, _ => titre.IdTitre);
return faker.Generate(count);
}
/// <summary>
/// Génére une liste d'albums pour seeder la base
/// de données.
/// </summary>
/// <param name="nombre">Le nombre d'albums à générer.</param>
/// <returns>Liste d'albums.</returns>
public static List<string> GenererListeAlbums(int nombre)
{
Faker faker = new Faker("fr");
HashSet<string> albums = new HashSet<string>();
while (albums.Count < nombre)
{
albums.Add(faker.Company.CatchPhrase());
}
return albums.ToList();
}
}

View File

@@ -1,6 +1,5 @@
namespace Webzine.EntitiesContext;
namespace Webzine.Entity.Fixtures;
public class SeedDataSpotify
{
}

View File

@@ -1,76 +0,0 @@
using System;
using System.Collections.Generic;
using Faker;
using Webzine.Entity;
namespace Webzine.Repository.Fake
{
/// <summary>
/// Classe de fabrique pour générer des données factices (fake data) pour les entités Artiste et Titre.
/// </summary>
public static class FakeDataFactory
{
//https://cdn-images.dzcdn.net/images/cover/311bba0fc112d15f72c8b5a65f0456c1/1900x1900-000000-80-0-0.jpg",
/// <summary>
/// Génère une liste d'artistes avec des données factices, incluant des titres associés à chaque artiste.
/// </summary>
/// <param name="count">Le nombre d'artistes à générer. Par défaut, 10 artistes seront générés.</param>
/// <returns>Une liste d'objets Artiste avec des titres associés, générés de manière aléatoire.</returns>
public static List<Artiste> GetArtistes(int count = 10)
{
var artistes = new List<Artiste>();
for (int i = 1; i <= count; i++)
{
artistes.Add(new Artiste
{
IdArtiste = i,
Nom = Name.FullName(),
Biographie = Lorem.Paragraph(),
Titres = new List<Titre>()
});
}
return artistes;
}
/// <summary>
/// Génère une liste de titres avec des données factices, en associant chaque titre à un artiste existant.
/// </summary>
/// <param name="count">Le nombre de titres à générer. Par défaut, 40 titres seront générés.</param>
/// <returns>Une liste d'objets Titre avec des données factices, associés à des artistes générés de manière aléatoire.</returns>
public static List<Titre> GetTitres(int count = 40)
{
var artistes = GetArtistes();
var titres = new List<Titre>();
for (int i = 1; i <= count; i++)
{
var artiste = artistes[RandomNumber.Next(0, artistes.Count - 1)];
var titre = new Titre
{
IdTitre = i,
IdArtiste = artiste.IdArtiste,
Artiste = artiste,
Libelle = Lorem.Sentence(3),
Chronique = Lorem.Paragraph(),
DateCreation = DateTime.Now.AddDays(-RandomNumber.Next(1, 100)),
DateSortie = DateTime.Now.AddYears(-RandomNumber.Next(1, 20)),
Duree = RandomNumber.Next(120, 420),
UrlJaquette = "https://picsum.photos/300",
UrlEcoute = Internet.Url(),
NbLectures = RandomNumber.Next(0, 500),
NbLikes = RandomNumber.Next(0, 200),
Album = Lorem.Sentence(2),
Commentaires = new List<Commentaire>()
};
titres.Add(titre);
artiste.Titres.Add(titre);
}
return titres;
}
}
}

View File

@@ -1,10 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
namespace Webzine.Entity
namespace Webzine.Entity
{
using System.ComponentModel.DataAnnotations;
/// <summary>
/// Classe représentant un style de musique.
/// </summary>
@@ -29,4 +26,4 @@ namespace Webzine.Entity
/// </summary>
public List<Titre> Titres { get; set; } = new List<Titre>();
}
}
}

View File

@@ -1,10 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
namespace Webzine.Entity
namespace Webzine.Entity
{
using System.ComponentModel.DataAnnotations;
/// <summary>
/// Classe représentant un titre de musique.
/// Lien avec l'entité <see cref="Artiste"/> : un artiste peut avoir plusieurs titres, mais un titre n'a qu'un seul artiste.
@@ -110,4 +107,4 @@ namespace Webzine.Entity
/// </summary>
public List<Style> Styles { get; set; } = new List<Style>();
}
}
}

View File

@@ -1,17 +1,48 @@
// using Webzine.Entity;
namespace Webzine.Repository.Contracts
{
using Webzine.Entity;
/// <summary>
/// Défini une interface <see cref="IArtisteRepository"/> pour gérer les opérations de base de données liées aux artistes.
/// </summary>
public interface IArtisteRepository
{
// void Add(Artiste artiste);
/// <summary>
/// Ajoute un nouvel artiste.
/// </summary>
/// <param name="artiste">L'artiste à ajouter à la collection. Ne peut pas être null.</param>
void Add(Artiste artiste);
// void Delete(Artiste artiste);
/// <summary>
/// Supprime un artiste.
/// </summary>
/// <param name="artiste">L'artiste à supprimer.</param>
void Delete(Artiste artiste);
// Artiste Find(int id);
/// <summary>
/// Récupère un artiste par son identifiant unique. Si aucun artiste n'est trouvé, retourne null.
/// </summary>
/// <param name="id">L'identifiant de l'artiste.</param>
/// <returns></returns>
Artiste Find(int id);
// IEnumerable<Artiste> FindAll();
/// <summary>
/// Récupère un artiste par son nom. Si aucun artiste n'est trouvé, retourne null.
/// </summary>
/// <param name="name">Le nom de l'artiste.</param>
/// <returns>L'artiste recherché ou null.</returns>
Artiste FindByName(string name);
// void Update(Artiste artiste);
/// <summary>
/// Récupère tous les artistes disponibles dans la collection. Si aucun artiste n'est trouvé, retourne une collection vide.
/// </summary>
/// <returns>Retourne une collection d'artistes.</returns>
IEnumerable<Artiste> FindAll();
/// <summary>
/// Met à jour les informations d'un artiste existant dans la collection.
/// </summary>
/// <param name="artiste">L'artiste à mettre à jour.</param>
void Update(Artiste artiste);
}
}

View File

@@ -1,15 +1,15 @@
// using Webzine.Entity;
namespace Webzine.Repository.Contracts
{
using Webzine.Entity;
public interface ICommentaireRepository
{
// void Add(Commentaire commentaire);
void Add(Commentaire commentaire);
// void Delete(Commentaire commentaire);
void Delete(Commentaire commentaire);
// Commentaire Find(int id);
Commentaire Find(int id);
// IEnumerable<Commentaire> FindAll();
IEnumerable<Commentaire> FindAll();
}
}

View File

@@ -1,17 +1,41 @@
// using Webzine.Entity;
namespace Webzine.Repository.Contracts
{
using Webzine.Entity;
/// <summary>
/// Interface définissant les opérations de base pour le repository de styles, permettant d'ajouter, supprimer, trouver et mettre à jour des styles dans la source de données.
/// </summary>
public interface IStyleRepository
{
// void Add(Style style);
/// <summary>
/// Ajoute un style à la liste des styles.
/// </summary>
/// <param name="style">L'objet style à ajouter.</param>
void Add(Style style);
// void Delete(Style style);
/// <summary>
/// Supprime un style de la liste des styles.
/// </summary>
/// <param name="style">L'objet style à supprimer.</param>
void Delete(Style style);
// Style Find(int id);
/// <summary>
/// Trouve un style dans la liste des styles en fonction de son identifiant.
/// </summary>
/// <param name="id">L'identifiant du style à trouver.</param>
/// <returns>Le style correspondant à l'identifiant fourni, ou null si aucun style n'est trouvé.</returns>
Style Find(int id);
// IEnumerable<Style> FindAll();
/// <summary>
/// Trouve tous les styles dans la liste des styles.
/// </summary>
/// <returns>Une collection de tous les styles présents dans la liste.</returns>
IEnumerable<Style> FindAll();
// void Update(Style style);
/// <summary>
/// Met à jour un style dans la liste des styles en fonction de son identifiant.
/// </summary>
/// <param name="style">L'objet style à mettre à jour.</param
void Update(Style style);
}
}

View File

@@ -0,0 +1,81 @@
namespace Webzine.Repository.Contracts
{
using Webzine.Entity;
/// <summary>
/// Interface qui définit les opérations de base pour la gestion des titres dans une source de données.
/// </summary>
public interface ITitreRepository
{
/// <summary>
/// Ajoute un titre à la liste des titres.
/// </summary>
/// <param name="titre">L'objet titre à ajouter.</param>
void Add(Titre titre);
/// <summary>
/// Remonte le nombre de titres.
/// </summary>
/// <returns>Le nombre total de titres présents dans la liste après l'incrémentation du nombre de lectures.</returns>
int Count();
/// <summary>
/// Supprime un titre de la liste des titres.
/// </summary>
/// <param name="titre">L'objet titre à supprimer.</param>
void Delete(Titre titre);
/// <summary>
/// Trouve un titre dans la liste des titres en fonction de son identifiant.
/// </summary>
/// <param name="idTitre">L'identifiant du titre à trouver.</param>
/// <returns>Le titre correspondant à l'identifiant fourni, ou null si aucun titre n'est trouvé.</returns>
Titre Find(int idTitre);
/// <summary>
/// Recherche les titres dans la liste des titres en fonction de l'offset et de la limite spécifiés, permettant ainsi une pagination des résultats.
/// </summary>
/// <param name="offset">Le nombre de titres à ignorer avant de commencer à retourner les résultats.</param>
/// <param name="limit">Le nombre maximum de titres à retourner.</param>
/// <returns>Une collection de titres correspondant au critère de pagination, triée par libellé.</returns>
IEnumerable<Titre> FindTitres(int offset, int limit);
/// <summary>
/// Trouve tous les titres dans la liste des titres.
/// </summary>
/// <returns>Une collection de tous les titres présents dans la liste.</returns>
IEnumerable<Titre> FindAll();
/// <summary>
/// Incrémente le nombre de lectures d'un titre donné. Si le titre est null, un message d'avertissement est enregistré dans les logs et aucune action n'est effectuée.
/// </summary>
/// <param name="titre">L'objet titre dont le nombre de lectures doit être incrémenté.</param>
void IncrementNbLectures(Titre titre);
/// <summary>
/// Incrémente le nombre de likes d'un titre donné. Si le titre est null, un message d'avertissement est enregistré dans les logs et aucune action n'est effectuée.
/// </summary>
/// <param name="titre">L'objet titre dont le nombre de likes doit être incrémenté.</param>
void IncrementNbLikes(Titre titre);
/// <summary>
/// Recherche les titres dont le libellé contient le mot spécifié, en ignorant la casse.
/// </summary>
/// <param name="mot">Le mot à rechercher dans les libellés des titres.</param>
/// <returns>Une collection de titres correspondant au critère de recherche, triée par libellé.</returns>
IEnumerable<Titre> Search(string mot);
/// <summary>
/// Recherche les titres associés à un style dont le libellé contient la chaîne spécifiée, en ignorant la casse.
/// </summary>
/// <param name="libelle">Le libellé du style à rechercher dans les titres.</param>
/// <returns>Une collection de titres correspondant au critère de recherche, triée par libellé.</returns>
IEnumerable<Titre> SearchByStyle(string libelle);
/// <summary>
/// Met à jour un titre dans la liste des titres en fonction de son identifiant. Si aucun titre correspondant à l'identifiant du titre fourni n'est trouvé, un message d'avertissement est enregistré dans les logs et aucune mise à jour n'est effectuée.
/// </summary>
/// <param name="titre">L'objet titre à mettre à jour.</param>
void Update(Titre titre);
}
}

View File

@@ -1,29 +0,0 @@
using Webzine.Entity;
namespace Webzine.Repository.Contracts
{
public interface ITitreRepository
{
// void Add(Titre titre);
// int Count();
// void Delete(Titre titre);
Titre? Find(int idTitre);
// IEnumerable<Titre> FindTitres(int offset, int limit);
IEnumerable<Titre> FindAll();
// void IncrementNbLectures(Titre titre);
// void IncrementNbLikes(Titre titre);
IEnumerable<Titre> Search(string mot);
IEnumerable<Titre> SearchByStyle(string libelle);
// void Update(Titre titre);
}
}

View File

@@ -0,0 +1,160 @@
// <copyright file="DbArtisteRepository.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Webzine.Repository
{
using System.Data.Common;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Webzine.EntitiesContext;
using Webzine.Entity;
using Webzine.Repository.Contracts;
/// <summary>
/// Initialise une classe <see cref="DbArtisteRepository"/> qui implémente l'interface <see cref="IArtisteRepository"/> pour gérer les opérations de base de données liées aux artistes.
/// Utilise <see cref="IArtisteRepository"/> en injection de dépendances.
/// </summary>
public class DbArtisteRepository : IArtisteRepository
{
private readonly WebzineDbContext context;
private readonly ILogger<LocalArtisteRepository> logger;
/// <summary>
/// Initializes a new instance of the <see cref="DbArtisteRepository"/> class.
/// </summary>
/// <param name="context">Le contexte de base de données à utiliser pour accéder aux entités et effectuer des opérations de
/// persistance. Ne peut pas être null.</param>
public DbArtisteRepository(WebzineDbContext context, ILogger<LocalArtisteRepository> logger)
{
this.logger = logger;
this.context = context;
}
/// <inheritdoc/>
public void Add(Artiste artiste)
{
try
{
this.context.Artistes.Add(artiste);
this.context.SaveChanges();
}
catch (DbUpdateException dbex)
{
this.logger.LogError(dbex, "Erreur de base de données lors de l'ajout de l'artiste: {id}", artiste.IdArtiste);
throw;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Une erreur est survenue lors de l'ajout de l'artiste {Nom}.", artiste?.Nom);
throw new Exception("Une erreur est survenue lors de l'ajout de l'artiste.", ex);
}
}
/// <inheritdoc/>
public void Delete(Artiste artiste)
{
try
{
if (artiste == null)
{
throw new ArgumentNullException(nameof(artiste), "L'artiste à supprimer ne peut pas être null.");
}
this.context.Artistes.Remove(artiste);
this.context.SaveChanges();
this.logger.LogDebug("L'artiste {IdArtiste} a bien été supprimé", artiste.IdArtiste);
}
catch (DbUpdateException dbex)
{
this.logger.LogError(dbex, "Erreur de base de données lors de la suppression de l'artiste: {Id}", artiste.IdArtiste);
throw;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Une erreur est survenue lors de la suppression de l'artiste {Nom}.", artiste?.Nom);
throw new Exception("Une erreur est survenue lors de la suppression de l'artiste.", ex);
}
}
/// <inheritdoc/>
public Artiste Find(int id)
{
try
{
Artiste artiste = this.context.Artistes
.Include(a => a.Titres)
.First(a => a.IdArtiste == id);
return artiste;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors de la recherche de l'artiste: {Id}", id);
throw;
}
}
/// <inheritdoc/>
public Artiste FindByName(string nom)
{
try
{
var artiste = this.context.Artistes
.Include(a => a.Titres)
.FirstOrDefault(a => a.Nom == nom);
return artiste;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors de la recherche de l'artiste avec le nom: {Nom}", nom);
throw;
}
}
/// <inheritdoc/>
public IEnumerable<Artiste> FindAll()
{
try
{
// .AsNoTracking() rend la requête beaucoup plus rapide pour de la lecture
var artistes = this.context.Artistes.AsNoTracking().ToList();
this.logger.LogDebug("{Count} artistes récupérés de la base.", artistes.Count);
return artistes;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors de la récupération de tous les artistes.");
return Enumerable.Empty<Artiste>(); // Retourne une liste vide au lieu de faire crash l'UI
}
}
/// <inheritdoc/>
public void Update(Artiste artiste)
{
if (artiste == null)
{
throw new ArgumentNullException(nameof(artiste));
}
try
{
this.context.Artistes.Update(artiste);
this.context.SaveChanges();
this.logger.LogDebug("Artiste {Id} ({Nom}) mis à jour avec succès.", artiste.IdArtiste, artiste.Nom);
}
catch (DbUpdateException ex)
{
this.logger.LogError(ex, "Erreur de base de données lors de la mise à jour de l'artiste ID: {IdArtiste}", artiste.IdArtiste);
throw;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors de la mise à jour de l'artiste {Id}.", artiste.IdArtiste);
throw;
}
}
}
}

View File

@@ -0,0 +1,141 @@
namespace Webzine.Repository;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Webzine.EntitiesContext;
using Webzine.Entity;
using Webzine.Repository.Contracts;
/// <summary>
/// Classe qui implémente le repository pour les commentaires en utilisant une base de données.
/// </summary>
public class DbCommentaireRepository : ICommentaireRepository
{
private readonly ILogger<DbCommentaireRepository> logger;
private readonly WebzineDbContext context;
/// <summary>
/// Initializes a new instance of the <see cref="DbCommentaireRepository"/> class.
/// Initialisation de <see cref="DbCommentaireRepository"/>.
/// </summary>
/// <param name="logger">Le service de journalisation injecté pour suivre les opérations du repository.</param>
/// <param name="context">Le contexte de base de données injecté.</param>
public DbCommentaireRepository(ILogger<DbCommentaireRepository> logger, WebzineDbContext context)
{
this.logger = logger;
this.context = context;
this.logger.LogDebug("NLog injecté dans DbCommentaireRepository");
}
/// <inheritdoc/>
public void Add(Commentaire commentaire)
{
try
{
this.logger.LogDebug("Ajout d'un nouveau commentaire de l'auteur : {Auteur}", commentaire.Auteur);
this.context.Commentaires.Add(commentaire);
this.context.SaveChanges();
this.logger.LogDebug("Commentaire ajouté avec l'id : {Id}", commentaire.IdCommentaire);
}
catch (DbUpdateException dbex)
{
this.logger.LogError(dbex, "Erreur de base de données lors de l'ajout du commentaire de l'auteur : {Auteur}", commentaire?.Auteur);
throw;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Une erreur est survenue lors de l'ajout d'un commentaire.");
throw new Exception("Une erreur est survenue lors de l'ajout du commentaire.", ex);
}
}
/// <inheritdoc/>
public void Delete(Commentaire commentaire)
{
try
{
if (commentaire == null)
{
throw new ArgumentNullException(nameof(commentaire), "Le commentaire à supprimer ne peut pas être null.");
}
this.context.Commentaires.Remove(commentaire);
this.context.SaveChanges();
this.logger.LogDebug("Le commentaire {IdCommentaire} a bien été supprimé", commentaire.IdCommentaire);
}
catch (DbUpdateException dbex)
{
this.logger.LogError(dbex, "Erreur de base de données lors de la suppression du commentaire : {Id}", commentaire.IdCommentaire);
throw;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Une erreur est survenue lors de la suppression du commentaire {Id}.", commentaire?.IdCommentaire);
throw new Exception("Une erreur est survenue lors de la suppression du commentaire.", ex);
}
}
/// <inheritdoc/>
public int Count()
{
var count = this.context.Commentaires.Count();
this.logger.LogDebug("Compte total des commentaires : {Count}", count);
return count;
}
/// <inheritdoc/>
public Commentaire Find(int idCommentaire)
{
this.logger.LogDebug("Recherche du commentaire avec l'id : {Id}", idCommentaire);
// On inclut le titre car il est souvent affiché avec le commentaire
return this.context.Commentaires
.Include(c => c.Titre)
.FirstOrDefault(c => c.IdCommentaire == idCommentaire);
}
/// <inheritdoc/>
public IEnumerable<Commentaire> FindAll()
{
this.logger.LogDebug("Récupération de tous les commentaires");
var commentaires = this.context.Commentaires
.Include(c => c.Titre)
.OrderByDescending(c => c.DateCreation)
.ToList();
this.logger.LogDebug("Nombre de commentaires trouvés : {Count}", commentaires.Count);
return commentaires;
}
/// <inheritdoc/>
public IEnumerable<Commentaire> FindCommentaires(int offset, int limit)
{
this.logger.LogDebug("Recherche paginée des commentaires (offset : {Offset}, limit : {Limit})", offset, limit);
var commentaires = this.context.Commentaires
.Include(c => c.Titre)
.OrderByDescending(c => c.DateCreation)
.Skip(offset)
.Take(limit)
.ToList();
this.logger.LogDebug("{Count} commentaires trouvés pour cette page", commentaires.Count);
return commentaires;
}
/// <inheritdoc/>
public IEnumerable<Commentaire> FindByIdTitre(int idTitre)
{
this.logger.LogDebug("Recherche des commentaires pour le titre ID : {IdTitre}", idTitre);
var commentaires = this.context.Commentaires
.Where(c => c.Titre.IdTitre == idTitre)
.OrderByDescending(c => c.DateCreation)
.ToList();
this.logger.LogDebug($"{commentaires.Count} commentaires trouvés pour l'ID de titre : {idTitre}");
return commentaires;
}
}

View File

@@ -1,6 +1,63 @@
namespace Webzine.Repository;
// <copyright file="DbEntityRepository.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
public class DbEntityRepository
namespace Webzine.Repository
{
using Webzine.EntitiesContext;
using Webzine.Entity;
using Webzine.Entity.Fixtures;
public class DbEntityRepository
{
private readonly WebzineDbContext context;
public DbEntityRepository(WebzineDbContext context)
{
this.context = context;
}
/// <summary>
/// Seed la base de donnée à l'aide de SeedDataLocal.
/// </summary>
/// <param name="nbArtistes">Nombre d'artiste.</param>
/// <param name="nbTitres">Nombre de titre.</param>
/// <param name="minStyles">Nombre min de style.</param>
/// <param name="maxStyles">Nombre mac de style.</param>
public void SeedBaseDeDonnees(
int nbArtistes = 100,
int nbTitres = 500,
int minStyles = 15,
int maxStyles = 20)
{
if (this.context.Artistes.Any() ||
this.context.Titres.Any() ||
this.context.Styles.Any() ||
this.context.Commentaires.Any())
{
return;
}
List<Artiste> artistes = SeedDataLocal.GenererListeArtiste(nbArtistes);
List<Style> styles = SeedDataLocal.GenererListeStyle(minStyles, maxStyles);
this.context.Artistes.AddRange(artistes);
this.context.Styles.AddRange(styles);
this.context.SaveChanges();
List<string> albums = SeedDataLocal.GenererListeAlbums(3);
List<Titre> titres = SeedDataLocal.GenererListeTitre(nbTitres, artistes, styles, albums);
int commentaireIdStart = 1;
foreach (var titre in titres)
{
var commentairesDuTitre = SeedDataLocal.GenererListeCommentaire(titre, 0, 5, commentaireIdStart);
titre.Commentaires.AddRange(commentairesDuTitre);
commentaireIdStart += commentairesDuTitre.Count;
}
this.context.Titres.AddRange(titres);
this.context.SaveChanges();
}
}
}

View File

@@ -0,0 +1,180 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Webzine.EntitiesContext;
using Webzine.Entity;
using Webzine.Repository.Contracts;
namespace Webzine.Repository;
/// <summary>
/// Classe qui implémente le repository pour les styles en utilisant une base de données.
/// </summary>
public class DbStyleRepository : IStyleRepository
{
private readonly ILogger<DbStyleRepository> logger;
private readonly WebzineDbContext context;
/// <summary>
/// Initializes a new instance of the <see cref="DbStyleRepository"/> class.
/// </summary>
/// <param name="logger">Le service de journalisation injecté pour suivre les opérations du repository.</param>
/// <param name="context">Le contexte de base de données injecté.</param>
public DbStyleRepository(ILogger<DbStyleRepository> logger, WebzineDbContext context)
{
this.logger = logger;
this.context = context;
this.logger.LogDebug(1, "NLog injecté dans DbStyleRepository");
}
/// <inheritdoc/>
public void Add(Style style)
{
try
{
this.logger.LogInformation("Ajout d'un nouveau style: {Libelle}", style.Libelle);
this.logger.LogDebug("Début de l'ajout du style en base de données");
this.context.Styles.Add(style);
this.context.SaveChanges();
this.logger.LogDebug("Style ajouté avec succès avec l'ID: {IdStyle}", style.IdStyle);
}
catch (DbUpdateException ex)
{
this.logger.LogError(ex, "Erreur de base de données lors de l'ajout du style: {Libelle}", style.Libelle);
throw;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors de l'ajout du style: {Libelle}", style.Libelle);
throw;
}
}
/// <inheritdoc/>
public void Delete(Style style)
{
try
{
this.logger.LogInformation("Suppression du style avec l'ID: {IdStyle}", style.IdStyle);
this.logger.LogDebug("Vérification de l'existence du style en base de données");
// Check if style exists
var existingStyle = this.context.Styles.Find(style.IdStyle);
if (existingStyle == null)
{
this.logger.LogWarning("Style avec l'ID {IdStyle} non trouvé pour la suppression", style.IdStyle);
throw new InvalidOperationException($"Style avec l'ID {style.IdStyle} non trouvé.");
}
this.logger.LogDebug("Style trouvé, suppression en cours");
this.context.Styles.Remove(existingStyle);
this.context.SaveChanges();
this.logger.LogDebug("Style supprimé avec succès: {IdStyle}", style.IdStyle);
}
catch (DbUpdateException ex)
{
this.logger.LogError(ex, "Erreur de base de données lors de la suppression du style ID: {IdStyle}", style.IdStyle);
throw;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors de la suppression du style ID: {IdStyle}", style.IdStyle);
throw;
}
}
/// <inheritdoc/>
public Style Find(int id)
{
try
{
this.logger.LogDebug("Recherche du style avec l'ID: {Id}", id);
if (id <= 0)
{
this.logger.LogWarning("Tentative de recherche d'un style avec un Id invalide: {Id}", id);
return new Style();
}
this.logger.LogDebug("Préparation de la requête avec inclusion des titres");
var style = this.context.Styles
.Include(s => s.Titres)
.FirstOrDefault(s => s.IdStyle == id);
if (style == null)
{
this.logger.LogWarning("Style avec l'ID {Id} non trouvé", id);
style = new Style();
}
else
{
this.logger.LogDebug("Style trouvé: {Libelle}", style.Libelle);
}
return style;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors de la recherche du style avec l'ID: {Id}", id);
throw;
}
}
/// <inheritdoc/>
public IEnumerable<Style> FindAll()
{
try
{
this.logger.LogDebug("Récupération de tous les styles");
this.logger.LogDebug("Tri des styles par libellé");
var styles = this.context.Styles
.OrderBy(s => s.Libelle)
.ToList();
this.logger.LogDebug("{Count} styles récupérés", styles.Count);
return styles;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors de la récupération de tous les styles");
throw;
}
}
/// <inheritdoc/>
public void Update(Style style)
{
try
{
this.logger.LogInformation("Mise à jour du style avec l'ID: {IdStyle}", style.IdStyle);
this.logger.LogDebug("Recherche du style en base de données");
var existingStyle = this.context.Styles.Find(style.IdStyle);
if (existingStyle == null)
{
this.logger.LogWarning("Style avec l'ID {IdStyle} non trouvé pour la mise à jour", style.IdStyle);
throw new InvalidOperationException($"Style avec l'ID {style.IdStyle} non trouvé.");
}
// Update properties
this.logger.LogDebug("Style trouvé, mise à jour des propriétés");
existingStyle.Libelle = style.Libelle;
this.context.SaveChanges();
this.logger.LogDebug("Style mis à jour avec succès: {IdStyle}", style.IdStyle);
}
catch (DbUpdateException ex)
{
this.logger.LogError(ex, "Erreur de base de données lors de la mise à jour du style ID: {IdStyle}", style.IdStyle);
throw;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors de la mise à jour du style ID: {IdStyle}", style.IdStyle);
throw;
}
}
}

View File

@@ -0,0 +1,335 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Webzine.EntitiesContext;
using Webzine.Entity;
using Webzine.Repository.Contracts;
namespace Webzine.Repository;
/// <summary>
/// Classe qui implémente le repository pour les titres en utilisant une base de données.
/// </summary>
public class DbTitreRepository : ITitreRepository
{
private readonly ILogger<DbTitreRepository> logger;
private readonly WebzineDbContext context;
/// <summary>
/// Initializes a new instance of the <see cref="DbTitreRepository"/> class.
/// </summary>
/// <param name="logger">Le service de journalisation injecté pour suivre les opérations du repository.</param>
/// <param name="context">Le contexte de base de données injecté.</param>
public DbTitreRepository(ILogger<DbTitreRepository> logger, WebzineDbContext context)
{
this.logger = logger;
this.context = context;
this.logger.LogDebug(1, "NLog injecté dans DbTitreRepository");
}
/// <inheritdoc/>
public void Add(Titre titre)
{
try
{
this.logger.LogInformation("Ajout d'un nouveau titre: {Libelle}", titre.Libelle);
this.logger.LogDebug("Début de l'ajout du titre en base de données");
this.context.Titres.Add(titre);
this.context.SaveChanges();
this.logger.LogDebug("Titre ajouté avec succès avec l'ID: {IdTitre}", titre.IdTitre);
}
catch (DbUpdateException ex)
{
this.logger.LogError(ex, "Erreur de base de données lors de l'ajout du titre: {Libelle}", titre.Libelle);
throw;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors de l'ajout du titre: {Libelle}", titre.Libelle);
throw;
}
}
/// <inheritdoc/>
public int Count()
{
try
{
this.logger.LogDebug("Comptage des titres en base de données");
var count = this.context.Titres.Count();
this.logger.LogDebug("Nombre total de titres: {Count}", count);
return count;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors du comptage des titres");
throw;
}
}
/// <inheritdoc/>
public void Delete(Titre titre)
{
try
{
this.logger.LogInformation("Suppression du titre avec l'ID: {IdTitre}", titre.IdTitre);
this.logger.LogDebug("Début de la suppression du titre en base de données");
this.context.Titres.Remove(titre);
this.context.SaveChanges();
this.logger.LogDebug("Titre supprimé avec succès: {IdTitre}", titre.IdTitre);
}
catch (DbUpdateException ex)
{
this.logger.LogError(ex, "Erreur de base de données lors de la suppression du titre ID: {IdTitre}", titre.IdTitre);
throw;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors de la suppression du titre ID: {IdTitre}", titre.IdTitre);
throw;
}
}
/// <inheritdoc/>
public IEnumerable<Titre> FindTitres(int offset, int limit)
{
try
{
this.logger.LogDebug("Recherche des titres avec offset: {Offset}, limit: {Limit}", offset, limit);
this.logger.LogDebug("Préparation de la requête avec les inclusions Artiste et Styles");
var titres = this.context.Titres
.Include(t => t.Artiste)
.Include(t => t.Styles)
.OrderBy(t => t.Libelle)
.Skip(offset)
.Take(limit)
.ToList();
this.logger.LogDebug("{Count} titres trouvés", titres.Count);
return titres;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors de la recherche des titres avec offset: {Offset}, limit: {Limit}", offset, limit);
throw;
}
}
/// <inheritdoc/>
public void IncrementNbLectures(Titre titre)
{
try
{
this.logger.LogInformation("Incrémentation du nombre de lectures pour le titre ID: {IdTitre}", titre.IdTitre);
this.logger.LogDebug("Recherche du titre en base de données");
var existingTitre = this.context.Titres.Find(titre.IdTitre);
if (existingTitre != null)
{
this.logger.LogDebug("Titre trouvé, incrémentation du compteur de lectures");
existingTitre.NbLectures++;
this.context.SaveChanges();
this.logger.LogDebug("Nouveau nombre de lectures: {NbLectures}", existingTitre.NbLectures);
}
else
{
this.logger.LogWarning("Titre avec l'ID {IdTitre} non trouvé pour l'incrémentation des lectures", titre.IdTitre);
}
}
catch (DbUpdateException ex)
{
this.logger.LogError(ex, "Erreur de base de données lors de l'incrémentation des lectures pour le titre ID: {IdTitre}", titre.IdTitre);
throw;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors de l'incrémentation des lectures pour le titre ID: {IdTitre}", titre.IdTitre);
throw;
}
}
/// <inheritdoc/>
public void IncrementNbLikes(Titre titre)
{
try
{
this.logger.LogInformation("Incrémentation du nombre de likes pour le titre ID: {IdTitre}", titre.IdTitre);
this.logger.LogDebug("Recherche du titre en base de données");
var existingTitre = this.context.Titres.Find(titre.IdTitre);
if (existingTitre != null)
{
this.logger.LogDebug("Titre trouvé, incrémentation du compteur de likes");
existingTitre.NbLikes++;
this.context.SaveChanges();
this.logger.LogDebug("Nouveau nombre de likes: {NbLikes}", existingTitre.NbLikes);
}
else
{
this.logger.LogWarning("Titre avec l'ID {IdTitre} non trouvé pour l'incrémentation des likes", titre.IdTitre);
}
}
catch (DbUpdateException ex)
{
this.logger.LogError(ex, "Erreur de base de données lors de l'incrémentation des likes pour le titre ID: {IdTitre}", titre.IdTitre);
throw;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors de l'incrémentation des likes pour le titre ID: {IdTitre}", titre.IdTitre);
throw;
}
}
/// <inheritdoc/>
public void Update(Titre titre)
{
try
{
this.logger.LogInformation("Mise à jour du titre avec l'ID: {IdTitre}", titre.IdTitre);
this.logger.LogDebug("Début de la mise à jour du titre en base de données");
var existingTitre = this.context.Titres.Find(titre.IdTitre);
if (existingTitre != null)
{
this.logger.LogDebug("Titre trouvé, mise à jour des propriétés");
this.context.Entry(existingTitre).CurrentValues.SetValues(titre);
// Handle many-to-many relationships
this.logger.LogDebug("Mise à jour des relations many-to-many (Styles)");
this.context.Entry(existingTitre).Collection(t => t.Styles).Load();
existingTitre.Styles.Clear();
foreach (var style in titre.Styles)
{
existingTitre.Styles.Add(style);
}
this.context.SaveChanges();
this.logger.LogDebug("Titre mis à jour avec succès: {IdTitre}", titre.IdTitre);
}
else
{
this.logger.LogWarning("Titre avec l'ID {IdTitre} non trouvé pour la mise à jour", titre.IdTitre);
throw new InvalidOperationException($"Titre avec l'ID {titre.IdTitre} non trouvé.");
}
}
catch (DbUpdateException ex)
{
this.logger.LogError(ex, "Erreur de base de données lors de la mise à jour du titre ID: {IdTitre}", titre.IdTitre);
throw;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors de la mise à jour du titre ID: {IdTitre}", titre.IdTitre);
throw;
}
}
/// <inheritdoc/>
public IEnumerable<Titre> Search(string mot)
{
try
{
this.logger.LogInformation("Recherche des titres avec le mot-clé: {Mot}", mot);
this.logger.LogDebug("Préparation de la requête de recherche avec les inclusions");
var titres = this.context.Titres
.Include(t => t.Artiste)
.Include(t => t.Styles)
.Where(t => t.Libelle.ToLower().Contains(mot.ToLower()))
.OrderBy(t => t.Libelle)
.ToList();
this.logger.LogDebug("{Count} titres trouvés correspondant à '{Mot}'", titres.Count, mot);
return titres;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors de la recherche des titres avec le mot-clé: {Mot}", mot);
throw;
}
}
/// <inheritdoc/>
public Titre Find(int idTitre)
{
try
{
this.logger.LogDebug("Recherche du titre avec l'ID: {IdTitre}", idTitre);
this.logger.LogDebug("Préparation de la requête avec les inclusions Artiste, Styles et Commentaires");
var titre = this.context.Titres
.Include(t => t.Artiste)
.Include(t => t.Styles)
.Include(t => t.Commentaires)
.First(t => t.IdTitre == idTitre);
this.logger.LogDebug("Titre trouvé: {Libelle}", titre.Libelle);
return titre;
}
catch (InvalidOperationException ex)
{
this.logger.LogWarning(ex, "Aucun titre trouvé avec l'ID: {IdTitre}", idTitre);
throw;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors de la recherche du titre avec l'ID: {IdTitre}", idTitre);
throw;
}
}
/// <inheritdoc/>
public IEnumerable<Titre> FindAll()
{
try
{
this.logger.LogDebug("Récupération de tous les titres");
this.logger.LogDebug("Préparation de la requête avec les inclusions Artiste et Styles");
var titres = this.context.Titres
.Include(t => t.Artiste)
.Include(t => t.Styles)
.Include(t => t.Commentaires)
.OrderBy(t => t.Libelle)
.ToList();
this.logger.LogDebug("{Count} titres récupérés", titres.Count);
return titres;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors de la récupération de tous les titres");
throw;
}
}
/// <inheritdoc/>
public IEnumerable<Titre> SearchByStyle(string libelle)
{
try
{
this.logger.LogInformation("Recherche des titres par style: {Libelle}", libelle);
this.logger.LogDebug("Préparation de la requête de recherche par style");
var titres = this.context.Titres
.Include(t => t.Artiste)
.Include(t => t.Styles)
.Where(t => t.Styles.Any(s => s.Libelle.ToLower() == libelle.ToLower()))
.OrderBy(t => t.Libelle)
.ToList();
this.logger.LogDebug("{Count} titres trouvés pour le style '{Libelle}'", titres.Count, libelle);
return titres;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Erreur lors de la recherche des titres par style: {Libelle}", libelle);
throw;
}
}
}

View File

@@ -0,0 +1,32 @@
namespace Webzine.Repository
{
using Webzine.Entity;
/// <summary>
/// Représente un entrepôt de données en mémoire (Mock) pour l'application.
/// Cette classe simule une base de données en stockant les entités dans des listes statiques
/// durant le cycle de vie de l'application.
/// </summary>
public class InMemoryDataStore
{
/// <summary>
/// Obtient ou définit la liste des artistes enregistrés.
/// </summary>
public List<Artiste> Artistes { get; set; } = new ();
/// <summary>
/// Obtient ou définit la liste des titres (morceaux) musicaux.
/// </summary>
public List<Titre> Titres { get; set; } = new ();
/// <summary>
/// Obtient ou définit la liste des styles musicaux disponibles.
/// </summary>
public List<Style> Styles { get; set; } = new ();
/// <summary>
/// Obtient ou définit la liste des commentaires rédigés par les utilisateurs.
/// </summary>
public List<Commentaire> Commentaires { get; set; } = new ();
}
}

View File

@@ -0,0 +1,89 @@
// <copyright file="LocalArtisteRepository.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Webzine.Repository
{
using Microsoft.Extensions.Logging;
using Webzine.Entity;
using Webzine.Repository.Contracts;
/// <summary>
/// Initialise une classe <see cref="LocalArtisteRepository"/> qui implémente l'interface <see cref="IArtisteRepository"/> pour gérer les opérations de base de données liées aux artistes.
/// Utilise <see cref="IArtisteRepository"/> en injection de dépendances.
/// </summary>
public class LocalArtisteRepository : IArtisteRepository
{
private readonly ILogger<LocalArtisteRepository> logger;
// private readonly List<Artiste> artistes;
private readonly InMemoryDataStore dataStore;
/// <summary>
/// Initializes a new instance of the <see cref="LocalArtisteRepository"/> class.
/// Est liéee à une liste d'artistes en local et utilise un logger pour enregistrer les opérations effectuées sur les artistes.
/// </summary>
/// <param name="artistes">La liste des artistes à initialiser. Ne peut pas être null.</param>
/// <param name="logger">Le logger à utiliser pour enregistrer les messages de journalisation. Ne peut pas être null.</param>
public LocalArtisteRepository(InMemoryDataStore dataStore, ILogger<LocalArtisteRepository> logger)
{
this.logger = logger;
// this.artistes = artistes;
this.dataStore = dataStore;
}
/// <inheritdoc/>
public void Add(Artiste artiste)
{
throw new NotSupportedException("Mode Local");
}
/// <inheritdoc/>
public void Delete(Artiste artiste)
{
throw new NotSupportedException("Mode Local");
}
/// <inheritdoc/>
public Artiste Find(int id)
{
var artiste = this.dataStore.Artistes.First(a => a.IdArtiste == id);
if (artiste == null)
{
return new Artiste();
}
return artiste;
}
/// <inheritdoc/>
public Artiste FindByName(string nom)
{
var artiste = this.dataStore.Artistes.FirstOrDefault(a => a.Nom == nom);
if (artiste != null)
{
artiste.Titres = this.dataStore.Titres
.Where(t => t.IdArtiste == artiste.IdArtiste)
.ToList();
}
return artiste;
}
/// <inheritdoc/>
/// La liste retournée est une copie de la liste interne, donc elle ne peut être nulle.
public IEnumerable<Artiste> FindAll()
{
return this.dataStore.Artistes;
}
/// <inheritdoc/>
public void Update(Artiste artiste)
{
throw new NotSupportedException("Mode Local");
}
}
}

View File

@@ -0,0 +1,100 @@
// <copyright file="LocalCommentaireRepository.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Webzine.Repository
{
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
using Webzine.Entity;
using Webzine.Repository.Contracts;
/// <summary>
/// Initialise une classe <see cref="LocalCommentaireRepository"/> qui implémente l'interface <see cref="ICommentaireRepository"/> pour gérer les opérations liées aux commentaires.
/// Utilise <see cref="ICommentaireRepository"/> en injection de dépendances.
/// </summary>
public class LocalCommentaireRepository : ICommentaireRepository
{
private readonly ILogger<LocalCommentaireRepository> logger;
private readonly InMemoryDataStore dataStore;
/// <summary>
/// Initializes a new instance of the <see cref="LocalCommentaireRepository"/> class.
/// Initialise une nouvelle instance du <see cref="LocalCommentaireRepository"/> .
/// Est liée à un magasin de données en mémoire et utilise un logger pour enregistrer les opérations.
/// </summary>
/// <param name="dataStore">Le magasin de données en mémoire. Ne peut pas être null.</param>
/// <param name="logger">Le logger à utiliser pour enregistrer les messages de journalisation. Ne peut pas être null.</param>
public LocalCommentaireRepository(InMemoryDataStore dataStore, ILogger<LocalCommentaireRepository> logger)
{
this.logger = logger;
this.dataStore = dataStore;
}
/// <inheritdoc/>
public void Add(Commentaire commentaire)
{
throw new NotSupportedException("Mode Local");
}
/// <inheritdoc/>
public void Delete(Commentaire commentaire)
{
throw new NotSupportedException("Mode Local");
}
/// <inheritdoc/>
public int Count()
{
return this.dataStore.Commentaires.Count;
}
/// <inheritdoc/>
public Commentaire Find(int idCommentaire)
{
var commentaire = this.dataStore.Commentaires.FirstOrDefault(c => c.IdCommentaire == idCommentaire);
if (commentaire == null)
{
return new Commentaire();
}
return commentaire;
}
/// <inheritdoc/>
public IEnumerable<Commentaire> FindAll()
{
return this.dataStore.Commentaires
.OrderByDescending(c => c.DateCreation)
.ToList();
}
/// <inheritdoc/>
public IEnumerable<Commentaire> FindCommentaires(int offset, int limit)
{
if (offset < 0 || limit <= 0)
{
return Enumerable.Empty<Commentaire>();
}
return this.dataStore.Commentaires
.OrderByDescending(c => c.DateCreation)
.Skip(offset)
.Take(limit)
.ToList();
}
/// <inheritdoc/>
public IEnumerable<Commentaire> FindByIdTitre(int idTitre)
{
return this.dataStore.Commentaires
.Where(c => c.Titre != null && c.Titre.IdTitre == idTitre)
.OrderByDescending(c => c.DateCreation)
.ToList();
}
}
}

View File

@@ -1,71 +0,0 @@
using Microsoft.Extensions.Logging;
using Webzine.Entity;
using Webzine.Entity.Fixtures;
using Webzine.Repository.Contracts;
namespace Webzine.Repository;
/// <summary>
/// Classe qui permet d'initialiser un jeu de données
/// pour tester l'application
/// </summary>
public class LocalEntityRepository : ITitreRepository
{
private readonly ILogger<LocalEntityRepository> _logger;
private readonly List<Titre> _titres;
/// <summary>
/// Initialise une nouvelle instance du <see cref="LocalEntityRepository"/> avec un service de journalisation injecte.
/// </summary>
/// <param name="logger">Service de journalisation injecte pour suivre les operations du repository.</param>
public LocalEntityRepository(ILogger<LocalEntityRepository> logger)
{
_logger = logger;
_logger.LogDebug(1, "NLog injected into LocalEntityRepository");
var factory = new DataFactory();
var artistes = factory.GenerateArtists(10);
var styles = factory.GenerateStyles(10);
_titres = factory.GenerateTitres(30, artistes, styles);
factory.GenerateCommentaires(50, _titres);
}
public IEnumerable<Titre> Search(string mot)
{
if (string.IsNullOrWhiteSpace(mot))
{
return Enumerable.Empty<Titre>();
}
return _titres
.Where(t => !string.IsNullOrWhiteSpace(t.Libelle)
&& t.Libelle.Contains(mot, StringComparison.OrdinalIgnoreCase))
.OrderBy(t => t.Libelle)
.ToList();
}
public Titre? Find(int idTitre)
{
return _titres.FirstOrDefault(t => t.IdTitre == idTitre);
}
public IEnumerable<Titre> FindAll()
{
return _titres;
}
public IEnumerable<Titre> SearchByStyle(string libelle)
{
if (string.IsNullOrWhiteSpace(libelle))
{
return Enumerable.Empty<Titre>();
}
return _titres
.Where(t => t.Styles.Any(s => !string.IsNullOrWhiteSpace(s.Libelle)
&& s.Libelle.Contains(libelle, StringComparison.OrdinalIgnoreCase)))
.OrderBy(t => t.Libelle)
.ToList();
}
}

View File

@@ -0,0 +1,58 @@
using Microsoft.Extensions.Logging;
using Webzine.Entity;
using Webzine.Repository.Contracts;
namespace Webzine.Repository;
/// <summary>
/// Classe qui implémente le repository pour les styles en utilisant une liste locale comme source de données.
/// </summary>
public class LocalStyleRepository : IStyleRepository
{
private readonly ILogger<LocalStyleRepository> logger;
// private readonly List<Style> styles;
private readonly InMemoryDataStore dataStore;
/// <summary>
/// Initializes a new instance of the <see cref="LocalStyleRepository"/> class.
/// </summary>
/// <param name="logger">Le service de journalisation injecté pour suivre les opérations du repository.</param>
/// <param name="styles">La liste de styles à utiliser comme source de données pour le repository.</param>
public LocalStyleRepository(ILogger<LocalStyleRepository> logger, InMemoryDataStore dataStore)
{
this.logger = logger;
this.dataStore = dataStore;
this.logger.LogDebug(1, "NLog injecté dans LocalStyleRepository");
}
/// <inheritdoc/>
public void Add(Style style)
{
throw new NotSupportedException("Mode local");
}
/// <inheritdoc/>
public void Delete(Style style)
{
throw new NotSupportedException("Mode local");
}
/// <inheritdoc/>
public Style Find(int id)
{
return this.dataStore.Styles.Find(s => s.IdStyle == id);
}
/// <inheritdoc/>
public IEnumerable<Style> FindAll()
{
return this.dataStore.Styles.ToList();
}
/// <inheritdoc/>
public void Update(Style style)
{
throw new NotSupportedException("Mode local");
}
}

View File

@@ -0,0 +1,99 @@
using Microsoft.Extensions.Logging;
using Webzine.Entity;
using Webzine.Repository.Contracts;
namespace Webzine.Repository;
/// <summary>
/// Classe qui implémente le repository pour les titres en utilisant une liste locale comme source de données.
/// </summary>
public class LocalTitreRepository : ITitreRepository
{
private readonly ILogger<LocalTitreRepository> logger;
private readonly InMemoryDataStore dataStore;
/// <summary>
/// Initializes a new instance of the <see cref="LocalTitreRepository"/> class.
/// </summary>
/// <param name="logger">Le service de journalisation injecté pour suivre les opérations du repository.</param>
/// <param name="dataStore">La liste de titres à utiliser comme source de données pour le repository.</param>
public LocalTitreRepository(ILogger<LocalTitreRepository> logger, InMemoryDataStore dataStore)
{
this.logger = logger;
this.dataStore = dataStore;
this.logger.LogDebug(1, "NLog injecté dans LocalTitreRepository");
}
/// <inheritdoc/>
public void Add(Titre titre)
{
throw new NotSupportedException("Mode local");
}
/// <inheritdoc/>
public int Count()
{
var count = this.dataStore.Titres.Count();
return count;
}
/// <inheritdoc/>
public void Delete(Titre titre)
{
throw new NotSupportedException("Mode Local");
}
/// <inheritdoc/>
public IEnumerable<Titre> FindTitres(int offset, int limit)
{
return this.dataStore.Titres
.OrderByDescending(t => t.DateCreation)
.Skip(offset)
.Take(limit);
}
/// <inheritdoc/>
public void IncrementNbLectures(Titre titre)
{
titre.NbLectures++;
}
/// <inheritdoc/>
public void IncrementNbLikes(Titre titre)
{
titre.NbLikes++;
}
/// <inheritdoc/>
public IEnumerable<Titre> Search(string mot)
{
return this.dataStore.Titres
.Where(t => t.Libelle != null && t.Libelle.Contains(mot));
}
/// <inheritdoc/>
public Titre Find(int idTitre)
{
return this.dataStore.Titres
.First(t => t.IdTitre == idTitre);
}
/// <inheritdoc/>
public IEnumerable<Titre> FindAll()
{
return this.dataStore.Titres;
}
/// <inheritdoc/>
public IEnumerable<Titre> SearchByStyle(string libelle)
{
return this.dataStore.Titres
.Where(t => t.Styles.Any(s => s.Libelle == libelle));
}
/// <inheritdoc/>
public void Update(Titre titre)
{
throw new NotSupportedException("Mode local");
}
}

View File

@@ -8,7 +8,8 @@
<ItemGroup>
<PackageReference Include="Faker.Net" Version="2.0.163" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.5" />
<PackageReference Include="NLog" Version="6.1.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
@@ -23,6 +24,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Webzine.EntitiesContext\Webzine.EntitiesContext.csproj" />
<ProjectReference Include="..\Webzine.Entity\Webzine.Entity.csproj" />
<ProjectReference Include="..\Webzine.Repository.Contracts\Webzine.Repository.Contracts.csproj" />
</ItemGroup>

View File

@@ -1,46 +1,46 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Webzine.Entity;
using Webzine.Entity.Fixtures;
using Webzine.Repository.Contracts;
using Webzine.WebApplication.Areas.Administration.ViewModels.Artiste;
using Webzine.WebApplication.Areas.Administration.ViewModels.Titre;
namespace Webzine.WebApplication.Areas.Administration.Controllers;
/// <summary>
/// Contrôleur pour la gestion des artistes dans l'administration du webzine.
/// </summary>
[Area("Administration")]
public class ArtisteController : Controller
{
// Injection du logger via le constructeur
private readonly ILogger<ArtisteController> _logger;
private readonly List<Artiste> _artistes;
private readonly ILogger<ArtisteController> logger;
private readonly IArtisteRepository artisteRepository;
public ArtisteController(ILogger<ArtisteController> logger)
{
_logger = logger;
this._logger.LogDebug(1, "initialisation du ArtisteController d'administration");
var factory = new DataFactory();
_artistes = factory.GenerateArtists(10);
}
/// <summary>
/// Affiche la liste des artistes. Pour l'instant, les artistes sont générés à partir de noms prédéfinis via la méthode SeedArtisteByName de la classe ArtisteFactory.
/// Chaque artiste est ensuite ajouté à une liste d'artistes qui est passée à la vue via un objet GroupeArtisteViewModel.
/// Initializes a new instance of the <see cref="ArtisteController"/> class.
/// Initialise une nouvelle instance de la classe <see cref="ArtisteController"/>.
/// </summary>
/// <param name="logger">Logger.</param>
/// <param name="artisteRepository">Repository pour les artistes.</param>
public ArtisteController(
ILogger<ArtisteController> logger,
IArtisteRepository artisteRepository)
{
this.logger = logger;
this.artisteRepository = artisteRepository;
}
/// <summary>
/// Affiche la liste des artistes. Pour l'instant, les artistes sont générés à partir de noms prédéfinis via la méthode SeedArtisteByName de la classe ArtisteFactory.
/// Chaque artiste est ensuite ajouté à une liste d'artistes qui est passée à la vue.
/// </summary>
/// <returns>Redirection.</returns>
public IActionResult Index()
{
IEnumerable<Artiste> artistes = this.artisteRepository.FindAll();
var _artistes_ordre = _artistes.OrderBy(t => t.Nom).ToList();
var artistes_ordre = artistes.OrderBy(t => t.Nom).ToList();
_logger.LogInformation("Initialisation du contrôleur TitreController pour l'Administration.");
GroupeArtisteViewModel groupeArtisteModel = new GroupeArtisteViewModel
{
Artistes = _artistes_ordre
};
return View(groupeArtisteModel);
return this.View(artistes_ordre);
}
/// <summary>
@@ -53,10 +53,10 @@ public class ArtisteController : Controller
{
Id = 0,
Nom = string.Empty,
Biographie = string.Empty
Biographie = string.Empty,
};
return View(model);
return this.View(model);
}
/// <summary>
@@ -66,32 +66,51 @@ public class ArtisteController : Controller
/// <returns>Redirection.</returns>
public IActionResult Edit(int id)
{
var artiste = _artistes.First(t => t.IdArtiste == id);
var artiste = this.artisteRepository.Find(id);
var model = new AdminArtisteForm
{
Id = artiste.IdArtiste,
Nom = artiste.Nom,
Biographie = artiste.Biographie
Biographie = artiste.Biographie,
};
return View(model);
return this.View(model);
}
/// <summary>
/// Renvoie à la page supprimer un artiste.
/// </summary>
/// <param name="id">L'identifiant de l'artiste à supprimer. </param>
/// <returns>Redirection.></returns>
/// <returns>Redirection.</returns>
public IActionResult Delete(int id)
{
var artiste = _artistes.First(t => t.IdArtiste == id);
var artiste = this.artisteRepository.Find(id);
var model = new AdminArtisteForm
{
Id = id,
Nom = artiste.Nom,
Biographie = artiste.Biographie
Biographie = artiste.Biographie,
};
return View(model);
return this.View(model);
}
/// <summary>
/// Méthode POST pour supprimer un artiste.
/// </summary>
/// <param name="model">L'artiste à supprimer.</param>
/// <returns>Redirige vers la page d'index d'admin artiste.</returns>
[HttpPost]
public IActionResult Delete(AdminArtisteForm model)
{
var artiste = this.artisteRepository.Find(model.Id);
if (artiste != null)
{
this.artisteRepository.Delete(artiste);
}
// 3. Redirect back to the list (or wherever you want them to go after)
return this.RedirectToAction("Index");
}
}

View File

@@ -1,129 +1,86 @@
using Microsoft.AspNetCore.Mvc;
using Webzine.Entity;
using Webzine.Entity.Fixtures;
using Webzine.WebApplication.Areas.Administration.ViewModels.Commentaire;
namespace Webzine.WebApplication.Areas.Administration.Controllers
{
using Microsoft.AspNetCore.Mvc;
using Webzine.Repository.Contracts;
using Webzine.WebApplication.Areas.Administration.ViewModels.Commentaire;
[Area("Administration")]
public class CommentaireController : Controller
{
private readonly ILogger<CommentaireController> _logger;
private readonly List<Commentaire> _commentaires;
private readonly ILogger<CommentaireController> logger;
private readonly ICommentaireRepository commentaireRepository;
/// <summary>
/// Initialise une nouvelle instance du <see cref="CommentaireController"/>.
/// Les données sont générées dynamiquement via <see cref="DataFactory"/>.
/// Initializes a new instance of the <see cref="CommentaireController"/> class.
/// Initialise une nouvelle instance de la classe <see cref="CommentaireController"/>.
/// Utilise l'injection de dépendances pour récupérer le repository.
/// </summary>
/// <param name="logger">Service de journalisation injecté.</param>
public CommentaireController(ILogger<CommentaireController> logger)
/// <param name="commentaireRepository">Le repository des commentaires injecté.</param>
public CommentaireController(ILogger<CommentaireController> logger, ICommentaireRepository commentaireRepository)
{
_logger = logger;
this.logger = logger;
this.commentaireRepository = commentaireRepository;
_logger.LogInformation("Initialisation du contrôleur CommentaireController.");
var factory = new DataFactory();
var _artistes = factory.GenerateArtists(10);
var _styles = factory.GenerateStyles(10);
var _titres = factory.GenerateTitres(30, _artistes, _styles);
_commentaires = factory.GenerateCommentaires(50, _titres);
_logger.LogInformation("Données fictives générées avec succès.");
this.logger.LogInformation("Initialisation du contrôleur CommentaireController.");
}
/// <summary>
/// Affiche la liste des commentaires dans la vue Index.
/// </summary>
/// <returns>>La vue Index avec le ViewModel contenant la liste des commentaires.</returns>
public ActionResult Index()
/// <returns>La vue Index avec le ViewModel contenant la liste des commentaires.</returns>
public IActionResult Index()
{
// Création de données "bouchon" (mock) pour tester l'affichage
var listeCommentaires = new List<Commentaire>
{
new Commentaire
{
IdCommentaire = 1, // Correction: Id -> IdCommentaire
Auteur = "Michel", // Correction: Nom -> Auteur
Contenu = "Nulla sed velit nec tellus gravida molestie",
DateCreation = new DateTime(2023, 1, 22, 15, 59, 28),
// Important : On initialise l'objet Titre pour accéder à Titre.Libelle
Titre = new Titre { Libelle = "St Germain - So Flute" },
},
new Commentaire
{
IdCommentaire = 2,
Auteur = "Jeff",
Contenu = "Lorem ipsum dolor sit.",
DateCreation = new DateTime(2023, 1, 22, 14, 27, 8),
Titre = new Titre { Libelle = "Queen - Bohemian Rapsody" },
},
new Commentaire
{
IdCommentaire = 3,
Auteur = "Eva",
Contenu = "Aenean vulputate eleifend tellus.",
DateCreation = new DateTime(2023, 1, 22, 13, 2, 17),
Titre = new Titre { Libelle = "Rammstein - Du hast" },
},
};
// Récupération des commentaires depuis le repository
var commentaires = this.commentaireRepository.FindAll();
// Initialisation du ViewModel
var viewModel = new CommentaireViewModel
{
Commentaires = listeCommentaires
Commentaires = commentaires,
};
return View(viewModel);
return this.View(viewModel);
}
/// <summary>
/// Affiche la vue de confirmation de suppression d'un commentaire, en récupérant les détails du commentaire à supprimer à partir de l'identifiant fourni.
/// Affiche la vue de confirmation de suppression d'un commentaire, en récupérant les détails à partir de l'identifiant fourni.
/// </summary>
/// <param name="id">L'identifiant du commentaire à supprimer.</param>
/// <returns>La vue de confirmation de suppression avec le ViewModel contenant les détails du commentaire à supprimer, ou une réponse NotFound si le commentaire n'existe pas.</returns>
public ActionResult Delete(int id)
/// <returns>La vue de confirmation de suppression avec le ViewModel contenant les détails, ou une redirection vers l'index si introuvable.</returns>
public IActionResult Delete(int id)
{
var commentaire = _commentaires
.FirstOrDefault(c => c.IdCommentaire == id);
var commentaire = this.commentaireRepository.Find(id);
if (commentaire == null)
return NotFound();
var vm = new CommentaireDeleteViewModel
var model = new CommentaireDeleteViewModel
{
IdCommentaire = commentaire.IdCommentaire,
Auteur = commentaire.Auteur,
Contenu = commentaire.Contenu,
DateCreation = commentaire.DateCreation,
TitreLibelle = commentaire.Titre?.Libelle
TitreLibelle = commentaire.Titre?.Libelle,
};
return View(vm);
return this.View(model);
}
/// <summary>
/// Traite la confirmation de suppression d'un commentaire. En cas de succès, redirige vers la liste des commentaires. En cas d'erreur, affiche à nouveau la vue de confirmation avec le message d'erreur.
/// Effectue la suppression réelle du commentaire.
/// </summary>
/// <param name="id">L'identifiant du commentaire à supprimer.</param>
/// <param name="model">Le ViewModel contenant les détails du commentaire à supprimer, utilisé pour afficher les informations en cas d'erreur.</param>
/// <returns>Redirection vers la liste des commentaires en cas de succès, ou la vue de confirmation avec le message d'erreur en cas d'échec.</returns>
/// <param name="model">Le CommentaireDeleteViewModel.</param>
/// <returns>Redirection vers la vue Index après suppression.</returns>
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(int id, CommentaireDeleteViewModel model)
public IActionResult Delete(CommentaireDeleteViewModel model)
{
try
var commentaire = this.commentaireRepository.Find(model.IdCommentaire);
if (commentaire != null)
{
return RedirectToAction();
}
catch (Exception e)
{
// Log de l'erreur
Console.WriteLine(e);
return View(model);
this.commentaireRepository.Delete(commentaire);
}
return this.RedirectToAction("Index");
}
}
}

View File

@@ -1,6 +1,6 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Webzine.Entity;
using Webzine.Entity.Fixtures;
using Webzine.Repository.Contracts;
using Webzine.WebApplication.Areas.Administration.ViewModels;
namespace Webzine.WebApplication.Areas.Administration.Controllers;
@@ -8,31 +8,25 @@ namespace Webzine.WebApplication.Areas.Administration.Controllers;
[Area("Administration")]
public class DashboardController : Controller
{
private readonly ILogger<DashboardController> _logger;
private readonly List<Titre> _titres;
private readonly List<Style> _styles;
private readonly List<Artiste> _artistes;
private readonly ILogger<DashboardController> logger;
private readonly IStyleRepository styleRepository;
private readonly IArtisteRepository artisteRepository;
private readonly ITitreRepository titreRepository;
/// <summary>
/// Initialise une nouvelle instance du <see cref="DashboardController"/>.
/// Les données sont générées dynamiquement via <see cref="DataFactory"/>.
/// Initializes a new instance of the <see cref="DashboardController"/> class.
/// Initialise une nouvelle instance de la classe <see cref="DashboardController"/>.
/// </summary>
/// <param name="logger">Service de journalisation injecté.</param>
public DashboardController(ILogger<DashboardController> logger)
/// <param name="styleRepository">Repository des styles injecté.</param>
public DashboardController(ILogger<DashboardController> logger, IStyleRepository styleRepository, IArtisteRepository artisteRepository, ITitreRepository titreRepository)
{
_logger = logger;
this.logger = logger;
this.styleRepository = styleRepository;
this.artisteRepository = artisteRepository;
this.titreRepository = titreRepository;
_logger.LogInformation("Initialisation du contrôleur TitreController.");
var factory = new DataFactory();
_artistes = factory.GenerateArtists(10);
_styles = factory.GenerateStyles(10);
_titres = factory.GenerateTitres(30, _artistes, _styles);
factory.GenerateCommentaires(50, _titres);
_logger.LogInformation("Données fictives générées avec succès.");
this.logger.LogInformation("Initialisation du contrôleur TitreController.");
}
/// <summary>
@@ -41,42 +35,42 @@ public class DashboardController : Controller
/// <returns>La vue Index du tableau de bord.</returns>
public IActionResult Index()
{
var mostChronicledArtist = _titres
var artisteLePlusChronique = this.titreRepository.FindAll()
.GroupBy(t => t.Artiste)
.OrderByDescending(g => g.Count())
.FirstOrDefault();
.First();
var topArtistAlbums = _titres
var albumLePlusChronique = this.titreRepository.FindAll()
.GroupBy(t => t.Artiste)
.OrderByDescending(g => g.Select(t => t.Album).Distinct().Count())
.FirstOrDefault();
.First();
var mostPlayedTrack = _titres
var musiqueLaPlusJouee = this.titreRepository.FindAll()
.OrderByDescending(t => t.NbLectures)
.FirstOrDefault();
.First();
var model = new DashboardViewModel
{
ArtistCount = _artistes.Count,
NombreArtistes = this.artisteRepository.FindAll().Count(),
MostChronicledArtistName = mostChronicledArtist?.Key.Nom,
ArtisteLePlusChronique = artisteLePlusChronique.Key.Nom,
TopArtistAlbumsName = topArtistAlbums?.Key.Nom,
AlbumLePlusChronique = albumLePlusChronique.Key.Nom,
BiographyCount = _artistes.Count(a => !string.IsNullOrEmpty(a.Biographie)),
NombreBiographies = this.artisteRepository.FindAll().Count(a => !string.IsNullOrEmpty(a.Biographie)),
MostPlayedTrackId = mostPlayedTrack?.IdTitre ?? 0,
MostPlayedTrack = mostPlayedTrack?.Libelle,
IdMusiqueLaPlusJouee = musiqueLaPlusJouee.IdTitre,
MusiqueLaPlusJouee = musiqueLaPlusJouee.Libelle,
TrackCount = _titres.Count,
NombreTitres = this.titreRepository.Count(),
GenreCount = _styles.Count,
NombreGenres = this.styleRepository.FindAll().Count(),
TotalPlays = _titres.Sum(t => t.NbLectures),
NombreLectures = this.titreRepository.FindAll().Sum(t => t.NbLectures),
TotalLikes = _titres.Sum(t => t.NbLikes)
NombreLikes = this.titreRepository.FindAll().Sum(t => t.NbLikes),
};
return View(model);
return this.View(model);
}
}

View File

@@ -1,110 +1,64 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Webzine.Entity;
using Webzine.Entity.Fixtures;
using Webzine.WebApplication.Areas.Administration.ViewModels.Style;
namespace Webzine.WebApplication.Areas.Administration.Controllers
{
using Microsoft.AspNetCore.Mvc;
using Webzine.Repository.Contracts;
using Webzine.WebApplication.Areas.Administration.ViewModels.Style;
/// <summary>
/// Contrôleur pour la gestion des styles dans l'administration du webzine.
/// </summary>
[Area("Administration")]
public class StyleController : Controller
{
private readonly ILogger<StyleController> _logger;
private readonly List<Style> _styles;
private readonly ILogger<StyleController> logger;
private readonly IStyleRepository styleRepository;
/// <summary>
/// Initialise une nouvelle instance du <see cref="StyleController"/>.
/// Les données sont générées dynamiquement via <see cref="DataFactory"/>.
/// Initializes a new instance of the <see cref="StyleController"/> class.
/// Initialise une nouvelle instance de la classe <see cref="StyleController"/>.
/// </summary>
/// <param name="logger">Service de journalisation injecté.</param>
public StyleController(ILogger<StyleController> logger)
/// <param name="styles">Repository des styles injecté.</param>
public StyleController(
ILogger<StyleController> logger,
IStyleRepository styleRepository)
{
_logger = logger;
this.logger = logger;
_logger.LogInformation("Initialisation du contrôleur StyleController.");
this.logger.LogInformation("Initialisation du contrôleur StyleController.");
var factory = new DataFactory();
_styles = factory.GenerateStyles(10);
_logger.LogInformation("Données fictives générées avec succès.");
this.styleRepository = styleRepository;
}
// GET: Administration/Styles
public ActionResult Index()
/// <summary>
/// Affiche la liste des styles dans la vue Index.
/// </summary>
/// <returns>La vue Index avec le ViewModel contenant la liste des styles.</returns>
public IActionResult Index()
{
// Création de données "bouchon" (mock) pour tester l'affichage
var listeStyles = new List<Style>
{
new Style
{
IdStyle = 1,
Libelle = "Rock",
},
new Style
{
IdStyle = 2,
Libelle = "Pop",
},
new Style
{
IdStyle = 3,
Libelle = "Jazz",
},
};
var listeStyles = this.styleRepository.FindAll().Take(10);
// Initialisation du ViewModel
var viewModel = new StyleViewModel
{
Styles = listeStyles,
};
return View(viewModel);
return this.View(listeStyles);
}
// GET: Administration/Styles/Create
public ActionResult Create()
/// <summary>
/// Affiche la vue de création d'un nouveau style.
/// </summary>
/// <returns>La vue Create pour ajouter un nouveau style.</returns>
public IActionResult Create()
{
return View();
return this.View();
}
// POST: Administration/Styles/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(StyleCreateViewModel model)
/// <summary>
/// Affiche la vue de confirmation de suppression d'un style, en récupérant les détails du style à supprimer à partir de l'identifiant fourni.
/// </summary>
/// <param name="id">L'identifiant du style à supprimer.</param>
/// <returns>La vue de confirmation de suppression avec le ViewModel contenant les détails du style à supprimer, ou une redirection vers l'index si le style n'existe pas.</returns>
public IActionResult Delete(int id)
{
if (!ModelState.IsValid)
{
return View(model);
}
try
{
_logger.LogInformation("Nouveau style créé : {Libelle}", model.Libelle);
return RedirectToAction(nameof(Index));
}
catch (Exception e)
{
_logger.LogError(e, "Erreur lors de la création du style");
ModelState.AddModelError("", "Une erreur est survenue lors de la création.");
return View(model);
}
}
// GET: Administration/Styles/Delete/5
public ActionResult Delete(int id)
{
var style = this._styles
.FirstOrDefault(c => c.IdStyle == id);
if (style == null)
{
return this.NotFound();
}
var style = this.styleRepository.Find(id);
var vm = new StyleDeleteViewModel
{
@@ -112,69 +66,44 @@ namespace Webzine.WebApplication.Areas.Administration.Controllers
Libelle = style.Libelle,
};
return View(vm);
return this.View(vm);
}
// POST: Administration/Styles/Delete/5
/// <summary>
/// Méthode POST pour supprimer un style.
/// </summary>
/// <param name="model">Le style à supprimer.</param>
/// <returns>Redirige vers la page d'index d'admin style.</returns>
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(int id, StyleDeleteViewModel model)
public IActionResult Delete(StyleEditViewModel model)
{
try
var style = this.styleRepository.Find(model.IdStyle);
if (style != null)
{
return RedirectToAction(nameof(Index));
}
catch (Exception e)
{
// Log de l'erreur
_logger.LogError(e, "Erreur lors de la suppression du style avec l'ID {StyleId}", id);
return View(model);
this.styleRepository.Delete(style);
}
return this.RedirectToAction("Index");
}
// GET: Administration/Styles/Edit/5
/// <summary>
/// Affiche la vue d'édition d'un style existant, en récupérant les détails du style à éditer à partir de l'identifiant fourni.
/// </summary>
/// <param name="id">L'identifiant du style à éditer.</param>
/// <returns>La vue d'édition avec le ViewModel contenant les détails du style à éditer, ou une redirection vers l'index si le style n'existe pas.</returns>
[HttpGet]
public ActionResult Edit(int id)
public IActionResult Edit(int id)
{
// Recherche du style (simulation avec la liste _styles)
var style = _styles.FirstOrDefault(s => s.IdStyle == id);
var style = this.styleRepository.Find(id);
if (style == null)
{
return NotFound();
}
// Mapping vers le ViewModel
var model = new StyleEditViewModel
{
IdStyle = style.IdStyle,
Libelle = style.Libelle
Libelle = style.Libelle,
};
return View(model);
}
// POST: Administration/Styles/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(int id, StyleEditViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
try
{
_logger.LogInformation("Style {Id} mis à jour : {Libelle}", id, model.Libelle);
return RedirectToAction(nameof(Index));
}
catch (Exception e)
{
_logger.LogError(e, "Erreur lors de la modification du style {Id}", id);
ModelState.AddModelError("", "Une erreur est survenue lors de la modification.");
return View(model);
}
return this.View(model);
}
}
}
}

View File

@@ -1,115 +1,93 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Webzine.Entity;
using Webzine.Entity.Fixtures;
using Webzine.Repository.Contracts;
using Webzine.WebApplication.Areas.Administration.ViewModels.Titre;
namespace Webzine.WebApplication.Areas.Administration.Controllers;
/// <summary>
/// Contrôleur pour la gestion des titres en administration. Ce contrôleur gère les opérations de création, modification, suppression et affichage des titres dans l'interface d'administration du webzine. Chaque action du contrôleur prépare un ViewModel spécifique pour la vue correspondante, permettant ainsi une séparation claire entre la logique métier et la présentation des données.
/// </summary>
[Area("Administration")]
public class TitreController : Controller
{
private readonly ILogger<TitreController> _logger;
private readonly List<Titre> _titres;
private readonly List<Style> _styles;
private readonly List<Artiste> _artistes;
private readonly ILogger<TitreController> logger;
private readonly ITitreRepository titreRepository;
private readonly IArtisteRepository artisteRepository;
private readonly IStyleRepository styleRepository;
/// <summary>
/// Initialise une nouvelle instance du <see cref="TitreController"/>.
/// Les données sont générées dynamiquement via <see cref="DataFactory"/>.
/// Initializes a new instance of the <see cref="TitreController"/> class.
/// Initialise une nouvelle instance de la classe <see cref="TitreController"/>.
/// </summary>
/// <param name="logger">Service de journalisation injecté.</param>
public TitreController(ILogger<TitreController> logger)
/// <param name="titreRepository">Repository des titres injecté pour accéder aux données des titres.</param>
/// <param name="artisteRepository">Repository des artistes injecté pour accéder aux données des artistes, nécessaires pour les associations avec les titres.</param>
/// <param name="styleRepository">Repository des styles injecté pour accéder aux données des styles, nécessaires pour les associations avec les titres.</param>
public TitreController(ILogger<TitreController> logger, ITitreRepository titreRepository, IArtisteRepository artisteRepository, IStyleRepository styleRepository)
{
_logger = logger;
_logger.LogInformation("Initialisation du contrôleur TitreController pour l'Administration.");
var factory = new DataFactory();
_artistes = factory.GenerateArtists(10);
_styles = factory.GenerateStyles(10);
_titres = factory.GenerateTitres(30, _artistes, _styles);
factory.GenerateCommentaires(50, _titres);
_logger.LogInformation("Données fictives générées avec succès.");
this.logger = logger;
this.titreRepository = titreRepository;
this.artisteRepository = artisteRepository;
this.styleRepository = styleRepository;
}
/// <summary>
/// Affiche la liste des titres dans la vue Index.
/// </summary>
/// <returns>La vue Index avec le ViewModel contenant la liste des titres.</returns>
public ActionResult Index()
public IActionResult Index()
{
var model = _titres.Select(t => new AdminTitreList
IEnumerable<Titre> titres = this.titreRepository.FindAll().Take(10);
var model = titres.Select(t => new AdminTitreList
{
Id = t.IdTitre,
Artiste = t.Artiste?.Nom,
Nom = t.Artiste.Nom,
Titre = t.Libelle,
Duree = TimeSpan.FromSeconds(t.Duree).ToString(@"mm\:ss"),
DateSortie = t.DateSortie,
NbLectures = t.NbLectures,
NbLikes = t.NbLikes,
NbCommentaires = t.Commentaires?.Count ?? 0
NbCommentaires = t.Commentaires?.Count ?? 0,
}).ToList();
return View(model);
return this.View(model);
}
/// <summary>
/// Affiche le formulaire de création d'un nouveau titre dans la vue Create.
/// </summary>
/// <returns>La vue Create avec le ViewModel contenant les listes déroulantes pour les artistes et les styles.</returns>
public ActionResult Create()
public IActionResult Create()
{
var model = new AdminTitreForm
{
Artistes = _artistes.Select(a => new SelectListItem
Artistes = this.artisteRepository.FindAll().Select(a => new SelectListItem
{
Value = a.IdArtiste.ToString(),
Text = a.Nom
Text = a.Nom,
}).ToList(),
AllStyles = _styles.Select(s => new SelectListItem
AllStyles = this.styleRepository.FindAll().Select(s => new SelectListItem
{
Value = s.IdStyle.ToString(),
Text = s.Libelle
}).ToList()
Text = s.Libelle,
}).ToList(),
};
return View(model);
}
/// <summary>
/// Traite la soumission du formulaire de création d'un nouveau titre. Actuellement, cette méthode est un stub qui redirige vers l'index sans effectuer de logique de création réelle.
/// </summary>
/// <param name="collection">Le formulaire soumis contenant les données du nouveau titre. Actuellement, ce paramètre n'est pas utilisé.</param>
/// <returns>Redirige vers l'action Index après la soumission du formulaire. En cas d'erreur, retourne la vue Create pour permettre à l'utilisateur de corriger les données.</returns>
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(IFormCollection collection)
{
try
{
return RedirectToAction(nameof(Index));
}
catch
{
return View();
}
return this.View(model);
}
/// <summary>
/// Affiche le formulaire de modification d'un titre existant dans la vue Edit, en préremplissant les champs avec les données du titre sélectionné. Les listes déroulantes pour les artistes et les styles sont également remplies pour permettre à l'utilisateur de modifier ces associations.
/// </summary>
/// <param name="id">L'identifiant du titre à modifier, utilisé pour récupérer les données du titre à partir de la liste des titres générés.</param>
/// <returns>La vue Edit avec le ViewModel contenant les données du titre à modifier, ainsi que les listes déroulantes pour les artistes et les styles. En cas d'erreur, retourne une réponse NotFound si le titre n'existe pas.</returns>
public ActionResult Edit(int id)
/// <returns>La vue Edit avec le ViewModel contenant les données du titre à modifier, ainsi que les listes déroulantes pour les artistes et les styles. </returns>
public IActionResult Edit(int id)
{
var titre = _titres.First(t => t.IdTitre == id);
var titre = this.titreRepository.Find(id);
var model = new AdminTitreForm
{
@@ -126,74 +104,55 @@ public class TitreController : Controller
NbLikes = titre.NbLikes,
Styles = titre.Styles.Select(s => s.IdStyle).ToList(),
Artistes = _artistes.Select(a => new SelectListItem
Artistes = this.artisteRepository.FindAll().Select(a => new SelectListItem
{
Value = a.IdArtiste.ToString(),
Text = a.Nom
Text = a.Nom,
}).ToList(),
AllStyles = _styles.Select(s => new SelectListItem
AllStyles = this.styleRepository.FindAll().Select(s => new SelectListItem
{
Value = s.IdStyle.ToString(),
Text = s.Libelle
}).ToList()
Text = s.Libelle,
}).ToList(),
};
return View(model);
}
/// <summary>
/// Traite la soumission du formulaire de modification d'un titre existant. Actuellement, cette méthode est un stub qui redirige vers l'index sans effectuer de logique de modification réelle.
/// </summary>
/// <param name="id">L'identifiant du titre à modifier, utilisé pour identifier le titre à mettre à jour. Actuellement, ce paramètre n'est pas utilisé dans la logique de traitement.</param>
/// <param name="collection">Le formulaire soumis contenant les données modifiées du titre. Actuellement, ce paramètre n'est pas utilisé dans la logique de traitement.</param>
/// <returns>Redirige vers l'action Index après la soumission du formulaire. En cas d'erreur, retourne la vue Edit pour permettre à l'utilisateur de corriger les données.</returns>
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(int id, IFormCollection collection)
{
try
{
return RedirectToAction(nameof(Index));
}
catch
{
return View();
}
return this.View(model);
}
/// <summary>
/// Affiche la vue de confirmation de suppression d'un titre, en récupérant les détails du titre à supprimer à partir de l'identifiant fourni. Le ViewModel contient les informations essentielles du titre, telles que le libellé et le nom de l'artiste, pour permettre à l'utilisateur de confirmer la suppression.
/// </summary>
/// <param name="id">L'identifiant du titre à supprimer, utilisé pour récupérer les données du titre à partir de la liste des titres générés.</param>
/// <returns>La vue de confirmation de suppression avec le ViewModel contenant les détails du titre à supprimer, ou une réponse NotFound si le titre n'existe pas.</returns>
public ActionResult Delete(int id)
/// <returns>La vue de confirmation de suppression avec le ViewModel contenant les détails du titre à supprimer.</returns>
public IActionResult Delete(int id)
{
var titre = _titres.First(t => t.IdTitre == id);
var titre = this.titreRepository.Find(id);
var model = new AdminTitreDelete
{
Id = titre.IdTitre,
Titre = titre.Libelle,
Artiste = titre.Artiste?.Nom
Artiste = titre.Artiste.Nom,
};
return View(model);
return this.View(model);
}
/// <summary>
/// Traite la confirmation de suppression d'un titre. En cas de succès, redirige vers la liste des titres après avoir supprimé le titre de la liste. En cas d'erreur, affiche à nouveau la vue de confirmation avec le message d'erreur.
/// Méthode POST pour supprimer un titre.
/// </summary>
/// <param name="model">Le ViewModel contenant les détails du titre à supprimer, utilisé pour identifier le titre à supprimer et pour afficher les informations en cas d'erreur.</param>
/// <returns>Redirection vers la liste des titres en cas de succès, ou la vue de confirmation avec le message d'erreur en cas d'échec.</returns>
/// <param name="model">Le titre à supprimer.</param>
/// <returns>Redirige vers la page d'index d'admin titre.</returns>
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(AdminTitreDelete model)
public IActionResult Delete(AdminTitreDelete model)
{
var titre = _titres.First(t => t.IdTitre == model.Id);
var titre = this.titreRepository.Find(model.Id);
if (titre != null)
{
this.titreRepository.Delete(titre);
}
_titres.Remove(titre);
return RedirectToAction(nameof(Index));
return this.RedirectToAction("Index");
}
}

View File

@@ -2,6 +2,7 @@
{
/// <summary>
/// ViewModel pour la création et la modification d'un artiste dans l'administration.
/// Ne contient pas les titres de l'artiste.
/// </summary>
public class AdminArtisteForm
{
@@ -9,13 +10,15 @@
/// Définit l'identifiant de l'artiste.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Définit le nom de l'artiste.
/// </summary>
public string Nom { get; set; }
/// <summary>
/// Définit la biographie de l'artiste.
/// </summary>
public string Biographie { get; set; }
}
}
}

View File

@@ -1,14 +0,0 @@
namespace Webzine.WebApplication.Areas.Administration.ViewModels.Artiste
{
using Webzine.Entity;
/// <summary>
/// ViewModel pour afficher un groupe d'artiste.
/// </summary>
public class GroupeArtisteViewModel
{
/// <summary>
/// Liste d'artistes.
/// </summary>
public IEnumerable<Artiste> Artistes { get; set; } = new List<Artiste>();
}
}

View File

@@ -1,5 +1,5 @@
// <copyright file="CommentaireViewModel.cs" company="Webzine">
// Copyright (c) Webzine. All rights reserved.
// <copyright file="CommentaireViewModel.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Webzine.WebApplication.Areas.Administration.ViewModels.Commentaire

View File

@@ -8,49 +8,50 @@ public class DashboardViewModel
/// <summary>
/// Définit le nombre total d'artistes chroniqués dans le webzine.
/// </summary>
public int ArtistCount { get; set; }
public int NombreArtistes { get; set; }
/// <summary>
/// Définit le nom de l'artiste le plus chroniqué dans le webzine.
/// </summary>
public string MostChronicledArtistName { get; set; }
public string ArtisteLePlusChronique { get; set; }
/// <summary>
/// Définit le nom de l'album le plus chroniqué dans le webzine.
/// </summary>
public string TopArtistAlbumsName { get; set; }
public string AlbumLePlusChronique { get; set; }
/// <summary>
/// Définit le nombre total de biographies d'artistes dans le webzine.
/// </summary>
public int BiographyCount { get; set; }
public int NombreBiographies { get; set; }
/// <summary>
/// Définit l'identifiant de la biographie d'artiste la plus lue dans le webzine.
/// </summary>
public int MostPlayedTrackId { get; set; }
public int IdMusiqueLaPlusJouee { get; set; }
/// <summary>
/// Définit le nom de la biographie d'artiste la plus lue dans le webzine.
/// </summary>
public string MostPlayedTrack { get; set; }
public string MusiqueLaPlusJouee { get; set; }
/// <summary>
/// Définit le nombre total de titres chroniqués dans le webzine.
/// </summary>
public int TrackCount { get; set; }
public int NombreTitres { get; set; }
/// <summary>
/// Définit le nombre total de genres musicaux chroniqués dans le webzine.
/// </summary>
public int GenreCount { get; set; }
public int NombreGenres { get; set; }
/// <summary>
/// Définit le nombre total de chroniques d'albums dans le webzine.
/// </summary>
public int TotalPlays { get; set; }
public int NombreLectures { get; set; }
/// <summary>
/// Définit le nombre total de likes sur les chroniques d'albums dans le webzine.
/// </summary>
public int TotalLikes { get; set; }
public int NombreLikes { get; set; }
}

View File

@@ -1,20 +1,17 @@
// <copyright file="StyleDeleteViewModel.cs" company="Webzine">
// Copyright (c) Webzine. Tout droit réservé.
// <copyright file="StyleCreateViewModel.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Webzine.WebApplication.Areas.Administration.ViewModels.Style
{
/// <summary>
/// ViewModel pour la création d'un style en administration.
/// </summary>
public class StyleCreateViewModel
{
/// <summary>
/// Obtient ou définit le libellé du style.
/// </summary>
public string Libelle { get; set; }
}
}
}

View File

@@ -1,26 +1,22 @@
// <copyright file="StyleDeleteViewModel.cs" company="Webzine">
// Copyright (c) Webzine. Tout droit réservé.
// <copyright file="StyleDeleteViewModel.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Webzine.WebApplication.Areas.Administration.ViewModels.Style
{
/// <summary>
/// ViewModel pour la suppression d'un style en administration.
/// </summary>
public class StyleDeleteViewModel
{
/// <summary>
/// Obtient ou définit l'identifiant du style à supprimer.
/// </summary>
public int IdStyle { get; set; }
/// <summary>
/// Obtient ou définit le libellé du style.
/// </summary>
public string Libelle { get; set; }
}
}
}

View File

@@ -1,27 +1,22 @@
// <copyright file="StyleDeleteViewModel.cs" company="Webzine">
// Copyright (c) Webzine. Tout droit réservé.
// <copyright file="StyleEditViewModel.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Webzine.WebApplication.Areas.Administration.ViewModels.Style
{
/// <summary>
/// ViewModel pour la modification d'un style en administration.
/// </summary>
public class StyleEditViewModel
{
/// <summary>
/// Obtient ou définit le libellé du style.
/// </summary>
public int IdStyle { get; set; }
/// <summary>
/// Obtient ou définit le libellé du style.
/// </summary>
public string Libelle { get; set; }
}
}
}

View File

@@ -1,17 +0,0 @@
// <copyright file="StyleViewModel.cs" company="Webzine">
// Copyright (c) Webzine. Tout droit réservé.
// </copyright>
namespace Webzine.WebApplication.Areas.Administration.ViewModels.Style
{
/// <summary>
/// ViewModel pour afficher la liste des commentaires en administration.
/// </summary>
public class StyleViewModel
{
/// <summary>
/// Obtient ou définit la liste des commentaires.
/// </summary>
public IEnumerable<Entity.Style> Styles { get; set; } = new List<Entity.Style>();
}
}

View File

@@ -65,7 +65,7 @@ public class AdminTitreForm
/// <summary>
/// Définit la liste des identifiants des styles associés au titre.
/// </summary>
public List<int> Styles { get; set; } = new();
public List<int> Styles { get; set; } = new ();
/// <summary>
/// Définit la liste des artistes disponibles pour la sélection dans le formulaire de création ou de modification d'un titre.

View File

@@ -13,7 +13,7 @@ namespace Webzine.WebApplication.Areas.Administration.ViewModels.Titre
/// <summary>
/// Définit le nom de l'artiste associé au titre.
/// </summary>
public string Artiste { get; set; }
public string Nom { get; set; }
/// <summary>
/// Définit le titre du titre.

View File

@@ -1,4 +1,4 @@
@model Webzine.WebApplication.Areas.Administration.ViewModels.Artiste.GroupeArtisteViewModel
@model IEnumerable<Webzine.Entity.Artiste>
@{
ViewData["Title"] = "Artiste";
@@ -16,12 +16,12 @@
<thead class="table-active">
<tr>
<th scope="col" class="p-2">Nom</th>
<th scope="col" class="text-center p-2" style="width: 100px;">Actions</th>
<th scope="col" class="text-center p-2">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var artiste in Model.Artistes)
@foreach (var artiste in Model)
{
<tr class="align-middle">
<td class="p-2">
@@ -29,13 +29,13 @@
</td>
<td class="text-center p-2">
<a asp-action="Edit" asp-route-id="@artiste.IdArtiste"
class="text-primary">
<a asp-action="Edit" asp-route-id="@artiste.IdArtiste">
<i class="fa fa-edit"></i>
</a>
<a asp-action="Delete" asp-route-id="@artiste.IdArtiste"
class="text-primary">
<a asp-action="Delete"
asp-controller="Artiste"
asp-route-id="@artiste.IdArtiste">
<i class="fa fa-trash"></i>
</a>

View File

@@ -2,7 +2,7 @@
<div class="container">
<!-- ARTISTE -->
<div class="row mb-3 align-items-center">
<div class="row mb-3">
<label class="col-md-3 col-form-label">Nom de l'artiste<span class="text-danger">*</span></label>
<div class="col-md-9">
<input asp-for="Nom" class="form-control" />
@@ -10,14 +10,15 @@
</div>
<!-- BIOGRAPHIE -->
<div class="row mb-3 align-items-center">
<label class="col-md-3 col-form-label">Biographie<span class="text-danger">*</span></label>
<div class="row mb-3">
<label class="col-md-3 col-form-label">Biographie</label>
<div class="col-md-9">
<input asp-for="Biographie" class="form-control"/>
<textarea asp-for="Biographie" class="form-control" rows="5"></textarea>
</div>
</div>
<!-- BOUTONS -->
<div class="row mt-4">
<div class="col-md-9 offset-md-3">

View File

@@ -16,11 +16,11 @@
<div class="mb-4">
<h4>@Model.Contenu</h4>
<div class="text-muted">
<blockquote>
— <strong>@Model.Auteur</strong>
le @Model.DateCreation.ToString("dd/MM/yyyy HH:mm:ss")
le @Model.DateCreation.ToString("dd/MM/yyyy HH:mm:ss")
sur <em>@Model.TitreLibelle</em>
</div>
</blockquote>
</div>
<form asp-action="Delete" method="post">

View File

@@ -14,10 +14,10 @@
<thead class="table-active">
<tr>
<th scope="col">Titre</th>
<th scope="col">Auteur</th>
<th scope="col">Nom</th>
<th scope="col">Commentaire</th>
<th scope="col">Date de création</th>
<th scope="col" class="text-center p-2" style="width: 100px" ;>Actions</th>
<th scope="col" class="text-center p-2">Actions</th>
</tr>
</thead>
<tbody>
@@ -25,7 +25,9 @@
{
<tr class="align-middle">
<td>
@commentaire.Titre.Libelle
<a asp-action="Details" asp-controller="Titre" asp-route-id="@commentaire.Titre.IdTitre">
@commentaire.Titre.Libelle
</a>
</td>
<td>
@commentaire.Auteur

View File

@@ -2,7 +2,7 @@
<h1 class="mb-4">Tableau de bord</h1>
<hr />
<hr/>
<div class="container">
@@ -11,43 +11,39 @@
<!-- ARTISTS -->
<div class="col-md-4">
<a asp-area="Administration"
asp-controller="Artiste"
class="text-decoration-none">
asp-controller="Artiste">
<div class="ratio ratio-4x3">
<div class="card shadow-sm p-4 bg-light h-100 dashboard-card">
<i class="fa fa-users fa-3x text-primary mb-3"></i>
<div class="py-5 bg-light rounded-3 d-flex flex-column justify-content-center align-items-center text-primary">
<i class="fa fa-users fa-5x text-primary mb-3"></i>
<h3 class="text-primary">
@Model.ArtistCount
</h3>
<p class="text-primary">
artistes
</p>
<h2>
@Model.NombreArtistes
</h2>
<p>
artistes
</p>
</div>
</div>
</a>
</div>
<!-- L'ARTIST LE PLUS CHRONICLED -->
<!-- L'ARTIST LE PLUS CHRONIQUE -->
<div class="col-md-4">
<a asp-area=""
asp-controller="Artiste"
asp-route-nom="@Model.MostChronicledArtistName"
class="text-decoration-none">
asp-route-nom="@Model.ArtisteLePlusChronique">
<div class="ratio ratio-4x3">
<div class="py-5 bg-light rounded-3 d-flex flex-column justify-content-center align-items-center text-primary">
<i class="fa fa-user fa-5x text-primary mb-3"></i>
<div class="card shadow-sm p-4 bg-light h-100 dashboard-card">
<i class="fa fa-user fa-3x text-primary mb-3"></i>
<h2>
@Model.ArtisteLePlusChronique
</h2>
<h3 class="text-primary">
@Model.MostChronicledArtistName
</h3>
<p class="text-primary">
artiste le plus chroniqué
</p>
<p>artiste le plus chroniqué</p>
</div>
</div>
</a>
</div>
@@ -55,42 +51,42 @@
<div class="col-md-4">
<a asp-area=""
asp-controller="Artiste"
asp-route-nom="@Model.TopArtistAlbumsName"
class="text-decoration-none">
asp-route-nom="@Model.AlbumLePlusChronique">
<div class="ratio ratio-4x3">
<div class="card shadow-sm p-4 bg-light h-100 dashboard-card">
<i class="fa fa-trophy fa-3x text-primary mb-3"></i>
<div class="py-5 bg-light rounded-3 d-flex flex-column justify-content-center align-items-center text-primary">
<i class="fa fa-trophy fa-5x text-primary mb-3"></i>
<h3 class="text-primary">
@Model.TopArtistAlbumsName
</h3>
<h2>
@Model.AlbumLePlusChronique
</h2>
<p class="text-primary">
<p>
artiste avec le plus d'albums distincts
</p>
</div>
</div>
</a>
</div>
<!-- BIOGRAPHIES -->
<div class="col-md-4">
<a asp-area="Administration"
asp-controller="Titre"
class="text-decoration-none">
asp-controller="Titre">
<div class="ratio ratio-4x3">
<div class="card shadow-sm p-4 bg-light h-100 dashboard-card">
<i class="fa fa-book fa-3x text-primary mb-3"></i>
<div class="py-5 bg-light rounded-3 d-flex flex-column justify-content-center align-items-center text-primary">
<i class="fa fa-book fa-5x text-primary mb-3"></i>
<h3 class="text-primary">
@Model.BiographyCount
</h3>
<h2>
@Model.NombreBiographies
</h2>
<p class="text-primary">
<p>
biographies d'artistes
</p>
</div>
</div>
</a>
</div>
@@ -99,94 +95,100 @@
<a asp-area=""
asp-controller="Titre"
asp-action="Details"
asp-route-id="@Model.MostPlayedTrackId"
class="text-decoration-none">
asp-route-id="@Model.IdMusiqueLaPlusJouee">
<div class="ratio ratio-4x3">
<div class="card shadow-sm p-4 bg-light h-100 dashboard-card">
<i class="fa fa-compact-disc fa-3x text-primary mb-3"></i>
<div class="py-5 bg-light rounded-3 d-flex flex-column justify-content-center align-items-center text-primary">
<i class="fa fa-compact-disc fa-5x text-primary mb-3"></i>
<h4 class="text-primary">
@Model.MostPlayedTrack
</h4>
<h2>
@Model.MusiqueLaPlusJouee
</h2>
<p class="text-primary">
<p>
titre le plus lu
</p>
</div>
</div>
</a>
</div>
<!-- TITRE NOMBRE -->
<div class="col-md-4">
<a asp-area="Administration"
asp-controller="Titre"
class="text-decoration-none">
asp-controller="Titre">
<div class="ratio ratio-4x3">
<div class="card shadow-sm p-4 bg-light h-100 dashboard-card">
<i class="fa fa-music fa-3x text-primary mb-3"></i>
<div class="py-5 bg-light rounded-3 d-flex flex-column justify-content-center align-items-center text-primary">
<i class="fa fa-music fa-5x text-primary mb-3"></i>
<h3 class="text-primary">
@Model.TrackCount
</h3>
<h2>
@Model.NombreTitres
</h2>
<p class="text-primary">
<p>
titres
</p>
</div>
</div>
</a>
</div>
<!-- GENRES -->
<div class="col-md-4">
<a asp-area="Administration"
asp-controller="Styles"
class="text-decoration-none">
asp-controller="Style">
<div class="ratio ratio-4x3">
<div class="card shadow-sm p-4 bg-light h-100 dashboard-card">
<i class="fa fa-tags fa-3x text-primary mb-3"></i>
<div class="py-5 bg-light rounded-3 d-flex flex-column justify-content-center align-items-center text-primary">
<i class="fa fa-tags fa-5x text-primary mb-3"></i>
<h3 class="text-primary">
@Model.GenreCount
</h3>
<h2>
@Model.NombreGenres
</h2>
<p class="text-primary">
<p>
styles de musique
</p>
</div>
</div>
</a>
</div>
<!-- NOMBRE DE LECTURES -->
<div class="col-md-4">
<div class="card shadow-sm p-4 bg-light h-100">
<i class="fa fa-eye fa-3x text-dark mb-3"></i>
<div class="ratio ratio-4x3">
<h3>
@Model.TotalPlays
</h3>
<div class="py-5 bg-light rounded-3 d-flex flex-column justify-content-center align-items-center">
<i class="fa fa-eye fa-5x text-dark mb-3"></i>
<p>
lectures
</p>
<h2>
@Model.NombreLectures
</h2>
<p>
lectures
</p>
</div>
</div>
</div>
<!-- TOTAL LIKES -->
<div class="col-md-4">
<div class="card shadow-sm p-4 bg-light h-100">
<i class="fa fa-thumbs-up fa-3x text-dark mb-3"></i>
<div class="ratio ratio-4x3">
<h3>
@Model.TotalLikes
</h3>
<div class="py-5 bg-light rounded-3 d-flex flex-column justify-content-center align-items-center">
<i class="fa fa-thumbs-up fa-5x text-dark mb-3"></i>
<h2>
@Model.NombreLikes
</h2>
<p>
likes
</p>
</div>
</div>
</div>
</div>

View File

@@ -22,7 +22,7 @@
@* Input *@
<div class="me-3">
<input asp-for="Libelle" class="form-control" style="width: 250px;" />
<input asp-for="Libelle" class="form-control" />
</div>
@* Bouton *@

View File

@@ -17,10 +17,6 @@
@* On affiche le Libellé en gros *@
<h4>@Model.Libelle</h4>
@* On affiche l'ID discrètement en dessous *@
<div class="text-muted">
Identifiant technique : @Model.IdStyle
</div>
</div>
<form asp-action="Delete" method="post">

View File

@@ -25,7 +25,7 @@
@* Input *@
<div class="me-3">
<input asp-for="Libelle" class="form-control" style="width: 250px;" />
<input asp-for="Libelle" class="form-control" />
</div>
@* Bouton *@

View File

@@ -1,4 +1,4 @@
@model Webzine.WebApplication.Areas.Administration.ViewModels.Style.StyleViewModel
@model IEnumerable<Webzine.Entity.Style>
@{
ViewData["Title"] = "Styles";
@@ -20,23 +20,23 @@
<thead class="table-active">
<tr>
<th scope="col" class="p-2">Libellé</th>
<th scope="col" class="text-center p-2" style="width: 100px;">Actions</th>
<th scope="col" class="text-center p-2">Actions</th>
</tr>
</thead>
<tbody>
@if (Model.Styles != null && Model.Styles.Any())
@if ( Model.Any())
{
@foreach (Webzine.Entity.Style style in Model.Styles)
@foreach (Webzine.Entity.Style style in Model)
{
<tr class="align-middle">
<td class="p-2">
<tr >
<td class="p-2 w-75">
@style.Libelle
</td>
<td class="text-center p-2">
<td class="text-center w-auto p-2">
<a asp-action="Edit" asp-route-id="@style.IdStyle" class="text-primary me-2" title="Éditer">
<i class="fas fa-edit"></i>
</a>
<a asp-action="Delete" asp-route-id="@style.IdStyle" class="text-primary" title="Supprimer">
<a asp-action="Delete" asp-route-id="@style.IdStyle" title="Supprimer">
<i class="fas fa-trash"></i>
</a>
</td>

View File

@@ -33,7 +33,7 @@
@foreach (var item in Model)
{
<tr>
<td>@item.Artiste</td>
<td>@item.Nom</td>
<td>@item.Titre</td>
<td>@item.Duree</td>
<td>@item.DateSortie.ToString("dd/MM/yyyy")</td>

View File

@@ -62,7 +62,7 @@
<!-- JAQUETTE -->
<div class="row mb-3 align-items-center">
<label class="col-md-3 col-form-label">Jaquette<span class="text-danger">*</span></label>
<label class="col-md-3 col-form-label">Jaquette de l'album<span class="text-danger">*</span></label>
<div class="col-md-9">
<input asp-for="UrlJaquette"
class="form-control"/>
@@ -102,13 +102,13 @@
</div>
<!-- LECTURES / LIKES (AFFICHAGE UNIQUEMENT) -->
<div class="row mb-4 align-items-center">
<div class="row align-items-center">
<label class="col-md-3 col-form-label">Nb de lectures<span class="text-danger">*</span></label>
<div class="col-md-3">
@Model.NbLectures
</div>
</div>
<div class="row mb-4 align-items-center">
<div class="row align-items-center">
<label class="col-md-3 col-form-label">Nb de likes<span class="text-danger">*</span></label>
<div class="col-md-3">
@Model.NbLikes

View File

@@ -1,53 +1,62 @@
using Microsoft.AspNetCore.Mvc;
using Webzine.Repository.Fake;
using Webzine.WebApplication.ViewModels.Accueil;
namespace Webzine.WebApplication.Controllers
namespace Webzine.WebApplication.Controllers
{
using Microsoft.AspNetCore.Mvc;
using Webzine.Repository.Contracts;
using Webzine.WebApplication.ViewModels.Accueil;
/// <summary>
/// Permet de retourner la page d'accueil avec tous les éléments.
/// </summary>
public class AccueilController : Controller
{
// Injection du logger via le constructeur
private readonly ILogger<AccueilController> _logger;
private readonly IConfiguration _configuration;
private readonly ILogger<AccueilController> logger;
private readonly IConfiguration configuration;
private readonly ITitreRepository titreRepository;
/// <summary>
/// Initialise une nouvelle instance du <see cref="AccueilController"/> avec un service de journalisation et de configuration injectés.
/// Initializes a new instance of the <see cref="AccueilController"/> class.
/// Initialise une nouvelle instance de la classe <see cref="AccueilController"/>.
/// </summary>
/// <param name="logger">Service de journalisation injecté pour enregistrer les événements et les erreurs.</param>
/// <param name="configuration">Service d'injection de configuration pour accéder aux paramètres de l'application.</param>
public AccueilController(ILogger<AccueilController> logger, IConfiguration configuration)
public AccueilController(
ILogger<AccueilController> logger,
IConfiguration configuration,
ITitreRepository titreRepository)
{
_logger = logger;
_configuration = configuration;
this._logger.LogDebug(1, "initialisation du AccueilController");
this.logger = logger;
this.configuration = configuration;
this.titreRepository = titreRepository;
this.logger.LogDebug(1, "initialisation du AccueilController");
this.titreRepository = titreRepository;
}
/// <summary>
/// Affiche la page d'accueil du webzine, présentant les derniers titres et les titres les plus populaires.
/// </summary>
/// <returns>La vue Index avec le ViewModel contenant les listes de titres à afficher.</returns>
public ActionResult Index()
public IActionResult Index()
{
_logger.LogInformation("Arrivée sur la page d'accueil");
this.logger.LogInformation("Arrivée sur la page d'accueil");
var derniereChronique = _configuration.GetValue<int>("Webzine:NombreDerniereChronique");
var topTitres = _configuration.GetValue<int>("Webzine:NombreDeTopTitres");
var titres = FakeDataFactory.GetTitres();
var derniereChronique = this.configuration.GetValue<int>("Webzine:NombreDerniereChronique");
var nbTopTitres = this.configuration.GetValue<int>("Webzine:NombreDeTopTitres");
var titres = this.titreRepository.FindAll();
var vm = new AccueilIndexViewModel
{
DerniersTitres = titres
.OrderByDescending(t => t.DateCreation)
.Take(derniereChronique)
.ToList(),
DerniersTitres = titres.Take(derniereChronique).ToList(),
TopTitres = titres
.OrderByDescending(t => t.NbLikes)
.Take(topTitres)
.ToList()
.Take(nbTopTitres)
.ToList(),
};
return View(vm);
return this.View(vm);
}
}
}
}

View File

@@ -4,26 +4,17 @@ namespace Webzine.WebApplication.Controllers;
public class ApiController : ControllerBase
{
private readonly ILogger<ApiController> _logger;
private readonly ILogger<ApiController> logger;
/// <summary>
/// Initialise une nouvelle instance du <see cref="ApiController"/> avec un service de journalisation injecté.
/// Initializes a new instance of the <see cref="ApiController"/> class.
/// Initialise une nouvelle instance de la classe <see cref="ApiController"/>.
/// </summary>
/// <param name="logger">Service de journalisation injecté pour enregistrer les événements et les erreurs.</param>
public ApiController(ILogger<ApiController> logger)
{
this._logger = logger;
this._logger.LogDebug(1, "initialisation du ApiController");
}
/// <summary>
/// Endpoint de test pour vérifier que l'API fonctionne correctement. Retourne une chaîne de caractères "Hello World !".
/// </summary>
/// <returns>Une chaîne de caractères "Hello World !".</returns>
[HttpGet]
public string HelloWorld()
{
return "Hello World !";
this.logger = logger;
this.logger.LogDebug(1, "initialisation du ApiController");
}
/// <summary>
@@ -33,12 +24,12 @@ public class ApiController : ControllerBase
[HttpGet]
public IActionResult Version()
{
this._logger.LogInformation("Get Version was called");
this.logger.LogInformation("Get Version was called");
return Ok(new
{
nom = "webzine",
version = "1.0",
});
return this.Ok(new
{
nom = "webzine",
version = "2.0",
});
}
}

View File

@@ -1,23 +1,28 @@
using Microsoft.AspNetCore.Mvc;
using Webzine.Entity.Fixtures;
using Webzine.WebApplication.ViewModels;
using Webzine.WebApplication.ViewModels.Artiste;
namespace Webzine.WebApplication.Controllers
namespace Webzine.WebApplication.Controllers
{
using Microsoft.AspNetCore.Mvc;
using Webzine.Repository.Contracts;
using Webzine.WebApplication.ViewModels.Artiste;
public class ArtisteController : Controller
{
// Injection du logger via le constructeur
private readonly ILogger<ArtisteController> _logger;
private readonly ILogger<ArtisteController> logger;
private readonly IArtisteRepository artisteRepository;
/// <summary>
/// Initialise une nouvelle instance du <see cref="ArtisteController"/> avec un service de journalisation injecté.
/// Initializes a new instance of the <see cref="ArtisteController"/> class.
/// Initialise une nouvelle instance du <see cref="ArtisteController"/>. avec un service de journalisation injecté.
/// </summary>
/// <param name="logger">Service de journalisation injecté pour enregistrer les événements et les erreurs.</param>
public ArtisteController(ILogger<ArtisteController> logger)
public ArtisteController(
ILogger<ArtisteController> logger,
IArtisteRepository artisteRepository)
{
_logger = logger;
this._logger.LogDebug(1, "initialisation du ArtisteController");
this.logger = logger;
this.logger.LogDebug("Initialisation du ArtisteController");
this.artisteRepository = artisteRepository;
}
/// <summary>
@@ -28,33 +33,41 @@ namespace Webzine.WebApplication.Controllers
[HttpGet("/artiste/{nom}")]
public IActionResult Index(string nom)
{
_logger.LogInformation("Tentative d'accès à l'artiste avec le nom : {NomArtiste}", nom);
this.logger.LogInformation("Tentative d'accès à l'artiste avec le nom : {NomArtiste}", nom);
if (string.IsNullOrEmpty(nom)) return RedirectToAction("Index", "Accueil");
if (string.IsNullOrEmpty(nom))
{
this.logger.LogWarning("Nom de l'artiste manquant dans la requête.");
return this.RedirectToAction("Index", "Accueil");
}
// On transforme "fatal-bazooka" en "Fatal Bazooka" pour la factory
string nomPropre = System.Globalization.CultureInfo.CurrentCulture.TextInfo
.ToTitleCase(nom.Replace("-", " "));
// On appelle la factory pour obtenir l'artiste unique
var artiste = ArtisteFactory.SeedArtisteByName(nomPropre);
var artiste = this.artisteRepository.FindByName(nomPropre);
// Check if artiste was found
if (artiste == null)
{
_logger.LogWarning("Artiste non trouvé pour le nom : {NomArtiste}", nomPropre);
return NotFound();
this.logger.LogWarning("Artiste non trouvé avec le nom : {NomArtiste}", nomPropre);
return this.RedirectToAction("Index", "Accueil");
}
_logger.LogInformation("Artiste trouvé : {NomArtiste}", nom);
// On remplit le ViewModel
var viewModel = new ArtisteModel
var viewModel = new ArtisteDetailsViewModel
{
Artiste = artiste,
Titres = artiste.Titres
IdArtiste = artiste.IdArtiste,
Nom = artiste.Nom,
Biographie = artiste.Biographie,
AlbumsGroupes = artiste.Titres
.OrderBy(t => t.Libelle)
.GroupBy(t => t.Album)
.OrderBy(g => g.Key),
};
return View(viewModel);
this.logger.LogInformation("Artiste trouvé : {NomArtiste}", nom);
return this.View(viewModel);
}
}
}
}

View File

@@ -1,23 +1,24 @@
using Microsoft.AspNetCore.Mvc;
namespace Webzine.WebApplication.Controllers
namespace Webzine.WebApplication.Controllers
{
using Microsoft.AspNetCore.Mvc;
/// <summary>
/// Controller pour la page contact.
/// </summary>
public class ContactController : Controller
{
// Injection du logger via le constructeur
private readonly ILogger<ContactController> _logger;
private readonly ILogger<ContactController> logger;
/// <summary>
/// Initialise une nouvelle instance du <see cref="ContactController"/> avec un service de journalisation injecté.
/// Initializes a new instance of the <see cref="ContactController"/> class.
/// Initialise une nouvelle instance de la classe <see cref="ContactController"/>.
/// </summary>
/// <param name="logger">Service de journalisation injecté pour enregistrer les événements et les erreurs.</param>
public ContactController(ILogger<ContactController> logger)
{
_logger = logger;
this._logger.LogDebug(1, "initialisation du ContactController");
this.logger = logger;
this.logger.LogDebug(1, "initialisation du ContactController");
}
/// <summary>
@@ -26,7 +27,7 @@ namespace Webzine.WebApplication.Controllers
/// <returns>La vue Index de la page de contact.</returns>
public IActionResult Index()
{
return View();
return this.View();
}
}
}
}

View File

@@ -1,63 +1,69 @@
using Microsoft.AspNetCore.Mvc;
using Webzine.Repository.Contracts;
using Webzine.WebApplication.ViewModels.Recherche;
using Webzine.WebApplication.ViewModels.Titre;
// <copyright file="RechercheController.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Webzine.WebApplication.Controllers;
[Route("recherche")]
public class RechercheController : Controller
namespace Webzine.WebApplication.Controllers
{
private readonly ILogger<RechercheController> _logger;
private readonly ITitreRepository _titreRepository;
using Microsoft.AspNetCore.Mvc;
public RechercheController(ILogger<RechercheController> logger, ITitreRepository titreRepository)
using Webzine.Repository.Contracts;
using Webzine.WebApplication.ViewModels.Recherche;
using Webzine.WebApplication.ViewModels.Titre;
[Route("recherche")]
public class RechercheController : Controller
{
_logger = logger;
_titreRepository = titreRepository;
}
private readonly ILogger<RechercheController> logger;
private readonly ITitreRepository titreRepository;
[HttpPost("")]
public IActionResult Index(string mot)
{
_logger.LogInformation("Recherche artistes/titres pour le mot : {Mot}.", mot);
var titres = _titreRepository.Search(mot)
.Concat(_titreRepository.SearchByStyle(mot))
.DistinctBy(t => t.IdTitre)
.OrderBy(t => t.Libelle)
.Select(t => new TitreStyleItem
{
IdTitre = t.IdTitre,
Libelle = t.Libelle,
ArtisteNom = t.Artiste?.Nom,
UrlJaquette = t.UrlJaquette,
Duree = t.Duree
})
.ToList();
var artistes = _titreRepository.FindAll()
.Select(t => t.Artiste)
.Where(a => a != null
&& !string.IsNullOrWhiteSpace(a.Nom)
&& !string.IsNullOrWhiteSpace(mot)
&& a.Nom.Contains(mot, StringComparison.OrdinalIgnoreCase))
.DistinctBy(a => a!.IdArtiste)
.OrderBy(a => a!.Nom)
.Select(a => new RechercheArtisteItem
{
Nom = a!.Nom,
NombreDeTitres = a.Titres?.Count ?? 0
})
.ToList();
var vm = new RechercheIndexViewModel
public RechercheController(ILogger<RechercheController> logger, ITitreRepository titreRepository)
{
Mot = mot,
Artistes = artistes,
Titres = titres
};
this.logger = logger;
this.titreRepository = titreRepository;
}
return View(vm);
[HttpPost("")]
public IActionResult Index(string mot)
{
this.logger.LogInformation("Recherche artistes/titres pour le mot : {Mot}.", mot);
var titres = this.titreRepository.Search(mot)
.Concat(this.titreRepository.SearchByStyle(mot))
.DistinctBy(t => t.IdTitre)
.OrderBy(t => t.Libelle)
.Select(t => new TitreStyleItem
{
IdTitre = t.IdTitre,
Libelle = t.Libelle,
ArtisteNom = t.Artiste?.Nom,
UrlJaquette = t.UrlJaquette,
Duree = t.Duree,
})
.ToList();
var artistes = this.titreRepository.FindAll()
.Select(t => t.Artiste)
.Where(a => a != null
&& !string.IsNullOrWhiteSpace(a.Nom)
&& !string.IsNullOrWhiteSpace(mot)
&& a.Nom.Contains(mot, StringComparison.OrdinalIgnoreCase))
.DistinctBy(a => a!.IdArtiste)
.OrderBy(a => a!.Nom)
.Select(a => new RechercheArtisteItem
{
Nom = a!.Nom,
NombreDeTitres = a.Titres?.Count ?? 0,
})
.ToList();
var vm = new RechercheIndexViewModel
{
Mot = mot,
Artistes = artistes,
Titres = titres,
};
return this.View(vm);
}
}
}
}

View File

@@ -1,171 +1,173 @@
using Microsoft.AspNetCore.Mvc;
using Webzine.Entity;
using Webzine.Repository.Contracts;
using Webzine.WebApplication.ViewModels.Titre;
// <copyright file="TitreController.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Webzine.WebApplication.Controllers;
/// <summary>
/// Controleur responsable de la gestion des titres musicaux :
/// affichage des details, filtrage par style,
/// ajout de likes, commentaires et recherche.
/// </summary>
[Route("titre")]
public class TitreController : Controller
namespace Webzine.WebApplication.Controllers
{
private readonly ILogger<TitreController> _logger;
private readonly ITitreRepository _titreRepository;
using Microsoft.AspNetCore.Mvc;
using Webzine.Entity;
using Webzine.Repository.Contracts;
using Webzine.WebApplication.ViewModels.Titre;
/// <summary>
/// Initialise une nouvelle instance du <see cref="TitreController"/>.
/// Controleur responsable de la gestion des titres musicaux :
/// affichage des details, filtrage par style,
/// ajout de likes, commentaires et recherche.
/// </summary>
/// <param name="logger">Service de journalisation injecte.</param>
/// <param name="titreRepository">Repository des titres injecte.</param>
public TitreController(ILogger<TitreController> logger, ITitreRepository titreRepository)
[Route("titre")]
public class TitreController : Controller
{
_logger = logger;
_titreRepository = titreRepository;
private readonly ILogger<TitreController> logger;
private readonly ITitreRepository titreRepository;
_logger.LogInformation("Initialisation du controleur TitreController.");
}
/// <summary>
/// Affiche le detail d'un titre specifique.
/// </summary>
/// <param name="id">Identifiant du titre.</param>
/// <returns>Vue des details ou 404 si introuvable.</returns>
[HttpGet("{id}")]
public IActionResult Details(int id)
{
_logger.LogInformation("Demande d'affichage du detail pour le titre ID {Id}.", id);
var titre = FindById(id);
if (titre == null)
/// <summary>
/// Initializes a new instance of the <see cref="TitreController"/> class.
/// Initialise une nouvelle instance de la classe <see cref="TitreController"/>.
/// </summary>
/// <param name="logger">Service de journalisation injecte.</param>
/// <param name="titreRepository">Repository des titres injecte.</param>
public TitreController(ILogger<TitreController> logger, ITitreRepository titreRepository)
{
_logger.LogWarning("Titre avec ID {Id} introuvable.", id);
return NotFound();
this.logger = logger;
this.titreRepository = titreRepository;
this.logger.LogInformation("Initialisation du controleur TitreController.");
}
var vm = new TitreDetail
/// <summary>
/// Affiche le detail d'un titre specifique.
/// </summary>
/// <param name="id">Identifiant du titre.</param>
/// <returns>Vue des details ou 404 si introuvable.</returns>
[HttpGet("{id}")]
public IActionResult Details(int id)
{
Details = new TitreContent
this.logger.LogInformation("Demande d'affichage du detail pour le titre ID {Id}.", id);
var titre = this.titreRepository.Find(id);
if (titre == null)
{
this.logger.LogWarning("Titre avec ID {Id} introuvable.", id);
return this.RedirectToAction("Index");
}
var vm = new TitreDetail
{
Details = new TitreContent
{
IdTitre = titre.IdTitre,
Libelle = titre.Libelle,
Chronique = titre.Chronique,
DateSortie = titre.DateSortie,
NbLikes = titre.NbLikes,
UrlJaquette = titre.UrlJaquette,
UrlEcoute = titre.UrlEcoute,
ArtisteNom = titre.Artiste.Nom,
Styles = titre.Styles,
Commentaires = titre.Commentaires,
},
CommentForm = new TitreComment
{
IdTitre = titre.IdTitre,
},
};
return this.View(vm);
}
/// <summary>
/// Affiche les titres correspondant a un style musical donne.
/// </summary>
/// <param name="style">Nom du style musical.</param>
/// <returns>Vue contenant la liste filtree.</returns>
[HttpGet("style/{style}")]
public IActionResult Style(string style)
{
this.logger.LogInformation("Recherche des titres pour le style : {Style}.", style);
var titresFiltres = this.titreRepository.SearchByStyle(style).ToList();
var vm = new TitreStyle
{
StyleName = style,
Titres = titresFiltres.Select(MapTitreItem).ToList(),
};
return this.View(vm);
}
/// <summary>
/// Ajoute un like a un titre.
/// </summary>
/// <param name="model">Modele contenant l'identifiant du titre.</param>
/// <returns>Redirection vers la page detail.</returns>
[HttpPost("like")]
public IActionResult Like(TitreLike model)
{
this.logger.LogInformation("Ajout d'un like pour le titre ID {Id}.", model.IdTitre);
var titre = this.titreRepository.Find(model.IdTitre);
if (titre == null)
{
this.logger.LogWarning("Impossible d'ajouter un like. Titre ID {Id} introuvable.", model.IdTitre);
return this.RedirectToAction("Index");
}
titre.NbLikes++;
return this.RedirectToAction("Details", new { id = model.IdTitre });
}
/// <summary>
/// Ajoute un commentaire a un titre.
/// </summary>
/// <param name="model">Donnees du commentaire.</param>
/// <returns>Redirection vers la page detail.</returns>
[HttpPost("comment")]
public IActionResult Comment(TitreComment model)
{
if (!this.ModelState.IsValid)
{
this.logger.LogWarning("Echec de validation du modele de commentaire pour le titre ID {Id}.", model.IdTitre);
return this.RedirectToAction("Details", new { id = model.IdTitre });
}
var titre = this.titreRepository.Find(model.IdTitre);
if (titre == null)
{
this.logger.LogWarning("Impossible d'ajouter le commentaire. Titre ID {Id} introuvable.", model.IdTitre);
return this.RedirectToAction("Index");
}
var commentaire = new Commentaire
{
Auteur = model.Auteur,
Contenu = model.Contenu,
DateCreation = DateTime.Now,
IdTitre = model.IdTitre,
};
titre.Commentaires.Add(commentaire);
this.logger.LogInformation("Commentaire ajoute avec succes au titre ID {Id}.", model.IdTitre);
return this.RedirectToAction("Details", new { id = model.IdTitre });
}
private static TitreStyleItem MapTitreItem(Titre titre)
{
return new TitreStyleItem
{
IdTitre = titre.IdTitre,
Libelle = titre.Libelle,
Chronique = titre.Chronique,
DateSortie = titre.DateSortie,
NbLikes = titre.NbLikes,
UrlJaquette = titre.UrlJaquette,
UrlEcoute = titre.UrlEcoute,
ArtisteNom = titre.Artiste?.Nom,
Styles = titre.Styles,
Commentaires = titre.Commentaires
},
CommentForm = new TitreComment
{
IdTitre = titre.IdTitre
}
};
return View(vm);
}
/// <summary>
/// Affiche les titres correspondant a un style musical donne.
/// </summary>
/// <param name="style">Nom du style musical.</param>
/// <returns>Vue contenant la liste filtree.</returns>
[HttpGet("style/{style}")]
public IActionResult Style(string style)
{
_logger.LogInformation("Recherche des titres pour le style : {Style}.", style);
var titresFiltres = _titreRepository.SearchByStyle(style).ToList();
var vm = new TitreStyle
{
StyleName = style,
Titres = titresFiltres.Select(MapTitreItem).ToList()
};
return View(vm);
}
/// <summary>
/// Ajoute un like a un titre.
/// </summary>
/// <param name="model">Modele contenant l'identifiant du titre.</param>
/// <returns>Redirection vers la page detail.</returns>
[HttpPost("like")]
public IActionResult Like(TitreLike model)
{
_logger.LogInformation("Ajout d'un like pour le titre ID {Id}.", model.IdTitre);
var titre = FindById(model.IdTitre);
if (titre == null)
{
_logger.LogWarning("Impossible d'ajouter un like. Titre ID {Id} introuvable.", model.IdTitre);
return NotFound();
UrlJaquette = titre.UrlJaquette,
Duree = titre.Duree,
};
}
titre.NbLikes++;
return RedirectToAction("Details", new { id = model.IdTitre });
}
/// <summary>
/// Ajoute un commentaire a un titre.
/// </summary>
/// <param name="model">Donnees du commentaire.</param>
/// <returns>Redirection vers la page detail.</returns>
[HttpPost("comment")]
public IActionResult Comment(TitreComment model)
{
if (!ModelState.IsValid)
{
_logger.LogWarning("Echec de validation du modele de commentaire pour le titre ID {Id}.", model.IdTitre);
return RedirectToAction("Details", new { id = model.IdTitre });
}
var titre = FindById(model.IdTitre);
if (titre == null)
{
_logger.LogWarning("Impossible d'ajouter le commentaire. Titre ID {Id} introuvable.", model.IdTitre);
return NotFound();
}
var commentaire = new Commentaire
{
Auteur = model.Auteur,
Contenu = model.Contenu,
DateCreation = DateTime.Now,
IdTitre = model.IdTitre
};
titre.Commentaires.Add(commentaire);
_logger.LogInformation("Commentaire ajoute avec succes au titre ID {Id}.", model.IdTitre);
return RedirectToAction("Details", new { id = model.IdTitre });
}
private Titre? FindById(int id)
{
return _titreRepository.Find(id);
}
private static TitreStyleItem MapTitreItem(Titre titre)
{
return new TitreStyleItem
{
IdTitre = titre.IdTitre,
Libelle = titre.Libelle,
ArtisteNom = titre.Artiste?.Nom,
UrlJaquette = titre.UrlJaquette,
Duree = titre.Duree
};
}
}
}

View File

@@ -0,0 +1 @@
*.sqlite*

View File

@@ -0,0 +1,18 @@
namespace Webzine.WebApplication.Extensions;
public static class RouteConfiguration
{
/// <summary>
/// Configure les routes de l'application.
/// </summary>
public static void MapCustomRoutes(this IEndpointRouteBuilder endpoints)
{
endpoints.MapControllerRoute(
name: "areas",
pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Accueil}/{action=Index}/{id?}");
}
}

View File

@@ -1,7 +1,14 @@
using Microsoft.EntityFrameworkCore;
using NLog;
using NLog.Web;
using Webzine.EntitiesContext;
using Webzine.Entity;
using Webzine.Entity.Fixtures;
using Webzine.Repository;
using Webzine.Repository.Contracts;
using Webzine.WebApplication.Extensions;
// Initiation du logger NLog pour la classe courante afin de pouvoir l'utiliser pour logger des messages d'information, d'erreur, etc avant la construction de l'application.
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
@@ -14,47 +21,134 @@ try
// Ajoute les services necessaires pour permettre l'utilisation des
// controllers avec des vues.
builder.Services.AddControllersWithViews()
// Ajoute la compilation des vues lors de l'execution de l'application.
// Cela nous evite de recompiler l'application a chaque modification de vue.
// Necessite le package Nuget Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.
.AddRazorRuntimeCompilation();
builder.Services.AddSingleton<ITitreRepository, LocalEntityRepository>();
// NLog: Setup NLog for Dependency injection
builder.Logging.ClearProviders();
builder.Host.UseNLog();
// En fonction de la configuration, utilise soit les repositories basés sur une base de données, soit les repositories basés sur des listes locales.
bool useDatabase = builder.Configuration.GetValue<bool>("UseDatabase");
bool isSQLite = builder.Configuration.GetValue<bool>("IsSQLite");
if (useDatabase)
{
if (isSQLite)
{
builder.Services.AddDbContext<WebzineDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("SqliteConnection")));
}
else
{
builder.Services.AddDbContext<WebzineDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("PostGreSQLConnection")));
}
builder.Services.AddScoped<DbEntityRepository>();
builder.Services.AddScoped<ITitreRepository, DbTitreRepository>();
builder.Services.AddScoped<IStyleRepository, DbStyleRepository>();
builder.Services.AddScoped<IArtisteRepository, DbArtisteRepository>();
builder.Services.AddScoped<ICommentaireRepository, DbCommentaireRepository>();
}
else
{
builder.Services.AddScoped<ITitreRepository, LocalTitreRepository>();
builder.Services.AddScoped<IStyleRepository, LocalStyleRepository>();
builder.Services.AddScoped<IArtisteRepository, LocalArtisteRepository>();
builder.Services.AddScoped<ICommentaireRepository, LocalCommentaireRepository>();
builder.Services.AddSingleton<InMemoryDataStore>();
}
// https://learn.microsoft.com/fr-fr/aspnet/core/performance/response-compression?view=aspnetcore-10.0#configuration
// Ajoute le service de compression des réponses HTTP pour réduire la taille des données envoyées au client et améliorer les performances de l'application.
builder.Services.AddResponseCompression();
var app = builder.Build();
// Active la possibilite de servir des fichiers statiques presents dans
if (useDatabase)
{
if (isSQLite)
{
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<WebzineDbContext>();
db.Database.EnsureDeleted();
db.Database.EnsureCreated();
var repo = scope.ServiceProvider.GetRequiredService<DbEntityRepository>();
repo.SeedBaseDeDonnees();
}
}
else
{
using (var scope = app.Services.CreateScope())
{
// TODO : A modifier pour ne pas supprimer la base de donnée en prod
var db = scope.ServiceProvider.GetRequiredService<WebzineDbContext>();
db.Database.EnsureDeleted();
db.Database.EnsureCreated();
var repo = scope.ServiceProvider.GetRequiredService<DbEntityRepository>();
repo.SeedBaseDeDonnees();
}
}
}
else
{
using (var scope = app.Services.CreateScope())
{
var store = scope.ServiceProvider.GetRequiredService<InMemoryDataStore>();
var artistes = SeedDataLocal.GenererListeArtiste(100);
var styles = SeedDataLocal.GenererListeStyle(15, 20);
var albums = SeedDataLocal.GenererListeAlbums(50);
var commentaires = new List<Commentaire>();
var titres = SeedDataLocal.GenererListeTitre(500, artistes, styles, albums);
foreach (var titre in titres)
{
var commentairesForTitre = SeedDataLocal.GenererListeCommentaire(titre, 0, 5);
titre.Commentaires.AddRange(commentairesForTitre);
commentaires.AddRange(commentairesForTitre);
}
store.Artistes.AddRange(artistes);
store.Styles = styles;
store.Titres = titres;
store.Commentaires.AddRange(commentaires);
}
}
app.UseResponseCompression();
// Active la possibilité de servir des fichiers statiques presents dans
// le dossier wwwroot.
app.UseStaticFiles();
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
// https://learn.microsoft.com/fr-fr/aspnet/core/fundamentals/static-files?view=aspnetcore-10.0#set-http-response-headers
ctx.Context.Response.Headers.Append("Cache-Control", "public, max-age=31536000");
},
});
// Active le middleware permettant le routage des requetes entrantes.
app.UseRouting();
// Ajoute une route pour les zones (Areas) comme Admin
app.MapControllerRoute(
name: "areas",
pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
// Ajoute un endpoint permettant de router les urls
// avec la forme /controller/action/id(optionnel).
app.MapControllerRoute(
name: "default",
pattern: "{controller=Accueil}/{action=Index}/{id?}");
// Appelle les routes définies dans le dossier Extensions.
app.MapCustomRoutes();
app.Run();
}
catch (Exception exception)
{
// NLog: attrape les exceptions non gerees et les logge.
// NLog: attrape les exceptions non gerees et les logger.
logger.Error(exception, "Stopped program because of exception");
throw;
}
finally
{
// Assure que NLog flush tous les messages de log avant de fermer l'application.
NLog.LogManager.Shutdown();
}
LogManager.Shutdown();
}

View File

@@ -0,0 +1,36 @@
namespace Webzine.WebApplication.ViewComponents
{
using Microsoft.AspNetCore.Mvc;
using Webzine.Repository.Contracts;
/// <summary>
/// View component pour la sidebar, récupère les styles depuis le repository.
/// </summary>
public class SidebarViewComponent : ViewComponent
{
private readonly IStyleRepository styleRepository;
/// <summary>
/// Initializes a new instance of the <see cref="SidebarViewComponent"/> class.
/// </summary>
/// <param name="styleRepository">Repository des styles injecté.</param>
public SidebarViewComponent(IStyleRepository styleRepository)
{
this.styleRepository = styleRepository;
}
/// <summary>
/// Récupère tous les styles triés par libellé et les passe à la vue.
/// </summary>
/// <returns>Une vue contenant la liste des styles.</returns>
public IViewComponentResult Invoke()
{
var styles = this.styleRepository.FindAll()
.OrderBy(s => s.Libelle)
.ToList();
return this.View(styles);
}
}
}

View File

@@ -1,20 +1,30 @@
using Webzine.Entity;
namespace Webzine.WebApplication.ViewModels.Accueil
namespace Webzine.WebApplication.ViewModels.Accueil
{
using Webzine.Entity;
/// <summary>
/// ViewModel pour la page d'accueil du webzine, affichant les derniers titres et les titres les plus populaires.
/// </summary>
public class AccueilIndexViewModel
{
/// <summary>
/// Définit la liste des derniers titres ajoutés au webzine.
/// Obtient ou définit la liste des derniers titres ajoutés au webzine.
/// </summary>
public List<Entity.Titre> DerniersTitres { get; set; } = [];
public List<Titre> DerniersTitres { get; set; } = new List<Titre>();
/// <summary>
/// Définit la liste des titres les plus populaires du webzine.
/// Obtient ou définit la liste des titres les plus populaires du webzine.
/// </summary>
public List<Entity.Titre> TopTitres { get; set; } = [];
public List<Titre> TopTitres { get; set; } = new List<Titre>();
/// <summary>
/// Obtient ou définit le nombre de titre disponible.
/// </summary>
// public int NombreDeTitre { get; set; } = 0;
/// <summary>
/// Obtient ou définit le nombre de titre paginé.
/// </summary>
// public int Pagination { get; set; } = 0;
}
}

View File

@@ -0,0 +1,31 @@
namespace Webzine.WebApplication.ViewModels.Artiste
{
using Webzine.Entity;
/// <summary>
/// ViewModel pour afficher les informations d'un artiste et ses titres groupés par album.
/// </summary>
public class ArtisteDetailsViewModel
{
/// <summary>
/// Obtient ou définit l'identifiant de l'artiste.
/// </summary>
public int IdArtiste { get; set; }
/// <summary>
/// Obtient ou définit le nom de l'artiste.
/// </summary>
public string Nom { get; set; } = string.Empty;
/// <summary>
/// Obtient ou définit la biographie de l'artiste.
/// </summary>
public string Biographie { get; set; } = string.Empty;
/// <summary>
/// Obtient ou définit défini la liste des titres de l'artiste groupés par nom d'Album.
/// </summary>
public IEnumerable<IGrouping<string?, Titre>> AlbumsGroupes { get; set; }
= Enumerable.Empty<IGrouping<string?, Titre>>();
}
}

View File

@@ -1,20 +0,0 @@
using Webzine.Entity;
namespace Webzine.WebApplication.ViewModels.Artiste
{
/// <summary>
/// ViewModel pour afficher les détails d'un artiste, incluant les informations de l'artiste et la liste de ses titres.
/// </summary>
public class ArtisteModel
{
/// <summary>
/// Artiste dont on affiche les détails.
/// </summary>
public Entity.Artiste Artiste { get; set; }
/// <summary>
/// Liste des titres de l'artiste.
/// </summary>
public List<Entity.Titre> Titres { get; set; }
}
}

View File

@@ -14,4 +14,4 @@ public class RechercheArtisteItem
/// Nombre de titres associes a l'artiste.
/// </summary>
public int NombreDeTitres { get; set; }
}
}

View File

@@ -15,10 +15,10 @@ public class RechercheIndexViewModel
/// <summary>
/// Artistes trouves.
/// </summary>
public List<RechercheArtisteItem> Artistes { get; set; } = new();
public List<RechercheArtisteItem> Artistes { get; set; } = new ();
/// <summary>
/// Titres trouves.
/// </summary>
public List<TitreStyleItem> Titres { get; set; } = new();
}
public List<TitreStyleItem> Titres { get; set; } = new ();
}

View File

@@ -50,10 +50,10 @@ public class TitreContent
/// <summary>
/// Définit le nom de l'album associé au titre.
/// </summary>
public List<Style> Styles { get; set; } = new();
public List<Style> Styles { get; set; } = new ();
/// <summary>
/// Définit la liste des commentaires associés au titre.
/// </summary>
public List<Commentaire> Commentaires { get; set; } = new();
public List<Commentaire> Commentaires { get; set; } = new ();
}

View File

@@ -13,5 +13,5 @@ public class TitreStyle
/// <summary>
/// Définit la liste des items de titre associés au style musical.
/// </summary>
public List<TitreStyleItem> Titres { get; set; } = new();
public List<TitreStyleItem> Titres { get; set; } = new ();
}

View File

@@ -5,48 +5,23 @@
<h1>Derniers titres chroniqués</h1>
@* TEMPLATE *@
@* <div class="container">
<div class="container bg-light row p-3 mt-3">
<div class="col-auto">
<img class="img-thumbnail"
src="" />
</div>
<div class="col">
<a class="text-primary text-decoration-none fw-light h4">Justice - D.A.N.C.E</a>
<p class="mt-2 mb-3 text-muted ">
Insérer texte
</p>
<div class="d-flex flex-wrap align-items-center gap-3">
<a class="btn btn-primary btn-sm">Lire la suite</a>
<div class="d-flex align-items-center text-muted small">
<i class="fa-solid fa-calendar"></i>
Date :
17/12/2022 11:08:08
</div>
<div class="d-flex align-items-center text-muted small">
<i class="fa-solid fa-tags"></i>
<a class="text-decoration-none m-1">Insérer style</a>
</div>
</div>
</div>
</div>
</div> *@
<div class="container">
@foreach (var titre in Model.DerniersTitres)
{
<div class="container bg-light row p-3 mt-3">
<div class="col-auto">
<img class="img-thumbnail img-fluid"
style="max-width:200px;"
@* UrlJaquette *@
src="@titre.UrlJaquette" />
<div class="row bg-light p-3 mt-3 align-items-center">
<!-- Image -->
<div class="col-12 col-md-3 text-center mb-3 mb-md-0">
<img class="img-fluid img-thumbnail"
src="@titre.UrlJaquette"
alt="@titre.Libelle"
loading="lazy" />
</div>
<div class="col">
@* Artiste - Titre @titre.Artiste - @titre.Libelle*@
<!-- Contenu -->
<div class="col-12 col-md-9">
<!-- Artiste - Titre -->
<div class="fw-light h4 text-primary">
<a asp-action="Index"
asp-controller="Artiste"
@@ -60,62 +35,72 @@
@titre.Libelle
</a>
</div>
@* Chronique *@
<p class="mt-2 mb-3 text-muted ">
@titre.Chronique
<!-- Chronique -->
<p class="mt-2 mb-3 text-muted">
@(titre.Chronique.Length > 200 ? titre.Chronique.Substring(0, 200) + "..." : titre.Chronique)
</p>
<!-- Footer -->
<div class="d-flex flex-wrap align-items-center gap-3">
<a asp-action="Details" asp-controller="Titre" asp-route-id="@titre.IdTitre" class="btn btn-primary btn-sm">Lire la suite</a>
<a asp-action="Details"
asp-controller="Titre"
asp-route-id="@titre.IdTitre"
class="btn btn-primary btn-sm">
Lire la suite
</a>
<div class="d-flex align-items-center text-muted small">
<i class="fa-solid fa-calendar me-1"> </i>
@* Date de création *@
<i class="fa-solid fa-calendar me-1"></i>
@titre.DateCreation
</div>
<div class="d-flex align-items-center text-muted small">
<i class="fa-solid fa-tags"></i>
@* Style *@
<a asp-controller="Titre" asp-action="Style" asp-route-id="Pop" class="text-decoration-none m-1">Pop</a>
<div class="d-flex align-items-center text-muted small flex-wrap">
<i class="fa-solid fa-tags me-2"></i>
@foreach (var style in titre.Styles)
{
<a asp-controller="Titre"
asp-action="Style"
asp-route-id="@style.Libelle"
class="text-decoration-none me-1">
@style.Libelle@(style != titre.Styles.Last() ? "," : "")
</a>
}
</div>
</div>
</div>
</div>
}
<!-- Bouton -->
<div class="row justify-content-end">
<button class="btn btn-secondary col-auto mt-3">Titres plus anciens >></button>
<button class="btn btn-secondary col-auto mt-3">
Titres plus anciens >>
</button>
</div>
</div>
@* TEMPLATE *@
@* <div class="container">
<div class="row">
<div class="card col m-1" style="width: 18rem;">
<img class="card-img-top"
src="" alt="Alternate Text" />
<div class="card-body">
<a class="card-link" href="#">Album</a><br />
par <a class="card-link">Artiste</a>
</div>
</div>
</div>
</div> *@
<div class="container">
<h1 class="mt-5">Titres les plus populaires</h1>
<div class="row">
@foreach (var titre in Model.TopTitres)
{
<div class="card col m-1" style="width: auto;">
<img class="card-img-top"
src="@titre.UrlJaquette" />
<div class="row g-3">
@foreach (var titre in Model.TopTitres)
{
<div class="col-12 col-md-6 col-lg-4">
<div class="card h-100">
<img class="card-img-top" src="@titre.UrlJaquette" alt="@titre.Album" loading="lazy" />
<div class="card-body">
<a asp-controller="Titre" asp-action="Details" asp-route-id="@titre.IdTitre" class="card-link">@titre.Album</a><br />
par <a asp-controller="Artiste" asp-action="Index" asp-route-nom="@titre.Artiste.Nom" class="card-link">@titre.Artiste.Nom</a>
<div class="card-body">
<a asp-controller="Titre" asp-action="Details" asp-route-id="@titre.IdTitre" class="card-link">
@titre.Libelle
</a>
<br />
par
<a asp-controller="Artiste" asp-action="Index" asp-route-nom="@titre.Artiste.Nom" class="card-link">
@titre.Artiste.Nom
</a>
</div>
</div>
</div>
</div>
}
}
</div>
</div>

View File

@@ -1,4 +1,4 @@
@model Webzine.WebApplication.ViewModels.Artiste.ArtisteModel
@model Webzine.WebApplication.ViewModels.Artiste.ArtisteDetailsViewModel;
@{
ViewData["Title"] = "Artiste";
@@ -6,24 +6,22 @@
<div class="container">
<h1>@Model.Artiste.Nom</h1>
<h1>@Model.Nom</h1>
<hr class="mb-5" />
<hr/>
<p class="lead">@Model.Artiste.Biographie</p>
<p class="lead">@Model.Biographie</p>
<h2 class="mt-5 mb-4">Albums</h2>
<hr class="mb-5" />
<hr/>
@* On groupe les titres par nom d'album *@
@{
var albumsGroupes = Model.Titres
.OrderBy(t => t.Libelle) // Trie les titres par ordre alphabétique au sein de chaque groupe futur
.GroupBy(t => t.Album) // Groupe par nom d'album
.OrderBy(g => g.Key); // Trie les albums par ordre alphabétique (la clé du groupe)
}
@foreach (var groupe in albumsGroupes)
@if (!Model.AlbumsGroupes.Any())
{
<p>Cet artiste n'a pas encore de titres répertoriés.</p>
}
else
{
@foreach (var groupe in Model.AlbumsGroupes)
{
// On récupère le premier titre du groupe pour afficher l'image de l'album
var premierTitre = groupe.First();
@@ -32,7 +30,8 @@
<div class="col-md-3 mb-3">
<img src="@premierTitre.UrlJaquette"
class="img-fluid shadow-sm rounded border"
alt="Pochette de @groupe.Key" />
alt="Pochette de @groupe.Key"
loading="lazy" />
</div>
<div class="col-md-9">
@@ -59,7 +58,7 @@
<a asp-controller="Titre"
asp-action="Details"
asp-route-id="@titre.IdTitre"
class="text-primary fw-bold">
class="text-primary">
@titre.Libelle
</a>
</td>
@@ -70,4 +69,5 @@
</div>
</div>
}
}
</div>

View File

@@ -4,14 +4,15 @@
<div class="container">
<h1>Contact</h1>
<div>
<div class="my-2">
C.U.C.D.B - DIIAGE <br />
69 Avenue Aristide Briand<br />
21000 Dijon
</div>
<div>
<div class ="my-2">
<i class="fa-solid fa-phone"></i> Phone : 03 80 40 50 60<br />
<i class="fa-solid fa-envelope"></i> secretariat@cucdb.fr
<i class="fa-solid fa-envelope"></i> <span class="text-primary">secretariat@cucdb.fr</span>
</div>
</div>
@@ -21,43 +22,43 @@
<h2>Suivez-nous</h2>
<div class="row g-4 text-center">
<div class="col-md-4">
<a href="#" class="card h-100 p-4 shadow-sm border-0 bg-light-subtle text-decoration-none">
<i class="fa-solid fa-link fa-3x text-primary mb-3"></i>
<a href="#" class="card h-100 p-4 border-0 bg-light">
<i class="fa-solid fa-link fa-3x text-primary mb-3 align-self-center"></i>
<div class="fw-bold text-primary">Site officiel du DIIAGE</div>
</a>
</div>
<div class="col-md-4">
<a href="#" class="card h-100 p-4 shadow-sm border-0 bg-light-subtle text-decoration-none">
<i class="fa-brands fa-facebook fa-3x text-primary mb-3"></i>
<a href="#" class="card h-100 p-4 border-0 bg-light">
<i class="fa-brands fa-facebook fa-3x text-primary mb-3 align-self-center"></i>
<div class="fw-bold text-primary">Facebook</div>
</a>
</div>
<div class="col-md-4">
<a href="#" class="card h-100 p-4 shadow-sm border-0 bg-light-subtle text-decoration-none">
<i class="fa-brands fa-instagram fa-3x text-primary mb-3"></i>
<a href="#" class="card h-100 p-4 border-0 bg-light">
<i class="fa-brands fa-instagram fa-3x text-primary mb-3 align-self-center"></i>
<div class="fw-bold text-primary">Instagram</div>
</a>
</div>
<div class="col-md-4">
<a href="#" class="card h-100 p-4 shadow-sm border-0 bg-light-subtle text-decoration-none">
<i class="fa-brands fa-linkedin fa-3x text-primary mb-3"></i>
<a href="#" class="card h-100 p-4 border-0 bg-light">
<i class="fa-brands fa-linkedin fa-3x text-primary mb-3 align-self-center"></i>
<div class="fw-bold text-primary">LinkedIn</div>
</a>
</div>
<div class="col-md-4">
<a href="#" class="card h-100 p-4 shadow-sm border-0 bg-light-subtle text-decoration-none">
<i class="fa-solid fa-map fa-3x text-primary mb-3"></i>
<a href="#" class="card h-100 p-4 border-0 bg-light">
<i class="fa-solid fa-map fa-3x text-primary mb-3 align-self-center"></i>
<div class="fw-bold text-primary">Google Maps</div>
</a>
</div>
<div class="col-md-4">
<a href="#" class="card h-100 p-4 shadow-sm border-0 bg-light-subtle text-decoration-none">
<i class="fa-brands fa-twitter fa-3x text-primary mb-3"></i>
<a href="#" class="card h-100 p-4 border-0 bg-light">
<i class="fa-brands fa-twitter fa-3x text-primary mb-3 align-self-center"></i>
<div class="fw-bold text-primary">Twitter</div>
</a>
</div>

View File

@@ -2,7 +2,6 @@
@{
ViewData["Title"] = "Recherche";
Layout = "_Layout";
}
<div class="container mt-4">
@@ -54,7 +53,7 @@
asp-action="Details"
asp-route-id="@titre.IdTitre"
class="me-3 text-black">
<img src="@titre.UrlJaquette" alt="@titre.Libelle" width="70" height="70" class="object-fit-cover" />
<img src="@titre.UrlJaquette" alt="@titre.Libelle" width="70" height="70" class="object-fit-cover" loading="lazy"/>
</a>
<div class="justify-content-center d-flex flex-column">

View File

@@ -0,0 +1,23 @@
@model IEnumerable<Webzine.Entity.Style>
<aside class="col-lg-3 d-none d-lg-block">
<div>
<h2>À propos</h2>
<p>Retrouvez les dernières pépites sur notre webzine.</p>
</div>
<div>
<h2>Styles</h2>
<ul>
@foreach (var style in Model)
{
<li>
<a asp-controller="Titre"
asp-action="Style"
asp-route-style="@style.Libelle">
@style.Libelle
</a>
</li>
}
</ul>
</div>
</aside>

View File

@@ -3,11 +3,6 @@
*@
@{
}
<div class="site-footer text-bg-light mt-auto">
<footer class="d-flex flex-wrap justify-content-between align-items-center py-3">
<div class="col-md-4 d-flex align-items-center">
<span class="mb-3 mb-md-0 ms-5 text-body-secondary">&copy; ASP .NET Core - DIIAGE 2025 - 2026</span>
</div>
</footer>
</div>
<footer class="py-3 text-bg-light">
<p class="ms-5">&copy; ASP .NET Core - DIIAGE 2025 - 2026</p>
</footer>

View File

@@ -27,7 +27,6 @@
</a>
</li>
@* TODO : Modifier, il s'agit d'une liste *@
<li class="nav-item">
@* <a class="nav-link" href="#">
<i class="fa-solid fa-screwdriver-wrench"></i> Administration

View File

@@ -6,29 +6,25 @@
<title>@ViewData["Title"] - Webzine</title>
@* Ajout de bootstrap *@
<script src="~/js/bootstrap.min.js" defer></script>
<script src="~/js/bootstrap.bundle.js" defer></script>
<link rel="stylesheet" href="~/css/app.css">
<link rel="stylesheet" href="~/css/bootstrap.min.css">
@* Ajout de font-awesome *@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="~/css/all.min.css">
<link rel="stylesheet" href="~/css/app.css">
</head>
<body>
<div class="site-shell">
<partial name="_Header"/>
<div class="container-fluid flex-grow-1 py-4">
<div class="row g-0">
<main class="col mx-3">
@RenderBody()
</main>
@if(ViewContext.RouteData.Values["area"]?.ToString() != "Administration")
{
<partial name="_Sidebar" />
}
</div>
</div>
<partial name="_Footer" />
<body class="d-flex flex-column min-vh-100">
<partial name="_Header"/>
<div class="container-fluid flex-grow-1 py-4">
<div class="row">
<main class="col mx-3">
@RenderBody()
</main>
@if(ViewContext.RouteData.Values["area"]?.ToString() != "Administration")
{
@await Component.InvokeAsync("Sidebar")
}
</div>
</div>
<partial name="_Footer" />
</body>
</html>
<script src="~/js/bootstrap.bundle.min.js" defer></script>

View File

@@ -1,51 +0,0 @@
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}
<aside class="col-3">
<div>
<h2>À propos</h2>
<p>Retrouvez les dernières pépites sur notre webzine.</p>
</div>
<div>
<h2>Styles</h2>
<ul>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Acid house">Acid house</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Ambient">Ambient</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Deep house">Deep house</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Disco">Disco</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Downtempo">Downtempo</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Drum n bass">Drum n bass</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Dub Techno">Dub Techno</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Electro">Electro</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Electronic">Electronic</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Experimental">Experimental</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Funk">Funk</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Garage">Garage</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Hardcore">Hardcore</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Hardstyle">Hardstyle</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Hip hop">Hip hop</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="House">House</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Indie">Indie</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Industrial">Industrial</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Jazz">Jazz</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Latin">Latin</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Metal">Metal</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Minimal">Minimal</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Pop">Pop</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Progressive">Progressive</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Punk">Punk</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="R&B">R&B</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Rap">Rap</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Reggae">Reggae</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Rock">Rock</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Soul">Soul</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Techno">Techno</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Trance">Trance</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="Trip hop">Trip hop</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="UK garage">UK garage</a></li>
<li><a asp-controller="Titre" asp-action="Style" asp-route-id="World">World</a></li>
</ul>
</div>
</aside>

View File

@@ -40,7 +40,7 @@
{
var style = Model.Details.Styles[i];
<a class="text-primary text-decoration-none fw-semibold"
<a class="text-primary fw-semibold"
asp-controller="Titre"
asp-action="Style"
asp-route-style="@style.Libelle">
@@ -86,7 +86,9 @@
<div class="col-md-4 text-center">
<img src="@Model.Details.UrlJaquette"
class="img-fluid rounded shadow"
alt="Jaquette" />
alt="Jaquette"
loading="lazy"
fetchpriority="high" />
</div>
</div>
@@ -154,7 +156,7 @@
<h4 class="mb-4">Commentaires</h4>
@if (Model.Details.Commentaires.Any())
@if (Model.Details.Commentaires != null && Model.Details.Commentaires.Any())
{
foreach (var comment in Model.Details.Commentaires.OrderByDescending(c => c.DateCreation))
{

View File

@@ -2,7 +2,6 @@
@{
ViewData["Title"] = $"Titres - {Model.StyleName}";
Layout = "_Layout";
}
<div class="container mt-4">
<div class="row">
@@ -30,7 +29,7 @@
<a asp-action="Details"
asp-route-id="@titre.IdTitre"
class="me-3 text-black">
<img src="@titre.UrlJaquette" alt="@titre.Libelle" width="70px" height="70px" class="object-fit-cover"/>
<img src="@titre.UrlJaquette" alt="@titre.Libelle" width="70px" height="70px" class="object-fit-cover" loading="lazy"/>
</a>
<!-- Infos -->

View File

@@ -11,19 +11,20 @@
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
<Content Include="..\Webzine.Documentation\StyleCop\stylecop.json">
<AdditionalFiles Include="..\Webzine.Documentation\StyleCop\stylecop.json">
<Link>stylecop.json</Link>
</Content>
</AdditionalFiles>
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\data\" />
<Folder Include="wwwroot\lib\" />
<Folder Include="Data\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Faker.Net" Version="2.0.163" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.*" />
<PackageReference Include="NLog" Version="6.1.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
@@ -33,6 +34,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Webzine.EntitiesContext\Webzine.EntitiesContext.csproj" />
<ProjectReference Include="..\Webzine.Entity\Webzine.Entity.csproj" />
<ProjectReference Include="..\Webzine.Repository\Webzine.Repository.csproj" />
</ItemGroup>

View File

@@ -7,7 +7,13 @@
},
"Webzine": {
"NombreDerniereChronique": 3,
"NombreDeTopTitres" : 3
"NombreDeTopTitres": 3
},
"UseDatabase": true,
"IsSQLite": true,
"ConnectionStrings": {
"SqliteConnection": "Data Source=Data/webzine.sqlite",
"PostGreSQLConnection" : "Host=localhost;Port=5432;Username=admin;Password=admin123;Database=webzine_db"
},
"AllowedHosts": "*"
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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