Initial commit
This commit is contained in:
		
							
								
								
									
										3
									
								
								Enquete/.pep8
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								Enquete/.pep8
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| [pycodestyle] | ||||
| max_line_length = 120 | ||||
| ignore = E501 | ||||
							
								
								
									
										18
									
								
								Enquete/.vscode/launch.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								Enquete/.vscode/launch.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| { | ||||
|     // Use IntelliSense to learn about possible attributes. | ||||
|     // Hover to view descriptions of existing attributes. | ||||
|     // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 | ||||
|     "version": "0.2.0", | ||||
|     "configurations": [ | ||||
|         { | ||||
|             "name": "Python: Django", | ||||
|             "type": "python", | ||||
|             "request": "launch", | ||||
|             "program": "cd ${workspaceFolder}/enquete && ./manage.py", | ||||
|             "args": [ | ||||
|                 "runserver" | ||||
|             ], | ||||
|             "django": true | ||||
|         } | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										1
									
								
								Enquete/enquete/apps/vragenlijst/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								Enquete/enquete/apps/vragenlijst/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| default_app_config = 'apps.vragenlijst.apps.VragenlijstConfig' | ||||
							
								
								
									
										42
									
								
								Enquete/enquete/apps/vragenlijst/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								Enquete/enquete/apps/vragenlijst/admin.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| from django.contrib import admin | ||||
| from .models import Questionnaire, QuestionnaireTopic, QuestionnaireQuestion, QuestionnaireResponse, QuestionnaireStorage | ||||
|  | ||||
| # Register your models here. | ||||
|  | ||||
|  | ||||
| @admin.register(Questionnaire) | ||||
| class QuestionnaireAdmin(admin.ModelAdmin): | ||||
|     list_display = ('name', 'id',) | ||||
|     readonly_fields = ('created_at', 'updated_at') | ||||
|  | ||||
|  | ||||
| @admin.register(QuestionnaireTopic) | ||||
| class QuestionnaireTopicAdmin(admin.ModelAdmin): | ||||
|     list_display = ('name', 'order', 'questionnaire',) | ||||
|     ordering = ('order', ) | ||||
|     readonly_fields = ('created_at', 'updated_at') | ||||
|  | ||||
|  | ||||
| @admin.register(QuestionnaireQuestion) | ||||
| class QuestionnaireQuestionAdmin(admin.ModelAdmin): | ||||
|     list_display = ('name', 'order', 'type', 'topic', 'questionnaire') | ||||
|     ordering = ('order', ) | ||||
|     readonly_fields = ('created_at', 'updated_at') | ||||
|  | ||||
|     def questionnaire(self, item): | ||||
|         return item.topic.questionnaire | ||||
|  | ||||
|  | ||||
| @admin.register(QuestionnaireResponse) | ||||
| class QuestionnaireResponseAdmin(admin.ModelAdmin): | ||||
|     list_display = ('id', 'created_at', 'questionnaire') | ||||
|     ordering = ('-created_at', ) | ||||
|     readonly_fields = ('questionnaire','response','created_at', 'updated_at') | ||||
|  | ||||
|  | ||||
| @admin.register(QuestionnaireStorage) | ||||
| class QuestionnaireStorageAdmin(admin.ModelAdmin): | ||||
|     list_display = ('name', 'type', 'server') | ||||
|     ordering = ('name', ) | ||||
|     readonly_fields = ('created_at', 'updated_at') | ||||
|  | ||||
							
								
								
									
										11
									
								
								Enquete/enquete/apps/vragenlijst/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								Enquete/enquete/apps/vragenlijst/apps.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| from django.apps import AppConfig | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
|  | ||||
|  | ||||
| class VragenlijstConfig(AppConfig): | ||||
|     default_auto_field = 'django.db.models.BigAutoField' | ||||
|     name = 'apps.vragenlijst' | ||||
|  | ||||
|     label = 'vragenlijst' | ||||
|     verbose_name = _('Questionnaire') | ||||
|     verbose_name_plural = _('Questionnaire') | ||||
							
								
								
									
										67
									
								
								Enquete/enquete/apps/vragenlijst/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								Enquete/enquete/apps/vragenlijst/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| # Generated by Django 4.0.2 on 2022-02-03 10:59 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| import uuid | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     initial = True | ||||
|  | ||||
|     dependencies = [ | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name='Questionnaire', | ||||
|             fields=[ | ||||
|                 ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='ID')), | ||||
|                 ('name', models.CharField(help_text='Name of the questionnaire.', max_length=200, verbose_name='Name')), | ||||
|                 ('description', models.TextField(blank=True, help_text='Enter a short description for this questionnaire.', null=True, verbose_name='Description')), | ||||
|                 ('created_at', models.DateTimeField(auto_now_add=True, help_text='The date and time this questionnaire has been created', verbose_name='Date created')), | ||||
|                 ('updated_at', models.DateTimeField(auto_now=True, help_text='The date and time this questionnaire has been updated', verbose_name='Date updated')), | ||||
|             ], | ||||
|             options={ | ||||
|                 'verbose_name': 'Questionnaire', | ||||
|                 'verbose_name_plural': 'Questionnaires', | ||||
|                 'ordering': ['name'], | ||||
|             }, | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name='QuestionnaireTopic', | ||||
|             fields=[ | ||||
|                 ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='ID')), | ||||
|                 ('name', models.CharField(help_text='Name of the questionnaire topic.', max_length=200, verbose_name='Name')), | ||||
|                 ('description', models.TextField(blank=True, help_text='Enter a short description for this questionnaire topic.', null=True, verbose_name='Description')), | ||||
|                 ('order', models.PositiveIntegerField(blank=True, verbose_name='Order')), | ||||
|                 ('created_at', models.DateTimeField(auto_now_add=True, help_text='The date and time this questionnaire topic has been created', verbose_name='Date created')), | ||||
|                 ('updated_at', models.DateTimeField(auto_now=True, help_text='The date and time this questionnaire topic has been updated', verbose_name='Date updated')), | ||||
|                 ('questionnaire', models.ForeignKey(help_text='The questionnaire topic for this questionnaire.', on_delete=django.db.models.deletion.CASCADE, to='vragenlijst.questionnaire', verbose_name='Questionnaire')), | ||||
|             ], | ||||
|             options={ | ||||
|                 'verbose_name': 'Questionnaire topic', | ||||
|                 'verbose_name_plural': 'Questionnaire topics', | ||||
|                 'ordering': ['name'], | ||||
|             }, | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name='QuestionnaireQuestion', | ||||
|             fields=[ | ||||
|                 ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='ID')), | ||||
|                 ('name', models.CharField(help_text='Name of the questionnaire topic.', max_length=200, verbose_name='Name')), | ||||
|                 ('type', models.CharField(choices=[('DATE', 'Date field'), ('NUMBER', 'Number field'), ('MULTIPLE', 'Multi options field'), ('SINGLE', 'Single option field'), ('TEXT', 'Single text line')], default='SINGLE', help_text='Question type', max_length=15, verbose_name='Type')), | ||||
|                 ('description', models.TextField(blank=True, help_text='Enter a short description for this questionnaire topic.', null=True, verbose_name='Description')), | ||||
|                 ('order', models.PositiveIntegerField(blank=True, verbose_name='Order')), | ||||
|                 ('choices', models.TextField(blank=True, help_text='Enter a short description for this questionnaire topic.', null=True, verbose_name='Choices')), | ||||
|                 ('created_at', models.DateTimeField(auto_now_add=True, help_text='The date and time this questionnaire topic has been created', verbose_name='Date created')), | ||||
|                 ('updated_at', models.DateTimeField(auto_now=True, help_text='The date and time this questionnaire topic has been updated', verbose_name='Date updated')), | ||||
|                 ('topic', models.ForeignKey(help_text='The questionnaire topic for this questionnaire.', on_delete=django.db.models.deletion.CASCADE, to='vragenlijst.questionnairetopic', verbose_name='Questionnaire topic')), | ||||
|             ], | ||||
|             options={ | ||||
|                 'verbose_name': 'Questionnaire question', | ||||
|                 'verbose_name_plural': 'Questionnaire questions', | ||||
|                 'ordering': ['name'], | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,19 @@ | ||||
| # Generated by Django 4.0.2 on 2022-02-03 11:45 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('vragenlijst', '0001_initial'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='questionnairetopic', | ||||
|             name='questionnaire', | ||||
|             field=models.ForeignKey(help_text='The questionnaire topic for this questionnaire.', on_delete=django.db.models.deletion.CASCADE, related_name='topics', to='vragenlijst.questionnaire', verbose_name='Questionnaire'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,19 @@ | ||||
| # Generated by Django 4.0.2 on 2022-02-03 12:21 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('vragenlijst', '0002_alter_questionnairetopic_questionnaire'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='questionnairequestion', | ||||
|             name='topic', | ||||
|             field=models.ForeignKey(help_text='The questionnaire topic for this questionnaire.', on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='vragenlijst.questionnairetopic', verbose_name='Questionnaire topic'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,49 @@ | ||||
| # Generated by Django 4.0.2 on 2022-02-08 10:01 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| import uuid | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('vragenlijst', '0003_alter_questionnairequestion_topic'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name='QuestionnaireStorage', | ||||
|             fields=[ | ||||
|                 ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='ID')), | ||||
|                 ('name', models.CharField(help_text='Name of the questionnaire storage.', max_length=200, verbose_name='Name')), | ||||
|                 ('type', models.CharField(choices=[('WEBDAV', 'WebDAV')], default='WEBDAV', help_text='Storage type', max_length=15, verbose_name='Type')), | ||||
|                 ('server', models.CharField(help_text='Server url', max_length=200, verbose_name='Server')), | ||||
|                 ('username', models.CharField(help_text='Username', max_length=200, verbose_name='Username')), | ||||
|                 ('password', models.CharField(help_text='Password', max_length=200, verbose_name='Password')), | ||||
|                 ('path', models.CharField(help_text='Location on disk', max_length=200, verbose_name='Path')), | ||||
|                 ('created_at', models.DateTimeField(auto_now_add=True, help_text='The date and time this questionnaire storage has been created', verbose_name='Date created')), | ||||
|                 ('updated_at', models.DateTimeField(auto_now=True, help_text='The date and time this questionnaire storage has been updated', verbose_name='Date updated')), | ||||
|             ], | ||||
|             options={ | ||||
|                 'verbose_name': 'Questionnaire storage', | ||||
|                 'verbose_name_plural': 'Questionnaire storages', | ||||
|                 'ordering': ['name'], | ||||
|             }, | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name='QuestionnaireResponse', | ||||
|             fields=[ | ||||
|                 ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='ID')), | ||||
|                 ('response', models.TextField(help_text='Questionaire response in CSV', verbose_name='Response')), | ||||
|                 ('created_at', models.DateTimeField(auto_now_add=True, help_text='The date and time this questionnaire response has been created', verbose_name='Date created')), | ||||
|                 ('updated_at', models.DateTimeField(auto_now=True, help_text='The date and time this questionnaire response has been updated', verbose_name='Date updated')), | ||||
|                 ('questionnaire', models.ForeignKey(help_text='The questionnaire for this response.', on_delete=django.db.models.deletion.CASCADE, to='vragenlijst.questionnaire', verbose_name='Questionnaire')), | ||||
|             ], | ||||
|             options={ | ||||
|                 'verbose_name': 'Questionnaire response', | ||||
|                 'verbose_name_plural': 'Questionnaire responses', | ||||
|                 'ordering': ['-created_at'], | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										134
									
								
								Enquete/enquete/apps/vragenlijst/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								Enquete/enquete/apps/vragenlijst/models.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,134 @@ | ||||
| from django.db import models | ||||
| from django.db.models import Max, Value | ||||
| from django.db.models.functions import Coalesce | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| import uuid | ||||
| from encrypted_model_fields.fields import EncryptedCharField | ||||
| # Create your models here. | ||||
|  | ||||
|  | ||||
| class Questionnaire(models.Model): | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _('Questionnaire') | ||||
|         verbose_name_plural = _('Questionnaires') | ||||
|         ordering = ['name'] | ||||
|  | ||||
|     id = models.UUIDField(_('ID'), default=uuid.uuid4, editable=False, unique=True, primary_key=True) | ||||
|     name = models.CharField(_('Name'), max_length=200, help_text=_('Name of the questionnaire.')) | ||||
|     description = models.TextField(_('Description'), blank=True, null=True, help_text=_('Enter a short description for this questionnaire.')) | ||||
|  | ||||
|     created_at = models.DateTimeField(_('Date created'), auto_now_add=True, help_text=_('The date and time this questionnaire has been created')) | ||||
|     updated_at = models.DateTimeField(_('Date updated'), auto_now=True, help_text=_('The date and time this questionnaire has been updated')) | ||||
|  | ||||
|  | ||||
|     @property | ||||
|     def allQuestions(self): | ||||
|         return QuestionnaireQuestion.objects.filter(topic__questionnaire=self.id) | ||||
|  | ||||
|     def __str__(self): | ||||
|         """str: Returns a readable string.""" | ||||
|         return f'{self.name}' | ||||
|  | ||||
|  | ||||
| class QuestionnaireTopic(models.Model): | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _('Questionnaire topic') | ||||
|         verbose_name_plural = _('Questionnaire topics') | ||||
|         ordering = ['name'] | ||||
|  | ||||
|     id = models.UUIDField(_('ID'), default=uuid.uuid4, editable=False, unique=True, primary_key=True) | ||||
|     name = models.CharField(_('Name'), max_length=200, help_text=_('Name of the questionnaire topic.')) | ||||
|     questionnaire = models.ForeignKey(Questionnaire, verbose_name=Questionnaire._meta.verbose_name, on_delete=models.CASCADE, help_text=_('The questionnaire topic for this questionnaire.'), related_name='topics') | ||||
|     description = models.TextField(_('Description'), blank=True, null=True, help_text=_('Enter a short description for this questionnaire topic.')) | ||||
|  | ||||
|     order = models.PositiveIntegerField(_('Order'), blank=True) | ||||
|  | ||||
|     created_at = models.DateTimeField(_('Date created'), auto_now_add=True, help_text=_('The date and time this questionnaire topic has been created')) | ||||
|     updated_at = models.DateTimeField(_('Date updated'), auto_now=True, help_text=_('The date and time this questionnaire topic has been updated')) | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
|         if self.order is None: | ||||
|             self.order = QuestionnaireTopic.objects.filter(questionnaire=self.questionnaire).aggregate(neworder=Coalesce(Max('order'), Value(0)))['neworder'] + 1 | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|     def __str__(self): | ||||
|         """str: Returns a readable string.""" | ||||
|         return f'{self.name}' | ||||
|  | ||||
|  | ||||
| class QuestionnaireQuestionTypes(models.TextChoices): | ||||
|  | ||||
|     DATE = ('DATE', _('Date field')) | ||||
|     NUMBER = ('NUMBER', _('Number field')) | ||||
|     MULTIPLE = ('MULTIPLE', _('Multi options field')) | ||||
|     SINGLE = ('SINGLE', _('Single option field')) | ||||
|     TEXT = ('TEXT', _('Single text line')) | ||||
|  | ||||
|  | ||||
| class QuestionnaireQuestion(models.Model): | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _('Questionnaire question') | ||||
|         verbose_name_plural = _('Questionnaire questions') | ||||
|         ordering = ['name'] | ||||
|  | ||||
|     id = models.UUIDField(_('ID'), default=uuid.uuid4, editable=False, unique=True, primary_key=True) | ||||
|     name = models.CharField(_('Name'), max_length=200, help_text=_('Name of the questionnaire topic.')) | ||||
|     type = models.CharField(_('Type'), max_length=15, choices=QuestionnaireQuestionTypes.choices, default=QuestionnaireQuestionTypes.SINGLE, help_text=_('Question type')) | ||||
|     description = models.TextField(_('Description'), blank=True, null=True, help_text=_('Enter a short description for this questionnaire topic.')) | ||||
|     order = models.PositiveIntegerField(_('Order'), blank=True,) | ||||
|     choices = models.TextField(_('Choices'), blank=True, null=True, help_text=_('Enter the choices 1 per line.<br />Use a format like \'= [Text]\' for an \'anders\' option.<br />Use format \'[Text]=[Value]\' for different value for each choice ')) | ||||
|     topic = models.ForeignKey(QuestionnaireTopic, verbose_name=QuestionnaireTopic._meta.verbose_name, on_delete=models.CASCADE, help_text=_('The questionnaire topic for this questionnaire.'), related_name='questions') | ||||
|  | ||||
|     created_at = models.DateTimeField(_('Date created'), auto_now_add=True, help_text=_('The date and time this questionnaire topic has been created')) | ||||
|     updated_at = models.DateTimeField(_('Date updated'), auto_now=True, help_text=_('The date and time this questionnaire topic has been updated')) | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
|         if self.order is None: | ||||
|             self.order = QuestionnaireQuestion.objects.filter(topic__questionnaire=self.topic.questionnaire).aggregate(neworder=Coalesce(Max('order'), Value(0)))['neworder'] + 1 | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|     def choices_list(self): | ||||
|         return [choice.strip() for choice in self.choices.strip("\n").split('\n')] | ||||
|  | ||||
|     def __str__(self): | ||||
|         """str: Returns a readable string.""" | ||||
|         return f'{self.name}' | ||||
|  | ||||
|  | ||||
| class QuestionnaireResponse(models.Model): | ||||
|     class Meta: | ||||
|         verbose_name = _('Questionnaire response') | ||||
|         verbose_name_plural = _('Questionnaire responses') | ||||
|         ordering = ['-created_at'] | ||||
|  | ||||
|     id = models.UUIDField(_('ID'), default=uuid.uuid4, editable=False, unique=True, primary_key=True) | ||||
|     questionnaire = models.ForeignKey(Questionnaire, verbose_name=Questionnaire._meta.verbose_name, on_delete=models.CASCADE, help_text=_('The questionnaire for this response.')) | ||||
|     response = models.TextField(_('Response'), help_text=_('Questionaire response in CSV')) | ||||
|  | ||||
|     created_at = models.DateTimeField(_('Date created'), auto_now_add=True, help_text=_('The date and time this questionnaire response has been created')) | ||||
|     updated_at = models.DateTimeField(_('Date updated'), auto_now=True, help_text=_('The date and time this questionnaire response has been updated')) | ||||
|  | ||||
|  | ||||
| class QuestionnaireStorageTypes(models.TextChoices): | ||||
|  | ||||
|     WEBDAV = ('WEBDAV', _('WebDAV')) | ||||
|  | ||||
| class QuestionnaireStorage(models.Model): | ||||
|     class Meta: | ||||
|         verbose_name = _('Questionnaire storage') | ||||
|         verbose_name_plural = _('Questionnaire storages') | ||||
|         ordering = ['name'] | ||||
|  | ||||
|     id = models.UUIDField(_('ID'), default=uuid.uuid4, editable=False, unique=True, primary_key=True) | ||||
|     name = models.CharField(_('Name'), max_length=200, help_text=_('Name of the questionnaire storage.')) | ||||
|     type = models.CharField(_('Type'), max_length=15, choices=QuestionnaireStorageTypes.choices, default=QuestionnaireStorageTypes.WEBDAV, help_text=_('Storage type')) | ||||
|     server = models.CharField(_('Server'), max_length=200, help_text=_('Server url')) | ||||
|     username = EncryptedCharField(_('Username'), max_length=200, help_text=_('Username')) | ||||
|     password = EncryptedCharField(_('Password'), max_length=200, help_text=_('Password')) | ||||
|     path = models.CharField(_('Path'), max_length=200, help_text=_('Location on disk')) | ||||
|  | ||||
|     created_at = models.DateTimeField(_('Date created'), auto_now_add=True, help_text=_('The date and time this questionnaire storage has been created')) | ||||
|     updated_at = models.DateTimeField(_('Date updated'), auto_now=True, help_text=_('The date and time this questionnaire storage has been updated')) | ||||
							
								
								
									
										382
									
								
								Enquete/enquete/apps/vragenlijst/templates/vragenlijst/form.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										382
									
								
								Enquete/enquete/apps/vragenlijst/templates/vragenlijst/form.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,382 @@ | ||||
| {% extends 'base.html' %} | ||||
| <!-- Add this for inheritance --> | ||||
| {% load replace %} | ||||
| {% load choice_value %} | ||||
| {% load i18n %} | ||||
| {% load static %} | ||||
|  | ||||
| {% block content %} | ||||
| <div class="container-fluid"> | ||||
|     <div class="row justify-content-center"> | ||||
|         <div class="col-12"> | ||||
|             <div class="card px-0 pt-4 pb-0 mt-3 mb-3"> | ||||
|                 <h2 id="heading">Fill out the forms</h2> | ||||
|                 <p>Fill all form field to go to next step</p> | ||||
|                 <form id="msform" method="POST"> | ||||
|                     {% csrf_token %} | ||||
|                     <!-- progressbar --> | ||||
|                     <ul id="progressbar"> | ||||
|                         {% for topic in questionnaire.topics.all|dictsort:"order" %} | ||||
|                         <li class="{% if forloop.first %} active {% endif %}"><strong><i class="fas fa-dot-circle"></i>{{ topic.name }}</strong></li> | ||||
|                         {% endfor %} | ||||
|                     </ul> | ||||
|                     <div class="progress"> | ||||
|                         <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuemin="0" aria-valuemax="100"></div> | ||||
|                     </div> | ||||
|                     <br> <!-- fieldsets --> | ||||
|                     {% for topic in questionnaire.topics.all|dictsort:"order" %} | ||||
|                         <fieldset> | ||||
|                             <div class="form-card"> | ||||
|                                 <div class="row"> | ||||
|                                     <div class="col-7"> | ||||
|                                         <h2 class="fs-title">{{topic.name}}:</h2> | ||||
|                                         <p>{{topic.description}}</p> | ||||
|                                     </div> | ||||
|                                     <div class="col-5"> | ||||
|                                         <h2 class="steps text-right">Step {{forloop.counter}} - {{ questionnaire.topics.all.count }}</h2> | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|                                 {% for question in topic.questions.all|dictsort:"order" %} | ||||
|                                     <br /><br /> | ||||
|                                     <label class="fieldlabels" for="{{question.id}}">{{ question.description.strip }}</label><br /> | ||||
|  | ||||
|                                     {% if question.type == 'SINGLE' %} | ||||
|                                         {% for choice in question.choices_list %} | ||||
|                                            <input type="radio" id="{{question.id}}_{{ forloop.counter }}" name="{{question.id}}" value="{{choice.strip|choice_value}}" required="required"> | ||||
|                                            <label for="{{question.id}}_{{forloop.counter}}">{{choice.strip|replace:"/(^=|=.*)/" }}</label> | ||||
|  | ||||
|                                             {% if choice.strip|slice:"0:1" == "=" %} | ||||
|                                                 <input type="text" name="{{question.id}}" id="{{question.id}}_{{ forloop.counter }}_anders" style="display:none" disabled="disabled"> | ||||
|                                                 <script> | ||||
|                                                     $(document).ready(function(){ | ||||
|                                                         $('input[type="radio"][name="{{question.id}}"]').on('change',(e) => { | ||||
|                                                             let choice = $(e.target) | ||||
|                                                             let andersTXT = $('input[type="text"][name="{{question.id}}"]') | ||||
|  | ||||
|                                                             if (choice.val().slice(0,1) == '=')  { | ||||
|                                                                 // Show | ||||
|                                                                 andersTXT.show() | ||||
|                                                                 andersTXT.prop('required',true) | ||||
|                                                                 andersTXT.prop('disabled',false) | ||||
|                                                             } else { | ||||
|                                                                 // Hide | ||||
|                                                                 andersTXT.hide() | ||||
|                                                                 andersTXT.prop('required',false) | ||||
|                                                                 andersTXT.prop('disabled',true) | ||||
|                                                             } | ||||
|                                                         }) | ||||
|                                                     }) | ||||
|                                                 </script> | ||||
|                                             {%endif%} | ||||
|                                         {% endfor %} | ||||
|                                     {%elif question.type == 'NUMBER' %} | ||||
|                                         <input type="{{question.type|lower}}" id="{{question.id}}" name="{{question.id}}" value="" required="required"> | ||||
|                                         {%elif question.type == 'DATE' %} | ||||
|                                         <input type="{{question.type|lower}}" id="{{question.id}}" name="{{question.id}}" value="" required="required"> | ||||
|                                     {%elif question.type == 'TEXT' %} | ||||
|                                         <textarea rows="1" id="{{question.id}}" name="{{question.id}}" required="required"></textarea> | ||||
|                                     {% endif %} | ||||
|  | ||||
|                                 {% endfor %} | ||||
|                             </div> | ||||
|                             {% if forloop.counter > 1 %} | ||||
|                             <input type="button" name="previous" class="previous action-button-previous" value="Previous" /> | ||||
|                             {% endif %} | ||||
|                             {% if forloop.last %} | ||||
|                             <input type="submit" name="next" class="next action-button" value="Save" /> | ||||
|                             {%else %} | ||||
|                             <input type="button" name="next" class="next action-button" value="Next" /> | ||||
|  | ||||
|                             {%endif%} | ||||
|                         </fieldset> | ||||
|                     {% endfor %} | ||||
|                 </form> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <style> | ||||
|     #grad1 { | ||||
|         background-color: #9C27B0; | ||||
|         background-image: linear-gradient(120deg, #FF4081, #81D4FA); | ||||
|     } | ||||
|  | ||||
|     #msform { | ||||
|         text-align: center; | ||||
|         position: relative; | ||||
|         margin-top: 20px | ||||
|     } | ||||
|  | ||||
|     #msform fieldset .form-card { | ||||
|         background: white; | ||||
|         border: 0 none; | ||||
|         border-radius: 0px; | ||||
|         box-shadow: 0 2px 2px 2px rgba(0, 0, 0, 0.2); | ||||
|         padding: 20px 40px 30px 40px; | ||||
|         box-sizing: border-box; | ||||
|         width: 94%; | ||||
|         margin: 0 3% 20px 3%; | ||||
|         position: relative | ||||
|     } | ||||
|  | ||||
|     #msform fieldset { | ||||
|         background: white; | ||||
|         border: 0 none; | ||||
|         border-radius: 0.5rem; | ||||
|         box-sizing: border-box; | ||||
|         width: 100%; | ||||
|         margin: 0; | ||||
|         padding-bottom: 20px; | ||||
|         position: relative | ||||
|     } | ||||
|  | ||||
|     #msform fieldset:not(:first-of-type) { | ||||
|         display: none | ||||
|     } | ||||
|  | ||||
|     #msform fieldset .form-card { | ||||
|         text-align: left; | ||||
|         color: #000000; | ||||
|     } | ||||
|  | ||||
|     #msform .action-button { | ||||
|         width: 100px; | ||||
|         background: skyblue; | ||||
|         font-weight: bold; | ||||
|         color: white; | ||||
|         border: 0 none; | ||||
|         border-radius: 0px; | ||||
|         cursor: pointer; | ||||
|         padding: 10px 5px; | ||||
|         margin: 10px 5px | ||||
|     } | ||||
|  | ||||
|     #msform .action-button:hover, | ||||
|     #msform .action-button:focus { | ||||
|         box-shadow: 0 0 0 2px white, 0 0 0 3px skyblue | ||||
|     } | ||||
|  | ||||
|     #msform .action-button-previous { | ||||
|         width: 100px; | ||||
|         background: #616161; | ||||
|         font-weight: bold; | ||||
|         color: white; | ||||
|         border: 0 none; | ||||
|         border-radius: 0px; | ||||
|         cursor: pointer; | ||||
|         padding: 10px 5px; | ||||
|         margin: 10px 5px | ||||
|     } | ||||
|  | ||||
|     #msform .action-button-previous:hover, | ||||
|     #msform .action-button-previous:focus { | ||||
|         box-shadow: 0 0 0 2px white, 0 0 0 3px #616161 | ||||
|     } | ||||
|  | ||||
|     select.list-dt { | ||||
|         border: none; | ||||
|         outline: 0; | ||||
|         border-bottom: 1px solid #ccc; | ||||
|         padding: 2px 5px 3px 5px; | ||||
|         margin: 2px | ||||
|     } | ||||
|  | ||||
|     select.list-dt:focus { | ||||
|         border-bottom: 2px solid skyblue | ||||
|     } | ||||
|  | ||||
|     .card { | ||||
|         z-index: 0; | ||||
|         border: none; | ||||
|         border-radius: 0.5rem; | ||||
|         position: relative | ||||
|     } | ||||
|  | ||||
|     .fs-title { | ||||
|         font-size: 25px; | ||||
|         color: #2C3E50; | ||||
|         margin-bottom: 10px; | ||||
|         font-weight: bold; | ||||
|         text-align: left | ||||
|     } | ||||
|  | ||||
|     #progressbar { | ||||
|         margin-bottom: 30px; | ||||
|         overflow: hidden; | ||||
|         color: lightgrey | ||||
|     } | ||||
|  | ||||
|     #progressbar .active { | ||||
|         color: #000000 | ||||
|     } | ||||
|  | ||||
|     #progressbar li { | ||||
|         list-style-type: none; | ||||
|         font-size: 12px; | ||||
|         width: {{menu_width}}%; | ||||
|         float: left; | ||||
|         position: relative | ||||
|     } | ||||
|  | ||||
|     #progressbar li:before { | ||||
|         width: 50px; | ||||
|         height: 50px; | ||||
|         line-height: 45px; | ||||
|         display: block; | ||||
|         font-size: 18px; | ||||
|         color: #ffffff; | ||||
|         background: lightgray; | ||||
|         border-radius: 50%; | ||||
|         margin: 0 auto 10px auto; | ||||
|         padding: 2px | ||||
|     } | ||||
|  | ||||
|     #progressbar li:after { | ||||
|         content: ''; | ||||
|         width: 100%; | ||||
|         height: 2px; | ||||
|         background: lightgray; | ||||
|         position: absolute; | ||||
|         left: 0; | ||||
|         top: 25px; | ||||
|         z-index: -1 | ||||
|     } | ||||
|  | ||||
|     #progressbar li.active:before, | ||||
|     #progressbar li.active:after { | ||||
|         background: skyblue | ||||
|     } | ||||
|  | ||||
|     .radio-group { | ||||
|         position: relative; | ||||
|         margin-bottom: 25px | ||||
|     } | ||||
|  | ||||
|     .radio { | ||||
|         display: inline-block; | ||||
|         width: 204; | ||||
|         height: 104; | ||||
|         border-radius: 0; | ||||
|         background: lightblue; | ||||
|         box-shadow: 0 2px 2px 2px rgba(0, 0, 0, 0.2); | ||||
|         box-sizing: border-box; | ||||
|         cursor: pointer; | ||||
|         margin: 8px 2px | ||||
|     } | ||||
|  | ||||
|     .radio:hover { | ||||
|         box-shadow: 2px 2px 2px 2px rgba(0, 0, 0, 0.3) | ||||
|     } | ||||
|  | ||||
|     .radio.selected { | ||||
|         box-shadow: 1px 1px 2px 2px rgba(0, 0, 0, 0.1) | ||||
|     } | ||||
|  | ||||
|     .fit-image { | ||||
|         width: 100%; | ||||
|         object-fit: cover | ||||
|     } | ||||
|  | ||||
|     .error { | ||||
|         /* border: solid 1px red; */ | ||||
|         color:red; | ||||
|         font-weight: bold; | ||||
|     } | ||||
| </style> | ||||
|  | ||||
| <script> | ||||
| $(document).ready(function(){ | ||||
|  | ||||
|     var current_fs, next_fs, previous_fs; //fieldsets | ||||
|     var opacity; | ||||
|     var current = 1; | ||||
|     var steps = $("fieldset").length; | ||||
|  | ||||
|     setProgressBar(current); | ||||
|  | ||||
|     $(".next").click(function(){ | ||||
|  | ||||
|         current_fs = $(this).parent(); | ||||
|         next_fs = $(this).parent().next(); | ||||
|  | ||||
|         let errors = false | ||||
|  | ||||
|         current_fs.find('input,textarea').each((counter, element) => { | ||||
|             if (element.name && !element.disabled) { | ||||
|                 if (!element.checkValidity()) { | ||||
|                 jQuery('label[for=' + element.name + ']').addClass('error') | ||||
|                 errors = true; | ||||
|                 } else { | ||||
|                     jQuery('label[for=' + element.name + ']').removeClass('error') | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|         if (errors) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         //Add Class Active | ||||
|         $("#progressbar li").eq($("fieldset").index(next_fs)).addClass("active"); | ||||
|  | ||||
|         //show the next fieldset | ||||
|         next_fs.show(); | ||||
|         //hide the current fieldset with style | ||||
|         current_fs.animate({opacity: 0}, { | ||||
|         step: function(now) { | ||||
|         // for making fielset appear animation | ||||
|         opacity = 1 - now; | ||||
|  | ||||
|         current_fs.css({ | ||||
|         'display': 'none', | ||||
|         'position': 'relative' | ||||
|         }); | ||||
|         next_fs.css({'opacity': opacity}); | ||||
|         }, | ||||
|         duration: 500 | ||||
|         }); | ||||
|         setProgressBar(++current); | ||||
|     }); | ||||
|  | ||||
|     $(".previous").click(function(){ | ||||
|  | ||||
|         current_fs = $(this).parent(); | ||||
|         previous_fs = $(this).parent().prev(); | ||||
|  | ||||
|         //Remove class active | ||||
|         $("#progressbar li").eq($("fieldset").index(current_fs)).removeClass("active"); | ||||
|  | ||||
|         //show the previous fieldset | ||||
|         previous_fs.show(); | ||||
|  | ||||
|         //hide the current fieldset with style | ||||
|         current_fs.animate({opacity: 0}, { | ||||
|         step: function(now) { | ||||
|         // for making fielset appear animation | ||||
|         opacity = 1 - now; | ||||
|  | ||||
|         current_fs.css({ | ||||
|         'display': 'none', | ||||
|         'position': 'relative' | ||||
|         }); | ||||
|         previous_fs.css({'opacity': opacity}); | ||||
|         }, | ||||
|         duration: 500 | ||||
|         }); | ||||
|         setProgressBar(--current); | ||||
|     }); | ||||
|  | ||||
|     function setProgressBar(curStep){ | ||||
|         var percent = parseFloat(100 / steps) * curStep; | ||||
|         percent = percent.toFixed(); | ||||
|         $(".progress-bar") | ||||
|         .css("width",percent+"%") | ||||
|     } | ||||
|  | ||||
|     // $(".submit").click(function(){ | ||||
|     //     return false; | ||||
|     // }) | ||||
|  | ||||
| }); | ||||
| </script> | ||||
| {% endblock %} | ||||
| @@ -0,0 +1,39 @@ | ||||
| {% extends 'base.html' %} | ||||
| <!-- Add this for inheritance --> | ||||
| {% load i18n %} | ||||
| {% load static %} | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| {% block content %} | ||||
|  | ||||
| {% if messages %} | ||||
| <p> | ||||
|     {% for message in messages %} | ||||
|         {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}Important: {% endif %} | ||||
|         {{ message }} <br /> | ||||
|     {% endfor %} | ||||
| {% endif %} | ||||
| </p> | ||||
|  | ||||
| <p>Select the type of questionnaire you want to use</p> | ||||
|  | ||||
|  | ||||
|  | ||||
| {% if questionnaires %} | ||||
|  | ||||
| {% for questionnaire in questionnaires %} | ||||
|  | ||||
| <a href="{% url 'questionnaire' questionnaire.id %}" class="btn btn-primary btn-lg" role="button" aria-pressed="true">{{ questionnaire.name }}</a> | ||||
|  | ||||
|  | ||||
| {% endfor %} | ||||
|  | ||||
| {% else %} | ||||
|  | ||||
| <p>No questionnaire are available.</p> | ||||
|  | ||||
| {% endif %} | ||||
|  | ||||
| {% endblock %} | ||||
| @@ -0,0 +1,9 @@ | ||||
| from django import template | ||||
| register = template.Library() | ||||
|  | ||||
| @register.filter | ||||
| def choice_value ( string ): | ||||
|     if string[0] != '=' and '=' in string: | ||||
|         return string.split('=')[1].strip() | ||||
|  | ||||
|     return string.strip() | ||||
							
								
								
									
										11
									
								
								Enquete/enquete/apps/vragenlijst/templatetags/replace.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								Enquete/enquete/apps/vragenlijst/templatetags/replace.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import re | ||||
|  | ||||
| from django import template | ||||
| register = template.Library() | ||||
|  | ||||
| @register.filter | ||||
| def replace ( string, args ): | ||||
|     search  = args.split(args[0])[1] | ||||
|     replace = args.split(args[0])[2] | ||||
|  | ||||
|     return re.sub( search, replace, string ) | ||||
							
								
								
									
										3
									
								
								Enquete/enquete/apps/vragenlijst/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								Enquete/enquete/apps/vragenlijst/tests.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| from django.test import TestCase | ||||
|  | ||||
| # Create your tests here. | ||||
							
								
								
									
										8
									
								
								Enquete/enquete/apps/vragenlijst/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								Enquete/enquete/apps/vragenlijst/urls.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| from django.urls import path | ||||
|  | ||||
| from . import views | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path('', views.index, name='index'), | ||||
|     path('<uuid:questionnaire_id>/', views.questionnaire, name='questionnaire'), | ||||
| ] | ||||
							
								
								
									
										69
									
								
								Enquete/enquete/apps/vragenlijst/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								Enquete/enquete/apps/vragenlijst/views.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| from django.http import HttpResponseRedirect | ||||
| from django.shortcuts import render | ||||
| from .models import Questionnaire, QuestionnaireResponse, QuestionnaireStorage | ||||
| from django.conf import settings | ||||
| from django.contrib import messages | ||||
|  | ||||
| import csv | ||||
| from uuid import uuid4 | ||||
| from pathlib import Path | ||||
|  | ||||
| from storage.storage import Storage | ||||
| # Create your views here. | ||||
|  | ||||
|  | ||||
| def index(request): | ||||
|     # latest_question_list = Question.objects.order_by('-pub_date')[:5] | ||||
|     questionnaires = Questionnaire.objects.order_by('name') | ||||
|     context = {'questionnaires': questionnaires} | ||||
|     return render(request, 'vragenlijst/index.html', context) | ||||
|  | ||||
|  | ||||
| def questionnaire(request, questionnaire_id): | ||||
|     if request.method == 'POST': | ||||
|  | ||||
|         # As we should have a Django form, we could use te form validator. For now, everyhing is valid ;) | ||||
|         questionnaire = Questionnaire.objects.get(pk=questionnaire_id) | ||||
|  | ||||
|         # Get all the questions in one list with a single query | ||||
|         allQuestions = {} | ||||
|         for question in list(questionnaire.allQuestions.order_by('order').values('id','name')): | ||||
|             allQuestions[str(question['id'])] = question['name'] | ||||
|  | ||||
|         # Store the response as a ';' seperated CSV file in order of the questionaire questions order | ||||
|         try: | ||||
|             csv_file = Path(f'{settings.HANZE_TEMP_CSV_STORAGE}/{questionnaire.name}-{uuid4()}.csv') | ||||
|             with open(csv_file, 'w') as csvfile: | ||||
|                 filewriter = csv.writer(csvfile, delimiter=';', quotechar='"', quoting=csv.QUOTE_MINIMAL) | ||||
|                 # Add headers | ||||
|                 filewriter.writerow(['vraag', 'antwoord']) | ||||
|  | ||||
|                 for question in allQuestions: | ||||
|                     if request.POST.get(question): | ||||
|                         filewriter.writerow([allQuestions[question], request.POST[question]]) | ||||
|  | ||||
|             # Store the data also in a database | ||||
|             QuestionnaireResponse(questionnaire=questionnaire, response=csv_file.read_text()).save() | ||||
|             storageSettings = QuestionnaireStorage.objects.first() | ||||
|             # Move data to external storage | ||||
|             webdav = Storage( | ||||
|                         storage_type=storageSettings.type, | ||||
|                         url=storageSettings.server, | ||||
|                         username=storageSettings.username, | ||||
|                         password=storageSettings.password | ||||
|                     ) | ||||
|  | ||||
|             webdav.upload_file(source=csv_file, destination=f'{storageSettings.path}/{csv_file.name}') | ||||
|  | ||||
|             messages.success(request, 'Questionaire is saved to disk.') | ||||
|  | ||||
|         except Exception as ex: | ||||
|             messages.error(request, f'Could not save the questionaire. Error: {ex}') | ||||
|  | ||||
|  | ||||
|         # Redirect to starting point | ||||
|         return HttpResponseRedirect('/vragenlijst/') | ||||
|  | ||||
|     questionnaire = Questionnaire.objects.get(pk=questionnaire_id) | ||||
|     context = {'questionnaire': questionnaire, 'menu_width': 100 / questionnaire.topics.count()} | ||||
|     return render(request, 'vragenlijst/form.html', context) | ||||
							
								
								
									
										0
									
								
								Enquete/enquete/enquete/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								Enquete/enquete/enquete/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										16
									
								
								Enquete/enquete/enquete/asgi.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								Enquete/enquete/enquete/asgi.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| """ | ||||
| ASGI config for enquete project. | ||||
|  | ||||
| It exposes the ASGI callable as a module-level variable named ``application``. | ||||
|  | ||||
| For more information on this file, see | ||||
| https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ | ||||
| """ | ||||
|  | ||||
| import os | ||||
|  | ||||
| from django.core.asgi import get_asgi_application | ||||
|  | ||||
| os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'enquete.settings') | ||||
|  | ||||
| application = get_asgi_application() | ||||
							
								
								
									
										135
									
								
								Enquete/enquete/enquete/settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								Enquete/enquete/enquete/settings.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | ||||
| """ | ||||
| Django settings for enquete project. | ||||
|  | ||||
| Generated by 'django-admin startproject' using Django 4.0.2. | ||||
|  | ||||
| For more information on this file, see | ||||
| https://docs.djangoproject.com/en/4.0/topics/settings/ | ||||
|  | ||||
| For the full list of settings and their values, see | ||||
| https://docs.djangoproject.com/en/4.0/ref/settings/ | ||||
| """ | ||||
|  | ||||
| from pathlib import Path | ||||
| from decouple import config | ||||
| from dj_database_url import parse as db_url | ||||
| import base64 | ||||
|  | ||||
| # Build paths inside the project like this: BASE_DIR / 'subdir'. | ||||
| BASE_DIR = Path(__file__).resolve().parent.parent | ||||
|  | ||||
| # Quick-start development settings - unsuitable for production | ||||
| # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ | ||||
|  | ||||
| # SECURITY WARNING: keep the secret key used in production secret! | ||||
| SECRET_KEY = config('SECRET_KEY') | ||||
|  | ||||
| FIELD_ENCRYPTION_KEY = config('FIELD_ENCRYPTION_KEY', str(base64.b64encode(SECRET_KEY[0:32].encode("utf-8")), "utf-8")) | ||||
|  | ||||
| # SECURITY WARNING: don't run with debug turned on in production! | ||||
| DEBUG = config('DEBUG', default=False, cast=bool) | ||||
| TEMPLATE_DEBUG = DEBUG | ||||
|  | ||||
| ALLOWED_HOSTS = [] | ||||
|  | ||||
|  | ||||
| # Application definition | ||||
|  | ||||
| INSTALLED_APPS = [ | ||||
|     'django.contrib.admin', | ||||
|     'django.contrib.auth', | ||||
|     'django.contrib.contenttypes', | ||||
|     'django.contrib.sessions', | ||||
|     'django.contrib.messages', | ||||
|     'django.contrib.staticfiles', | ||||
|     'encrypted_model_fields', | ||||
|  | ||||
|     'apps.vragenlijst' | ||||
| ] | ||||
|  | ||||
| MIDDLEWARE = [ | ||||
|     'django.middleware.security.SecurityMiddleware', | ||||
|     'django.contrib.sessions.middleware.SessionMiddleware', | ||||
|     'django.middleware.common.CommonMiddleware', | ||||
|     'django.middleware.csrf.CsrfViewMiddleware', | ||||
|     'django.contrib.auth.middleware.AuthenticationMiddleware', | ||||
|     'django.contrib.messages.middleware.MessageMiddleware', | ||||
|     'django.middleware.clickjacking.XFrameOptionsMiddleware', | ||||
| ] | ||||
|  | ||||
| ROOT_URLCONF = 'enquete.urls' | ||||
|  | ||||
| TEMPLATES = [ | ||||
|     { | ||||
|         'BACKEND': 'django.template.backends.django.DjangoTemplates', | ||||
|         'DIRS': ['templates'], | ||||
|         'APP_DIRS': True, | ||||
|         'OPTIONS': { | ||||
|             'context_processors': [ | ||||
|                 'django.template.context_processors.debug', | ||||
|                 'django.template.context_processors.request', | ||||
|                 'django.contrib.auth.context_processors.auth', | ||||
|                 'django.contrib.messages.context_processors.messages', | ||||
|             ], | ||||
|         }, | ||||
|     }, | ||||
| ] | ||||
|  | ||||
| WSGI_APPLICATION = 'enquete.wsgi.application' | ||||
|  | ||||
|  | ||||
| # Database | ||||
| # https://docs.djangoproject.com/en/4.0/ref/settings/#databases | ||||
| DATABASES = { | ||||
|     'default': config( | ||||
|         'DATABASE_URL', | ||||
|         default='sqlite:///' + f'{BASE_DIR}' + '/db.sqlite3', | ||||
|         cast=db_url | ||||
|     ) | ||||
| } | ||||
|  | ||||
|  | ||||
| # Password validation | ||||
| # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators | ||||
|  | ||||
| AUTH_PASSWORD_VALIDATORS = [ | ||||
|     { | ||||
|         'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', | ||||
|     }, | ||||
|     { | ||||
|         'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', | ||||
|     }, | ||||
|     { | ||||
|         'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', | ||||
|     }, | ||||
|     { | ||||
|         'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', | ||||
|     }, | ||||
| ] | ||||
|  | ||||
|  | ||||
| # Internationalization | ||||
| # https://docs.djangoproject.com/en/4.0/topics/i18n/ | ||||
|  | ||||
| LANGUAGE_CODE = 'en-us' | ||||
|  | ||||
| TIME_ZONE = 'Europe/Amsterdam' | ||||
|  | ||||
| USE_I18N = True | ||||
|  | ||||
| USE_TZ = True | ||||
|  | ||||
|  | ||||
| # Static files (CSS, JavaScript, Images) | ||||
| # https://docs.djangoproject.com/en/4.0/howto/static-files/ | ||||
|  | ||||
| STATIC_URL = 'static/' | ||||
|  | ||||
| # Default primary key field type | ||||
| # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field | ||||
|  | ||||
| DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' | ||||
|  | ||||
|  | ||||
| HANZE_TEMP_CSV_STORAGE = BASE_DIR / '../uitkomsten' | ||||
| HANZE_TEMP_CSV_STORAGE.mkdir(exist_ok=True) | ||||
							
								
								
									
										22
									
								
								Enquete/enquete/enquete/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								Enquete/enquete/enquete/urls.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| """enquete URL Configuration | ||||
|  | ||||
| The `urlpatterns` list routes URLs to views. For more information please see: | ||||
|     https://docs.djangoproject.com/en/4.0/topics/http/urls/ | ||||
| Examples: | ||||
| Function views | ||||
|     1. Add an import:  from my_app import views | ||||
|     2. Add a URL to urlpatterns:  path('', views.home, name='home') | ||||
| Class-based views | ||||
|     1. Add an import:  from other_app.views import Home | ||||
|     2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home') | ||||
| Including another URLconf | ||||
|     1. Import the include() function: from django.urls import include, path | ||||
|     2. Add a URL to urlpatterns:  path('blog/', include('blog.urls')) | ||||
| """ | ||||
| from django.contrib import admin | ||||
| from django.urls import path, include | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path('vragenlijst/', include('apps.vragenlijst.urls')), | ||||
|     path('admin/', admin.site.urls), | ||||
| ] | ||||
							
								
								
									
										16
									
								
								Enquete/enquete/enquete/wsgi.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								Enquete/enquete/enquete/wsgi.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| """ | ||||
| WSGI config for enquete project. | ||||
|  | ||||
| It exposes the WSGI callable as a module-level variable named ``application``. | ||||
|  | ||||
| For more information on this file, see | ||||
| https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ | ||||
| """ | ||||
|  | ||||
| import os | ||||
|  | ||||
| from django.core.wsgi import get_wsgi_application | ||||
|  | ||||
| os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'enquete.settings') | ||||
|  | ||||
| application = get_wsgi_application() | ||||
							
								
								
									
										22
									
								
								Enquete/enquete/manage.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										22
									
								
								Enquete/enquete/manage.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| #!/usr/bin/env python | ||||
