Coverage for mongo/homework.py: 100%

176 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2024-11-05 04:22 +0000

1from typing import List, Optional 

2from . import engine 

3from .user import User 

4from .base import MongoBase 

5from .course import Course 

6from .utils import doc_required 

7from .problem.problem import Problem 

8from .ip_filter import IPFilter 

9from datetime import datetime 

10 

11__all__ = ['Homework'] 

12 

13 

14class Error(): 

15 Illegal_penalty = 1 

16 Invalid_penalty = 2 

17 

18 

19def check_penalty(penalty: Optional[str]) -> int: 

20 if penalty is None: 

21 return 0 

22 allowed_chars = ["+", "-", "*", "/", "=", ".", "(", ")", ":", ">", "<"] 

23 allowed_words = ["score", "overtime", "if", "else"] 

24 checkstring = "" 

25 for i in penalty: 

26 checkstring += (" " if i in allowed_chars else i) 

27 for i in checkstring.split(): 

28 if i not in allowed_words: 

29 try: 

30 int(i) 

31 except: 

32 return Error.Illegal_penalty 

33 try: 

34 score = 0 

35 overtime = 0 

36 exec(penalty) 

37 except: 

38 return Error.Invalid_penalty 

39 return 0 

40 

41 

42# TODO: unittest for class `Homework` 

43class Homework(MongoBase, engine=engine.Homework): 

44 

45 def is_valid_ip(self, ip: str) -> bool: 

46 # no restriction, always valid 

47 if not self.ip_filters: 

48 return True 

49 ip_filters = map(IPFilter, self.ip_filters) 

50 return any(_filter.match(ip) for _filter in ip_filters) 

51 

52 @classmethod 

53 @doc_required('course_name', 'course', Course) 

54 def add( 

55 cls, 

56 user, 

57 course: Course, 

58 hw_name: str, 

59 problem_ids: List[int] = [], 

60 markdown: str = '', 

61 scoreboard_status: int = 0, 

62 start: Optional[float] = None, 

63 end: Optional[float] = None, 

64 penalty: Optional[str] = '', 

65 ): 

66 # check user is teacher or ta 

67 if not course.permission(user, Course.Permission.GRADE): 

68 raise PermissionError('user is not teacher or ta') 

69 course_id = course.id 

70 if cls.engine.objects( 

71 course_id=str(course_id), 

72 homework_name=hw_name, 

73 ): 

74 raise engine.NotUniqueError('homework exist') 

75 # check problems exist 

76 

77 penalty_stat = check_penalty(penalty) 

78 if penalty_stat == Error.Illegal_penalty: 

79 raise ValueError("Illegal penalty") 

80 elif penalty_stat == Error.Invalid_penalty: 

81 raise ValueError("Invalid penalty") 

82 

83 problems = [*map(Problem, problem_ids)] 

84 if not all(problems): 

85 raise engine.DoesNotExist('some problems not found!') 

86 homework = cls.engine( 

87 homework_name=hw_name, 

88 course_id=str(course_id), 

89 problem_ids=problem_ids, 

90 scoreboard_status=scoreboard_status, 

91 markdown=markdown, 

92 ) 

93 if penalty: 

94 homework.penalty = penalty 

95 if start: 

96 homework.duration.start = datetime.fromtimestamp(start) 

97 if end: 

98 homework.duration.end = datetime.fromtimestamp(end) 

99 homework.save() 

100 # init student status 

101 user_problems = {} 

102 for problem in problems: 

103 problem_id = str(problem.problem_id) 

104 user_problems[problem_id] = cls.default_problem_status() 

105 problem.update(push__homeworks=homework) 

106 homework.update(student_status={ 

107 s: user_problems 

108 for s in course.student_nicknames 

109 }) 

110 # add homework to course 

111 course.update(push__homeworks=homework.id) 

112 return homework 

113 

114 @classmethod 

115 def update( 

116 cls, 

117 user, 

118 homework_id: str, 

119 markdown: str, 

120 new_hw_name: str, 

121 problem_ids: List[int], 

122 penalty: str, 

123 start: Optional[datetime] = None, 

124 end: Optional[datetime] = None, 

125 scoreboard_status: Optional[int] = None, 

126 ): 

127 homework = cls.engine.objects.get(id=homework_id) 

128 course = Course(engine.Course.objects.get(id=homework.course_id)) 

129 # check user is teacher or ta 

130 if not course.permission(user, Course.Permission.GRADE): 

131 raise PermissionError('user is not teacher or ta') 

132 # check the new_name hasn't been use in this course 

133 

134 if penalty is not None: 

135 penalty_stat = check_penalty(penalty) 

