From 4cf55f55c7f8edd87a4f2eade525c1393f4fa076 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 10:40:14 +0000 Subject: [PATCH 01/29] . --- longs/models.py | 51 +++++++++++++++++++++++++++++++++++ longs/urls.py | 2 +- longs/views.py | 69 ------------------------------------------------ rapids/models.py | 51 +++++++++++++++++++++++++++++++++++ rapids/urls.py | 2 +- rapids/views.py | 67 ---------------------------------------------- 6 files changed, 104 insertions(+), 138 deletions(-) diff --git a/longs/models.py b/longs/models.py index ee6b788b..b68439a6 100644 --- a/longs/models.py +++ b/longs/models.py @@ -18,6 +18,8 @@ from django.utils.html import mark_safe from sortedm2m.fields import SortedManyToManyField import string +from collections import defaultdict +from helpers.images import image_as_base64 from anatomy.models import Modality @@ -303,6 +305,55 @@ class Exam(ExamBase): default=4500, ) + def get_exam_json(self): + questions = exam.exam_questions.all() + + exam_questions = defaultdict(dict) + + exam_order = [] + + for q in questions: + exam_order.append(q.id) + + # Loop through longimage associations + images = [] + feedback_images = [] + for i in q.images.all(): + if i.feedback_image == True: + feedback_images.append(image_as_base64(i.image)) + # feedback_images.append(i.image.url) + else: + images.append(image_as_base64(i.image)) + #images.append(i.image.url) + + + + exam_questions[q.id] = { + "images": images, + #"feedback_image": [], + #"annotations": [str(q.image_annotations)], + "type": "long", + } + + #if feedback_images: + # exam_questions[q.id]["feedback_image"] = feedback_images + + + exam_json = { + "eid": "long/{}".format(exam.id), + "cached": False, + "exam_type": "long", + "exam_name": exam.name, + "exam_mode": True, + "exam_order": exam_order, + "questions": exam_questions, + } + + + if exam.time_limit: + exam_json["exam_time"] = exam.time_limit + + return exam_json class CidUserAnswer(models.Model): """User answers by candidate""" diff --git a/longs/urls.py b/longs/urls.py index 07aebd3d..fc8379a9 100755 --- a/longs/urls.py +++ b/longs/urls.py @@ -48,7 +48,7 @@ urlpatterns = [ path("exam/submit", views.LongExamViews.postExamAnswers, name="exam_answers_submit"), path("exam/", views.LongExamViews.exam_list, name="exam_list"), path("exam/json/", views.LongExamViews.active_exams, name="active_exams"), - path("exam/json/", views.exam_json, name="exam_json"), + path("exam/json/", views.LongExamViews.exam_json, name="exam_json"), path( "exam/json//recreate", views.LongExamViews.exam_json_recreate, diff --git a/longs/views.py b/longs/views.py index 500c7c5f..25797fa8 100755 --- a/longs/views.py +++ b/longs/views.py @@ -796,75 +796,6 @@ def mark(request, pk, sk): ) - -def exam_json(request, pk): - - exam = get_object_or_404(Exam, pk=pk) - - if not exam.active: - raise Http404("No available exam") - - exam_json_cache = cache.get("longs_exam_json_{}".format(pk)) - - if exam_json_cache is not None and not exam.recreate_json: - exam_json_cache["cached"] = True - return JsonResponse(exam_json_cache) - - questions = exam.exam_questions.all() - - exam_questions = defaultdict(dict) - - exam_order = [] - - for q in questions: - exam_order.append(q.id) - - # Loop through longimage associations - images = [] - feedback_images = [] - for i in q.images.all(): - if i.feedback_image == True: - feedback_images.append(image_as_base64(i.image)) - # feedback_images.append(i.image.url) - else: - images.append(image_as_base64(i.image)) - #images.append(i.image.url) - - - - exam_questions[q.id] = { - "images": images, - #"feedback_image": [], - #"annotations": [str(q.image_annotations)], - "type": "long", - } - - #if feedback_images: - # exam_questions[q.id]["feedback_image"] = feedback_images - - - exam_json = { - "eid": "long/{}".format(exam.id), - "cached": False, - "exam_type": "long", - "exam_name": exam.name, - "exam_mode": True, - "exam_order": exam_order, - "questions": exam_questions, - } - - - if exam.time_limit: - exam_json["exam_time"] = exam.time_limit - - exam.recreate_json = False - exam.save() - - cache.set("longs_exam_json_{}".format(pk), exam_json, 3600) - - return JsonResponse(exam_json) - - @login_required def exam_scores_cid(request, pk): exam = get_object_or_404(Exam, pk=pk) diff --git a/rapids/models.py b/rapids/models.py index 6c8edbce..90656cb9 100644 --- a/rapids/models.py +++ b/rapids/models.py @@ -16,6 +16,8 @@ from django.utils.html import mark_safe from sortedm2m.fields import SortedManyToManyField import string +from collections import defaultdict +from helpers.images import image_as_base64 from generic.models import Site, Condition, Sign, ExamBase @@ -350,6 +352,55 @@ class Exam(ExamBase): help_text="Exam time limit (in seconds). Default is 2100 secondse (35 minutes)", default=2100 ) + def get_exam_json(self): + questions = self.exam_questions.all() + + exam_questions = defaultdict(dict) + + exam_order = [] + + for q in questions: + exam_order.append(q.id) + + # Loop through rapidimage associations + images = [] + feedback_images = [] + for i in q.images.all(): + if i.feedback_image == True: + feedback_images.append(image_as_base64(i.image)) + # feedback_images.append(i.image.url) + else: + images.append(image_as_base64(i.image)) + #images.append(i.image.url) + + + + exam_questions[q.id] = { + "images": images, + #"feedback_image": [], + #"annotations": [str(q.image_annotations)], + "type": "rapid", + } + + #if feedback_images: + # exam_questions[q.id]["feedback_image"] = feedback_images + + + exam_json = { + "eid": "rapid/{}".format(exam.id), + "cached": False, + "exam_type": "rapid", + "exam_name": exam.name, + "exam_mode": True, + "exam_order": exam_order, + "questions": exam_questions, + } + + if exam.time_limit: + exam_json["exam_time"] = exam.time_limit + + return + class CidUserAnswer(models.Model): """User answers by candidate""" diff --git a/rapids/urls.py b/rapids/urls.py index a5df6aec..32acb33d 100755 --- a/rapids/urls.py +++ b/rapids/urls.py @@ -32,7 +32,7 @@ urlpatterns = [ path("exam/submit", views.RapidExamViews.postExamAnswers, name="exam_answers_submit"), path("exam/", views.RapidExamViews.exam_list, name="exam_list"), path("exam/json/", views.RapidExamViews.active_exams, name="active_exams"), - path("exam/json/", views.exam_json, name="exam_json"), + path("exam/json/", views.RapidExamViews.exam_json, name="exam_json"), path("exam/json//recreate", views.RapidExamViews.exam_json_recreate, name="exam_json_recreate"), path("create/", views.RapidCreate.as_view(), name="rapid_create"), path("create/defaults", diff --git a/rapids/views.py b/rapids/views.py index 6e761d0c..ba4dab7e 100755 --- a/rapids/views.py +++ b/rapids/views.py @@ -699,73 +699,6 @@ def mark(request, pk, sk): ) -def exam_json(request, pk): - - exam = get_object_or_404(Exam, pk=pk) - - if not exam.active: - raise Http404("No available exam") - - exam_json_cache = cache.get("rapids_exam_json_{}".format(pk)) - - if exam_json_cache is not None and not exam.recreate_json: - exam_json_cache["cached"] = True - return JsonResponse(exam_json_cache) - - questions = exam.exam_questions.all() - - exam_questions = defaultdict(dict) - - exam_order = [] - - for q in questions: - exam_order.append(q.id) - - # Loop through rapidimage associations - images = [] - feedback_images = [] - for i in q.images.all(): - if i.feedback_image == True: - feedback_images.append(image_as_base64(i.image)) - # feedback_images.append(i.image.url) - else: - images.append(image_as_base64(i.image)) - #images.append(i.image.url) - - - - exam_questions[q.id] = { - "images": images, - #"feedback_image": [], - #"annotations": [str(q.image_annotations)], - "type": "rapid", - } - - #if feedback_images: - # exam_questions[q.id]["feedback_image"] = feedback_images - - - exam_json = { - "eid": "rapid/{}".format(exam.id), - "cached": False, - "exam_type": "rapid", - "exam_name": exam.name, - "exam_mode": True, - "exam_order": exam_order, - "questions": exam_questions, - } - - if exam.time_limit: - exam_json["exam_time"] = exam.time_limit - - exam.recreate_json = False - exam.save() - - cache.set("rapids_exam_json_{}".format(pk), exam_json, 3600) - - return JsonResponse(exam_json) - - @login_required def exam_scores_cid(request, pk): exam = get_object_or_404(Exam, pk=pk) From 643d3ca6c2eb6b58a9f3ddd6caccd9acb9155f24 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 10:43:23 +0000 Subject: [PATCH 02/29] . --- generic/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generic/views.py b/generic/views.py index a72fe095..ed517883 100644 --- a/generic/views.py +++ b/generic/views.py @@ -210,7 +210,7 @@ class ExamViews(View, LoginRequiredMixin): return JsonResponse(active_exams) @method_decorator(csrf_exempt) - def postExamAnswers(request): + def postExamAnswers(self, request): if request.is_ajax and request.method == "POST": n = 0 From 2fdb4318ff76488857180258a54b73610368b0f8 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 11:15:46 +0000 Subject: [PATCH 03/29] . --- anatomy/filters.py | 18 +++++++++++++++++- anatomy/models.py | 35 ---------------------------------- anatomy/tables.py | 19 ++++++++++++++++++- anatomy/urls.py | 1 + anatomy/views.py | 11 +++++++++-- generic/filters.py | 18 ++++++++++++++++++ generic/tables.py | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 110 insertions(+), 39 deletions(-) create mode 100644 generic/filters.py create mode 100644 generic/tables.py diff --git a/anatomy/filters.py b/anatomy/filters.py index 8358550f..b5995911 100644 --- a/anatomy/filters.py +++ b/anatomy/filters.py @@ -1,6 +1,22 @@ import django_filters -from .models import AnatomyQuestion +from .models import AnatomyQuestion, CidUserAnswer + +#from generic.filters import UserAnswerFilter + + +class AnatomyUserAnswerFilter(django_filters.FilterSet): + class Meta: + model= CidUserAnswer + fields = ( + "cid", + "question", + "answer", + #"answer_compare", + "exam", + "created", + "updated", + ) class AnatomyQuestionFilter(django_filters.FilterSet): diff --git a/anatomy/models.py b/anatomy/models.py index cc3f74c0..4dc61b51 100644 --- a/anatomy/models.py +++ b/anatomy/models.py @@ -310,41 +310,6 @@ class Exam(ExamBase): return exam_json -# class UserAnswer(models.Model): -# question = models.ForeignKey( -# AnatomyQuestion, related_name="user_answers", on_delete=models.CASCADE -# ) -# answer = models.TextField(max_length=500, blank=True) -# user = models.ForeignKey( -# settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL -# ) - -# cid = models.BigIntegerField(blank=True, null=True, help_text="Candidate ID (limitied by BigIntegerField size)") - -# # Each user answer is associated with a particular exam -# exam = models.ForeignKey( -# Exam, related_name="exam", on_delete=models.CASCADE, null=True -# ) - -# flagged = models.BooleanField(default=False) - -# created = models.DateTimeField(auto_now_add=True) -# updated = models.DateTimeField(auto_now=True) - -# def __str__(self): -# try: -# exam = self.exam -# except (Exam.DoesNotExist, KeyError) as e: -# exam = "None" -# return "{}/{}/{}: {}".format( -# exam, self.cid, self.question.GetPrimaryAnswer(), self.answer -# ) - -# def clean(self): -# if self.answer: -# self.answer = self.answer.strip() - - class CidUserAnswer(models.Model): """User answers by candidate""" diff --git a/anatomy/tables.py b/anatomy/tables.py index c3994922..20255f6c 100644 --- a/anatomy/tables.py +++ b/anatomy/tables.py @@ -1,7 +1,7 @@ import django_tables2 as tables from django_tables2.utils import A -from .models import AnatomyQuestion +from .models import AnatomyQuestion, CidUserAnswer from django.utils.html import format_html @@ -9,6 +9,23 @@ from easy_thumbnails.files import get_thumbnailer from easy_thumbnails.exceptions import InvalidImageFormatError +#from generic.tables import UserAnswerTable + + +class AnatomyUserAnswerTable(tables.Table): + class Meta: + model = CidUserAnswer + template_name = "django_tables2/bootstrap4.html" + fields = ( + "cid", + "question", + "answer", + #"answer_compare", + "exam", + "created", + "updated", + ) + class ImageColumn(tables.Column): def render(self, value): diff --git a/anatomy/urls.py b/anatomy/urls.py index 0c4202fc..1827e0e0 100644 --- a/anatomy/urls.py +++ b/anatomy/urls.py @@ -51,4 +51,5 @@ urlpatterns = [ path("examination/ajax/get_examination_id", views.get_examination_id, name="get_examination_id"), + path("user_answers/", views.AnatomyUserAnswerView.as_view(), name="anatomy_user_answer_view"), ] diff --git a/anatomy/views.py b/anatomy/views.py index f83dbfc9..f2f0f9c4 100644 --- a/anatomy/views.py +++ b/anatomy/views.py @@ -46,8 +46,8 @@ from .models import ( from generic.models import Examination from generic.views import ExamViews -from .tables import AnatomyQuestionTable -from .filters import AnatomyQuestionFilter +from .tables import AnatomyQuestionTable, AnatomyUserAnswerTable +from .filters import AnatomyQuestionFilter, AnatomyUserAnswerFilter from django_tables2 import SingleTableView, SingleTableMixin from django_filters.views import FilterView @@ -919,6 +919,13 @@ class AnatomyQuestionView(SingleTableMixin, FilterView): filterset_class = AnatomyQuestionFilter +class AnatomyUserAnswerView(SingleTableMixin, FilterView): + model = CidUserAnswer + table_class = AnatomyUserAnswerTable + template_name = "anatomy/anatomy_question_view.html" + + filterset_class = AnatomyUserAnswerFilter + def question_save_annotation(request, pk): if request.is_ajax() and request.method == "POST": diff --git a/generic/filters.py b/generic/filters.py new file mode 100644 index 00000000..3eddff2e --- /dev/null +++ b/generic/filters.py @@ -0,0 +1,18 @@ +import django_filters + + + +#class UserAnswerFilter(django_filters.FilterSet): +# class Meta: +# #model = AnatomyQuestion +# abstract = True +# fields = ( +# "cid", +# "question", +# "answer", +# #"answer_compare", +# "exam", +# "created", +# "updateded", +# "created_date", +# ) \ No newline at end of file diff --git a/generic/tables.py b/generic/tables.py new file mode 100644 index 00000000..e87ae678 --- /dev/null +++ b/generic/tables.py @@ -0,0 +1,47 @@ +import django_tables2 as tables +from django_tables2.utils import A + +from django.utils.html import format_html + +from easy_thumbnails.files import get_thumbnailer + +from easy_thumbnails.exceptions import InvalidImageFormatError + + +class ImageColumn(tables.Column): + def render(self, value): + try: + thumbnailer = get_thumbnailer(value) + return format_html('', thumbnailer["exam-list"]) + except InvalidImageFormatError: + return format_html('Invalid image url', value) + + +#class UserAnswerTable(tables.Table): +# #edit = tables.LinkColumn( +# # "anatomy:anatomy_question_update", text="Edit", args=[A("pk")], orderable=False +# #) +# #view = tables.LinkColumn( +# # "anatomy:question_detail", text="View", args=[A("pk")], orderable=False +# #) +# #image = ImageColumn("image", orderable=False) +# # clone = tables.LinkColumn('anatomy:anatomy_question_clone', +# # text='Clone', +# # args=[A('pk')], +# # orderable=False) +# #exams = tables.ManyToManyColumn(verbose_name="Exams") +# +# class Meta: +# #model = AnatomyQuestion +# abstract = True +# template_name = "django_tables2/bootstrap4.html" +# fields = ( +# "cid", +# "question", +# "answer", +# #"answer_compare", +# "exam", +# "created", +# "updated", +# ) +# #sequence = ("view") \ No newline at end of file From d30ca04286736854b5fe529b7e3acecbc109012b Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 11:29:56 +0000 Subject: [PATCH 04/29] . --- anatomy/tables.py | 3 +++ anatomy/templates/anatomy/ciduseranswer_confirm_delete.html | 4 ++++ anatomy/urls.py | 1 + anatomy/views.py | 6 +++++- 4 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 anatomy/templates/anatomy/ciduseranswer_confirm_delete.html diff --git a/anatomy/tables.py b/anatomy/tables.py index 20255f6c..07f3e136 100644 --- a/anatomy/tables.py +++ b/anatomy/tables.py @@ -13,6 +13,9 @@ from easy_thumbnails.exceptions import InvalidImageFormatError class AnatomyUserAnswerTable(tables.Table): + delete = tables.LinkColumn( + "anatomy:anatomy_user_answer_delete", text="Delete", args=[A("pk")], orderable=False + ) class Meta: model = CidUserAnswer template_name = "django_tables2/bootstrap4.html" diff --git a/anatomy/templates/anatomy/ciduseranswer_confirm_delete.html b/anatomy/templates/anatomy/ciduseranswer_confirm_delete.html new file mode 100644 index 00000000..41ddc888 --- /dev/null +++ b/anatomy/templates/anatomy/ciduseranswer_confirm_delete.html @@ -0,0 +1,4 @@ +
{% csrf_token %} +