| """Django's command-line utility for administrative tasks.""" | ||||
| import os | ||||
| import sys | ||||
|  | ||||
|  | ||||
| def main(): | ||||
|     """Run administrative tasks.""" | ||||
|     os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'enquete.settings') | ||||
|     try: | ||||
|         from django.core.management import execute_from_command_line | ||||
|     except ImportError as exc: | ||||
|         raise ImportError( | ||||
|             "Couldn't import Django. Are you sure it's installed and " | ||||
|             "available on your PYTHONPATH environment variable? Did you " | ||||
|             "forget to activate a virtual environment?" | ||||
|         ) from exc | ||||
|     execute_from_command_line(sys.argv) | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     main() | ||||
							
								
								
									
										9
									
								
								Enquete/enquete/requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								Enquete/enquete/requirements.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| dj-database-url==0.5.0 | ||||
| Django==4.0.2 | ||||
| django-encrypted-model-fields==0.6.1 | ||||
| python-decouple==3.6 | ||||
|  | ||||
| python-irodsclient==1.1.1 | ||||
| giteapy==1.0.8 | ||||
| webdavclient3==3.14.6 | ||||
| PyGithub==1.55 | ||||
							
								
								
									
										1
									
								
								Enquete/enquete/storage/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								Enquete/enquete/storage/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| A small questionnaire tool that stores the data trough a WebDAV connection. | ||||
							
								
								
									
										8
									
								
								Enquete/enquete/storage/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								Enquete/enquete/storage/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| import os | ||||
