diff --git a/.gitignore b/.gitignore index be6c789d..92cb2dc3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ __pycache__/ # NOTES dev-notes.md +data/ # Django # *.log diff --git a/citizenvoice/apiapp/migrations/0003_rename_descripion_locationcollection_description.py b/citizenvoice/apiapp/migrations/0003_rename_descripion_locationcollection_description.py new file mode 100644 index 00000000..a6511fea --- /dev/null +++ b/citizenvoice/apiapp/migrations/0003_rename_descripion_locationcollection_description.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2024-06-12 14:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('apiapp', '0002_polygonfeature'), + ] + + operations = [ + migrations.RenameField( + model_name='locationcollection', + old_name='descripion', + new_name='description', + ), + ] diff --git a/citizenvoice/apiapp/migrations/0004_alter_locationcollection_description.py b/citizenvoice/apiapp/migrations/0004_alter_locationcollection_description.py new file mode 100644 index 00000000..f2d9127f --- /dev/null +++ b/citizenvoice/apiapp/migrations/0004_alter_locationcollection_description.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2024-06-19 01:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('apiapp', '0003_rename_descripion_locationcollection_description'), + ] + + operations = [ + migrations.AlterField( + model_name='locationcollection', + name='description', + field=models.CharField(blank=True, max_length=300, null=True), + ), + ] diff --git a/citizenvoice/apiapp/migrations/0005_question_show_text.py b/citizenvoice/apiapp/migrations/0005_question_show_text.py new file mode 100644 index 00000000..dda3ce05 --- /dev/null +++ b/citizenvoice/apiapp/migrations/0005_question_show_text.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2024-06-26 05:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('apiapp', '0004_alter_locationcollection_description'), + ] + + operations = [ + migrations.AddField( + model_name='question', + name='show_text', + field=models.BooleanField(default=True, verbose_name='If the question must show the text field or not'), + ), + ] diff --git a/citizenvoice/apiapp/migrations/0006_question_explanation_alter_question_show_text_and_more.py b/citizenvoice/apiapp/migrations/0006_question_explanation_alter_question_show_text_and_more.py new file mode 100644 index 00000000..1a0b51c5 --- /dev/null +++ b/citizenvoice/apiapp/migrations/0006_question_explanation_alter_question_show_text_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.0 on 2024-06-26 06:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('apiapp', '0005_question_show_text'), + ] + + operations = [ + migrations.AddField( + model_name='question', + name='explanation', + field=models.TextField(blank=True, null=True, verbose_name='Explanation for the Question'), + ), + migrations.AlterField( + model_name='question', + name='show_text', + field=models.BooleanField(default=True, verbose_name='Show the input text field'), + ), + migrations.AlterField( + model_name='question', + name='text', + field=models.TextField(verbose_name='Question'), + ), + ] diff --git a/citizenvoice/apiapp/migrations/0007_alter_question_explanation.py b/citizenvoice/apiapp/migrations/0007_alter_question_explanation.py new file mode 100644 index 00000000..a9b8bf3e --- /dev/null +++ b/citizenvoice/apiapp/migrations/0007_alter_question_explanation.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2024-06-26 07:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('apiapp', '0006_question_explanation_alter_question_show_text_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='question', + name='explanation', + field=models.TextField(blank=True, max_length=200, null=True, verbose_name='Explanation for the question'), + ), + ] diff --git a/citizenvoice/apiapp/migrations/0008_rename_show_text_question_has_text_input_and_more.py b/citizenvoice/apiapp/migrations/0008_rename_show_text_question_has_text_input_and_more.py new file mode 100644 index 00000000..705879b0 --- /dev/null +++ b/citizenvoice/apiapp/migrations/0008_rename_show_text_question_has_text_input_and_more.py @@ -0,0 +1,50 @@ +# Generated by Django 5.0 on 2024-07-03 09:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('apiapp', '0007_alter_question_explanation'), + ] + + operations = [ + migrations.RenameField( + model_name='question', + old_name='show_text', + new_name='has_text_input', + ), + migrations.RemoveField( + model_name='linefeature', + name='description', + ), + migrations.RemoveField( + model_name='pointfeature', + name='description', + ), + migrations.RemoveField( + model_name='polygonfeature', + name='description', + ), + migrations.AddField( + model_name='linefeature', + name='annotation', + field=models.CharField(blank=True, max_length=150, null=True), + ), + migrations.AddField( + model_name='mapview', + name='description', + field=models.TextField(blank=True, max_length=200, null=True, verbose_name='Description of the MapView'), + ), + migrations.AddField( + model_name='pointfeature', + name='annotation', + field=models.CharField(blank=True, max_length=150, null=True), + ), + migrations.AddField( + model_name='polygonfeature', + name='annotation', + field=models.CharField(blank=True, max_length=150, null=True), + ), + ] diff --git a/citizenvoice/apiapp/migrations/0009_alter_linefeature_location_and_more.py b/citizenvoice/apiapp/migrations/0009_alter_linefeature_location_and_more.py new file mode 100644 index 00000000..6fdddbd3 --- /dev/null +++ b/citizenvoice/apiapp/migrations/0009_alter_linefeature_location_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 5.0 on 2024-07-03 11:45 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('apiapp', '0008_rename_show_text_question_has_text_input_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='linefeature', + name='location', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='apiapp.locationcollection'), + ), + migrations.AlterField( + model_name='pointfeature', + name='location', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='apiapp.locationcollection'), + ), + migrations.AlterField( + model_name='polygonfeature', + name='location', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='apiapp.locationcollection'), + ), + ] diff --git a/citizenvoice/apiapp/migrations/0010_survey_submit_messsage.py b/citizenvoice/apiapp/migrations/0010_survey_submit_messsage.py new file mode 100644 index 00000000..675381a5 --- /dev/null +++ b/citizenvoice/apiapp/migrations/0010_survey_submit_messsage.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2024-07-03 11:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('apiapp', '0009_alter_linefeature_location_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='survey', + name='submit_messsage', + field=models.TextField(blank=True, default='Thank you for your participation!', verbose_name='Message to be displayed after survey is submited'), + ), + ] diff --git a/citizenvoice/apiapp/migrations/0011_rename_submit_messsage_survey_submit_message.py b/citizenvoice/apiapp/migrations/0011_rename_submit_messsage_survey_submit_message.py new file mode 100644 index 00000000..1a8f01ff --- /dev/null +++ b/citizenvoice/apiapp/migrations/0011_rename_submit_messsage_survey_submit_message.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2024-07-03 11:59 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('apiapp', '0010_survey_submit_messsage'), + ] + + operations = [ + migrations.RenameField( + model_name='survey', + old_name='submit_messsage', + new_name='submit_message', + ), + ] diff --git a/citizenvoice/apiapp/migrations/0012_alter_survey_submit_message.py b/citizenvoice/apiapp/migrations/0012_alter_survey_submit_message.py new file mode 100644 index 00000000..7def67e1 --- /dev/null +++ b/citizenvoice/apiapp/migrations/0012_alter_survey_submit_message.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2024-07-03 12:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('apiapp', '0011_rename_submit_messsage_survey_submit_message'), + ] + + operations = [ + migrations.AlterField( + model_name='survey', + name='submit_message', + field=models.TextField(blank=True, default='Thank you for your participation!', verbose_name='Message to be displayed after survey is submitted'), + ), + ] diff --git a/citizenvoice/apiapp/models/location.py b/citizenvoice/apiapp/models/location.py index baf828f3..b329204b 100644 --- a/citizenvoice/apiapp/models/location.py +++ b/citizenvoice/apiapp/models/location.py @@ -8,25 +8,24 @@ class PointFeature(gis_models.Model) : Represents the location of a question or answer as a POINT """ geom = gis_models.PointField() - description = models.CharField(max_length=100, blank=True, null=True) - location = models.ForeignKey('LocationCollection', on_delete=models.RESTRICT) + annotation = models.CharField(max_length=150, blank=True, null=True) + location = models.ForeignKey('LocationCollection', on_delete=models.CASCADE) class PolygonFeature(gis_models.Model): """ Represents the location of a question or answer as a POLYGON """ geom = gis_models.PolygonField() - description = models.CharField(max_length=100, blank=True, null=True) - location = models.ForeignKey('LocationCollection', on_delete=models.RESTRICT) - + annotation = models.CharField(max_length=150, blank=True, null=True) + location = models.ForeignKey('LocationCollection', on_delete=models.CASCADE) class LineFeature(gis_models.Model): """ Represents the location of a question or answer as a LINESTRING """ geom = gis_models.LineStringField() - description = models.CharField(max_length=100, blank=True, null=True) - location = models.ForeignKey('LocationCollection', on_delete=models.RESTRICT) + annotation = models.CharField(max_length=150, blank=True, null=True) + location = models.ForeignKey('LocationCollection', on_delete=models.CASCADE) class LocationCollection(models.Model): @@ -41,7 +40,7 @@ class for representing geographic locations of """ name = models.CharField(max_length=100, blank=True) - descripion = models.CharField(max_length=300, blank=True) + description = models.CharField(max_length=300, blank=True, null=True) def __str__(self): "Returs the name of the location" diff --git a/citizenvoice/apiapp/models/mapview.py b/citizenvoice/apiapp/models/mapview.py index 8e18ad98..e4a711e3 100644 --- a/citizenvoice/apiapp/models/mapview.py +++ b/citizenvoice/apiapp/models/mapview.py @@ -17,6 +17,7 @@ class MapView(models.Model): """ name = models.CharField(_("Name of the MapView location"), max_length=150, blank=True, default="Delft") map_service_url = models.CharField(_("Map Service URL"), max_length=150, default=default_service_url) + description = models.TextField(_("Description of the MapView"), max_length=200, blank=True, null=True) options = models.JSONField(_("Map service specific options"), default=default_options) location = models.ForeignKey(LocationCollection, on_delete=models.SET_NULL, blank=True, null=True) diff --git a/citizenvoice/apiapp/models/question.py b/citizenvoice/apiapp/models/question.py index 7d5105f0..c9876b55 100644 --- a/citizenvoice/apiapp/models/question.py +++ b/citizenvoice/apiapp/models/question.py @@ -44,9 +44,11 @@ class Question(models.Model): (DATE, _("date")), ) - text = models.TextField(_("Text of the Question")) + text = models.TextField(_("Question")) + explanation = models.TextField(_("Explanation for the question"), max_length=200, blank=True, null=True) order = models.IntegerField(_("Order of where question is placed")) required = models.BooleanField(_("Question must be filled out"), default=True) + has_text_input = models.BooleanField(_("Show the input text field"), default=True) question_type = models.CharField(_("Type of question"), max_length=150, choices=QUESTION_TYPES, default=TEXT) choices = models.TextField(_("Choices for answers"), blank=True, null=True) survey = models.ForeignKey(Survey, on_delete=models.CASCADE, default=1) @@ -66,10 +68,10 @@ def question_count(self): return self.question_set.count() - class Meta: - verbose_name = _("question") - verbose_name_plural = _("questions") - ordering = ("survey", "order") + # class Meta: + # verbose_name = _("question") + # verbose_name_plural = _("questions") + # ordering = ("survey", "order") - def question_count(self): - return self.question_set.count() + # def question_count(self): + # return self.question_set.count() diff --git a/citizenvoice/apiapp/models/survey.py b/citizenvoice/apiapp/models/survey.py index 5536d8b2..be033757 100644 --- a/citizenvoice/apiapp/models/survey.py +++ b/citizenvoice/apiapp/models/survey.py @@ -22,6 +22,7 @@ class Survey(models.Model): is_published = models.BooleanField(_("Survey is visible and accessible to users"), default=False) need_logged_user = models.BooleanField(_("Only authenticated users have access to this survey"), default=False) editable_answers = models.BooleanField(_("Answers can be edited after submission"), default=True) + submit_message = models.TextField(_("Message to be displayed after survey is submitted"), blank=True, default="Thank you for your participation!") publish_date = models.DateTimeField(_("Date that survey was made available")) expire_date = models.DateTimeField(_("Expiry date of survey")) public_url = models.CharField(_("Public URL"), max_length=255, blank=True) # TODO: [manuel] this should be auto-generated when chosen by the designer diff --git a/citizenvoice/apiapp/serializers.py b/citizenvoice/apiapp/serializers.py index 5beac3e8..86502305 100644 --- a/citizenvoice/apiapp/serializers.py +++ b/citizenvoice/apiapp/serializers.py @@ -6,23 +6,20 @@ from django.contrib.auth.models import User -# =============================================ß -# Create serializer classes that allow for exposing certain model fields to be used in the API # ============================================= - - +# Create serializer classes for exposing certain model fields to be used in the API +# ============================================= class QuestionSerializer(serializers.HyperlinkedModelSerializer): """ Serializes 'text', 'order', 'required', 'question_type', 'choices', 'is_geospatial', 'map_view' fields of the Question model for the API. """ - # survey = serializers.PrimaryKeyRelatedField(queryset=Survey.objects.all()) survey = serializers.HyperlinkedRelatedField(view_name='survey-detail',read_only=True) class Meta: model = Question - fields = ('id', 'url', 'text', 'order', 'required', 'question_type', + fields = ('id', 'url', 'text', 'explanation', 'has_text_input', 'order', 'required', 'question_type', 'choices', 'survey', 'is_geospatial', 'mapview') read_only_fields = ('id', 'url') @@ -35,12 +32,12 @@ def create(self, validated_data): choices=validated_data.get('choices', None), survey=validated_data['survey'], is_geospatial=validated_data.get('is_geospatial', False), + has_text_input=validated_data.get('has_text_input', True), mapview=validated_data.get('mapview', None), ) return question - class ResponseSerializer(serializers.HyperlinkedModelSerializer): """ Serializes 'response_id', 'url', 'survey', 'respondent', 'created', 'updated' @@ -72,7 +69,7 @@ def create(self, validated_data): class SurveySerializer(serializers.HyperlinkedModelSerializer): """ - Serialises 'id', 'name', 'description', 'is_published', 'need_logged_user', + Serialises 'id', 'name', 'description', 'submit_message', 'is_published', 'need_logged_user', 'editable_answers', 'publish_date', 'expire_date', 'public_url', 'designer' fields of the Survey model for the API. """ @@ -82,7 +79,8 @@ class SurveySerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Survey - fields = ('id', 'url', 'name', 'description', 'is_published', 'need_logged_user', 'editable_answers', + fields = ('id', 'url', 'name', 'description', 'submit_message', 'is_published', + 'need_logged_user', 'editable_answers', 'publish_date', 'expire_date', 'public_url', 'designer') @@ -98,53 +96,128 @@ class Meta: class PointFeatureSerializer(serializers.HyperlinkedModelSerializer): """ - Serialises 'id', 'name', 'descripton', fields of the PointLocation model for the API. + Serialises 'id', 'url', 'geom', 'name', 'annotation', 'location' + fields of the PointLocation model for the API. + The 'geom' field is serialized as a GeoJSON field. """ - location = serializers.HyperlinkedRelatedField(view_name='locationcollection-detail',read_only=True) + location = serializers.HyperlinkedRelatedField(queryset=LocationCollection.objects.all(), + view_name='locationcollection-detail') class Meta: model = PointFeature - fields = ('id', 'url', 'geom', 'description', 'location') + geo_field = 'geom' + fields = ('id', 'url', 'annotation', 'location', 'geom') + read_only_fields = ('id', 'url') + + def create(self, validated_data): + response = PointFeature.objects.create( + **validated_data + ) + return response class PolygonFeatureSerializer(serializers.HyperlinkedModelSerializer): """ - Serialises 'id', 'geom', 'descripton', fields of the PolygonLocation model for the API. + Serialises 'id', 'geom', 'annotation', fields of the PolygonLocation model for the API. + The 'geom' field is serialized as a GeoJSON field. """ - location = serializers.HyperlinkedRelatedField(view_name='locationcollection-detail',read_only=True) + location = serializers.HyperlinkedRelatedField(queryset=LocationCollection.objects.all(), + view_name='locationcollection-detail') class Meta: model = PolygonFeature - fields = ('id', 'url', 'geom', 'description', 'location') + geo_field = 'geom' + fields = ('id', 'url', 'annotation', 'location', 'geom') read_only_fields = ('id', 'url') - + + def create(self, validated_data): + response = PolygonFeature.objects.create( + **validated_data + ) + return response class LineFeatureSerializer(serializers.HyperlinkedModelSerializer): """ - Serialises 'id', 'geom', 'description' fields of the LineStringLocation model for the API. + Serialises 'id', 'geom', 'annotation' fields of the LineStringLocation model for the API. + The 'geom' field is serialized as a GeoJSON field. """ - location = serializers.HyperlinkedRelatedField(view_name='locationcollection-detail',read_only=True) + location = serializers.HyperlinkedRelatedField(queryset=LocationCollection.objects.all(), + view_name='locationcollection-detail') class Meta: - model = PolygonFeature - fields = ('id', 'url', 'geom', 'description', 'location') + model = LineFeature + geo_field = 'geom' + fields = ('id', 'url', 'annotation', 'location', 'geom') read_only_fields = ('id', 'url') - + + + def create(self, validated_data): + response = LineFeature.objects.create( + **validated_data + ) + return response + class LocationCollectionSerializer(serializers.HyperlinkedModelSerializer): """ Serialises 'name', 'question', 'answer', 'points', 'lines', 'polygons' fields of the Location model for the API. """ - - # TODO: read DRF documentation to understand how to fix the issues with location-detail - # https://www.django-rest-framework.org/api-guide/serializers/#modelserializer - + features = serializers.SerializerMethodField() class Meta: model = LocationCollection - fields = ('id', 'url', 'name') + fields = ('id', 'url', 'name', 'description', 'features') read_only_fields = ('id', 'url') + + def get_features(self, obj): + """ + Returns a list of URLs of all the features (points, lines, polygons) + associated with the location collection. + """ + points = PointFeatureSerializer(PointFeature.objects.filter(location__id=obj.pk), + many=True, + context={'request': self.context.get('request')}).data + lines = LineFeatureSerializer(LineFeature.objects.filter(location__id=obj.pk), + many=True, + context={'request': self.context.get('request')}).data + polygons = PolygonFeatureSerializer(PolygonFeature.objects.filter(location__id=obj.pk), + many=True, + context={'request': self.context.get('request')}).data + + features = points + lines + polygons + feature_urls = [f['url'] for f in features] + + return feature_urls + + +class AnswerCSVSerializer(serializers.ModelSerializer): + """ + Serialises 'response', 'question', 'created', 'updated', 'body' + fields of the Answer model for the API. + """ + response = serializers.SerializerMethodField() + mapview = serializers.SerializerMethodField() + question = serializers.SerializerMethodField() + + class Meta: + model = Answer + fields = ('id', 'created', 'updated', 'question', 'body', 'response', 'mapview') + read_only_fields = ('id', 'created', 'updated', 'question', 'response', 'mapview') + + + def get_response(self, obj): + serializer = ResponseSerializer(obj.response, context={'request': self.context.get('request')}) + return serializer.data + + def get_mapview(self, obj): + serializer = MapViewSerializer(obj.mapview, context={'request': self.context.get('request')}) + return serializer.data + + def get_question(self, obj): + serializer = QuestionSerializer(obj.question, context={'request': self.context.get('request')}) + return serializer.data + class AnswerSerializer(serializers.HyperlinkedModelSerializer): @@ -161,6 +234,7 @@ class Meta: model = Answer fields = ('id', 'url', 'created', 'updated', 'body', 'question', 'response', 'mapview') read_only_fields = ('id', 'url', 'created') + depth = 2 def create(self, validated_data): response = Answer.objects.create( @@ -168,7 +242,6 @@ def create(self, validated_data): ) return response - class MapViewSerializer(serializers.HyperlinkedModelSerializer): """ Serialises 'name', 'map_service_url' and 'options' @@ -177,8 +250,9 @@ class MapViewSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = MapView - fields = ('id', 'url', 'name', 'map_service_url', + fields = ('id', 'url', 'name', 'description', 'map_service_url', 'options', 'location') + read_only_fields = ('id', 'url') def create(self, validated_data): diff --git a/citizenvoice/apiapp/urls.py b/citizenvoice/apiapp/urls.py index 6b235462..28a971f5 100644 --- a/citizenvoice/apiapp/urls.py +++ b/citizenvoice/apiapp/urls.py @@ -25,7 +25,7 @@ router.register(r'locations', views.LocationViewSet, basename='locationcollection') router.register(r'polygonfeatures', views.PolygonFeatureViewSet, basename='polygonfeature') -router.register(r'linesfeatures', +router.register(r'linefeatures', views.LineFeatureViewSet, basename='linefeature') router.register(r'map-views', views.MapViewViewSet, basename='mapview') router.register(r'pointfeatures', views.PointFeatureViewSet, diff --git a/citizenvoice/apiapp/views.py b/citizenvoice/apiapp/views.py index 2f332ca6..c951ab8a 100644 --- a/citizenvoice/apiapp/views.py +++ b/citizenvoice/apiapp/views.py @@ -11,12 +11,13 @@ from django.utils import timezone from .serializers import AnswerSerializer, LocationCollectionSerializer, PointFeatureSerializer, \ QuestionSerializer, SurveySerializer, ResponseSerializer, UserSerializer, \ - MapViewSerializer, LineFeatureSerializer, PolygonFeatureSerializer + MapViewSerializer, LineFeatureSerializer, PolygonFeatureSerializer, AnswerCSVSerializer from rest_framework.permissions import AllowAny, IsAuthenticated from django.contrib.auth.models import User from datetime import datetime from django.shortcuts import get_object_or_404 - +import csv +from django.http import HttpResponse @api_view(['GET']) @@ -74,7 +75,54 @@ def GetAnswerByResponse(response_id): """ queryset = Answer.objects.filter(response=response_id) return queryset + + @action(detail=False, methods=['get'], url_path='csv') + def download_csv(self, request, *args, **kwargs): + queryset = Answer.objects.all() + serializer = AnswerCSVSerializer(queryset, context={'request': request}, many=True) + + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename="answers.csv"' + + # TODO: THIS IS BETTER DONW WITH SQL QUERY + def flatten_dict(d, parent_key='', sep='.'): + items = [] + for k, v in d.items(): + new_key = f"{parent_key}{sep}{k}" if parent_key else k + if isinstance(v, dict): + items.extend(flatten_dict(v, new_key, sep=sep).items()) + else: + items.append((new_key, v)) + return dict(items) + + flatten_data = [flatten_dict(answer) for answer in serializer.data] + # print ('flatten data \n', flatten_data) + + writer = csv.writer(response) + headers = set() + for item in flatten_data: + headers.update(item.keys()) + writer.writerow(list(headers)) + + print ('headers \n', headers) + + for answer in flatten_data: + _rows= [] + for field in headers: + try: + _row = answer[field] + _rows.append(_row) + except KeyError: + print ('KeyError', field) + _row = '' + _rows.append(_row) + writer.writerow(_rows) + + # writer.writerow([answer[field] for field in headers]) + + return response + class QuestionViewSet(viewsets.ModelViewSet, UpdateModelMixin): """ @@ -111,8 +159,8 @@ def create(self, request, *args, **kwargs): question = serializer.save() questions.append(question) - update_fields = ['text', 'order', 'required', 'question_type', - 'choices', 'survey', 'is_geospatial', 'map_view'] + update_fields = ['text', 'explanation', 'order', 'required', 'question_type', + 'choices', 'survey', 'is_geospatial', 'has_text_input', 'map_view'] # update or create multiple questions in bulk Question.objects.bulk_update_or_create(questions, update_fields, match_field='id') @@ -122,19 +170,21 @@ def create(self, request, *args, **kwargs): return rf_response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) def perform_create(self, serializer): - update_fields = ['text', 'order', 'required', 'question_type', - 'choices', 'survey', 'is_geospatial', 'map_view'] + update_fields = ['text', 'explanation', 'order', 'required', 'question_type', + 'choices', 'survey', 'is_geospatial', 'has_text_input', 'map_view'] serializer.save(update_fields=update_fields) def perform_update(self, serializer): - serializer.save(update_fields=['text', 'order', 'required', 'question_type', 'choices', 'survey', 'is_geospatial', 'map_view'], update_conflicts={ + serializer.save(update_fields=['text','explanation', 'order', 'required', 'question_type', 'choices', 'survey', 'is_geospatial', 'show_text', 'map_view'], update_conflicts={ 'text': 'keep', + 'explanation': 'keep', 'order': 'keep', 'required': 'keep', 'question_type': 'keep', 'choices': 'keep', 'survey': 'keep', 'is_geospatial': 'keep', + 'has_text_input': 'keep', 'map_view': 'keep' }) @@ -185,8 +235,6 @@ def my_surveys(self, request, *args, **kwargs): if (type(user) == User): surveys_of_user = Survey.objects.all().filter(designer=user.id).order_by('name') survey_serializer = self.get_serializer(surveys_of_user, many=True) - # print("User Id: ", user.id) - # print(survey_serializer.data) return rf_response(survey_serializer.data) return rf_response({}) @@ -230,7 +278,7 @@ def get_questions_of_survey(self, request, pk=None): questions = Question.objects.all().filter(survey_id=pk).order_by('order') question_serializer = QuestionSerializer( questions, many=True, context={'request': request}) - print(question_serializer.data) + # print(question_serializer.data) return rf_response(question_serializer.data) # else: # print("User was anonymous") @@ -478,7 +526,18 @@ class PointFeatureViewSet(viewsets.ModelViewSet): """ serializer_class = PointFeatureSerializer - queryset = PointFeature.objects.all() + + def get_queryset(response): + """ + Returns a set of all PointFeature instances in the database. + + Return: + queryset: containing all PointFeature instances + """ + + queryset = PointFeature.objects.all() + return queryset + class PolygonFeatureViewSet(viewsets.ModelViewSet): @@ -498,22 +557,23 @@ def get_queryset(response): queryset = PolygonFeature.objects.all() return queryset - + @staticmethod def GetLocationsByQuestion(question): """ - Get a list of PolygonFeatures associated to this question. + Get a list of PointFeatures associated to this question. Parameters: - question (int): Question ID to be used for finding related PolygonFeatures. + question (int): Question ID to be used for finding related PointFeatures. Return: - queryset: containing the PolygonFeature instances related to this Question + queryset: containing the PointFeature instances related to this Question """ - queryset = PolygonFeature.objects.filter(question=question) + queryset = PointFeature.objects.filter(question=question) return queryset + @staticmethod def GetLocationsByAnswer(answer): """ @@ -595,7 +655,7 @@ def get_queryset(response): queryset: containing all MapView instances """ - queryset = MapView.objects.all() + queryset = MapView.objects.all().order_by('id') return queryset @action(detail=False, methods=['get']) diff --git a/citizenvoice/citizenvoice/settings.py b/citizenvoice/citizenvoice/settings.py index d494df0d..9a576814 100644 --- a/citizenvoice/citizenvoice/settings.py +++ b/citizenvoice/citizenvoice/settings.py @@ -69,6 +69,7 @@ 'django.contrib.sites', 'apiapp', 'rest_framework', + 'rest_framework_gis', 'rest_framework.permissions', 'users.apps.UsersConfig', 'survey_design.apps.SurveyDesignConfig', @@ -277,6 +278,6 @@ SPECTACULAR_SETTINGS = { "TITLE": "CitizenVoice API", "DESCRIPTION": "Documentation of API endpoints for CitizenVoice", - "VERSION": "2.0.0", + "VERSION": "2.0.6", "SCHEMA_PATH_PREFIX": "/api", } diff --git a/citizenvoice/schema.yml b/citizenvoice/schema.yml index 41eaf91c..8d5e4bfb 100644 --- a/citizenvoice/schema.yml +++ b/citizenvoice/schema.yml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: CitizenVoice API - version: 2.0.1 + version: 2.0.6 description: Documentation of API endpoints for CitizenVoice paths: /api/auth/email/resend/: @@ -367,6 +367,22 @@ paths: responses: '204': description: No response body + /api/v2/answers/csv/: + get: + operationId: v2_answers_csv_retrieve + description: Answer ViewSet used internally to query data from database. + tags: + - v2 + security: + - tokenAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Answer' + description: '' /api/v2/csrf/: get: operationId: v2_csrf_retrieve @@ -378,9 +394,9 @@ paths: responses: '200': description: No response body - /api/v2/linesfeatures/: + /api/v2/linefeatures/: get: - operationId: v2_linesfeatures_list + operationId: v2_linefeatures_list description: LineStringLocation ViewSet used internally to query data from database for all users. tags: @@ -398,7 +414,7 @@ paths: $ref: '#/components/schemas/LineFeature' description: '' post: - operationId: v2_linesfeatures_create + operationId: v2_linefeatures_create description: LineStringLocation ViewSet used internally to query data from database for all users. tags: @@ -425,9 +441,9 @@ paths: schema: $ref: '#/components/schemas/LineFeature' description: '' - /api/v2/linesfeatures/{id}/: + /api/v2/linefeatures/{id}/: get: - operationId: v2_linesfeatures_retrieve + operationId: v2_linefeatures_retrieve description: LineStringLocation ViewSet used internally to query data from database for all users. parameters: @@ -450,7 +466,7 @@ paths: $ref: '#/components/schemas/LineFeature' description: '' put: - operationId: v2_linesfeatures_update + operationId: v2_linefeatures_update description: LineStringLocation ViewSet used internally to query data from database for all users. parameters: @@ -485,7 +501,7 @@ paths: $ref: '#/components/schemas/LineFeature' description: '' patch: - operationId: v2_linesfeatures_partial_update + operationId: v2_linefeatures_partial_update description: LineStringLocation ViewSet used internally to query data from database for all users. parameters: @@ -519,7 +535,7 @@ paths: $ref: '#/components/schemas/LineFeature' description: '' delete: - operationId: v2_linesfeatures_destroy + operationId: v2_linefeatures_destroy description: LineStringLocation ViewSet used internally to query data from database for all users. parameters: @@ -2188,8 +2204,9 @@ components: default: Verification e-mail sent. LineFeature: type: object - description: Serialises 'id', 'geom', 'description' fields of the LineStringLocation - model for the API. + description: |- + Serialises 'id', 'geom', 'annotation' fields of the LineStringLocation model for the API. + The 'geom' field is serialized as a GeoJSON field. properties: id: type: integer @@ -2198,16 +2215,38 @@ components: type: string format: uri readOnly: true - geom: - type: string - description: + annotation: type: string nullable: true - maxLength: 100 + maxLength: 150 location: type: string format: uri - readOnly: true + geom: + type: object + properties: + type: + type: string + enum: + - LineString + coordinates: + type: array + items: + type: array + items: + type: number + format: float + example: + - 12.9721 + - 77.5933 + minItems: 2 + maxItems: 3 + example: + - - 22.4707 + - 70.0577 + - - 12.9721 + - 77.5933 + minItems: 2 required: - geom - id @@ -2229,7 +2268,15 @@ components: name: type: string maxLength: 100 + description: + type: string + nullable: true + maxLength: 300 + features: + type: string + readOnly: true required: + - features - id - url MapView: @@ -2250,6 +2297,11 @@ components: default: Delft title: Name of the MapView location maxLength: 150 + description: + type: string + nullable: true + title: Description of the MapView + maxLength: 200 map_service_url: type: string maxLength: 150 @@ -2302,8 +2354,9 @@ components: nullable: true PatchedLineFeature: type: object - description: Serialises 'id', 'geom', 'description' fields of the LineStringLocation - model for the API. + description: |- + Serialises 'id', 'geom', 'annotation' fields of the LineStringLocation model for the API. + The 'geom' field is serialized as a GeoJSON field. properties: id: type: integer @@ -2312,16 +2365,38 @@ components: type: string format: uri readOnly: true - geom: - type: string - description: + annotation: type: string nullable: true - maxLength: 100 + maxLength: 150 location: type: string format: uri - readOnly: true + geom: + type: object + properties: + type: + type: string + enum: + - LineString + coordinates: + type: array + items: + type: array + items: + type: number + format: float + example: + - 12.9721 + - 77.5933 + minItems: 2 + maxItems: 3 + example: + - - 22.4707 + - 70.0577 + - - 12.9721 + - 77.5933 + minItems: 2 PatchedLocationCollection: type: object description: |- @@ -2338,6 +2413,13 @@ components: name: type: string maxLength: 100 + description: + type: string + nullable: true + maxLength: 300 + features: + type: string + readOnly: true PatchedMapView: type: object description: |- @@ -2356,6 +2438,11 @@ components: default: Delft title: Name of the MapView location maxLength: 150 + description: + type: string + nullable: true + title: Description of the MapView + maxLength: 200 map_service_url: type: string maxLength: 150 @@ -2369,8 +2456,10 @@ components: nullable: true PatchedPointFeature: type: object - description: Serialises 'id', 'name', 'descripton', fields of the PointLocation - model for the API. + description: |- + Serialises 'id', 'url', 'geom', 'name', 'annotation', 'location' + fields of the PointLocation model for the API. + The 'geom' field is serialized as a GeoJSON field. properties: id: type: integer @@ -2379,20 +2468,35 @@ components: type: string format: uri readOnly: true - geom: - type: string - description: + annotation: type: string nullable: true - maxLength: 100 + maxLength: 150 location: type: string format: uri - readOnly: true + geom: + type: object + properties: + type: + type: string + enum: + - Point + coordinates: + type: array + items: + type: number + format: float + example: + - 12.9721 + - 77.5933 + minItems: 2 + maxItems: 3 PatchedPolygonFeature: type: object - description: Serialises 'id', 'geom', 'descripton', fields of the PolygonLocation - model for the API. + description: |- + Serialises 'id', 'geom', 'annotation', fields of the PolygonLocation model for the API. + The 'geom' field is serialized as a GeoJSON field. properties: id: type: integer @@ -2401,16 +2505,51 @@ components: type: string format: uri readOnly: true - geom: - type: string - description: + annotation: type: string nullable: true - maxLength: 100 + maxLength: 150 location: type: string format: uri - readOnly: true + geom: + type: object + properties: + type: + type: string + enum: + - Polygon + coordinates: + type: array + items: + type: array + items: + type: array + items: + type: number + format: float + example: + - 12.9721 + - 77.5933 + minItems: 2 + maxItems: 3 + example: + - - 22.4707 + - 70.0577 + - - 12.9721 + - 77.5933 + minItems: 4 + example: + - - - 0.0 + - 0.0 + - - 0.0 + - 50.0 + - - 50.0 + - 50.0 + - - 50.0 + - 0.0 + - - 0.0 + - 0.0 PatchedQuestion: type: object description: |- @@ -2426,7 +2565,16 @@ components: readOnly: true text: type: string - title: Text of the Question + title: Question + explanation: + type: string + nullable: true + title: Explanation for the question + maxLength: 200 + has_text_input: + type: boolean + default: true + title: Show the input text field order: type: integer maximum: 2147483647 @@ -2492,7 +2640,7 @@ components: PatchedSurvey: type: object description: |- - Serialises 'id', 'name', 'description', 'is_published', 'need_logged_user', + Serialises 'id', 'name', 'description', 'submit_message', 'is_published', 'need_logged_user', 'editable_answers', 'publish_date', 'expire_date', 'public_url', 'designer' fields of the Survey model for the API. properties: @@ -2509,6 +2657,10 @@ components: maxLength: 150 description: type: string + submit_message: + type: string + default: Thank you for your participation! + title: Message to be displayed after survey is submitted is_published: type: boolean default: false @@ -2555,8 +2707,10 @@ components: title: Email address PointFeature: type: object - description: Serialises 'id', 'name', 'descripton', fields of the PointLocation - model for the API. + description: |- + Serialises 'id', 'url', 'geom', 'name', 'annotation', 'location' + fields of the PointLocation model for the API. + The 'geom' field is serialized as a GeoJSON field. properties: id: type: integer @@ -2565,16 +2719,30 @@ components: type: string format: uri readOnly: true - geom: - type: string - description: + annotation: type: string nullable: true - maxLength: 100 + maxLength: 150 location: type: string format: uri - readOnly: true + geom: + type: object + properties: + type: + type: string + enum: + - Point + coordinates: + type: array + items: + type: number + format: float + example: + - 12.9721 + - 77.5933 + minItems: 2 + maxItems: 3 required: - geom - id @@ -2582,8 +2750,9 @@ components: - url PolygonFeature: type: object - description: Serialises 'id', 'geom', 'descripton', fields of the PolygonLocation - model for the API. + description: |- + Serialises 'id', 'geom', 'annotation', fields of the PolygonLocation model for the API. + The 'geom' field is serialized as a GeoJSON field. properties: id: type: integer @@ -2592,16 +2761,51 @@ components: type: string format: uri readOnly: true - geom: - type: string - description: + annotation: type: string nullable: true - maxLength: 100 + maxLength: 150 location: type: string format: uri - readOnly: true + geom: + type: object + properties: + type: + type: string + enum: + - Polygon + coordinates: + type: array + items: + type: array + items: + type: array + items: + type: number + format: float + example: + - 12.9721 + - 77.5933 + minItems: 2 + maxItems: 3 + example: + - - 22.4707 + - 70.0577 + - - 12.9721 + - 77.5933 + minItems: 4 + example: + - - - 0.0 + - 0.0 + - - 0.0 + - 50.0 + - - 50.0 + - 50.0 + - - 50.0 + - 0.0 + - - 0.0 + - 0.0 required: - geom - id @@ -2622,7 +2826,16 @@ components: readOnly: true text: type: string - title: Text of the Question + title: Question + explanation: + type: string + nullable: true + title: Explanation for the question + maxLength: 200 + has_text_input: + type: boolean + default: true + title: Show the input text field order: type: integer maximum: 2147483647 @@ -2723,7 +2936,7 @@ components: Survey: type: object description: |- - Serialises 'id', 'name', 'description', 'is_published', 'need_logged_user', + Serialises 'id', 'name', 'description', 'submit_message', 'is_published', 'need_logged_user', 'editable_answers', 'publish_date', 'expire_date', 'public_url', 'designer' fields of the Survey model for the API. properties: @@ -2740,6 +2953,10 @@ components: maxLength: 150 description: type: string + submit_message: + type: string + default: Thank you for your participation! + title: Message to be displayed after survey is submitted is_published: type: boolean default: false diff --git a/frontend/assets/img/logos/logo-white.png b/frontend/assets/img/logos/logo-white.png new file mode 100644 index 00000000..991ab68e Binary files /dev/null and b/frontend/assets/img/logos/logo-white.png differ diff --git a/frontend/components/AnswerMapView.vue b/frontend/components/AnswerMapView.vue deleted file mode 100644 index db0179db..00000000 --- a/frontend/components/AnswerMapView.vue +++ /dev/null @@ -1,373 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/frontend/components/Header.vue b/frontend/components/Header.vue index 2f40a0fb..6dc8d437 100644 --- a/frontend/components/Header.vue +++ b/frontend/components/Header.vue @@ -1,9 +1,13 @@