fishertools 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fishertools/__init__.py +82 -0
- fishertools/config/__init__.py +24 -0
- fishertools/config/manager.py +247 -0
- fishertools/config/models.py +96 -0
- fishertools/config/parser.py +265 -0
- fishertools/decorators.py +93 -0
- fishertools/documentation/__init__.py +38 -0
- fishertools/documentation/api.py +242 -0
- fishertools/documentation/generator.py +502 -0
- fishertools/documentation/models.py +126 -0
- fishertools/documentation/visual.py +583 -0
- fishertools/errors/__init__.py +29 -0
- fishertools/errors/exceptions.py +191 -0
- fishertools/errors/explainer.py +303 -0
- fishertools/errors/formatters.py +386 -0
- fishertools/errors/models.py +228 -0
- fishertools/errors/patterns.py +119 -0
- fishertools/errors/recovery.py +467 -0
- fishertools/examples/__init__.py +22 -0
- fishertools/examples/models.py +118 -0
- fishertools/examples/repository.py +770 -0
- fishertools/helpers.py +116 -0
- fishertools/integration.py +451 -0
- fishertools/learn/__init__.py +18 -0
- fishertools/learn/examples.py +550 -0
- fishertools/learn/tips.py +281 -0
- fishertools/learning/__init__.py +32 -0
- fishertools/learning/core.py +349 -0
- fishertools/learning/models.py +112 -0
- fishertools/learning/progress.py +314 -0
- fishertools/learning/session.py +500 -0
- fishertools/learning/tutorial.py +626 -0
- fishertools/legacy/__init__.py +76 -0
- fishertools/legacy/deprecated.py +261 -0
- fishertools/legacy/deprecation.py +149 -0
- fishertools/safe/__init__.py +16 -0
- fishertools/safe/collections.py +242 -0
- fishertools/safe/files.py +240 -0
- fishertools/safe/strings.py +15 -0
- fishertools/utils.py +57 -0
- fishertools-0.2.1.dist-info/METADATA +256 -0
- fishertools-0.2.1.dist-info/RECORD +81 -0
- fishertools-0.2.1.dist-info/WHEEL +5 -0
- fishertools-0.2.1.dist-info/licenses/LICENSE +21 -0
- fishertools-0.2.1.dist-info/top_level.txt +2 -0
- tests/__init__.py +6 -0
- tests/conftest.py +25 -0
- tests/test_config/__init__.py +3 -0
- tests/test_config/test_basic_config.py +57 -0
- tests/test_config/test_config_error_handling.py +287 -0
- tests/test_config/test_config_properties.py +435 -0
- tests/test_documentation/__init__.py +3 -0
- tests/test_documentation/test_documentation_properties.py +253 -0
- tests/test_documentation/test_visual_documentation_properties.py +444 -0
- tests/test_errors/__init__.py +3 -0
- tests/test_errors/test_api.py +301 -0
- tests/test_errors/test_error_handling.py +354 -0
- tests/test_errors/test_explainer.py +173 -0
- tests/test_errors/test_formatters.py +338 -0
- tests/test_errors/test_models.py +248 -0
- tests/test_errors/test_patterns.py +270 -0
- tests/test_examples/__init__.py +3 -0
- tests/test_examples/test_example_repository_properties.py +204 -0
- tests/test_examples/test_specific_examples.py +303 -0
- tests/test_integration.py +298 -0
- tests/test_integration_enhancements.py +462 -0
- tests/test_learn/__init__.py +3 -0
- tests/test_learn/test_examples.py +221 -0
- tests/test_learn/test_tips.py +285 -0
- tests/test_learning/__init__.py +3 -0
- tests/test_learning/test_interactive_learning_properties.py +337 -0
- tests/test_learning/test_learning_system_properties.py +194 -0
- tests/test_learning/test_progress_tracking_properties.py +279 -0
- tests/test_legacy/__init__.py +3 -0
- tests/test_legacy/test_backward_compatibility.py +236 -0
- tests/test_legacy/test_deprecation_warnings.py +208 -0
- tests/test_safe/__init__.py +3 -0
- tests/test_safe/test_collections_properties.py +189 -0
- tests/test_safe/test_files.py +104 -0
- tests/test_structure.py +58 -0
- tests/test_structure_enhancements.py +115 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data models for the Learning System module.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import List, Optional, Any, Dict, Literal
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DifficultyLevel(Enum):
|
|
12
|
+
"""Difficulty levels for learning content."""
|
|
13
|
+
BEGINNER = "beginner"
|
|
14
|
+
INTERMEDIATE = "intermediate"
|
|
15
|
+
ADVANCED = "advanced"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ExerciseStatus(Enum):
|
|
19
|
+
"""Status of an interactive exercise."""
|
|
20
|
+
NOT_STARTED = "not_started"
|
|
21
|
+
IN_PROGRESS = "in_progress"
|
|
22
|
+
COMPLETED = "completed"
|
|
23
|
+
FAILED = "failed"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class CodeContext:
|
|
28
|
+
"""Context information for code analysis."""
|
|
29
|
+
file_path: Optional[str] = None
|
|
30
|
+
function_name: Optional[str] = None
|
|
31
|
+
class_name: Optional[str] = None
|
|
32
|
+
imports: List[str] = None
|
|
33
|
+
variables: Dict[str, Any] = None
|
|
34
|
+
|
|
35
|
+
def __post_init__(self):
|
|
36
|
+
if self.imports is None:
|
|
37
|
+
self.imports = []
|
|
38
|
+
if self.variables is None:
|
|
39
|
+
self.variables = {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class StepExplanation:
|
|
44
|
+
"""Detailed explanation of a single code step."""
|
|
45
|
+
step_number: int
|
|
46
|
+
description: str
|
|
47
|
+
code_snippet: str
|
|
48
|
+
input_example: Optional[str] = None
|
|
49
|
+
output_example: Optional[str] = None
|
|
50
|
+
related_concepts: List[str] = None
|
|
51
|
+
visual_aid: Optional[str] = None
|
|
52
|
+
|
|
53
|
+
def __post_init__(self):
|
|
54
|
+
if self.related_concepts is None:
|
|
55
|
+
self.related_concepts = []
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class ValidationResult:
|
|
60
|
+
"""Result of validating an exercise solution."""
|
|
61
|
+
is_correct: bool
|
|
62
|
+
feedback: str
|
|
63
|
+
errors: List[str] = None
|
|
64
|
+
suggestions: List[str] = None
|
|
65
|
+
|
|
66
|
+
def __post_init__(self):
|
|
67
|
+
if self.errors is None:
|
|
68
|
+
self.errors = []
|
|
69
|
+
if self.suggestions is None:
|
|
70
|
+
self.suggestions = []
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class InteractiveExercise:
|
|
75
|
+
"""An interactive coding exercise."""
|
|
76
|
+
id: str
|
|
77
|
+
title: str
|
|
78
|
+
description: str
|
|
79
|
+
starter_code: str
|
|
80
|
+
expected_output: str
|
|
81
|
+
hints: List[str]
|
|
82
|
+
difficulty_level: DifficultyLevel
|
|
83
|
+
topic: str
|
|
84
|
+
status: ExerciseStatus = ExerciseStatus.NOT_STARTED
|
|
85
|
+
attempts: int = 0
|
|
86
|
+
max_attempts: int = 3
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class LearningProgress:
|
|
91
|
+
"""User's learning progress tracking."""
|
|
92
|
+
user_id: str
|
|
93
|
+
completed_topics: List[str]
|
|
94
|
+
current_level: DifficultyLevel
|
|
95
|
+
total_exercises_completed: int
|
|
96
|
+
last_activity: datetime
|
|
97
|
+
achievements: List[str]
|
|
98
|
+
session_count: int = 0
|
|
99
|
+
total_time_spent: int = 0 # in minutes
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class TutorialSession:
|
|
104
|
+
"""A tutorial learning session."""
|
|
105
|
+
session_id: str
|
|
106
|
+
topic: str
|
|
107
|
+
level: DifficultyLevel
|
|
108
|
+
start_time: datetime
|
|
109
|
+
exercises: List[InteractiveExercise]
|
|
110
|
+
current_exercise_index: int = 0
|
|
111
|
+
is_completed: bool = False
|
|
112
|
+
end_time: Optional[datetime] = None
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Progress tracking system for learning activities.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from typing import List, Optional, Dict
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from .models import LearningProgress, DifficultyLevel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ProgressSystem:
|
|
13
|
+
"""
|
|
14
|
+
Tracks user learning progress and manages achievements.
|
|
15
|
+
|
|
16
|
+
Provides persistent progress tracking between sessions and
|
|
17
|
+
suggests appropriate next steps based on completion status.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, storage_path: Optional[str] = None):
|
|
21
|
+
"""
|
|
22
|
+
Initialize the progress system.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
storage_path: Optional path for persistent storage
|
|
26
|
+
"""
|
|
27
|
+
self.storage_path = storage_path or os.path.expanduser("~/.fishertools_progress")
|
|
28
|
+
self._progress_data: Dict[str, LearningProgress] = {}
|
|
29
|
+
|
|
30
|
+
# Topic progression paths
|
|
31
|
+
self._topic_progression = {
|
|
32
|
+
"beginner": [
|
|
33
|
+
"variables", "data_types", "input_output", "operators",
|
|
34
|
+
"conditionals", "loops", "lists", "functions"
|
|
35
|
+
],
|
|
36
|
+
"intermediate": [
|
|
37
|
+
"dictionaries", "file_operations", "error_handling",
|
|
38
|
+
"modules", "classes", "list_comprehensions"
|
|
39
|
+
],
|
|
40
|
+
"advanced": [
|
|
41
|
+
"decorators", "generators", "context_managers",
|
|
42
|
+
"metaclasses", "async_programming", "testing"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Achievement definitions
|
|
47
|
+
self._achievements = {
|
|
48
|
+
"first_steps": "Completed first tutorial",
|
|
49
|
+
"variable_master": "Mastered variables and data types",
|
|
50
|
+
"loop_expert": "Completed all loop exercises",
|
|
51
|
+
"function_guru": "Created 10 functions",
|
|
52
|
+
"error_handler": "Learned error handling",
|
|
53
|
+
"level_up": "Advanced to next difficulty level",
|
|
54
|
+
"persistent_learner": "Studied for 5 consecutive days",
|
|
55
|
+
"completionist": "Finished all beginner topics"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
def create_user_profile(self, user_id: str, initial_level: DifficultyLevel = DifficultyLevel.BEGINNER) -> LearningProgress:
|
|
59
|
+
"""
|
|
60
|
+
Create a new user progress profile.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
user_id: Unique identifier for the user
|
|
64
|
+
initial_level: Starting difficulty level
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
LearningProgress: New progress profile
|
|
68
|
+
"""
|
|
69
|
+
if not user_id or not isinstance(user_id, str):
|
|
70
|
+
raise ValueError("User ID must be a non-empty string")
|
|
71
|
+
|
|
72
|
+
progress = LearningProgress(
|
|
73
|
+
user_id=user_id,
|
|
74
|
+
completed_topics=[],
|
|
75
|
+
current_level=initial_level,
|
|
76
|
+
total_exercises_completed=0,
|
|
77
|
+
last_activity=datetime.now(),
|
|
78
|
+
achievements=[],
|
|
79
|
+
session_count=0,
|
|
80
|
+
total_time_spent=0
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
self._progress_data[user_id] = progress
|
|
84
|
+
self.save_progress(user_id)
|
|
85
|
+
|
|
86
|
+
return progress
|
|
87
|
+
|
|
88
|
+
def update_progress(self, user_id: str, topic: str, completed: bool) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Update user progress for a specific topic.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
user_id: Unique identifier for the user
|
|
94
|
+
topic: Topic that was studied
|
|
95
|
+
completed: Whether the topic was completed successfully
|
|
96
|
+
"""
|
|
97
|
+
if not user_id or not isinstance(user_id, str):
|
|
98
|
+
raise ValueError("User ID must be a non-empty string")
|
|
99
|
+
|
|
100
|
+
if not topic or not isinstance(topic, str):
|
|
101
|
+
raise ValueError("Topic must be a non-empty string")
|
|
102
|
+
|
|
103
|
+
# Get or create user progress
|
|
104
|
+
progress = self.get_progress(user_id)
|
|
105
|
+
if not progress:
|
|
106
|
+
progress = self.create_user_profile(user_id)
|
|
107
|
+
|
|
108
|
+
# Update last activity
|
|
109
|
+
progress.last_activity = datetime.now()
|
|
110
|
+
|
|
111
|
+
# Add topic to completed list if completed and not already there
|
|
112
|
+
if completed and topic not in progress.completed_topics:
|
|
113
|
+
progress.completed_topics.append(topic)
|
|
114
|
+
progress.total_exercises_completed += 1
|
|
115
|
+
|
|
116
|
+
# Check for achievements
|
|
117
|
+
self._check_achievements(progress, topic)
|
|
118
|
+
|
|
119
|
+
# Check for level progression
|
|
120
|
+
self._check_level_progression(progress)
|
|
121
|
+
|
|
122
|
+
self.save_progress(user_id)
|
|
123
|
+
|
|
124
|
+
def get_progress(self, user_id: str) -> Optional[LearningProgress]:
|
|
125
|
+
"""
|
|
126
|
+
Get current progress for a user.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
user_id: Unique identifier for the user
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Optional[LearningProgress]: User's progress or None if not found
|
|
133
|
+
"""
|
|
134
|
+
if not user_id or not isinstance(user_id, str):
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
# Try to get from memory first
|
|
138
|
+
if user_id in self._progress_data:
|
|
139
|
+
return self._progress_data[user_id]
|
|
140
|
+
|
|
141
|
+
# Try to load from storage
|
|
142
|
+
return self.load_progress(user_id)
|
|
143
|
+
|
|
144
|
+
def suggest_next_topics(self, user_id: str) -> List[str]:
|
|
145
|
+
"""
|
|
146
|
+
Suggest appropriate next topics based on user progress.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
user_id: Unique identifier for the user
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
List[str]: List of suggested topic names
|
|
153
|
+
"""
|
|
154
|
+
progress = self.get_progress(user_id)
|
|
155
|
+
if not progress:
|
|
156
|
+
# Return beginner topics for new users
|
|
157
|
+
return self._topic_progression["beginner"][:3]
|
|
158
|
+
|
|
159
|
+
level_key = progress.current_level.value
|
|
160
|
+
available_topics = self._topic_progression.get(level_key, [])
|
|
161
|
+
|
|
162
|
+
# Filter out completed topics
|
|
163
|
+
suggested = [topic for topic in available_topics
|
|
164
|
+
if topic not in progress.completed_topics]
|
|
165
|
+
|
|
166
|
+
# If all topics at current level are completed, suggest next level
|
|
167
|
+
if not suggested and level_key == "beginner":
|
|
168
|
+
suggested = self._topic_progression["intermediate"][:3]
|
|
169
|
+
elif not suggested and level_key == "intermediate":
|
|
170
|
+
suggested = self._topic_progression["advanced"][:3]
|
|
171
|
+
|
|
172
|
+
return suggested[:5] # Limit to 5 suggestions
|
|
173
|
+
|
|
174
|
+
def add_achievement(self, user_id: str, achievement: str) -> None:
|
|
175
|
+
"""
|
|
176
|
+
Add an achievement to the user's profile.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
user_id: Unique identifier for the user
|
|
180
|
+
achievement: Achievement name or description
|
|
181
|
+
"""
|
|
182
|
+
progress = self.get_progress(user_id)
|
|
183
|
+
if progress and achievement not in progress.achievements:
|
|
184
|
+
progress.achievements.append(achievement)
|
|
185
|
+
self.save_progress(user_id)
|
|
186
|
+
|
|
187
|
+
def save_progress(self, user_id: str) -> None:
|
|
188
|
+
"""
|
|
189
|
+
Save user progress to persistent storage.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
user_id: Unique identifier for the user
|
|
193
|
+
"""
|
|
194
|
+
progress = self._progress_data.get(user_id)
|
|
195
|
+
if not progress:
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
# Ensure storage directory exists
|
|
200
|
+
os.makedirs(os.path.dirname(self.storage_path), exist_ok=True)
|
|
201
|
+
|
|
202
|
+
# Load existing data
|
|
203
|
+
all_progress = {}
|
|
204
|
+
if os.path.exists(self.storage_path):
|
|
205
|
+
with open(self.storage_path, 'r', encoding='utf-8') as f:
|
|
206
|
+
all_progress = json.load(f)
|
|
207
|
+
|
|
208
|
+
# Convert progress to serializable format
|
|
209
|
+
progress_dict = {
|
|
210
|
+
"user_id": progress.user_id,
|
|
211
|
+
"completed_topics": progress.completed_topics,
|
|
212
|
+
"current_level": progress.current_level.value,
|
|
213
|
+
"total_exercises_completed": progress.total_exercises_completed,
|
|
214
|
+
"last_activity": progress.last_activity.isoformat(),
|
|
215
|
+
"achievements": progress.achievements,
|
|
216
|
+
"session_count": progress.session_count,
|
|
217
|
+
"total_time_spent": progress.total_time_spent
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# Update and save
|
|
221
|
+
all_progress[user_id] = progress_dict
|
|
222
|
+
with open(self.storage_path, 'w', encoding='utf-8') as f:
|
|
223
|
+
json.dump(all_progress, f, indent=2)
|
|
224
|
+
|
|
225
|
+
except (OSError, json.JSONEncodeError) as e:
|
|
226
|
+
# Silently fail - progress tracking shouldn't break the system
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
def load_progress(self, user_id: str) -> Optional[LearningProgress]:
|
|
230
|
+
"""
|
|
231
|
+
Load user progress from persistent storage.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
user_id: Unique identifier for the user
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Optional[LearningProgress]: Loaded progress or None if not found
|
|
238
|
+
"""
|
|
239
|
+
try:
|
|
240
|
+
if not os.path.exists(self.storage_path):
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
with open(self.storage_path, 'r', encoding='utf-8') as f:
|
|
244
|
+
all_progress = json.load(f)
|
|
245
|
+
|
|
246
|
+
progress_dict = all_progress.get(user_id)
|
|
247
|
+
if not progress_dict:
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
# Convert back to LearningProgress object
|
|
251
|
+
progress = LearningProgress(
|
|
252
|
+
user_id=progress_dict["user_id"],
|
|
253
|
+
completed_topics=progress_dict["completed_topics"],
|
|
254
|
+
current_level=DifficultyLevel(progress_dict["current_level"]),
|
|
255
|
+
total_exercises_completed=progress_dict["total_exercises_completed"],
|
|
256
|
+
last_activity=datetime.fromisoformat(progress_dict["last_activity"]),
|
|
257
|
+
achievements=progress_dict["achievements"],
|
|
258
|
+
session_count=progress_dict.get("session_count", 0),
|
|
259
|
+
total_time_spent=progress_dict.get("total_time_spent", 0)
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Cache in memory
|
|
263
|
+
self._progress_data[user_id] = progress
|
|
264
|
+
return progress
|
|
265
|
+
|
|
266
|
+
except (OSError, json.JSONDecodeError, KeyError, ValueError):
|
|
267
|
+
# Return None if loading fails
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
def _check_achievements(self, progress: LearningProgress, completed_topic: str) -> None:
|
|
271
|
+
"""Check and award achievements based on progress."""
|
|
272
|
+
# First tutorial completion
|
|
273
|
+
if len(progress.completed_topics) == 1:
|
|
274
|
+
self.add_achievement(progress.user_id, self._achievements["first_steps"])
|
|
275
|
+
|
|
276
|
+
# Topic-specific achievements
|
|
277
|
+
if completed_topic == "variables" and "data_types" in progress.completed_topics:
|
|
278
|
+
self.add_achievement(progress.user_id, self._achievements["variable_master"])
|
|
279
|
+
|
|
280
|
+
if completed_topic in ["for_loops", "while_loops"] and all(
|
|
281
|
+
topic in progress.completed_topics for topic in ["for_loops", "while_loops"]
|
|
282
|
+
):
|
|
283
|
+
self.add_achievement(progress.user_id, self._achievements["loop_expert"])
|
|
284
|
+
|
|
285
|
+
if completed_topic == "error_handling":
|
|
286
|
+
self.add_achievement(progress.user_id, self._achievements["error_handler"])
|
|
287
|
+
|
|
288
|
+
# Milestone achievements
|
|
289
|
+
if progress.total_exercises_completed >= 10:
|
|
290
|
+
self.add_achievement(progress.user_id, self._achievements["function_guru"])
|
|
291
|
+
|
|
292
|
+
# Check if all beginner topics completed
|
|
293
|
+
beginner_topics = set(self._topic_progression["beginner"])
|
|
294
|
+
completed_topics = set(progress.completed_topics)
|
|
295
|
+
if beginner_topics.issubset(completed_topics):
|
|
296
|
+
self.add_achievement(progress.user_id, self._achievements["completionist"])
|
|
297
|
+
|
|
298
|
+
def _check_level_progression(self, progress: LearningProgress) -> None:
|
|
299
|
+
"""Check if user should advance to next level."""
|
|
300
|
+
current_level = progress.current_level.value
|
|
301
|
+
current_topics = set(self._topic_progression.get(current_level, []))
|
|
302
|
+
completed_topics = set(progress.completed_topics)
|
|
303
|
+
|
|
304
|
+
# Check if 80% of current level topics are completed
|
|
305
|
+
if len(current_topics) > 0:
|
|
306
|
+
completion_rate = len(current_topics.intersection(completed_topics)) / len(current_topics)
|
|
307
|
+
|
|
308
|
+
if completion_rate >= 0.8:
|
|
309
|
+
if current_level == "beginner":
|
|
310
|
+
progress.current_level = DifficultyLevel.INTERMEDIATE
|
|
311
|
+
self.add_achievement(progress.user_id, self._achievements["level_up"])
|
|
312
|
+
elif current_level == "intermediate":
|
|
313
|
+
progress.current_level = DifficultyLevel.ADVANCED
|
|
314
|
+
self.add_achievement(progress.user_id, self._achievements["level_up"])
|