| import logging | ||||
| import logging.config | ||||
|  | ||||
| if os.path.isfile('logging.custom.ini'): | ||||
|     logging.config.fileConfig('logging.custom.ini') | ||||
| elif os.path.isfile('logging.ini'): | ||||
|     logging.config.fileConfig('logging.ini') | ||||
							
								
								
									
										28
									
								
								Enquete/enquete/storage/engines/fs.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								Enquete/enquete/storage/engines/fs.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| import shutil | ||||
| import os | ||||
| from storage.storage import BaseStorage | ||||
| import logging | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class LocalStorage(BaseStorage): | ||||
|  | ||||
|     TYPE = 'fs' | ||||
|  | ||||
|     def file_exists(self, filepath): | ||||
|         return os.path.exists(filepath) and os.path.isfile(filepath) | ||||
|  | ||||
|     def directory_exists(self, filepath): | ||||
|         return os.path.exists(filepath) and os.path.isdir(filepath) | ||||
|  | ||||
|     def _make_folder_action(self, path): | ||||
|         os.makedirs(path) | ||||
|         return True | ||||
|  | ||||
|     def _upload_file_action(self, source, destination): | ||||
|         shutil.copy(source, destination) | ||||
|         return True | ||||
|  | ||||
|     def _download_file_action(self, source, destination): | ||||
|         shutil.copy(source, destination) | ||||
|         return True | ||||
							
								
								
									
										89
									
								
								Enquete/enquete/storage/engines/gitea.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								Enquete/enquete/storage/engines/gitea.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | ||||
