mirror of
				https://github.com/Alexander-D-Karpov/akarpov
				synced 2025-10-31 19:07:26 +03:00 
			
		
		
		
	added form generate and process logic
This commit is contained in:
		
							parent
							
								
									2d8576ff9b
								
							
						
					
					
						commit
						da12a206be
					
				
							
								
								
									
										0
									
								
								akarpov/static/js/form_logic.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								akarpov/static/js/form_logic.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -25,15 +25,9 @@ | ||||||
|     ================================================== --> |     ================================================== --> | ||||||
|     {# Placed at the top of the document so pages load faster with defer #} |     {# Placed at the top of the document so pages load faster with defer #} | ||||||
|     {% block javascript %} |     {% block javascript %} | ||||||
|       <!-- Bootstrap JS --> |  | ||||||
|       <script defer src="{% static 'js/bootstrap.bundle.min.js' %}" crossorigin="anonymous" referrerpolicy="no-referrer"></script> |  | ||||||
|       <!-- Your stuff: Third-party javascript libraries go here --> |  | ||||||
| 
 |  | ||||||
|       <!-- place project specific Javascript in this file --> |  | ||||||
|       <script defer src="{% static 'js/project.js' %}"></script> |  | ||||||
| 
 |  | ||||||
|     {% endblock javascript %} |     {% endblock javascript %} | ||||||
| 
 |     <script defer src="{% static 'js/bootstrap.bundle.min.js' %}" crossorigin="anonymous" referrerpolicy="no-referrer"></script> | ||||||
|  |     <script defer src="{% static 'js/project.js' %}"></script> | ||||||
|   </head> |   </head> | ||||||
| 
 | 
 | ||||||
|   <body> |   <body> | ||||||
|  |  | ||||||
							
								
								
									
										10
									
								
								akarpov/templates/test_platform/admin.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								akarpov/templates/test_platform/admin.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | <!DOCTYPE html> | ||||||
|  | <html lang="en"> | ||||||
|  | <head> | ||||||
|  |   <meta charset="UTF-8"> | ||||||
|  |   <title>Title</title> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  | 
 | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
|  | @ -5,22 +5,63 @@ | ||||||
| {% block title %}Creating new Form on akarpov{% endblock %} | {% block title %}Creating new Form on akarpov{% endblock %} | ||||||
| 
 | 
 | ||||||
| {% block content %} | {% block content %} | ||||||
| <form class="pt-2" enctype="multipart/form-data" method="POST" id="designer-form"> | <form class="pt-2 needs-validation" enctype="multipart/form-data" method="POST" id="designer-form"> | ||||||
|     {% csrf_token %} |     {% csrf_token %} | ||||||
|     {{ form.media }} |     {{ form.media }} | ||||||
|     {% for field in form %} |     {% for field in form %} | ||||||
|         {{ field|as_crispy_field }} |         {{ field|as_crispy_field }} | ||||||
|     {% endfor %} |     {% endfor %} | ||||||
|     <div class="mt-4 flex justify-end space-x-4"> |  | ||||||
|         <button class="btn btn-secondary" type="submit" id="submit"> |  | ||||||
|             Save Changes |  | ||||||
|         </button> |  | ||||||
|     </div> |  | ||||||
| {% for question, question_form in questions.items %} |  | ||||||
| {{ question }} |  | ||||||
| {% for field in question_form %} |  | ||||||
|     {{ field|as_crispy_field }} |  | ||||||
| {% endfor %} |  | ||||||
| {% endfor %} |  | ||||||
| </form> | </form> | ||||||
|  | <div class="row d-flex justify-content-center"> | ||||||
|  |   <div class="m-3 col-md-2 col-sm-auto" id="questions"> | ||||||
|  |       <div id="new_btn" class="input-group mb-3"> | ||||||
|  |         <button type="button" id="create_question_button" class="btn btn-outline-primary"><i class="bi bi-plus-circle"></i></button> | ||||||
|  |         <select class="form-select" id="inputGroupSelect" aria-label="select with addon"> | ||||||
|  |         </select> | ||||||
|  |       </div> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
|  | <div class="mt-4 flex justify-end space-x-4"> | ||||||
|  |     <input class="btn btn-success" type="submit" form="designer-form" value="Create"/> | ||||||
|  | </div> | ||||||
|  | {% endblock %} | ||||||
|  | 
 | ||||||
|  | {% block inline_javascript %} | ||||||
|  | <script> | ||||||
|  | let questionTypes = [] | ||||||
|  | let questionTypesDescription = [] | ||||||
|  | let questions = [] | ||||||
|  | let q_index = 1; | ||||||
|  | 
 | ||||||
|  | const button = document.getElementById("create_question_button"); | ||||||
|  | const form = document.getElementById("designer-form"); | ||||||
|  | const select = document.getElementById('inputGroupSelect') | ||||||
|  | 
 | ||||||
|  | {% for question, question_form in questions.items %} | ||||||
|  |   type = '{{ question.type }}' | ||||||
|  |   description = '{{ question.type_plural }}' | ||||||
|  | 
 | ||||||
|  |   questionTypes.push(type) | ||||||
|  |   questionTypesDescription.push(description) | ||||||
|  |   select.innerHTML += `<option value="${type}">${description}</option>` | ||||||
|  | 
 | ||||||
|  |   questionPrototype = ` | ||||||
|  |   <div class="border p-4 mt-5 rounded-2"> | ||||||
|  |   <input type="hidden" value="${type}" name="type" /> | ||||||
|  |   {% for field in question_form %} | ||||||
|  |       {{ field|as_crispy_field }} | ||||||
|  |   {% endfor %} | ||||||
|  |   </div> | ||||||
|  |   ` | ||||||
|  |   questions.push(questionPrototype) | ||||||
|  | {% endfor %} | ||||||
|  | 
 | ||||||
|  | button.onclick = function () { | ||||||
|  |     console.log(q_index) | ||||||
|  |     let index = select.selectedIndex; | ||||||
|  |     let div = questions[index]; | ||||||
|  |     form.innerHTML += div; | ||||||
|  |     q_index += 1; | ||||||
|  | } | ||||||
|  | </script> | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
|  | @ -12,8 +12,12 @@ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class FormFormClass(forms.ModelForm): | class FormFormClass(forms.ModelForm): | ||||||
|     time_since = forms.DateField(widget=forms.TextInput(attrs={"type": "date"})) |     time_since = forms.DateField( | ||||||
|     time_till = forms.DateField(widget=forms.TextInput(attrs={"type": "date"})) |         widget=forms.TextInput(attrs={"type": "date"}), required=False | ||||||
|  |     ) | ||||||
|  |     time_till = forms.DateField( | ||||||
|  |         widget=forms.TextInput(attrs={"type": "date"}), required=False | ||||||
|  |     ) | ||||||
| 
 | 
 | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = Form |         model = Form | ||||||
|  |  | ||||||
|  | @ -0,0 +1,48 @@ | ||||||
|  | # Generated by Django 4.1.7 on 2023-02-25 11:28 | ||||||
|  | 
 | ||||||
|  | from django.db import migrations, models | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  | 
 | ||||||
|  |     dependencies = [ | ||||||
|  |         ("test_platform", "0002_form_time_since_selectanswerquestion_question"), | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name="numberrangequestion", | ||||||
|  |             name="number_range_max", | ||||||
|  |             field=models.IntegerField(blank=True), | ||||||
|  |         ), | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name="numberrangequestion", | ||||||
|  |             name="number_range_min", | ||||||
|  |             field=models.IntegerField(blank=True), | ||||||
|  |         ), | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name="selectquestion", | ||||||
|  |             name="max_required_answers", | ||||||
|  |             field=models.IntegerField(blank=True), | ||||||
|  |         ), | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name="selectquestion", | ||||||
|  |             name="min_required_answers", | ||||||
|  |             field=models.IntegerField(blank=True), | ||||||
|  |         ), | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name="textquestion", | ||||||
|  |             name="answer_should_contain", | ||||||
|  |             field=models.CharField(blank=True, max_length=250), | ||||||
|  |         ), | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name="textquestion", | ||||||
|  |             name="answer_should_not_contain", | ||||||
|  |             field=models.CharField(blank=True, max_length=250), | ||||||
|  |         ), | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name="textquestion", | ||||||
|  |             name="correct_answer", | ||||||
|  |             field=models.CharField(blank=True, max_length=250), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| import uuid | import uuid | ||||||
| 
 | 
 | ||||||