Are you sure you want to delete "{{ object }}"?

+ +
\ No newline at end of file diff --git a/anatomy/urls.py b/anatomy/urls.py index 1827e0e0..8049cbf7 100644 --- a/anatomy/urls.py +++ b/anatomy/urls.py @@ -52,4 +52,5 @@ urlpatterns = [ views.get_examination_id, name="get_examination_id"), path("user_answers/", views.AnatomyUserAnswerView.as_view(), name="anatomy_user_answer_view"), + path("user_answers/delete", views.UserAnswerDelete.as_view(), name="anatomy_user_answer_delete"), ] diff --git a/anatomy/views.py b/anatomy/views.py index f2f0f9c4..fe22a9a8 100644 --- a/anatomy/views.py +++ b/anatomy/views.py @@ -942,4 +942,8 @@ def question_save_annotation(request, pk): return JsonResponse(data, status=400) -AnatomyExamViews = ExamViews(Exam, "anatomy", "anatomy", loadJsonAnswer) \ No newline at end of file +AnatomyExamViews = ExamViews(Exam, "anatomy", "anatomy", loadJsonAnswer) + +class UserAnswerDelete(DeleteView): + model = CidUserAnswer + success_url = reverse_lazy("anatomy:anatomy_user_answer_view") \ No newline at end of file From 37e16dda35faa8103d1964d4bef4f6efcd5b087e Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 11:31:14 +0000 Subject: [PATCH 05/29] . --- anatomy/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anatomy/urls.py b/anatomy/urls.py index 8049cbf7..d27dd7a8 100644 --- a/anatomy/urls.py +++ b/anatomy/urls.py @@ -52,5 +52,5 @@ urlpatterns = [ views.get_examination_id, name="get_examination_id"), path("user_answers/", views.AnatomyUserAnswerView.as_view(), name="anatomy_user_answer_view"), - path("user_answers/delete", views.UserAnswerDelete.as_view(), name="anatomy_user_answer_delete"), + path("user_answers//delete", views.UserAnswerDelete.as_view(), name="anatomy_user_answer_delete"), ] From 0ac8eabe3547858ee713c9a2adb5100f3a5c5476 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 11:43:56 +0000 Subject: [PATCH 06/29] . --- anatomy/tables.py | 1 + anatomy/views.py | 4 +++- generic/mixins.py | 6 ++++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 generic/mixins.py diff --git a/anatomy/tables.py b/anatomy/tables.py index 07f3e136..ae73768b 100644 --- a/anatomy/tables.py +++ b/anatomy/tables.py @@ -13,6 +13,7 @@ from easy_thumbnails.exceptions import InvalidImageFormatError class AnatomyUserAnswerTable(tables.Table): + select = tables.CheckBoxColumn(accessor=("")) delete = tables.LinkColumn( "anatomy:anatomy_user_answer_delete", text="Delete", args=[A("pk")], orderable=False ) diff --git a/anatomy/views.py b/anatomy/views.py index fe22a9a8..bf303ab7 100644 --- a/anatomy/views.py +++ b/anatomy/views.py @@ -62,6 +62,8 @@ from helpers.images import image_as_base64 from django.template.defaulttags import register +from generic.mixins import SuperuserRequiredMixin + @register.filter def get_item(dictionary, key): @@ -944,6 +946,6 @@ def question_save_annotation(request, pk): AnatomyExamViews = ExamViews(Exam, "anatomy", "anatomy", loadJsonAnswer) -class UserAnswerDelete(DeleteView): +class UserAnswerDelete(SuperuserRequiredMixin, DeleteView): model = CidUserAnswer success_url = reverse_lazy("anatomy:anatomy_user_answer_view") \ No newline at end of file diff --git a/generic/mixins.py b/generic/mixins.py new file mode 100644 index 00000000..875aa998 --- /dev/null +++ b/generic/mixins.py @@ -0,0 +1,6 @@ +from django.contrib.auth.mixins import UserPassesTestMixin + + +class SuperuserRequiredMixin(UserPassesTestMixin): + def test_func(self): + return self.request.user.is_superuser \ No newline at end of file From 083fe952ed09e444f443a2fbedb6497f1d1309f9 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 11:45:36 +0000 Subject: [PATCH 07/29] . --- anatomy/tables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anatomy/tables.py b/anatomy/tables.py index ae73768b..be2b7287 100644 --- a/anatomy/tables.py +++ b/anatomy/tables.py @@ -13,7 +13,7 @@ from easy_thumbnails.exceptions import InvalidImageFormatError class AnatomyUserAnswerTable(tables.Table): - select = tables.CheckBoxColumn(accessor=("")) + select = tables.CheckBoxColumn(accessor=("test")) delete = tables.LinkColumn( "anatomy:anatomy_user_answer_delete", text="Delete", args=[A("pk")], orderable=False ) From f08bdb05f1a5af0a4f0f32d672c6918be5d9a48d Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 11:47:28 +0000 Subject: [PATCH 08/29] . --- anatomy/tables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anatomy/tables.py b/anatomy/tables.py index be2b7287..181be430 100644 --- a/anatomy/tables.py +++ b/anatomy/tables.py @@ -13,7 +13,7 @@ from easy_thumbnails.exceptions import InvalidImageFormatError class AnatomyUserAnswerTable(tables.Table): - select = tables.CheckBoxColumn(accessor=("test")) + select = tables.CheckBoxColumn(accessor=("pk")) delete = tables.LinkColumn( "anatomy:anatomy_user_answer_delete", text="Delete", args=[A("pk")], orderable=False ) From e754d8d8edd33017935c8345305d939e8f6fdf2a Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 11:49:02 +0000 Subject: [PATCH 09/29] . --- anatomy/tables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anatomy/tables.py b/anatomy/tables.py index 181be430..da63eb33 100644 --- a/anatomy/tables.py +++ b/anatomy/tables.py @@ -13,7 +13,7 @@ from easy_thumbnails.exceptions import InvalidImageFormatError class AnatomyUserAnswerTable(tables.Table): - select = tables.CheckBoxColumn(accessor=("pk")) + select = tables.CheckBoxColumn(text="Select", accessor=("pk")) delete = tables.LinkColumn( "anatomy:anatomy_user_answer_delete", text="Delete", args=[A("pk")], orderable=False ) From 43c31adc99c4fff4ad718b6c780122d2002445ab Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 11:49:45 +0000 Subject: [PATCH 10/29] . --- anatomy/tables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anatomy/tables.py b/anatomy/tables.py index da63eb33..181be430 100644 --- a/anatomy/tables.py +++ b/anatomy/tables.py @@ -13,7 +13,7 @@ from easy_thumbnails.exceptions import InvalidImageFormatError class AnatomyUserAnswerTable(tables.Table): - select = tables.CheckBoxColumn(text="Select", accessor=("pk")) + select = tables.CheckBoxColumn(accessor=("pk")) delete = tables.LinkColumn( "anatomy:anatomy_user_answer_delete", text="Delete", args=[A("pk")], orderable=False ) From 42b1a72a4a8f88358ebda1bfd898e76f61df1a06 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 11:59:08 +0000 Subject: [PATCH 11/29] . --- longs/models.py | 18 +++++++----------- rapids/models.py | 6 +++--- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/longs/models.py b/longs/models.py index b68439a6..5468ed4b 100644 --- a/longs/models.py +++ b/longs/models.py @@ -306,7 +306,7 @@ class Exam(ExamBase): ) def get_exam_json(self): - questions = exam.exam_questions.all() + questions = self.exam_questions.all() exam_questions = defaultdict(dict) @@ -317,14 +317,10 @@ class Exam(ExamBase): # Loop through longimage associations images = [] - feedback_images = [] - for i in q.images.all(): - if i.feedback_image == True: - feedback_images.append(image_as_base64(i.image)) - # feedback_images.append(i.image.url) - else: - images.append(image_as_base64(i.image)) - #images.append(i.image.url) + for series in q.series: + for i in series.images.all(): + #images.append(image_as_base64(i.image)) + images.append(i.image.url) @@ -343,7 +339,7 @@ class Exam(ExamBase): "eid": "long/{}".format(exam.id), "cached": False, "exam_type": "long", - "exam_name": exam.name, + "exam_name": self.name, "exam_mode": True, "exam_order": exam_order, "questions": exam_questions, @@ -351,7 +347,7 @@ class Exam(ExamBase): if exam.time_limit: - exam_json["exam_time"] = exam.time_limit + exam_json["exam_time"] = self.time_limit return exam_json diff --git a/rapids/models.py b/rapids/models.py index 90656cb9..a0ce3e0d 100644 --- a/rapids/models.py +++ b/rapids/models.py @@ -387,17 +387,17 @@ class Exam(ExamBase): exam_json = { - "eid": "rapid/{}".format(exam.id), + "eid": "rapid/{}".format(self.id), "cached": False, "exam_type": "rapid", - "exam_name": exam.name, + "exam_name": self.name, "exam_mode": True, "exam_order": exam_order, "questions": exam_questions, } if exam.time_limit: - exam_json["exam_time"] = exam.time_limit + exam_json["exam_time"] = self.time_limit return From 6586599c46e565bd6d034840f484ca7cb069b627 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 11:59:33 +0000 Subject: [PATCH 12/29] . --- longs/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/longs/models.py b/longs/models.py index 5468ed4b..8458d9f4 100644 --- a/longs/models.py +++ b/longs/models.py @@ -317,7 +317,7 @@ class Exam(ExamBase): # Loop through longimage associations images = [] - for series in q.series: + for series in q.series.all(): for i in series.images.all(): #images.append(image_as_base64(i.image)) images.append(i.image.url) From 15961ee986356bd809f433828846a650b646bfa9 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 12:00:01 +0000 Subject: [PATCH 13/29] . --- longs/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/longs/models.py b/longs/models.py index 8458d9f4..df45a73d 100644 --- a/longs/models.py +++ b/longs/models.py @@ -336,7 +336,7 @@ class Exam(ExamBase): exam_json = { - "eid": "long/{}".format(exam.id), + "eid": "long/{}".format(self.id), "cached": False, "exam_type": "long", "exam_name": self.name, From 4db7e31ac5f25da3de463d5635d562d38ba98704 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 12:00:59 +0000 Subject: [PATCH 14/29] . --- longs/models.py | 2 +- rapids/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/longs/models.py b/longs/models.py index df45a73d..fc687f32 100644 --- a/longs/models.py +++ b/longs/models.py @@ -346,7 +346,7 @@ class Exam(ExamBase): } - if exam.time_limit: + if self.time_limit: exam_json["exam_time"] = self.time_limit return exam_json diff --git a/rapids/models.py b/rapids/models.py index a0ce3e0d..5e5ff1da 100644 --- a/rapids/models.py +++ b/rapids/models.py @@ -396,7 +396,7 @@ class Exam(ExamBase): "questions": exam_questions, } - if exam.time_limit: + if self.time_limit: exam_json["exam_time"] = self.time_limit return From 790fce3e615bc7f8a39412dd13257a5d0655e698 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 12:13:58 +0000 Subject: [PATCH 15/29] . --- longs/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/longs/models.py b/longs/models.py index fc687f32..c7941c0d 100644 --- a/longs/models.py +++ b/longs/models.py @@ -318,13 +318,16 @@ class Exam(ExamBase): # Loop through longimage associations images = [] for series in q.series.all(): + image_array = [] for i in series.images.all(): #images.append(image_as_base64(i.image)) - images.append(i.image.url) + image_array.append(i.image.url) + images.append(image_array) exam_questions[q.id] = { + "title": q.history, "images": images, #"feedback_image": [], #"annotations": [str(q.image_annotations)], From 3b54c64599b6abef37f2928b0630a60a37bef640 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 12:17:33 +0000 Subject: [PATCH 16/29] . --- longs/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/longs/models.py b/longs/models.py index c7941c0d..d25846ae 100644 --- a/longs/models.py +++ b/longs/models.py @@ -320,8 +320,8 @@ class Exam(ExamBase): for series in q.series.all(): image_array = [] for i in series.images.all(): - #images.append(image_as_base64(i.image)) - image_array.append(i.image.url) + image_array.append(image_as_base64(i.image)) + #image_array.append(i.image.url) images.append(image_array) From 9d8ef99a30d47f5c88f4acf3e436ba7c4c8b642d Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 12:21:55 +0000 Subject: [PATCH 17/29] . --- rapids/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rapids/models.py b/rapids/models.py index 5e5ff1da..68384d2b 100644 --- a/rapids/models.py +++ b/rapids/models.py @@ -399,7 +399,7 @@ class Exam(ExamBase): if self.time_limit: exam_json["exam_time"] = self.time_limit - return + return exam_json class CidUserAnswer(models.Model): From 69c8784f9af74b631c2f589405aa1d5d1d1cf001 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 12:40:05 +0000 Subject: [PATCH 18/29] . --- longs/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/longs/models.py b/longs/models.py index d25846ae..8ee131ab 100644 --- a/longs/models.py +++ b/longs/models.py @@ -317,18 +317,21 @@ class Exam(ExamBase): # Loop through longimage associations images = [] + image_titles = [] for series in q.series.all(): image_array = [] for i in series.images.all(): image_array.append(image_as_base64(i.image)) #image_array.append(i.image.url) images.append(image_array) + image_titles.append(series.description) exam_questions[q.id] = { "title": q.history, "images": images, + "image_titles": image_titles, #"feedback_image": [], #"annotations": [str(q.image_annotations)], "type": "long", From 51a81699a9121fd263dd256ae9e8f9fee154b899 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 12:44:15 +0000 Subject: [PATCH 19/29] . --- longs/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/longs/models.py b/longs/models.py index 8ee131ab..990ba457 100644 --- a/longs/models.py +++ b/longs/models.py @@ -321,10 +321,10 @@ class Exam(ExamBase): for series in q.series.all(): image_array = [] for i in series.images.all(): - image_array.append(image_as_base64(i.image)) - #image_array.append(i.image.url) + #image_array.append(image_as_base64(i.image)) + image_array.append(i.image.url) images.append(image_array) - image_titles.append(series.description) + image_titles.append(series.examination) From 1ac5ec03d828d794df213e19d220fb512739f056 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 12:45:55 +0000 Subject: [PATCH 20/29] . --- longs/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/longs/models.py b/longs/models.py index 990ba457..91df2f38 100644 --- a/longs/models.py +++ b/longs/models.py @@ -324,7 +324,7 @@ class Exam(ExamBase): #image_array.append(image_as_base64(i.image)) image_array.append(i.image.url) images.append(image_array) - image_titles.append(series.examination) + image_titles.append(series.get_examination()) From 165c64c306002e1855e913001121b824c5dd304c Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 12:57:06 +0000 Subject: [PATCH 21/29] . --- longs/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/longs/models.py b/longs/models.py index 91df2f38..d8e92f10 100644 --- a/longs/models.py +++ b/longs/models.py @@ -321,8 +321,8 @@ class Exam(ExamBase): for series in q.series.all(): image_array = [] for i in series.images.all(): - #image_array.append(image_as_base64(i.image)) - image_array.append(i.image.url) + image_array.append(image_as_base64(i.image)) + #image_array.append(i.image.url) images.append(image_array) image_titles.append(series.get_examination()) From a72d9bfe8e88af6600d53d0b90406d704f8d4cb9 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 16:08:55 +0000 Subject: [PATCH 22/29] . --- longs/models.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/longs/models.py b/longs/models.py index d8e92f10..d1bdaf02 100644 --- a/longs/models.py +++ b/longs/models.py @@ -141,8 +141,13 @@ class Long(models.Model): # return self.GetImages() +def test_image_validator(file): + if 100 == 200: + raise ValidationError("Error") + return True + class LongSeriesImage(models.Model): - image = models.ImageField(upload_to=image_directory_path, storage=image_storage) + image = models.ImageField(upload_to=image_directory_path, storage=image_storage, validators=[test_image_validator]) position = models.IntegerField(default=0) series = models.ForeignKey( "LongSeries", related_name="images", on_delete=models.SET_NULL, null=True From 2b7f8b94b85010a1b9c8404d53c8f4628fee97ee Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 16:10:58 +0000 Subject: [PATCH 23/29] . --- longs/tables.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/longs/tables.py b/longs/tables.py index 64c5a07a..b1119226 100755 --- a/longs/tables.py +++ b/longs/tables.py @@ -31,6 +31,9 @@ class LongSeriesImageColumn(tables.Column): def render(self, value): obj = value.first() + if obj is None: + return "None" + image_object = obj.image try: thumbnailer = get_thumbnailer(image_object) From 13d67064ca1658248aa905c0e2d6427733d3b2ac Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 16:13:20 +0000 Subject: [PATCH 24/29] . --- longs/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/longs/models.py b/longs/models.py index d1bdaf02..8532d52b 100644 --- a/longs/models.py +++ b/longs/models.py @@ -207,7 +207,7 @@ class LongSeries(models.Model): return authors def get_author_display(self): - return ", ".join(self.get_author_objects()) + return ", ".join([self.get_author_objects()]) def get_examination(self): """Returns a comma seperated text list of regions""" From 30ad525ae0cb7bbb7d85db9d3d71fc5c29704903 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 16:16:23 +0000 Subject: [PATCH 25/29] . --- longs/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/longs/models.py b/longs/models.py index 8532d52b..74f2cdd6 100644 --- a/longs/models.py +++ b/longs/models.py @@ -207,7 +207,7 @@ class LongSeries(models.Model): return authors def get_author_display(self): - return ", ".join([self.get_author_objects()]) + return ", ".join([i.username for i in self.get_author_objects()]) def get_examination(self): """Returns a comma seperated text list of regions""" From 8d5eae2af2cc43becf58e73f62393e22be027ef5 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 8 Feb 2021 16:56:48 +0000 Subject: [PATCH 26/29] . --- longs/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/longs/models.py b/longs/models.py index 74f2cdd6..624acb23 100644 --- a/longs/models.py +++ b/longs/models.py @@ -15,6 +15,8 @@ from django.urls import reverse from django.utils.translation import ugettext_lazy as _ from django.utils.html import mark_safe +from django.core.exceptions import ValidationError + from sortedm2m.fields import SortedManyToManyField import string From 1fe3fa675351792e08e930289aa81f2a7737e4d2 Mon Sep 17 00:00:00 2001 From: Ross Date: Thu, 11 Feb 2021 18:30:52 +0000 Subject: [PATCH 27/29] . --- anatomy/migrations/0029_auto_20210209_2239.py | 20 +++++++++++++++++++ longs/migrations/0014_auto_20210211_1827.py | 20 +++++++++++++++++++ longs/models.py | 8 +++----- rapids/migrations/0013_auto_20210209_2239.py | 20 +++++++++++++++++++ 4 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 anatomy/migrations/0029_auto_20210209_2239.py create mode 100644 longs/migrations/0014_auto_20210211_1827.py create mode 100644 rapids/migrations/0013_auto_20210209_2239.py diff --git a/anatomy/migrations/0029_auto_20210209_2239.py b/anatomy/migrations/0029_auto_20210209_2239.py new file mode 100644 index 00000000..60fc1770 --- /dev/null +++ b/anatomy/migrations/0029_auto_20210209_2239.py @@ -0,0 +1,20 @@ +# Generated by Django 3.1.3 on 2021-02-09 22:39 + +import anatomy.models +import django.core.files.storage +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('anatomy', '0028_auto_20210206_0905'), + ] + + operations = [ + migrations.AlterField( + model_name='anatomyquestion', + name='image', + field=models.ImageField(storage=django.core.files.storage.FileSystemStorage(base_url='/media/anatomy/', location='/home/ross/rad/media//anatomy/'), upload_to=anatomy.models.image_directory_path), + ), + ] diff --git a/longs/migrations/0014_auto_20210211_1827.py b/longs/migrations/0014_auto_20210211_1827.py new file mode 100644 index 00000000..414ef2f2 --- /dev/null +++ b/longs/migrations/0014_auto_20210211_1827.py @@ -0,0 +1,20 @@ +# Generated by Django 3.1.3 on 2021-02-11 18:27 + +import django.core.files.storage +from django.db import migrations, models +import longs.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('longs', '0013_auto_20210206_2147'), + ] + + operations = [ + migrations.AlterField( + model_name='longseriesimage', + name='image', + field=models.FileField(storage=django.core.files.storage.FileSystemStorage(base_url='/media/longs/', location='/home/ross/rad/media/longs/'), upload_to=longs.models.image_directory_path), + ), + ] diff --git a/longs/models.py b/longs/models.py index 624acb23..40fd7934 100644 --- a/longs/models.py +++ b/longs/models.py @@ -34,7 +34,7 @@ from easy_thumbnails.exceptions import InvalidImageFormatError image_storage = FileSystemStorage( # Physical file location ROOT - location=u"{0}/longs/".format(settings.MEDIA_ROOT), + location=u"{0}longs/".format(settings.MEDIA_ROOT), # Url for file base_url=u"{0}longs/".format(settings.MEDIA_URL), ) @@ -144,12 +144,10 @@ class Long(models.Model): def test_image_validator(file): - if 100 == 200: - raise ValidationError("Error") - return True + pass class LongSeriesImage(models.Model): - image = models.ImageField(upload_to=image_directory_path, storage=image_storage, validators=[test_image_validator]) + image = models.FileField(upload_to=image_directory_path, storage=image_storage) position = models.IntegerField(default=0) series = models.ForeignKey( "LongSeries", related_name="images", on_delete=models.SET_NULL, null=True diff --git a/rapids/migrations/0013_auto_20210209_2239.py b/rapids/migrations/0013_auto_20210209_2239.py new file mode 100644 index 00000000..31981c9a --- /dev/null +++ b/rapids/migrations/0013_auto_20210209_2239.py @@ -0,0 +1,20 @@ +# Generated by Django 3.1.3 on 2021-02-09 22:39 + +import django.core.files.storage +from django.db import migrations, models +import rapids.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rapids', '0012_auto_20210206_0905'), + ] + + operations = [ + migrations.AlterField( + model_name='rapidimage', + name='image', + field=models.ImageField(storage=django.core.files.storage.FileSystemStorage(base_url='/media/rapids/', location='/home/ross/rad/media//rapids/'), upload_to=rapids.models.image_directory_path), + ), + ] From bf8a775a05ab09fa4a992803c0964d0efaea4a42 Mon Sep 17 00:00:00 2001 From: Ross Date: Thu, 11 Feb 2021 20:16:57 +0000 Subject: [PATCH 28/29] . --- helpers/images.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/helpers/images.py b/helpers/images.py index 7bea4e8f..bd926a37 100644 --- a/helpers/images.py +++ b/helpers/images.py @@ -14,4 +14,6 @@ def image_as_base64(image_file): # encoded_string = base64.b64encode(img_f.read()) encoded_string = base64.b64encode(image_file.file.read()) mimetype, enc = mimetypes.guess_type(image_file.path) + if "dicom" in mimetype: + return "data:{};base64,{}".format(mimetype, encoded_string.decode("utf-8")) return "data:image/{};base64,{}".format(mimetype, encoded_string.decode("utf-8")) \ No newline at end of file From 5763aae287cbceee318f975495f7d9429517c88f Mon Sep 17 00:00:00 2001 From: Ross Date: Thu, 11 Feb 2021 20:35:30 +0000 Subject: [PATCH 29/29] . --- rad/settings.py | 7 ++++++- rapids/migrations/0014_auto_20210211_2034.py | 20 ++++++++++++++++++++ rapids/migrations/0015_auto_20210211_2035.py | 20 ++++++++++++++++++++ rapids/models.py | 4 ++-- requirements.txt | 3 ++- 5 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 rapids/migrations/0014_auto_20210211_2034.py create mode 100644 rapids/migrations/0015_auto_20210211_2035.py diff --git a/rad/settings.py b/rad/settings.py index 1371a688..ee2eab7b 100644 --- a/rad/settings.py +++ b/rad/settings.py @@ -50,6 +50,7 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'debug_toolbar', "tagulous", + "dbbackup", ] MIDDLEWARE = [ @@ -198,7 +199,11 @@ THUMBNAIL_ALIASES = { }, } +DBBACKUP_STORAGE = 'django.core.files.storage.FileSystemStorage' +DBBACKUP_STORAGE_OPTIONS = {'location': os.path.join(BASE_DIR, '../backups')} + try: from .settings_local import * except ImportError: - pass \ No newline at end of file + pass + diff --git a/rapids/migrations/0014_auto_20210211_2034.py b/rapids/migrations/0014_auto_20210211_2034.py new file mode 100644 index 00000000..d6fc2db2 --- /dev/null +++ b/rapids/migrations/0014_auto_20210211_2034.py @@ -0,0 +1,20 @@ +# Generated by Django 3.1.3 on 2021-02-11 20:34 + +import django.core.files.storage +from django.db import migrations, models +import rapids.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rapids', '0013_auto_20210209_2239'), + ] + + operations = [ + migrations.AlterField( + model_name='rapidimage', + name='image', + field=models.FileField(storage=django.core.files.storage.FileSystemStorage(base_url='/media/rapids/', location='/home/ross/rad/media//rapids/'), upload_to=rapids.models.image_directory_path), + ), + ] diff --git a/rapids/migrations/0015_auto_20210211_2035.py b/rapids/migrations/0015_auto_20210211_2035.py new file mode 100644 index 00000000..2743692c --- /dev/null +++ b/rapids/migrations/0015_auto_20210211_2035.py @@ -0,0 +1,20 @@ +# Generated by Django 3.1.3 on 2021-02-11 20:35 + +import django.core.files.storage +from django.db import migrations, models +import rapids.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rapids', '0014_auto_20210211_2034'), + ] + + operations = [ + migrations.AlterField( + model_name='rapidimage', + name='image', + field=models.FileField(storage=django.core.files.storage.FileSystemStorage(base_url='/media/rapids/', location='/home/ross/rad/media/rapids/'), upload_to=rapids.models.image_directory_path), + ), + ] diff --git a/rapids/models.py b/rapids/models.py index 68384d2b..75754638 100644 --- a/rapids/models.py +++ b/rapids/models.py @@ -24,7 +24,7 @@ from generic.models import Site, Condition, Sign, ExamBase image_storage = FileSystemStorage( # Physical file location ROOT - location=u"{0}/rapids/".format(settings.MEDIA_ROOT), + location=u"{0}rapids/".format(settings.MEDIA_ROOT), # Url for file base_url=u"{0}rapids/".format(settings.MEDIA_URL), ) @@ -288,7 +288,7 @@ class RapidImage(models.Model): rapid = models.ForeignKey(Rapid, related_name="images", on_delete=models.CASCADE) - image = models.ImageField(upload_to=image_directory_path, storage=image_storage) + image = models.FileField(upload_to=image_directory_path, storage=image_storage) feedback_image = models.BooleanField(default=False) diff --git a/requirements.txt b/requirements.txt index 733a804c..416b8e8e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,4 +13,5 @@ django-filer psycopg2-binary django-sortedm2m pytesseract -opencv-python \ No newline at end of file +opencv-python +django-dbbackup \ No newline at end of file