| from giteapy.rest import ApiException | ||||
| import giteapy | ||||
| import base64 | ||||
| from storage.storage import BaseStorage | ||||
| import logging | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| # Gitea Support - https://pypi.org/project/giteapy/ | ||||
|  | ||||
|  | ||||
| class GiteaStorage(BaseStorage): | ||||
|  | ||||
|     TYPE = 'gitea' | ||||
|  | ||||
|     def __init__(self, url=None, username=None, password=None, source=None, destination=None, encryption_key=None, sender_name=None, sender_email=None): | ||||
|         # The repository is added to the url parameter. Use a '#' as seperator. The repository needs to be created first. | ||||
|         # Ex: https://git.web.rug.nl/api/v1#RepositoryName | ||||
|         (url, self.repository) = url.split('#') | ||||
|         destination = destination.strip('/') | ||||
|  | ||||
|         super().__init__(url, username, password, source, destination, encryption_key, sender_name, sender_email) | ||||
|  | ||||
|         # Create a commiter object when the data is uploaded through one of the invited accounts. | ||||
|         self.committer = None | ||||
|         if sender_name is not None or sender_email is not None: | ||||
|             self.committer = giteapy.Identity(name=sender_name, email=sender_email) | ||||
|  | ||||
|     def __connect(self): | ||||
|         try: | ||||
|             assert(self.client) | ||||
|         except AttributeError: | ||||
|             # Configuration for the GITEA connection | ||||
|             configuration = giteapy.Configuration() | ||||
|             # Overrule the host url....? | ||||
|             configuration.host = self.url | ||||
|             #configuration.debug = False | ||||
|             configuration.api_key['access_token'] = self.password | ||||
|  | ||||
|             # Create the client | ||||
|             self.client = giteapy.RepositoryApi(giteapy.ApiClient(configuration)) | ||||
|             logger.info(f'Created Gitea connection to url: {self.url}') | ||||
|  | ||||
|     def file_exists(self, filepath): | ||||
|         self.__connect() | ||||
|         try: | ||||
|             self.client.repo_get_contents(self.username, self.repository, filepath) | ||||
|             return True | ||||
|         except ApiException: | ||||
|             return False | ||||
|  | ||||
|     def directory_exists(self, filepath): | ||||
|         self.__connect() | ||||
|         return self.file_exists(filepath) | ||||
|  | ||||
|     def _make_folder_action(self, path): | ||||
|         # On GitHub you cannot create empty directories. So this actions will always succeed | ||||
|         return True | ||||
|  | ||||
|     def _upload_file_action(self, source, destination): | ||||
|         self.__connect() | ||||
|         try: | ||||
|             with open(source, 'rb') as datafile: | ||||
|                 # This is a very big issue. Big files will be stored completely in memory :( | ||||
|                 body = giteapy.CreateFileOptions(content=base64.b64encode(datafile.read()).decode(), | ||||
|                                                  message=f'Upload from VRE DataDropOff\n Added file: {destination}', | ||||
|                                                  committer=self.committer) | ||||
|         except Exception: | ||||
|             return False | ||||
|  | ||||
|         try: | ||||
|             # Create a file in a repository | ||||
|             api_response = self.client.repo_create_file(self.username, self.repository, destination, body) | ||||
|             return True | ||||
|         except ApiException as ex: | ||||
|             logger.exception(f'Exception when calling RepositoryApi->repo_create_file: {ex}') | ||||
|  | ||||
|         return True | ||||
|  | ||||
|     def _download_file_action(self, source, destination): | ||||
|         self.__connect() | ||||
|         with open(destination, 'wb') as destination_file: | ||||
|             try: | ||||
|                 data = self.client.repo_get_contents(self.username, self.repository, source) | ||||
|                 destination_file.write(base64.b64decode(data.content)) | ||||
|             except ApiException as ex: | ||||
|                 logger.exception(f'Exception when calling RepositoryApi->repo_get_contents: {ex}') | ||||
|  | ||||
|         return True | ||||
							
								
								
									
										66
									
								
								Enquete/enquete/storage/engines/github.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								Enquete/enquete/storage/engines/github.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| from github.GithubException import UnknownObjectException | ||||