| from django.db import models | from django.db import models | ||||||
|  | from django.urls import reverse | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
| from polymorphic.models import PolymorphicModel | from polymorphic.models import PolymorphicModel | ||||||
|  | @ -38,6 +39,10 @@ def available(self) -> bool: | ||||||
|             else True |             else True | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  |     def get_absolute_url(self): | ||||||
|  |         # TODO change to admin | ||||||
|  |         return reverse("test_platform:create") | ||||||
|  | 
 | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         return f"form: {self.name}" |         return f"form: {self.name}" | ||||||
| 
 | 
 | ||||||
|  | @ -59,29 +64,29 @@ def __str__(self): | ||||||
| class TextQuestion(BaseQuestion): | class TextQuestion(BaseQuestion): | ||||||
|     type = "text" |     type = "text" | ||||||
|     type_plural = _("Text question") |     type_plural = _("Text question") | ||||||
|     correct_answer = models.CharField(max_length=250, blank=False) |     correct_answer = models.CharField(max_length=250, blank=True) | ||||||
|     answer_should_contain = models.CharField(max_length=250, blank=False) |     answer_should_contain = models.CharField(max_length=250, blank=True) | ||||||
|     answer_should_not_contain = models.CharField(max_length=250, blank=False) |     answer_should_not_contain = models.CharField(max_length=250, blank=True) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class NumberQuestion(BaseQuestion): | class NumberQuestion(BaseQuestion): | ||||||
|     type = "number" |     type = "number" | ||||||
|     type_plural = _("Number question") |     type_plural = _("Number question") | ||||||
|     correct_answer = models.IntegerField() |     correct_answer = models.IntegerField(blank=True) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class NumberRangeQuestion(BaseQuestion): | class NumberRangeQuestion(BaseQuestion): | ||||||
|     type = "range" |     type = "range" | ||||||
|     type_plural = _("Number question") |     type_plural = _("Number range question") | ||||||
|     number_range_min = models.IntegerField(blank=False) |     number_range_min = models.IntegerField(blank=True) | ||||||
|     number_range_max = models.IntegerField(blank=False) |     number_range_max = models.IntegerField(blank=True) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class SelectQuestion(BaseQuestion): | class SelectQuestion(BaseQuestion): | ||||||
|     type = "select" |     type = "select" | ||||||
|     type_plural = _("Select question") |     type_plural = _("Select question") | ||||||
|     min_required_answers = models.IntegerField(blank=False) |     min_required_answers = models.IntegerField(blank=True) | ||||||
|     max_required_answers = models.IntegerField(blank=False) |     max_required_answers = models.IntegerField(blank=True) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class SelectAnswerQuestion(models.Model): | class SelectAnswerQuestion(models.Model): | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| from akarpov.test_platform.forms import ( | from akarpov.test_platform.forms import ( | ||||||
|  |     BaseQuestionForm, | ||||||
|     NumberQuestionForm, |     NumberQuestionForm, | ||||||
|     NumberRangeQuestionForm, |     NumberRangeQuestionForm, | ||||||
|     SelectQuestionForm, |     SelectQuestionForm, | ||||||
|  | @ -20,9 +21,35 @@ | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def get_question_types(): | def _get_fields_from_type(type: str): | ||||||
|  |     for question in BaseQuestion.get_subclasses()[::-1]: | ||||||
|  |         if question.type == type: | ||||||
|  |             form = question_forms[question] | ||||||
|  |             return form.Meta.fields | ||||||
|  |     raise ValueError | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_question_types() -> dict[BaseQuestion, BaseQuestionForm]: | ||||||
|     res = {} |     res = {} | ||||||
|     questions = BaseQuestion.get_subclasses() |     questions = BaseQuestion.get_subclasses()[::-1] | ||||||
|     for question in questions: |     for question in questions: | ||||||
|         res[question.type_plural] = question_forms[question] |         res[question] = question_forms[question] | ||||||
|  |     return res | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def parse_form_create(values) -> list[dict[str, str]]: | ||||||
|  |     offset: dict[str, int] = {} | ||||||
|  |     res: list[dict[str, str]] = [] | ||||||
|  |     question_amount = len(values.getlist("type")) | ||||||
|  |     for i in range(question_amount): | ||||||
|  |         type = values.getlist("type")[i] | ||||||
|  |         res.append({"type": type}) | ||||||
|  |         fields = _get_fields_from_type(type) | ||||||
|  |         for field in fields: | ||||||
|  |             if field in offset: | ||||||
|  |                 offset[field] += 1 | ||||||
|  |             else: | ||||||
|  |                 offset[field] = 0 | ||||||
|  |             value = values.getlist(field)[offset[field]] | ||||||
|  |             res[i][field] = value | ||||||
|     return res |     return res | ||||||
|  |  | ||||||
|  | @ -1,12 +1,13 @@ | ||||||
| from django.contrib.auth.mixins import LoginRequiredMixin | from django.contrib.auth.mixins import LoginRequiredMixin | ||||||
|  | from django.contrib.messages.views import SuccessMessageMixin | ||||||
| from django.views.generic import CreateView | from django.views.generic import CreateView | ||||||
| 
 | 
 | ||||||
| from akarpov.test_platform.forms import FormFormClass | from akarpov.test_platform.forms import FormFormClass | ||||||
| from akarpov.test_platform.models import Form | from akarpov.test_platform.models import Form | ||||||
| from akarpov.test_platform.services.forms import get_question_types | from akarpov.test_platform.services.forms import get_question_types, parse_form_create | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class FromCreateView(LoginRequiredMixin, CreateView): | class FromCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): | ||||||
|     model = Form |     model = Form | ||||||
|     form_class = FormFormClass |     form_class = FormFormClass | ||||||
| 
 | 
 | ||||||
|  | @ -19,6 +20,7 @@ def get_context_data(self, **kwargs): | ||||||
| 
 | 
 | ||||||
|     def form_valid(self, form): |     def form_valid(self, form): | ||||||
|         form.instance.creator = self.request.user |         form.instance.creator = self.request.user | ||||||
|  |         print(parse_form_create(self.request.POST)) | ||||||
|         return super().form_valid(form) |         return super().form_valid(form) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										20
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										20
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							|  | @ -988,14 +988,14 @@ Pillow = ">=9.0.0" | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "django-cors-headers" | name = "django-cors-headers" | ||||||
| version = "3.13.0" | version = "3.14.0" | ||||||
| description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." | description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." | ||||||
| category = "main" | category = "main" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.7" | python-versions = ">=3.7" | ||||||
| files = [ | files = [ | ||||||
|     {file = "django-cors-headers-3.13.0.tar.gz", hash = "sha256:f9dc6b4e3f611c3199700b3e5f3398c28757dcd559c2f82932687f3d0443cfdf"}, |     {file = "django_cors_headers-3.14.0-py3-none-any.whl", hash = "sha256:684180013cc7277bdd8702b80a3c5a4b3fcae4abb2bf134dceb9f5dfe300228e"}, | ||||||
|     {file = "django_cors_headers-3.13.0-py3-none-any.whl", hash = "sha256:37e42883b5f1f2295df6b4bba96eb2417a14a03270cb24b2a07f021cd4487cf4"}, |     {file = "django_cors_headers-3.14.0.tar.gz", hash = "sha256:5fbd58a6fb4119d975754b2bc090f35ec160a8373f276612c675b00e8a138739"}, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
|  | @ -1492,14 +1492,14 @@ doc = ["Sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "faker" | name = "faker" | ||||||
| version = "17.1.0" | version = "17.3.0" | ||||||
| description = "Faker is a Python package that generates fake data for you." | description = "Faker is a Python package that generates fake data for you." | ||||||
| category = "main" | category = "main" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.7" | python-versions = ">=3.7" | ||||||
| files = [ | files = [ | ||||||
|     {file = "Faker-17.1.0-py3-none-any.whl", hash = "sha256:abc383d8e02403fe2aa3b5369283dd468494a450533b513ab6d23637f3f25396"}, |     {file = "Faker-17.3.0-py3-none-any.whl", hash = "sha256:1dfffa43b4492b899a839619f2056060b585ba0bc76f329525194ca04dde0988"}, | ||||||
|     {file = "Faker-17.1.0.tar.gz", hash = "sha256:b94dc47512234fc5fff6f0752aaddd5e6ffea550f6ba31f4865b48d2b35429a1"}, |     {file = "Faker-17.3.0.tar.gz", hash = "sha256:26b2864a5332094f2c7f3968deebabce69be39ed5db4dbf22b4fa0ba3d1acdae"}, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [package.dependencies] | [package.dependencies] | ||||||
|  | @ -2051,7 +2051,6 @@ category = "main" | ||||||
| optional = false | optional = false | ||||||
| python-versions = "*" | python-versions = "*" | ||||||
| files = [ | files = [ | ||||||
|     {file = "livereload-2.6.3-py2.py3-none-any.whl", hash = "sha256:ad4ac6f53b2d62bb6ce1a5e6e96f1f00976a32348afedcb4b6d68df2a1d346e4"}, |  | ||||||
|     {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, |     {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | @ -3679,6 +3678,7 @@ category = "main" | ||||||
| optional = false | optional = false | ||||||
| python-versions = "*" | python-versions = "*" | ||||||
| files = [ | files = [ | ||||||
|  |     {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"}, | ||||||
|     {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, |     {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | @ -3703,14 +3703,14 @@ watchdog = ["watchdog"] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "whitenoise" | name = "whitenoise" | ||||||
| version = "6.3.0" | version = "6.4.0" | ||||||
| description = "Radically simplified static file serving for WSGI applications" | description = "Radically simplified static file serving for WSGI applications" | ||||||
| category = "main" | category = "main" | ||||||
| optional = false | optional = false | ||||||
| python-versions = ">=3.7" | python-versions = ">=3.7" | ||||||
| files = [ | files = [ | ||||||
|     {file = "whitenoise-6.3.0-py3-none-any.whl", hash = "sha256:cf8ecf56d86ba1c734fdb5ef6127312e39e92ad5947fef9033dc9e43ba2777d9"}, |     {file = "whitenoise-6.4.0-py3-none-any.whl", hash = "sha256:599dc6ca57e48929dfeffb2e8e187879bfe2aed0d49ca419577005b7f2cc930b"}, | ||||||
|     {file = "whitenoise-6.3.0.tar.gz", hash = "sha256:fe0af31504ab08faa1ec7fc02845432096e40cc1b27e6a7747263d7b30fb51fa"}, |     {file = "whitenoise-6.4.0.tar.gz", hash = "sha256:a02d6660ad161ff17e3042653c8e3f5ecbb2a2481a006bde125b9efb9a30113a"}, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [package.extras] | [package.extras] | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user