136 if penalty_stat == Error.Illegal_penalty: 

137 raise ValueError("Illegal penalty") 

138 elif penalty_stat == Error.Invalid_penalty: 

139 raise ValueError("Invalid penalty") 

140 else: 

141 homework.penalty = penalty 

142 

143 if new_hw_name is not None: 

144 if cls.engine.objects( 

145 course_id=str(course.id), 

146 homework_name=new_hw_name, 

147 ): 

148 raise engine.NotUniqueError('homework exist') 

149 else: 

150 homework.update(homework_name=new_hw_name) 

151 # update fields 

152 if start is not None: 

153 homework.duration.start = datetime.fromtimestamp(start) 

154 if end is not None: 

155 homework.duration.end = datetime.fromtimestamp(end) 

156 if scoreboard_status is not None: 

157 homework.scoreboard_status = scoreboard_status 

158 if markdown is not None: 

159 homework.markdown = markdown 

160 homework.save() 

161 drop_ids = set(homework.problem_ids) - set(problem_ids) 

162 new_ids = set(problem_ids) - set(homework.problem_ids) 

163 student_status = homework.student_status 

164 # add 

165 for pid in new_ids: 

166 problem = Problem(pid) 

167 if not problem: 

168 continue 

169 homework.update(push__problem_ids=pid) 

170 problem.update(push__homeworks=homework) 

171 for key in course.student_nicknames: 

172 student_status[key][str(pid)] = cls.default_problem_status() 

173 # delete 

174 for pid in drop_ids: 

175 problem = Problem(pid) 

176 if not problem: 

177 continue 

178 homework.update(pull__problem_ids=pid) 

179 problem.update(pull__homeworks=homework) 

180 for status in student_status.values(): 

181 del status[str(pid)] 

182 homework.update(student_status=student_status) 

183 return homework 

184 

185 # delete problems/paticipants in hw 

186 @doc_required('course', 'course', Course) 

187 def delete_problems( 

188 self, 

189 user, 

190 course: Course, 

191 ): 

192 # check user is teacher or ta 

193 if not course.permission(user, Course.Permission.GRADE): 

194 raise PermissionError('user is not teacher or ta') 

195 for pid in self.problem_ids: 

196 problem = Problem(pid) 

197 if not problem: 

198 continue 

199 problem.update(pull__homeworks=self.obj) 

200 self.delete() 

201 return self 

202 

203 @classmethod 

204 @doc_required('course_name', 'course', Course) 

205 def get_homeworks(cls, course: Course): 

206 homeworks = course.homeworks or [] 

207 homeworks = sorted(homeworks, key=lambda h: h.duration.start) 

208 return homeworks 

209 

210 @classmethod 

211 def get_by_id(cls, homework_id): 

212 try: 

213 homework = cls.engine.objects.get(id=homework_id) 

214 except engine.DoesNotExist: 

215 raise engine.DoesNotExist('homework not exist') 

216 return homework 

217 

218 @classmethod 

219 def get_by_name(cls, course_name, homework_name): 

220 try: 

221 homework = cls.engine.objects.get( 

222 course_id=str(Course(course_name).obj.id), 

223 homework_name=homework_name, 

224 ) 

225 except engine.DoesNotExist: 

226 raise engine.DoesNotExist('homework not exist') 

227 return homework 

228 

229 @classmethod 

230 def default_problem_status(cls): 

231 return { 

232 'score': 0, 

233 'problemStatus': None, 

234 'submissionIds': [], 

235 } 

236 

237 def add_student(self, students: List[User]): 

238 if any(u.username in self.student_status for u in students): 

239 raise ValueError('Student already in homework') 

240 user_status = { 

241 str(pid): self.default_problem_status() 

242 for pid in self.problem_ids 

243 } 

244 for student in students: 

245 self.student_status[student.username] = user_status 

246 self.save() 

247 

248 def remove_student(self, students: List[User]): 

249 if any(u.username not in self.student_status for u in students): 

250 raise ValueError('Student not in homework') 

251 for student in students: 

252 del self.student_status[student.username] 

253 self.save() 

254 

255 def do_penalty(self, submission, stat): 

256 d = {} 

257 

258 d['score'] = submission.score - stat['rawScore'] 

259 if d['score'] > 0: 

260 d['overtime'] = int((submission.timestamp.timestamp() - 

261 self.duration.end.timestamp()) / 86400) 

262 exec(self.penalty, d) 

263 d['score'] = int(d['score']) 

264 stat['score'] += d['score'] 

265 stat['rawScore'] = submission.score 

266 

267 return [stat['score'], stat['rawScore']]