| from github import Github, InputGitAuthor, GithubObject | ||||
| from storage.storage import BaseStorage | ||||
| import os | ||||
| import logging | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| # Github Support - https://pypi.org/project/PyGithub/ | ||||
|  | ||||
|  | ||||
| class GithubStorage(BaseStorage): | ||||
|  | ||||
|     TYPE = 'github' | ||||
|  | ||||
|     def __init__(self, url=None, username=None, password=None, source=None, destination=None, encryption_key=None, sender_name=None, sender_email=None): | ||||
|         # The repository is added to the url parameter. Use a '#' as seperator. The repository needs to be created first. | ||||
|         # Ex: https://api.github.com/#RepositoryName | ||||
|         (url, self.repository) = url.split('#') | ||||
|         destination = destination.strip('/') | ||||
|  | ||||
|         super().__init__(url, username, password, source, destination, encryption_key, sender_name, sender_email) | ||||
|  | ||||
|         # Create a commiter object when the data is uploaded through one of the invited accounts. | ||||
|         self.committer = GithubObject.NotSet | ||||
|         if sender_name is not None or sender_email is not None: | ||||
|             self.committer = InputGitAuthor(name=sender_name, email=sender_email) | ||||
|  | ||||
|     def __connect(self): | ||||
|         try: | ||||
|             assert(self.repo) | ||||
|         except AttributeError: | ||||
|             client = Github(self.password) | ||||
|             self.repo = client.get_user().get_repo(self.repository) | ||||
|             logger.info('Created Github.com connection') | ||||
|  | ||||
|     def file_exists(self, filepath): | ||||
|         self.__connect() | ||||
|         try: | ||||
|             self.repo.get_contents(filepath) | ||||
|             return True | ||||
|         except UnknownObjectException: | ||||
|             return False | ||||
|  | ||||
|     def directory_exists(self, filepath): | ||||
|         return True | ||||
|  | ||||
|     def _make_folder_action(self, path): | ||||
|         # On GitHub you cannot create empty directories. So this actions will always succeed | ||||
|         return True | ||||
|  | ||||
|     def _upload_file_action(self, source, destination): | ||||
|         self.__connect() | ||||
|         # Read the file and post to Github. The library will convert to Base64 | ||||
|         with open(source, 'rb') as datafile: | ||||
|             self.repo.create_file(destination.strip('/'), f'Upload from VRE DataDropOff\n Added file: {destination}', datafile.read(), committer=self.committer) | ||||
|  | ||||
|         return True | ||||
|  | ||||
|     def _download_file_action(self, source, destination): | ||||
|         self.__connect() | ||||
|         download = self.repo.get_contents(source) | ||||
|         with open(destination, 'wb') as destination_file: | ||||
|             destination_file.write(download.decoded_content) | ||||
|  | ||||
|         return True | ||||
							
								
								
									
										139
									
								
								Enquete/enquete/storage/engines/irods.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								Enquete/enquete/storage/engines/irods.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,139 @@ | ||||
