我一直在从事可能是我最大的项目,我希望你们能给我一些反馈,我写的代码到目前为止。
这是一个eLearning应用程序,它为用户提供了一系列的功能,基本上是为了更好地掌握学校科目。它的中心是由教师添加的选择题组成的模拟测试。
下面是存储库:https://github.com/samul-1/elearning,您将找到对我正在研究的特性的更深入的解释。
这个项目已经完全发挥了作用,但我正在改进一些东西。后端由Django运行,前端由Vue.js创建。我已经在这两个框架中工作了大约6-7个月了,所以你可以说我才刚刚开始。
就我个人而言,我为到目前为止能够建立起来感到非常自豪,但我一直在努力使自己变得更好,所以我想就以下几点提出一些反馈意见:
我希望这个问题不会像现在这样过于笼统。如果是的话,让我知道,我会编辑它,询问更具体的方面。
我可以说的是,我对这些文件的反馈很感兴趣:
elearningapp/models.py
elearningapp/views.py
elearningapp/forms.py
users/models.py以及elearningapp/vue_frontend/src/components的vue组件(这可能是一个痛处)。
这个项目的核心可能可以归结为elearningapp Django应用程序的模型和视图,所以我将共享这两个文件的代码。
elearningapp/models.py
# imports
class Course(models.Model):
name = models.CharField(max_length=100)
number_of_questions_per_test = models.IntegerField(default=10)
# True if the course has a category distribution, i.e. tests choose a fixed amount of questions from each category
uses_category_distribution = models.BooleanField(default=False)
points_for_correct_answer = models.FloatField(default=1)
points_for_unanswered = models.FloatField(default=0)
points_for_wrong_answer = models.FloatField(default=-0.5)
minimum_passing_score = models.FloatField(default=5)
def __str__(self):
return self.name
# returns 'amount' random questions that user hasn't seen yet; if category is specified,
# the questions will be from that category
# raises OutOfQuestionsException if there aren't enough questions that satisfy requirements
def get_questions_for(self, user, amount, category=None):
# get list of questions already seen by user
seen_questions = SeenQuestion.objects.filter(user=user)
seen_question_ids = map(lambda q: q.question.pk, seen_questions)
questions = Question.objects.filter(course=self).exclude(
id__in=seen_question_ids
)
if category:
questions = questions.filter(category=category)
# pick random questions
try:
random_questions = random.sample(list(questions), amount)
except ValueError as err: # if there aren't enough questions left, return None
raise OutOfQuestionsException
return random_questions
# returns an object containing info about the course
def get_aggregated_info(self):
subscribers = apps.get_model(
"users", model_name="CourseSpecificProfile"
).objects.filter(course=self)
taken_tests = TakenTest.objects.filter(course=self)
avg_score = (
(sum([test.score for test in list(taken_tests)]) / taken_tests.count())
if taken_tests.count() > 0
else 0
)
return {
"number_of_subscribers": subscribers.count(),
"number_of_tests_taken": taken_tests.count(),
"average_score": round(avg_score, 1),
}
# returns 'amount' questions, showing correct answer and solution too
# (meant for use inside of course control panel)
def get_complete_questions(self, amount, pk_greater_than=0, category=None):
questions = self.question_set.filter(pk__gt=pk_greater_than)
if category is not None:
cat = Category.objects.get(pk=category)
questions = questions.filter(category=cat)
questions = questions.order_by("pk")[:amount]
# TODO add sorting
return list(
map(
lambda q: q.format_complete_question(),
questions,
)
)
# ? move this to CourseSpecificProfile
def get_seen_questions(self, user, amount, pk_greater_than=0, category=None):
questions = user.seenquestion_set.filter(pk__gt=pk_greater_than)
if category is not None:
cat = Category.objects.get(pk=category)
questions = questions.filter(category=cat)
questions = questions.order_by("pk")[:amount]
# TODO add sorting
return list(
map(
lambda q: q.serialize(),
questions,
)
)
# returns the 'quantity' hardest questions from this course,
# i.e. those with the lowest percentage of times they were answered correctly
def get_hardest_questions(self, quantity):
return list(
map(
lambda q: q.format_complete_question(),
self.question_set.all().order_by("percentage_of_correct_answers")[
:quantity
],
)
)
# returns the last 'quantity' actions taken by course admins or collaborators
def get_last_actions(self, quantity):
return list(
map(
lambda a: a.serialize(),
self.staffaction_set.all().order_by("-timestamp")[:quantity],
)
)
# returns the profiles of users who are subscribed to this course
def get_subscribed_users(self):
return list(map(lambda u: u.serialize(), self.coursespecificprofile_set.all()))
# returns all the reports that have been made to questions from this course
# if resolved is specified, only reports with that status are returned
def get_reports(self, resolved=None):
reports = Report.objects.filter(question__course=self)
if resolved is not None:
reports = reports.filter(resolved=resolved)
return list(map(lambda r: r.serialize(), reports))
def maximum_score(self):
return self.points_for_correct_answer * self.number_of_questions_per_test
# used to keep track of courses assistants' permissions
# (it's meant to work kinda like Django built-in permission system, but on a per-instance basis rather than per-model)
class CoursePermission(models.Model):
user = models.OneToOneField("users.CourseSpecificProfile", on_delete=models.CASCADE)
# course = models.ForeignKey(Course, on_delete=models.CASCADE)
can_add_questions = models.BooleanField(default=True)
can_edit_questions = models.BooleanField(default=True)
can_manage_contributors = models.BooleanField(default=False)
# can_edit_contributors = models.BooleanField(default=False)
def serialize(self):
return {
"can_add_questions": self.can_add_questions,
"can_edit_questions": self.can_edit_questions,
"can_manage_contributors": self.can_manage_contributors,
}
class Category(models.Model):
course = models.ForeignKey(
Course, on_delete=models.CASCADE, related_name="categories"
) # many to one
name = models.CharField(max_length=100)
# how many questions from this category need to appear in each test of this course
# (used only if the course has 'category distribution' enabled)
quantity = models.PositiveIntegerField(default=None, null=True)
def __str__(self):
return self.name
class Question(models.Model):
course = models.ForeignKey(Course, on_delete=models.CASCADE) # many to one
category = models.ForeignKey(
Category, null=True, blank=True, default=None, on_delete=models.SET_NULL
) # many to one
rendered_text = (
models.TextField()
) # contains public text including html generated by mathjax
text = models.TextField(
default=""
) # contains the actual test that was input upon creating the question
correct_answer_index = models.PositiveIntegerField()
solution_text_rendered = models.TextField()
solution_text = models.TextField(default="")
# TODO add logic to track who added a question
added_by = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
number_of_appearances = models.PositiveIntegerField(default=0)
percentage_of_correct_answers = models.FloatField(default=100.0)
def __str__(self):
return self.text
# when model is saved, process the question text to render TeX as svg
def save(self, re_render_text=True, *args, **kwargs):
# TODO raise exception if question is saved to a category for a course different than that specified in fk Course
if re_render_text:
self.rendered_text = tex_to_svg(self.text)
self.solution_text_rendered = tex_to_svg(self.solution_text)
return super(Question, self).save(*args, **kwargs)
# returns a dict containing the public information about the question;
# i.e. its text and the text of its answers
def format_question_for_user(self):
output = {}
output["text"] = self.rendered_text
# get all answers to the question
answers = self.answer_set.all() # Answer.objects.filter(question=self)
output["answers"] = [a.rendered_text for a in list(answers)]
return output
# returns a dict containing all the information about the question;
# i.e. all its public information, the index of the correct answer, and the solution,
# as well as the source code for all the texts (question, solution, answers), used for editing a question
def format_complete_question(self):
info = self.format_question_for_user()
info["textSource"] = self.text
info["solution"] = self.solution_text_rendered
info["solutionSource"] = self.solution_text
info["correctAnswerIndex"] = self.correct_answer_index
info["questionId"] = self.pk
info["category"] = self.category.pk
info["wrongAnswersPercentage"] = 100 - self.percentage_of_correct_answers
# get the source text for all the answers
answers_sources = Answer.objects.filter(question=self)
info["answersSources"] = [a.text for a in list(answers_sources)]
return info
# returns the PERCENTAGE of times this question was answered correctly relative to
# how many times it appeared in tests
# this is intended to be called ONLY by Answer.save() each time this question is answered
# to access this property from somewhere else, use the field percentage_of_correct_answers
def get_percentage_right_answers(self):
right_answer = self.answer_set.get(answer_index=self.correct_answer_index)
if self.number_of_appearances == 0:
return 100
return right_answer.selections / self.number_of_appearances * 100
# a report made by a user about a question containing a mistake
class Report(models.Model):
timestamp = models.DateTimeField(auto_now=True)
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True
)
question = models.ForeignKey(Question, on_delete=models.CASCADE, null=True)
text = models.TextField(default="")
resolved = models.BooleanField(default=False)
def serialize(self):
return {
"reportId": self.pk,
"timestamp": str(self.timestamp),
"userId": self.user.pk,
"username": self.user.username,
"firstName": self.user.first_name,
"lastName": self.user.last_name,
"question": self.question.format_complete_question(),
"text": self.text,
"resolved": 1 if self.resolved else 0,
}
class Answer(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE) # many to one
rendered_text = models.TextField()
text = models.TextField(default="")
answer_index = models.PositiveIntegerField()
selections = models.PositiveIntegerField(default=0)
def __str__(self):
return self.text
# when model is saved, process the question text to render TeX as svg
# and update the percentage of times the corresponding question was answered correctly
def save(self, re_render_text=True, *args, **kwargs):
if re_render_text:
self.rendered_text = tex_to_svg(self.text)
instance = super(Answer, self).save(*args, **kwargs)
# re-compute the percentage of correct answers to the question
self.question.percentage_of_correct_answers = (
self.question.get_percentage_right_answers()
)
self.question.save(re_render_text=False)
return instance
"""
Models to manage history, active tests, and course cp logs
"""
class StaffAction(models.Model):
ACTIONS = [
("C", "Create"),
("E", "Edit"),
]
course = models.ForeignKey(Course, on_delete=models.CASCADE) # many to one
action = models.CharField(max_length=1, choices=ACTIONS)
question = models.ForeignKey(Question, on_delete=models.CASCADE) # many to one
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
timestamp = models.DateTimeField(auto_now=True)
def __str__(self):
return (
str(self.course)
+ ": "
+ str(self.user)
+ " "
+ str(self.action)
+ " "
+ str(self.question)
)
def serialize(self):
return {
"action": self.action,
"user": self.user.username,
"question": self.question.text,
"questionId": self.question.pk,
"timestamp": str(self.timestamp),
}
# a test that was taken by a user, detailed with its outcome
class TakenTest(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) # many to one
course = models.ForeignKey(
Course, on_delete=models.CASCADE, null=True
) # many to one
timestamp = models.DateTimeField(auto_now=True)
score = models.FloatField()
passing = models.IntegerField(default=0) # boolean?
# returns a json representation of self that the client can consume
def serialize(self):
json_self = {
"score": self.score,
"timestamp": str(self.timestamp),
"correctlyAnsweredQuestions": [],
"unansweredQuestions": [],
"incorrectlyAnsweredQuestions": [],
"passing": self.passing,
}
for answer in AnswersInTakenTest.objects.filter(test=self):
# get question info and add the answer that was given to this question in the test
question_with_your_answer = answer.question.format_complete_question()
question_with_your_answer["yourAnswer"] = answer.answer_index
# append question to corresponding list based on whether it was answered correctly,
# incorrectly, or left unanswered
if answer.answer_index == -1:
json_self["unansweredQuestions"].append(question_with_your_answer)
elif answer.answer_index == answer.question.correct_answer_index:
json_self["correctlyAnsweredQuestions"].append(
question_with_your_answer
)
else:
json_self["incorrectlyAnsweredQuestions"].append(
question_with_your_answer
)
return json_self
# a question that appeared on a TakenTest, and its answer
class AnswersInTakenTest(models.Model):
test = models.ForeignKey(TakenTest, on_delete=models.CASCADE)
question = models.ForeignKey(Question, on_delete=models.CASCADE)
# ? should probably use null instead of -1 for unanswered
answer_index = models.IntegerField() # -1 if unanswered
# a question that was seen by the user, together with its answer
# needs a separate model from TakenTest because the history of seen questions
# is erasable, whereas that of taken tests isn't
class SeenQuestion(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
question = models.ForeignKey(Question, null=True, on_delete=models.CASCADE)
# ? should probably use null instead of -1 for unanswered
answer_index = models.IntegerField(default=-1) # -1 if unanswered
timestamp = models.DateTimeField(auto_now_add=True)
def serialize(self):
return {
"questionId": self.pk,
"question": self.question.format_complete_question(),
"givenAnswer": self.answer_index,
}
# a test currently, associated to a user: used in case they leave and come back to the test
# to retrieve the data without generating a new one, as well as to keep track of what questions
# the answers given by the user need to be checked against
class ActiveTest(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
questions = models.ManyToManyField(Question)
course = models.ForeignKey(Course, on_delete=models.CASCADE)
timestamp = models.DateTimeField(auto_now_add=True)
# chooses random questions that the requesting user hasn't seen yet according to the course distribution (if any)
# and adds them to the ManyToMany relationship with Question
def init_test(self):
if not self.course.uses_category_distribution:
# course doesn't have a category distribution (or questions for this course aren't grouped in categories)
for question in self.course.get_questions_for(
self.user, self.course.number_of_questions_per_test
):
self.questions.add(question)
else:
chosen_questions = []
# get the right number of questions for each category
for category in self.course.categories.all():
chosen_questions.extend(
list(
self.course.get_questions_for(
self.user, category.quantity, category
)
)
)
random.shuffle(chosen_questions)
for question in chosen_questions:
self.questions.add(question)
self.save()
# returns a list containing all questions of the test,
# formatted to display their public information
def format_test_for_user(self):
output = []
i = 1
for question in self.questions.all():
# add question index to dict for displaying to the user
temp = question.format_question_for_user()
temp["idx"] = i
output.append(temp)
i += 1
return output
# takes in a dict containing the answers to the test's questions; computes the result,
# updates data about questions and answers, saves the test questions to the history of
# seen questions, and returns a TakenTest object detailing the outcome of the test
def evaluate_answers(self, answers):
taken_test = TakenTest(user=self.user, course=self.course, score=0)
taken_test.save()
# update the number of tests taken by the user
user_profile = self.user.coursespecificprofile_set.get(course=self.course)
user_profile.number_of_tests_taken += 1
user_profile.save()
questions = self.questions.all() # ? could use select_related('answer_set')
score = 0
for question, answer in zip(
questions, map(lambda a: answers[a], answers)
): # map {index:answer} to answer
# increment number of appearances of this question
question.number_of_appearances += 1
question.save(re_render_text=False)
# increment number of selections for this answer
if answer != -1:
given_answer = question.answer_set.get(answer_index=answer)
print(given_answer)
given_answer.selections += 1
given_answer.save(re_render_text=False)
# record given answer for history
ans = AnswersInTakenTest(
answer_index=answer, question=question, test=taken_test
)
ans.save()
# record question for (deletable) history
seen_question = SeenQuestion(
user=self.user, question=question, answer_index=answer
)
seen_question.save()
if int(answer) == question.correct_answer_index:
score += self.course.points_for_correct_answer
elif int(answer) == -1:
score += self.course.points_for_unanswered
else:
score += self.course.points_for_wrong_answer
if score > self.course.minimum_passing_score:
taken_test.passing = 1 # using 1, 0 instead of True, False to make it easier to convert to JSON
else:
taken_test.passing = 0
taken_test.score = score
taken_test.save()
user_profile.last_score = score
user_profile.save()
return taken_testelearningapp/views.py
# imports
@login_required
def create_course(request):
if not request.user.globalprofile.is_teacher:
return HttpResponseForbidden()
if request.method == "POST":
form_data = json.loads(request.body.decode("utf-8"))
form = CourseForm(form_data, user=request.user.globalprofile)
print(form_data)
if form.is_valid():
new_course = form.save()
CourseSpecificProfile.objects.create(user=request.user, course=new_course)
else:
print(form.errors)
return JsonResponse({"courseId": new_course.pk}, safe=False)
# GET
return render(
request,
"elearningapp/createcourse.html",
)
# course control panel
@login_required
def course_cp(request, course_id):
course = get_object_or_404(Course, pk__exact=course_id)
# reject with 403 if user isn't authorized to view the control panel for this course
if course not in request.user.globalprofile.admin_of.all() and (
request.user.coursespecificprofile_set.filter(course=course).count == 0
or not hasattr(
request.user.coursespecificprofile_set.get(course=course),
"coursepermission",
)
):
return HttpResponseForbidden()
aggregated_info = course.get_aggregated_info()
context = {
"course": course,
"reports": course.get_reports(resolved=False),
"last_actions": course.get_last_actions(5),
"number_of_subscribers": aggregated_info["number_of_subscribers"],
"number_of_tests_taken": aggregated_info["number_of_tests_taken"],
"average_score": aggregated_info["average_score"],
"hardest_questions": course.get_hardest_questions(3),
"admin": "true"
if course in request.user.globalprofile.admin_of.all()
else "false", # using 'true' and 'false' to prevent issues with js frontend consuming the value
}
# get user's permissions, if they have a permission object associated to them (admins don't)
if len(
course_profile := request.user.coursespecificprofile_set.filter(course=course)
) != 0 and hasattr(course_profile[0], "coursepermission"):
context["my_permissions"] = json.dumps(
course_profile[0].coursepermission.serialize()
)
context["user_id"] = course_profile[0].pk
else:
context["my_permissions"] = {}
context[
"user_id"
] = "null" # once again using 'null' as a string for easier passing of the value as a prop
return render(
request,
"elearningapp/course_cp.html",
context,
)
# accessed via GET, returns a list of users subscribed to the course
@login_required
def get_course_users(request, course_id):
course = get_object_or_404(Course, pk__exact=course_id)
return JsonResponse(course.get_subscribed_users(), safe=False)
# accessed via GET, returns a lit of reports that have been made to questions from the course
@login_required
def get_course_reports(request, course_id):
course = get_object_or_404(Course, pk__exact=course_id)
return JsonResponse(course.get_reports(), safe=False)
# if accessed via GET, gets the first 5 questions for the course and renders template containing the EditQuestion component
# if accessed via PUT, updates the question
# if question_id is specified, the id is passed via the context object to EditQuestion vue component
@login_required
def edit_question(request, course_id, question_id=None):
course = get_object_or_404(Course, pk__exact=course_id)
# reject with 403 if user isn't authorized to edit questions for this course
if course not in request.user.globalprofile.admin_of.all() and (
request.user.coursespecificprofile_set.filter(course=course).count == 0
or not hasattr(
request.user.coursespecificprofile_set.get(course=course),
"coursepermission",
)
or not request.user.coursespecificprofile_set.get(
course=course
).coursepermission.can_edit_questions
):
return HttpResponseForbidden()
if request.method == "PUT":
form_data = json.loads(request.body.decode("utf-8"))
question = get_object_or_404(Question, pk=form_data["questionId"])
form = QuestionForm(form_data, user=request.user, action="E", instance=question)
print(form_data)
if form.is_valid():
print("is valid")
updated_question = form.save()
print(updated_question)
return JsonResponse(updated_question.format_complete_question(), safe=False)
else:
print(form.errors)
# GET
categories = Category.objects.filter(course=course)
questions = course.get_complete_questions(5)
context = {
"course_id": course_id,
"categories": [{c.pk: c.name} for c in categories],
"questions": questions,
}
if question_id is not None:
context["editing_id"] = question_id
return render(
request,
"elearningapp/edit_question.html",
context,
)
# if accessed via POST, creates a new report for the specified question
# if accessed via PUT, updates the status of the specified report
@login_required
def report_question(request):
if request.method != "POST" and request.method != "PUT":
return HttpResponseNotAllowed()
form_data = json.loads(request.body.decode("utf-8"))
if request.method == "POST":
question = get_object_or_404(Question, pk__exact=form_data["questionId"])
form = ReportForm(form_data, user=request.user, question=question)
if form.is_valid():
# add new report to db
form.save()
return JsonResponse({"success": True})
else:
print(form.errors)
if request.method == "PUT":
report = get_object_or_404(Report, pk__exact=form_data["reportId"])
# add report text to form data as it is a mandatory field that isn't supplied when the view is accessed via PUT
# ? maybe there's a better way to do this
form_data["text"] = report.text
form = ReportForm(form_data, instance=report, resolved=form_data["resolved"])
print(form_data)
if form.is_valid():
print("is valid")
form.save()
return JsonResponse({"success": True})
else:
print(form.errors)
# if accessed via PUT, updates or creates the permissions of a user for a course,
# if accessed via DELETE, deletes the permission object for the specified user and course
def update_course_permissions(request, course_id):
if request.method == "GET":
return HttpResponseNotAllowed()
course = get_object_or_404(Course, pk__exact=course_id)
# reject with 403 if user isn't authorized to add assistants for this course
if course not in request.user.globalprofile.admin_of.all() and (
request.user.coursespecificprofile_set.filter(course=course).count == 0
or not hasattr(
request.user.coursespecificprofile_set.get(course=course),
"coursepermission",
)
or not request.user.coursespecificprofile_set.get(
course=course
).coursepermission.can_manage_contributors
):
return HttpResponseForbidden()
form_data = json.loads(request.body.decode("utf-8"))
print(form_data)
editing_profile = get_object_or_404(
CourseSpecificProfile, pk=form_data["profile_id"]
)
if request.method == "PUT":
# cannot edit permissions of a course admin
if course in editing_profile.user.globalprofile.admin_of.all():
return HttpResponseForbidden()
# retrieve or create permissions for this user
# we don't need the boolean returned by get_or_create, hence the _ wildcard
permissions, _ = CoursePermission.objects.get_or_create(user=editing_profile)
form = PermissionForm(form_data["permissions"], instance=permissions)
print(form_data)
if form.is_valid():
form.save()
return JsonResponse({"success": True})
else:
print(form.errors)
if request.method == "DELETE":
try:
permissions = CoursePermission.objects.get(user=editing_profile)
except CoursePermission.DoesNotExist:
return HttpResponseNotFound()
permissions.delete()
return JsonResponse({"success": True})
# accessed via GET by the client for infinite scrolling in the EditQuestion vue component
@login_required
def get_questions(request, course_id, amount, starting_from_pk, category=None):
course = get_object_or_404(Course, pk__exact=course_id)
questions = course.get_complete_questions(
int(amount), pk_greater_than=int(starting_from_pk), category=category
)
return JsonResponse(questions, safe=False)
# accessed via GET by the client for infinite scrolling in the QuestionHistory vue component
@login_required
def get_seen_questions(request, course_id, amount, starting_from_pk, category=None):
course = get_object_or_404(Course, pk__exact=course_id)
questions = course.get_seen_questions(
request.user,
int(amount),
pk_greater_than=int(starting_from_pk),
category=category,
)
print(questions)
return JsonResponse(questions, safe=False)
# renders template containing CreateQuestion vue component when accessed via GET,
# handles question creation using Question ModelForm when accessed via POST
@login_required
def add_question(request, course_id):
course = get_object_or_404(Course, pk__exact=course_id)
# reject with 403 if user isn't authorized to add questions to this course
if course not in request.user.globalprofile.admin_of.all() and (
request.user.coursespecificprofile_set.filter(course=course).count == 0
or not hasattr(
request.user.coursespecificprofile_set.get(course=course),
"coursepermission",
)
or not request.user.coursespecificprofile_set.get(
course=course
).coursepermission.can_add_questions
):
return HttpResponseForbidden()
categories = Category.objects.filter(course=course)
context = {
"course_id": course_id,
"categories": [{c.pk: c.name} for c in categories],
}
if request.method == "POST":
form_data = json.loads(request.body.decode("utf-8"))
form = QuestionForm(form_data, user=request.user, action="C")
if form.is_valid():
# add new question to db
new_question = form.save()
return JsonResponse("ok", safe=False)
else:
print(form.errors)
return JsonResponse({"success": True})
# GET
return render(
request,
"elearningapp/add_question.html",
context,
)
# renders a test for the user
@login_required
def render_test(request, course_id):
requesting_user = request.user # User.objects.get(pk=user_id)
course = Course.objects.get(pk=course_id)
if ActiveTest.objects.filter(user=requesting_user, course=course).count() > 0:
# user already has an active test associated to them; use that one
# instead of creating a new one
current_test = ActiveTest.objects.get(user=requesting_user, course=course)
else:
# no active tests found for this user;
# create a new test associated to requesting user in selected course
try:
current_test = ActiveTest(user=requesting_user, course=course)
current_test.save()
current_test.init_test()
except OutOfQuestionsException:
# delete the test that was attempted to be initialized if exception occurs
current_test.delete()
# TODO show an actual page
return HttpResponse("out of questions")
# get user's global data
global_profile = GlobalProfile.objects.get(user=request.user)
global_user_data = {
"name": global_profile.user.username,
"id": global_profile.user.pk,
}
context = {
"course_id": course.pk,
"questions": current_test.format_test_for_user(),
"global_user_data": global_user_data,
}
return render(request, "elearningapp/test.html", context)
# returns the list of questions that the user has seen in past tests
@login_required
def question_history(request, course_id):
user_profile = get_object_or_404(
CourseSpecificProfile, user__exact=request.user, course__pk__exact=course_id
)
course = Course.objects.get(pk=course_id)
seen_questions = course.get_seen_questions(user=request.user, amount=5)
# seen_questions = map(
# lambda sq: sq.serialize(), list(user_profile.get_seen_questions())
# )
return render(
request,
"elearningapp/question_history.html",
{
"questions": list(seen_questions),
"user_id": request.user.pk,
"course_id": course_id,
},
)
# empties the list of seen question for given user and course
@login_required
def delete_question_history(request, course_id):
user_profile = get_object_or_404(
CourseSpecificProfile, user__exact=request.user, course__pk__exact=course_id
)
for question in user_profile.get_seen_questions():
question.delete()
return JsonResponse("ok", safe=False)
# returns the list of tests that the user has taken in the past
@login_required
def test_history(request, course_id):
user_profile = get_object_or_404(
CourseSpecificProfile, user__exact=request.user, course__pk__exact=course_id
)
taken_tests = map(lambda t: t.serialize(), list(user_profile.get_taken_tests()))
return render(
request,
"elearningapp/test_history.html",
{
"tests": list(taken_tests),
"maxScore": Course.objects.get(pk=course_id).maximum_score(),
},
)
# calls a method to evaluate the answers given, save the question and test outcome to user's history;
# returns details about the outcome of the test
@login_required
def check_answers(request):
if request.method != "POST":
return HttpResponseNotAllowed()
answers = json.loads(request.body.decode("utf-8"))
print(answers)
# TODO check validity of sent json object
requesting_user = request.user
# ! TODO check course in addition to requesting user
current_test = ActiveTest.objects.get(user=requesting_user)
outcome = current_test.evaluate_answers(answers)
# current_test.delete()
return JsonResponse(outcome.serialize())
# retrieves context for rendering the course dashboard
@login_required
def view_course(request, course_id):
course = get_object_or_404(Course, pk=course_id)
# get course's data
course_data = {
"name": course.name,
"id": course.pk,
}
# get user's global data
global_profile = GlobalProfile.objects.get(
user=request.user
) # we can assume this exists because it's created at signup time and the view has @login_required
global_user_data = {
"name": global_profile.user.first_name
if global_profile.user.first_name != ""
else global_profile.user.username,
"id": global_profile.user.pk,
}
try:
course_profile = CourseSpecificProfile.objects.get(
user=request.user, course__pk=course_id
)
except CourseSpecificProfile.DoesNotExist:
# user isn't signed up to this course, give them a chance to
return render(
request,
"course_register.html",
{
"global_user_data": global_user_data,
"course_data": course_data,
},
)
# get user's course specific data
course_specific_user_data = {
"number_of_tests_taken": course_profile.number_of_tests_taken,
"last_score": course_profile.last_score,
"average_score": round(course_profile.get_average_score(), 1),
"last_scores": course_profile.get_last_scores(5),
}
return render(
request,
"elearningapp/course_dashboard.html",
{
"global_user_data": global_user_data,
"course_specific_user_data": course_specific_user_data,
"course_data": course_data,
},
)下面是这个应用程序所做的简单说明:教师可以注册并创建课程,他们可以在这些课程中添加问题和分类,将问题分成几类。然后,学生可以注册课程,并参加由随机选择的问题组成的考试在课程中。测试结束后,问题将保存到用户的个人历史记录(可擦除的历史记录)中,不会再次出现(除非清除历史记录),测试结果将保存到单独(持久)历史记录中,以检查随着时间的推移所取得的进展。还有很多额外的功能,比如课程控制面板,教师可以跟踪课程的统计信息,添加助理,等等,以及用户报告问题中的错误的可能性。
我真的很喜欢这样做,我想尽我所能做到最好。因此,感谢任何人谁将花费时间来审查我的代码。
发布于 2021-02-20 16:38:07
FYKI -我不是Python专家!
阿尼特的推荐信是这样标记的!
我要做的第一件事就是为这些类创建单独的文件。
class Course(models.Model):
name = models.CharField(max_length=100)
number_of_questions_per_test = models.IntegerField(default=10)让我们删除这样的评论!代码应该足够好来完成它。#如果该课程有一个类别分布,即测试从我不反对snake_case的每个类别中选择固定数量的问题,但我更喜欢CamelCase。为什么要浪费
_。uses_category_distribution到useDistribution?你喜欢这个吗?
uses_category_distribution = models.BooleanField(default=False)
points_for_correct_answer = models.FloatField(default=1)
points_for_unanswered = models.FloatField(default=0)
points_for_wrong_answer = models.FloatField(default=-0.5)
minimum_passing_score = models.FloatField(default=5)
def __str__(self):
return self.name很高兴看到您正在尝试文档化函数,但是我相信当我们遵循一些标准时,它会有很大的帮助。你认为如何?这是一个很好的资源:https://docs.python-guide.org/writing/documentation
# returns 'amount' random questions that user hasn't seen yet; if category is specified,
# the questions will be from that category
# raises OutOfQuestionsException if there aren't enough questions that satisfy requirements函数名可以更具体、更精确。我相信,简单地说,
quesiton、makeQuestion或generateQuestion就足够了!
def get_questions_for(self, user, amount, category=None):你认为如果我们把它移到另一个函数,它会帮助使这个函数更干净吗?可能只是在这里传递ID,哪个更容易测试?#获取用户seen_questions = SeenQuestion.objects.filter(user=user) seen_question_ids = map(lambda : q.question.pk,seen_questions)已经看到的问题列表
questions = Question.objects.filter(course=self).exclude(
id__in=seen_question_ids
)下面的代码可以选择一个问题,如果类别退出,否则它正在做一些事情。对不起,我有点糊涂。所以,我们为什么不找个更好的方法去做呢?这显然是需要更多的条件来澄清事情。此外,我们还可以找到一些更好的方法来使用try/catch。如果类别:问题= questions.filter(category=category)
# pick random questions
try:
random_questions = random.sample(list(questions), amount)
except ValueError as err: # if there aren't enough questions left, return None
raise OutOfQuestionsException
return random_questions
# returns an object containing info about the course
def get_aggregated_info(self):
subscribers = apps.get_model(
"users", model_name="CourseSpecificProfile"
).objects.filter(course=self)
taken_tests = TakenTest.objects.filter(course=self)
avg_score = (
(sum([test.score for test in list(taken_tests)]) / taken_tests.count())
if taken_tests.count() > 0
else 0
)
return {
"number_of_subscribers": subscribers.count(),
"number_of_tests_taken": taken_tests.count(),
"average_score": round(avg_score, 1),
}
# returns 'amount' questions, showing correct answer and solution too
# (meant for use inside of course control panel)
def get_complete_questions(self, amount, pk_greater_than=0, category=None):
questions = self.question_set.filter(pk__gt=pk_greater_than)
if category is not None:
cat = Category.objects.get(pk=category)
questions = questions.filter(category=cat)
questions = questions.order_by("pk")[:amount]
# TODO add sorting
return list(
map(
lambda q: q.format_complete_question(),
questions,
)
)
# ? move this to CourseSpecificProfile
def get_seen_questions(self, user, amount, pk_greater_than=0, category=None):
questions = user.seenquestion_set.filter(pk__gt=pk_greater_than)条件是我们理解的重要部分,所以IMHO在他们上面看到一条线是很好的。如果类别不是无: cat = Category.objects.get(pk=category)问题= questions.filter(category=cat)
questions = questions.order_by("pk")[:amount]我们不要在这里发表这样的评论。每个实现都必须完成。# TODO添加排序
return list(
map(
lambda q: q.serialize(),
questions,
)
)
# returns the 'quantity' hardest questions from this course,
# i.e. those with the lowest percentage of times they were answered correctly
def get_hardest_questions(self, quantity):
return list(
map(
lambda q: q.format_complete_question(),
self.question_set.all().order_by("percentage_of_correct_answers")[
:quantity
],
)
)
# returns the last 'quantity' actions taken by course admins or collaborators
def get_last_actions(self, quantity):
return list(
map(
lambda a: a.serialize(),
self.staffaction_set.all().order_by("-timestamp")[:quantity],
)
)这种功能可能更适合于
user模型。您认为呢?#返回订阅本课程的用户的配置文件: def get_subscribed_users(self):返回列表(map(lambda u: u.serialize(),self.coursespecificprofile_set.all()
# returns all the reports that have been made to questions from this course
# if resolved is specified, only reports with that status are returned
def get_reports(self, resolved=None):
reports = Report.objects.filter(question__course=self)
if resolved is not None:
reports = reports.filter(resolved=resolved)
return list(map(lambda r: r.serialize(), reports))我们可以想出一个更好的函数名:/) def maximum_score(self):返回self.points_for_correct_answer * self.number_of_questions_per_test
从模型实现的角度来看,我相信存储库模式会有帮助。此外,我可以看到很多lambda的使用情况,但是如果我们可以使用写普通的sql查询来获取数据,而不是使用其他model的对象实例,会更好,因为这样会更有效。
这确实是一个伟大的开端,在开始阶段,许多学习等待着。我建议你
这是一个很长的路要走,但努力是值得的!
祝你们一切顺利!
希望能帮上忙。
干杯,
https://codereview.stackexchange.com/questions/255751
复制相似问题