| import atexit | ||||
| from irods.session import iRODSSession | ||||
| import irods | ||||
| import storage.exceptions as StorageException | ||||
| from storage.storage import BaseStorage | ||||
| import logging | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| # iRods support - https://pypi.org/project/python-irodsclient/ | ||||
|  | ||||
|  | ||||
| class iRODSStorage(BaseStorage): | ||||
|  | ||||
|     TYPE = 'irods' | ||||
|  | ||||
|     def __init__(self, url=None, username=None, password=None, source=None, destination=None, encryption_key=None, sender_name=None, sender_email=None): | ||||
|         # The iRODS zone is added to the url parameter. Use a '#' as seperator. This needs to be an Existing iRODS zone | ||||
|         # Ex: rdms-prod-icat.data.rug.nl#rug | ||||
|         (url, self.irods_zone) = url.split('#') | ||||
|         if destination: | ||||
|             destination = destination.strip('/') | ||||
|  | ||||
|         super().__init__(url, username, password, source, destination, encryption_key, sender_name, sender_email) | ||||
|  | ||||
|         # We need to clean up the iRODS session. Using atexit is the easiest way. | ||||
|         atexit.register(self.__close) | ||||
|  | ||||
|     def __connect(self): | ||||
|         try: | ||||
|             assert(self.client) | ||||
|         except AttributeError: | ||||
|             # Connect to the iRODS server | ||||
|             self.client = None | ||||
|             try: | ||||
|                 self.client = iRODSSession(host=self.url, port=1247, user=self.username, password=self.password, zone=self.irods_zone) | ||||
|                 # Need to make a call to validate the authentication. So by checking the version, we know if we can authenticate... | ||||
|                 logger.debug(f'iRODS {self.client.server_version} connection through *native* authentication') | ||||
|             except irods.exception.CAT_INVALID_AUTHENTICATION: | ||||
|                 # Authentication scheme is not native (default), so we try PAM here | ||||
|                 try: | ||||
|                     self.client = iRODSSession(host=self.url, port=1247, user=self.username, password=self.password, zone=self.irods_zone, irods_authentication_scheme='pam') | ||||
|                     logger.debug(f'iRODS {self.client.server_version} connection through *PAM* authentication') | ||||
|                 except irods.exception.CAT_INVALID_AUTHENTICATION: | ||||
|                     # Authentication scheme is not PAM either last try: GIS | ||||
|                     try: | ||||
|                         self.client = iRODSSession(host=self.url, port=1247, user=self.username, password=self.password, zone=self.irods_zone, irods_authentication_scheme='gis') | ||||
|                         logger.debug(f'iRODS {self.client.server_version} connection through *GIS* authentication') | ||||
|                     except irods.exception.CAT_INVALID_AUTHENTICATION: | ||||
|                         pass | ||||
|  | ||||
|             if self.client is None: | ||||
|                 logger.error('Unable to login to the iRODS instance. Please check username and password combination!') | ||||
|                 raise StorageException.InvalidAuthentication(self.username) | ||||
|  | ||||
|             logger.info('Created iRODS connection') | ||||
|  | ||||
|     def __close(self): | ||||
|         logger.debug('Closing iRODS storage connection and clean up') | ||||
|         self.client.cleanup() | ||||
|  | ||||
|     def _file_exists_action(self, path): | ||||
|         self.__connect() | ||||
|         try: | ||||
|             self.client.data_objects.get(f'/{self.irods_zone}/home/{self.username}/{path}') | ||||
|         except irods.exception.DataObjectDoesNotExist: | ||||
|             logger.debug(f'File \'{path}\' does NOT exists on the iRODS server') | ||||
|             return False | ||||
|         except irods.exception.CollectionDoesNotExist: | ||||
|             logger.debug(f'Parent folder of file \'{path}\' does NOT exists on the iRODS server') | ||||
|             return False | ||||
|  | ||||
|         return True | ||||
|  | ||||
|     def _directory_exists_action(self, path): | ||||
|         self.__connect() | ||||
|         try: | ||||
|             self.client.collections.get(f'/{self.irods_zone}/home/{self.username}/{path}') | ||||
|             logger.debug(f'Folder \'{path}\' exists on the iRODS server') | ||||
|         except irods.exception.CollectionDoesNotExist: | ||||
|             logger.debug(f'Folder \'{path}\' does NOT exists on the iRODS server') | ||||
|             return False | ||||
|  | ||||
|         return True | ||||
|  | ||||
|     def _make_folder_action(self, path): | ||||
|         self.__connect() | ||||
|         try: | ||||
|             self.client.collections.create(f'/{self.irods_zone}/home/{self.username}/{path}') | ||||
|         except irods.exception.CollectionDoesNotExist: | ||||
|             logger.debug(f'Parent folder of file \'{path}\' does NOT exists on the iRODS server') | ||||
|             return False | ||||
|  | ||||
|         return True | ||||
|  | ||||
|     def _upload_file_action(self, source, destination): | ||||
|         self.__connect() | ||||
|         # The upload path consists of a zone, username and path | ||||
|         destination = f'/{self.irods_zone}/home/{self.username}/{destination}' | ||||
|         logger.debug(f'Uploading to file: \'{destination}\'') | ||||
|         try: | ||||
|             obj = self.client.data_objects.create(destination) | ||||
|             logger.debug(f'Created file: \'{destination}\'') | ||||
|             # Open 'both' files and copy 4K data each time. | ||||
|             with obj.open('w') as irods_file, open(source, 'rb') as source_file_binary: | ||||
|                 while True: | ||||
|                     buf = source_file_binary.read(4096) | ||||
|                     if buf: | ||||
|                         irods_file.write(buf) | ||||
|                     else: | ||||
|                         break | ||||
|  | ||||
|             obj.metadata.add('source', f'Upload from VRE DataDropOff\n Added file: {destination} uploaded by: {self.sender_name}({self.sender_email})') | ||||
|  | ||||
|         except irods.exception.OVERWRITE_WITHOUT_FORCE_FLAG: | ||||
|             logger.warning('The uploaded file already exists. So we did NOT upload the new file!') | ||||
|             return False | ||||
|  | ||||
|         return True | ||||
|  | ||||
|     def _download_file_action(self, source, destination): | ||||
|         self.__connect() | ||||
|         logger.debug(f'Downloading file: \'{source}\' to \'{destination}\'') | ||||
|         try: | ||||
|             obj = self.client.data_objects.get(f'/{self.irods_zone}/home/{self.username}/{source}') | ||||
|             # Open 'both' files and copy 4K data each time. | ||||
|             with obj.open('r') as irods_source_file, open(destination, 'wb') as local_destination_file: | ||||
|                 while True: | ||||
|                     buf = irods_source_file.read(4096) | ||||
|                     if buf: | ||||
|                         local_destination_file.write(buf) | ||||
|                     else: | ||||
|                         break | ||||
|  | ||||
|         except irods.exception.DataObjectDoesNotExist: | ||||
|             logger.error(f'File: \'{source}\' does not exists on the iRODS server') | ||||
|             return False | ||||
|  | ||||
|         return True | ||||
							
								
								
									
										65
									
								
								Enquete/enquete/storage/engines/webdav.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								Enquete/enquete/storage/engines/webdav.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| from webdav3.exceptions import WebDavException, ResponseErrorCode | ||||
| from webdav3.client import Client | ||||
| import storage.exceptions as StorageException | ||||
| from storage.utils import human_filesize | ||||
| from storage.storage import BaseStorage | ||||
| import logging | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| # WebDAV Support - https://pypi.org/project/webdavclient3/ | ||||
|  | ||||
|  | ||||
| class WebDAVStorage(BaseStorage): | ||||
|  | ||||
|     TYPE = 'webdav' | ||||
|  | ||||
|     def __connect(self): | ||||
|         # Connect to the external storage. This function can be run multiple times. It will check if it has already a connection to re-use | ||||
|         try: | ||||
|             # When this fails with an Attribute error, that means that the 'client' variable is not set and we need to make a new connection | ||||
|             assert(self.client) | ||||
|         except AttributeError: | ||||
|             # Because the 'client' variable is not known, the WebDAV connections is not created yet. So do it now! | ||||
|             self.client = Client({ | ||||
|                 'webdav_hostname': self.url, | ||||
|                 'webdav_login': self.username, | ||||
|                 'webdav_password': self.password, | ||||
|             }) | ||||
|  | ||||
|             try: | ||||
|                 # Here we abuse the .free check to see if the login credentials do work | ||||
|                 free_space = self.client.free() | ||||
|                 logger.info(f'Created WebDAV connection to url: \'{self.url}\', with space left: {human_filesize(free_space)}') | ||||
|             except ResponseErrorCode as ex: | ||||
|                 # Login went wrong, so delete the client variable for next run/try | ||||
|                 del(self.client) | ||||
|  | ||||
|                 # If there was an authentication error, raise exception and quit. | ||||
|                 if 401 == ex.code: | ||||
|                     raise StorageException.InvalidAuthentication(self.username) | ||||
|  | ||||
|                 # TODO: More errors..... | ||||
|  | ||||
|     def _file_exists_action(self, path): | ||||
|         self.__connect() | ||||
|         return self.client.check(path) | ||||
|  | ||||
|     def _directory_exists_action(self, path): | ||||
|         self.__connect() | ||||
|         return self.client.check(path) | ||||
|  | ||||
|     def _make_folder_action(self, path): | ||||
|         self.__connect() | ||||
|         self.client.mkdir(path) | ||||
|         return True | ||||
|  | ||||
|     def _upload_file_action(self, source, destination): | ||||
|         self.__connect() | ||||
|         self.client.upload(local_path=source, remote_path=destination) | ||||
|         return True | ||||
|  | ||||
|     def _download_file_action(self, source, destination): | ||||
|         self.__connect() | ||||
|         self.client.download(source, destination) | ||||
|         return True | ||||
							
								
								
									
										63
									
								
								Enquete/enquete/storage/exceptions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								Enquete/enquete/storage/exceptions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
|  | ||||
| class BaseStorageError(Exception): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class StorageActionNotImplemented(Exception): | ||||
|     def __init__(self, storage, action, message='is not implemented'): | ||||
|         self.storage = storage | ||||
|         self.action = action | ||||
|         self.message = message | ||||
|         super().__init__(self.message) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return f'{self.action} on class {self.storage} {self.message}' | ||||
|  | ||||
|  | ||||
| class FileDoesNotExist(BaseStorageError): | ||||
|     def __init__(self, source, message='File does not exists on disk'): | ||||
|         self.source = source | ||||
|         self.message = message | ||||
|         super().__init__(self.message) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return f'{self.source} -> {self.message}' | ||||
|  | ||||
|  | ||||
| class InvalidLocation(BaseStorageError): | ||||
|     def __init__(self, location, message='Location does not exists or is not valid'): | ||||
|         self.location = location | ||||
|         self.message = message | ||||
|         super().__init__(self.message) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return f'{self.location} -> {self.message}' | ||||
|  | ||||
|  | ||||
| class InvalidAuthentication(BaseStorageError): | ||||
|     def __init__(self, user, message='Authentication failed'): | ||||
|         self.user = user | ||||
|         self.message = message | ||||
|         super().__init__(self.message) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return f'{self.user} -> {self.message}' | ||||
|  | ||||
|  | ||||
| class UnknownStorageEngine(BaseStorageError): | ||||
|     def __init__(self, engine, message='Storage engine is unknown, not available'): | ||||
|         self.engine = engine | ||||
|         self.message = message | ||||
|         super().__init__(self.message) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return f'{self.engine} -> {self.message}' | ||||
|  | ||||
|  | ||||
| class MissingEncryptionKey(BaseStorageError): | ||||
|     def __init__(self, message='The encryption keys are missing'): | ||||
|         self.message = message | ||||
|         super().__init__(self.message) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return f'{self.message}' | ||||
							
								
								
									
										248
									
								
								Enquete/enquete/storage/storage.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										248
									
								
								Enquete/enquete/storage/storage.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,248 @@ | ||||
| import shlex | ||||
| import subprocess | ||||
| import storage.exceptions as StorageException | ||||
| import importlib | ||||
| from pathlib import Path | ||||
| import re | ||||
| import glob | ||||
| import os | ||||
| import shutil | ||||
| from datetime import datetime | ||||
| import tempfile | ||||
| import logging | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class Storage(): | ||||
|  | ||||
|     CLASS_REGEX = re.compile(r'class\s+(?P<class_name>[^\s\(]+)\s*\(\s*BaseStorage\s*\)\s*:') | ||||
|  | ||||
|     # This acts like a factory function. It will return a storage object from the requested engine | ||||
|     def __new__(self, storage_type, url=None, username=None, password=None, source=None, destination=None, encryption_key=None, sender_name=None, sender_email=None): | ||||
|         storage_type = storage_type.lower() | ||||
|         engines = Storage.__load_storage_engines() | ||||
|         logger.debug(f'Available storage engines({len(Storage.available_engines())}): {Storage.available_engines()}') | ||||
|         if storage_type not in engines: | ||||
|             raise StorageException.UnknownStorageEngine(storage_type) | ||||
|  | ||||
|         return engines[storage_type](url, username, password, source, destination, encryption_key, sender_name, sender_email) | ||||
|  | ||||
|     @staticmethod | ||||
|     def __load_storage_engines(): | ||||
|         loaded_engines = {} | ||||
|         engines = (Path(__file__)).parent.joinpath('engines') | ||||
|  | ||||
|         for engine in [x for x in engines.glob('*.py') if x.is_file()]: | ||||
|             with engine.open() as python_file: | ||||
|                 data = python_file.read() | ||||
|                 class_name = Storage.CLASS_REGEX.findall(data) | ||||
|                 if len(class_name) == 1: | ||||
|                     storage_engine_module = importlib.import_module('.{}' . format(engine.stem), package='storage.engines') | ||||
|                     storage_engine_class = getattr(storage_engine_module, class_name[0]) | ||||
|                     loaded_engines[storage_engine_class.TYPE.lower()] = storage_engine_class | ||||
|  | ||||
|         return loaded_engines | ||||
|  | ||||
|     @staticmethod | ||||
|     def available_engines(): | ||||
|         engines = list(Storage.__load_storage_engines().keys()) | ||||
|         engines.sort() | ||||
|         return engines | ||||
|  | ||||
|  | ||||
| class BaseStorage(): | ||||
|  | ||||
|     ENCFS_XML = '.encfs6.xml' | ||||
|     ENCRYPT_CMD = '/usr/bin/encfs' | ||||
|     FUSER_MOUNT = '/bin/fusermount' | ||||
|  | ||||
|     TYPE = '' | ||||
|  | ||||
|     def __init__(self, url=None, username=None, password=None, source=None, destination=None, encryption_key=None, sender_name=None, sender_email=None): | ||||
|         if source is not None and not os.path.exists(source): | ||||
|             logger.error(f'Source file is not available on disk! It has vanished from: {source}') | ||||
|             raise StorageException.FileDoesNotExist(source) | ||||
|  | ||||
|         # if destination is None: | ||||
|         #     logger.error(f'Destination is not valid: {destination}') | ||||
|         #     raise StorageException.InvalidLocation(destination) | ||||
|  | ||||
|         self.source = source | ||||
|         self.destination_dir = None if destination is None else os.path.dirname(destination) | ||||
|         self.destination_file = None if destination is None else os.path.basename(destination) | ||||
|         self.encryption_key = encryption_key | ||||
|         self.encrypted = False | ||||
|  | ||||
|         self.url = url | ||||
|         self.username = username | ||||
|         self.password = password | ||||
|         self.sender_name = sender_name | ||||
|         self.sender_email = sender_email | ||||
|  | ||||
|     def encrypt_source(self): | ||||
|         if self.encryption_key is None: | ||||
|             logger.error(f'Cannot encrypt source file {self.source} due to missing encryption key!') | ||||
|             raise StorageException.MissingEncryptionKey() | ||||
|  | ||||
|         if self.encrypted: | ||||
|             logger.warning('File is already encrypted') | ||||
|             return True | ||||
|  | ||||
|         start_time = datetime.now() | ||||
|         logger.info(f'Encrypting new uploaded file: {self.source}') | ||||
|  | ||||
|         encrypted_dir = tempfile.mkdtemp() | ||||
|         logger.debug(f'Created encrypted source folder: {encrypted_dir}') | ||||
|  | ||||
|         decoded_dir = tempfile.mkdtemp() | ||||
|         logger.debug(f'Created decoded folder: {decoded_dir}') | ||||
|  | ||||
|         new_encryption_setup = True | ||||
|         existing_encfs_file = os.path.join(self.destination_dir, BaseStorage.ENCFS_XML) | ||||
|         logger.debug(f'Check for existing encryption key file \'{existing_encfs_file}\' on the destination location.') | ||||
|         if self.file_exists(existing_encfs_file): | ||||
|             logger.debug(f'Copying existing \'{BaseStorage.ENCFS_XML}\' file...') | ||||
|             self.download_file(existing_encfs_file, os.path.join(encrypted_dir, BaseStorage.ENCFS_XML)) | ||||
|             logger.info(f'Using existing \'{existing_encfs_file}\' from destination location.') | ||||
|             new_encryption_setup = False | ||||
|  | ||||
|         # Mounting part between source and encrypted folder | ||||
|         # TODO: Check what happens when there are spaces in the dir names... need some quotes I guess | ||||
|         cmd = f'{BaseStorage.ENCRYPT_CMD} --standard -S {encrypted_dir} {decoded_dir}' | ||||
|         logger.debug(f'Creating an encrypted EncFS mount point with command: {cmd}') | ||||
|  | ||||
|         process = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) | ||||
|         # # Send encryption password | ||||
|         logger.debug('Mounting in action. Sending encryption key...') | ||||
|         (output, error) = process.communicate(input=self.encryption_key.encode()) | ||||
|  | ||||
|         if process.wait(timeout=30) != 0: | ||||
|             output = output.decode().strip() | ||||
|             logger.error(f'Error creating an encrypted mount with EncFS. Error: \'{output}\'') | ||||
|             raise RuntimeError(f'Mounting error EncFS: {output}') | ||||
|  | ||||
|         logger.debug(f'Mountpoint is ready at path: {decoded_dir}') | ||||
|  | ||||
|         if new_encryption_setup: | ||||
|             logger.info(f'We have a new \'{BaseStorage.ENCFS_XML}\' file that needs to be moved to the same destination: {self.destination_dir}') | ||||
|             self.upload_file(os.path.join(encrypted_dir, BaseStorage.ENCFS_XML), existing_encfs_file, True) | ||||
|  | ||||
|         # Here we ignore the subdirectories on the destination. This will be fixed during the upload | ||||
|         destination_file = os.path.join(decoded_dir, self.destination_dir, self.destination_file) | ||||
|         logger.debug(f'Moving source file \'{self.source}\' to \'{destination_file}\' for encryption.') | ||||
|         os.makedirs(os.path.dirname(destination_file)) | ||||
|         shutil.move(self.source, destination_file) | ||||
|  | ||||
|         # Here we umount the decoded directory, so we only have the encypted data left | ||||
|         logger.debug(f'Encrypting is done, un-mounting decoded folder: {decoded_dir}') | ||||
|         cmd = f'{BaseStorage.FUSER_MOUNT} -u {decoded_dir}' | ||||
|         logger.debug(f'Umounting cmd: {cmd}') | ||||
|         process = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) | ||||
|         if process.wait() != 0: | ||||
|             # TODO: Better error handling... Add raise exception | ||||
|             logger.error(f'Error un-mounting mount point: {decoded_dir}') | ||||
|             raise RuntimeError(f'Un-mounting error EncFS: {process}') | ||||
|  | ||||
|         logger.debug(f'Cleanup temporary decoded dir: {decoded_dir}') | ||||
|         shutil.rmtree(decoded_dir) | ||||
|  | ||||
|         # Find the newly created encrypted file and move it back to the original source file | ||||
|         # We use the glob function so we can also support subdirectories in the encrypted storage | ||||
|         logger.debug(f'Finding newly created encrypted file in the encrypted source folder: {encrypted_dir}') | ||||
|         encrypted_listing = glob.glob(f'{encrypted_dir}/**', recursive=True) | ||||
|         logger.debug(f'Found encrypted file: {encrypted_listing[-1]}') | ||||
|  | ||||
|         # Source file is been changed to the new encrypted file name. So use that for the file upload process | ||||
|         self.source = os.path.join(os.path.dirname(self.source), os.path.basename(encrypted_listing[-1])) | ||||
|         self.destination_file = os.path.basename(self.source) | ||||
|         logger.debug(f'Moving encrypted file {encrypted_listing[-1]} back to original file: {self.source}') | ||||
|         logger.debug(f'Updated the destination file name based on the encrypted name: {self.destination_file}') | ||||
|         shutil.move(encrypted_listing[-1], self.source) | ||||
|         logger.info(f'Encrypted to \'{self.source}\' in {datetime.now() - start_time} (h:mm:ss.ms)') | ||||
|  | ||||
|         self.encrypted = True | ||||
|         return True | ||||
|  | ||||
|     def file_exists(self, path): | ||||
|         logger.debug(f'Check if file exists at path: \'{path}\' with engine: \'{self.TYPE}\'') | ||||
|         file_exists = self._file_exists_action(path) | ||||
|         exists = 'exist' if file_exists else 'does not exist' | ||||
|         logger.debug(f'File \'{path}\' {exists} on storage \'{self.TYPE}\'') | ||||
|         return file_exists | ||||
|  | ||||
|     def upload_file(self, source=None, destination=None, move=False): | ||||
|         source = self.source if source is None else source | ||||
|         destination = os.path.join(self.destination_dir, self.destination_file) if destination is None else destination | ||||
|         upload_ok = None | ||||
|  | ||||
|         if source is None or destination is None: | ||||
|             logger.error(f'Error uploading file. Either source: \'{source}\' or destination: \'{destination}\' is not set!') | ||||
|  | ||||
|         start_time = datetime.now() | ||||
|         logger.debug(f'Start uploading file: \'{source}\' to: \'{destination}\' with engine: \'{self.TYPE}\'') | ||||
|  | ||||
|         if not self.directory_exists(os.path.dirname(destination)): | ||||
|             self.create_directories(os.path.dirname(destination)) | ||||
|  | ||||
|         upload_ok = self._upload_file_action(source, destination) | ||||
|         if upload_ok: | ||||
|             logger.info(f'Uploaded \'{source}\' to: \'{destination}\' with engine: \'{self.TYPE}\' in {datetime.now() - start_time} (h:mm:ss.ms)') | ||||
|             if move or self.encrypted: | ||||
|                 os.unlink(source) | ||||
|                 logger.debug('Removed source file from disk!') | ||||
|         else: | ||||
|             logger.error(f'Error uploading \'{source}\' to: \'{destination}\' with engine: \'{self.TYPE}\' in {datetime.now() - start_time} (h:mm:ss.ms)') | ||||
|  | ||||
|         return upload_ok | ||||
|  | ||||
|     def directory_exists(self, path): | ||||
|         # logger.debug() | ||||
|         return self._directory_exists_action(path) | ||||
|  | ||||
|     def download_file(self, source=None, destination=None, move=False): | ||||
|         source = self.source if source is None else source | ||||
|         #destination = self.destination if destination is None else destination | ||||
|         destination = os.path.join(self.destination_dir, os.path.basename(self.destination_file)) if destination is None else destination | ||||
|         download_ok = None | ||||
|  | ||||
|         if source is None or destination is None: | ||||
|             logger.error(f'Error downloading file. Either source: {source} or destination: {destination} is not set!') | ||||
|  | ||||
|         start_time = datetime.now() | ||||
|         logger.debug('Downloading file: {source} to: {destination}') | ||||
|         download_ok = self._download_file_action(source, destination) | ||||
|  | ||||
|         if download_ok: | ||||
|             logger.info(f'Downloaded \'{source}\' to: \'{destination}\' in {datetime.now() - start_time} (h:mm:ss.ms)') | ||||
|         else: | ||||
|             logger.error(f'Downloading failed for \'{source}\' to: \'{destination}\' in {datetime.now() - start_time} (h:mm:ss.ms)') | ||||
|  | ||||
|         return download_ok | ||||
|  | ||||
|     def create_directories(self, path): | ||||
|         folders = [] | ||||
|         for folder in path.strip('/').split('/'): | ||||
|             # Store travelled path. We need this to make the directories on the remote servers | ||||
|             folders.append(folder) | ||||
|             if not self.directory_exists('/'.join(folders)): | ||||
|                 logger.debug(f'Creating folder {folder} with full path: {"/".join(folders)}') | ||||
|                 self._make_folder_action('/'.join(folders)) | ||||
|             else: | ||||
|                 logger.debug(f'Folder \'{folder}\' already exists.') | ||||
|  | ||||
|         return True | ||||
|  | ||||
|     def _file_exists_action(self, path): | ||||
|         raise StorageException.StorageActionNotImplemented('BaseStorage', 'file_exists') | ||||
|  | ||||
|     def _directory_exists_action(self, path): | ||||
|         raise StorageException.StorageActionNotImplemented('BaseStorage', 'directory_exists') | ||||
|  | ||||
|     def _upload_file_action(self, source, destination): | ||||
|         raise StorageException.StorageActionNotImplemented('BaseStorage', '_upload_file') | ||||
|  | ||||
|     def _download_file_action(self, source, destination): | ||||
|         raise StorageException.StorageActionNotImplemented('BaseStorage', '_download_file') | ||||
|  | ||||
|     def _make_folder_action(self, path): | ||||
|         raise StorageException.StorageActionNotImplemented('BaseStorage', '_make_folder_action') | ||||
							
								
								
									
										8
									
								
								Enquete/enquete/storage/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								Enquete/enquete/storage/utils.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| def human_filesize(nbytes): | ||||
|     suffixes = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'] | ||||
|     i = 0 | ||||
|     while nbytes >= 1024 and i < len(suffixes) - 1: | ||||
|         nbytes /= 1024. | ||||
|         i += 1 | ||||
|     f = ('%.2f' % nbytes).rstrip('0').rstrip('.') | ||||
|     return '%s %s' % (f, suffixes[i]) | ||||
							
								
								
									
										19
									
								
								Enquete/enquete/templates/404.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								Enquete/enquete/templates/404.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| {% extends 'base.html' %} <!-- Add this for inheritance --> | ||||
| {% load i18n %} | ||||
|  | ||||
| {% block title %}{% trans "Oops, sorry, page not found (404)" %}{% endblock %} | ||||
| {% block pagetitle %}<span style="color:red">{% trans "Oops, sorry, page not found (404)" %}</span>{% endblock %} | ||||
| {% block content %} | ||||
| <div class="rug-embed__container"> | ||||
|     <iframe class="rug-embed__iframe" src="https://www.youtube-nocookie.com/embed/WOdjCb4LwQY?controls=0" frameborder="0" allow="autoplay; clipboard-write; encrypted-media" allowfullscreen></iframe> | ||||
| </div> | ||||
| <style> | ||||
| .rug-breadcrumbs { | ||||
|     display:none | ||||
| } | ||||
|  | ||||
| .rug-layout__item.rug-width-m-8-24 { | ||||
|     display:none | ||||
| } | ||||
| </style> | ||||
| {% endblock %} | ||||
							
								
								
									
										26
									
								
								Enquete/enquete/templates/base.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								Enquete/enquete/templates/base.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| {% load static %} | ||||
| {% load i18n %} | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> | ||||
|     <meta charset="utf-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||
|     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"> | ||||
|     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.0.0/css/fontawesome.min.css" integrity="sha256-scTmoQvbqwHzP/+deIFu5oz5qacx8HZor9VGp5kky4A=" crossorigin="anonymous"> | ||||
|     <title>{% block title %}{% trans "Hanze enquete" %}{% endblock %}</title> | ||||
|     <!-- Optional JavaScript --> | ||||
|     <!-- jQuery first, then Popper.js, then Bootstrap JS --> | ||||
|     <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> | ||||
|     <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script> | ||||
|     <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.min.js" integrity="sha384-VHvPCCyXqtD5DqJeNxl2dtTyhF78xXNXdkwX1CZeRusQfRKp+tA7hAShOK/B/fQ2" crossorigin="anonymous"></script> | ||||
|   </head> | ||||
|   <body> | ||||
|     <div class="container"> | ||||
|       <noscript> | ||||
|         <strong>Javascript must be enabled for the correct page display</strong> | ||||
|       </noscript> | ||||
|       {% block content %} -=PageContent=- {% endblock %} | ||||
|     </div> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										20
									
								
								Enquete/enquete/templates/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								Enquete/enquete/templates/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| {% extends 'base.html' %} <!-- Add this for inheritance --> | ||||
| {% load i18n %} | ||||
|  | ||||
| {% block title %}{% trans "Virtual Research Environment" %}{% endblock %} | ||||
| {% block pagetitle %}{% trans "Virtual Research Environment" %}{% endblock %} | ||||
| {% block content %} | ||||
| <p> | ||||
| <strong>{% trans "Secure data drops to RUG Phd students" %}</strong> | ||||
| </p> | ||||
| <p>{% trans "Here you can securely upload files for Phd students and researchers." %}</p> | ||||
| <p> | ||||
|   {% trans "The following actions can be done:" %} | ||||
|   <ol> | ||||
|     <li>{% trans "Study overview for grouping uploads for a single research study" %}</li> | ||||
|     <li>{% trans "Create a new study" %}</li> | ||||
|     <li>{% trans "Get a list of all the uploaded files for your studies" %}</li> | ||||
|     <li>{% trans "Logout" %}</li> | ||||
|   </ol> | ||||
| </p> | ||||
| {% endblock %} | ||||
							
								
								
									
										44
									
								
								Enquete/enquete/templates/menu.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								Enquete/enquete/templates/menu.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| {% load i18n %} | ||||
|  | ||||
| <li class="rug-nav--secondary__item"> | ||||
|   <a class="rug-nav--secondary__link js--togglable-switch" data-toggle-class="rug-nav--secondary__link--selected" data-toggle-group="submenu" data-toggle-id="menu-2427370b-9435-44d9-bca7-b93ec9d03cc0-33.31" data-toggle-mode="togglable">{% trans "VRE" %}</a> | ||||
|   <ul class="rug-nav--secondary__sub rug-nav--secondary__sub--hidden js--togglable-item" data-toggle-class="rug-block" data-toggle-group="submenu" data-toggle-id="menu-2427370b-9435-44d9-bca7-b93ec9d03cc0-33.31"> | ||||
|     {% if not user.is_authenticated %} | ||||
|     <li class="rug-nav--secondary__sub__item" data-menu-id="b512aa55-f0cb-4588-9054-302caa5fa951-33.34"> | ||||
|       <a class="rug-nav--secondary__sub__link" href="{% url 'login' %}"><span class="rug-nav--secondary__sub__link-text">{% trans "Login" %}</span></a> | ||||
|     </li> | ||||
|     {% endif %} | ||||
|  | ||||
|     {% if user.is_authenticated %} | ||||
|     <li class="rug-nav--secondary__sub__item" data-menu-id="b512aa55-f0cb-4588-9054-302caa5fa951-33.34"> | ||||
|       <a class="rug-nav--secondary__sub__link" href="{% url 'study:list' %}"><span class="rug-nav--secondary__sub__link-text">{% trans "Studies" %}</span></a> | ||||
|     </li> | ||||
|     <li class="rug-nav--secondary__sub__item" data-menu-id="ea2e3668-6206-4f68-9e29-fea84b472fb4-33.34"> | ||||
|       <a class="rug-nav--secondary__sub__link" href="{% url 'study:new' %}"><span class="rug-nav--secondary__sub__link-text" style="margin-left: 50px;">{% trans "New" %}</span></a> | ||||
|     </li> | ||||
|  | ||||
|     <li class="rug-nav--secondary__sub__item" data-menu-id="ea2e3668-6206-4f68-9e29-fea84b472fb4-33.34"> | ||||
|       <a class="rug-nav--secondary__sub__link" href="{% url 'storage:list' %}"><span class="rug-nav--secondary__sub__link-text">{% trans "Storages" %}</span></a> | ||||
|     </li> | ||||
|     <li class="rug-nav--secondary__sub__item" data-menu-id="ea2e3668-6206-4f68-9e29-fea84b472fb4-33.34"> | ||||
|       <a class="rug-nav--secondary__sub__link" href="{% url 'storage:new' %}"><span class="rug-nav--secondary__sub__link-text" style="margin-left: 50px;">{% trans "New" %}</span></a> | ||||
|     </li> | ||||
|  | ||||
|     <li class="rug-nav--secondary__sub__item" data-menu-id="ea2e3668-6206-4f68-9e29-fea84b472fb4-33.34"> | ||||
|       <a class="rug-nav--secondary__sub__link" href="{% url 'virtual_machine:list' %}"><span class="rug-nav--secondary__sub__link-text">{% trans "Virtual machines" %}</span></a> | ||||
|     </li> | ||||
|     <li class="rug-nav--secondary__sub__item" data-menu-id="ea2e3668-6206-4f68-9e29-fea84b472fb4-33.34"> | ||||
|       <a class="rug-nav--secondary__sub__link" href="{% url 'virtual_machine:new' %}"><span class="rug-nav--secondary__sub__link-text" style="margin-left: 50px;">{% trans "New" %}</span></a> | ||||
|     </li> | ||||
|  | ||||
|  | ||||
|     <li class="rug-nav--secondary__sub__item" data-menu-id="ea2e3668-6206-4f68-9e29-fea84b472fb4-33.34"> | ||||
|       <a class="rug-nav--secondary__sub__link" href="{% url 'dropoff:list' %}"><span class="rug-nav--secondary__sub__link-text">{% trans "Activity" %}</span></a> | ||||
|     </li> | ||||
|  | ||||
|     <li class="rug-nav--secondary__sub__item" data-menu-id="b512aa55-f0cb-4588-9054-302caa5fa951-33.36"> | ||||
|       <a class="rug-nav--secondary__sub__link" href="{% url 'logout' %}"><span class="rug-nav--secondary__sub__link-text">{% trans "Logout" %}</span></a> | ||||
|     </li> | ||||
|     {% endif %} | ||||
|   </ul> | ||||
| </li> | ||||
							
								
								
									
										12
									
								
								Enquete/enquete/templates/singup.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								Enquete/enquete/templates/singup.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| {% extends 'base.html' %} <!-- Add this for inheritance --> | ||||
| {% load i18n %} | ||||
|  | ||||
| {% block title %}{% trans "Signup" %}{% endblock %} | ||||
| {% block pagetitle %}{% trans "Signup" %}{% endblock %} | ||||
| {% block content %} | ||||
| <p> | ||||
| <strong>{% trans "Signup" %}</strong> | ||||
| <br /> | ||||
| {% blocktrans %}Please contact x@y.z{% endblocktrans %} | ||||
| </p> | ||||
| {% endblock %} | ||||
							
								
								
									
										2
									
								
								Enquete/uitkomsten/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								Enquete/uitkomsten/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| * | ||||
| !*.gitignore | ||||
		Reference in New Issue
	
	Block a user