Repository: City-of-Helsinki/linkedevents

Issue:
Set publication_status to public by default
Long running useless feature if there ever was one. Drafts may still be saved by specifying the status, if someone wishes to.


Discussion:

Resolving patch:
diff --git a/events/api.py b/events/api.py
--- a/events/api.py
+++ b/events/api.py
@@ -11,8 +11,7 @@
 
 # django and drf
 from django.db.transaction import atomic
-from django.http import Http404
-from django.http import HttpResponseGone
+from django.http import Http404, HttpResponsePermanentRedirect
 from django.utils import translation
 from django.core.exceptions import ValidationError, PermissionDenied
 from django.db.utils import IntegrityError
@@ -755,6 +754,16 @@ def get_serializer_context(self):
         context.setdefault('skip_fields', set()).add('origin_id')
         return context
 
+    def retrieve(self, request, *args, **kwargs):
+        place = Place.objects.get(pk=kwargs['pk'])
+        if place.deleted:
+            if place.replaced_by:
+                place = place.replaced_by
+                return HttpResponsePermanentRedirect(reverse('place-detail',
+                                                             kwargs={'pk': place.pk},
+                                                             request=request))
+        return super().retrieve(request, *args, **kwargs)
+
 
 class PlaceListViewSet(GeoModelAPIView,
                        viewsets.GenericViewSet,
@@ -768,23 +777,34 @@ class PlaceListViewSet(GeoModelAPIView,
 
     def get_queryset(self):
         """
-        Return Place queryset. If request has parameter show_all_places=1
-        all Places are returned, otherwise only which have events.
-        Additional query parameters:
-        event.data_source
-        event.start
-        event.end
+        Return Place queryset.
+
+        If the request has no filter parameters, we only return places that meet the following criteria:
+        -the place has events
+        -the place is not deleted
+
+        Supported places filtering parameters:
+        data_source (only places with the given data sources are included)
+        filter (only places containing the specified string are included)
+        show_all_places (places without events are included)
+        show_deleted (deleted places are included)
         """
         queryset = Place.objects.prefetch_related('divisions__type', 'divisions__municipality')
-        if self.request.query_params.get('show_all_places'):
-            pass
-        else:
-            events = Event.objects.all()
-            params = _clean_qp(self.request.query_params)
-            events = _filter_event_queryset(events, params)
-            location_ids = events.values_list('location_id',
-                                              flat=True).distinct().order_by()
-            queryset = queryset.filter(id__in=location_ids)
+        data_source = self.request.query_params.get('data_source')
+        # Filter by data source, multiple sources separated by comma
+        if data_source:
+            data_source = data_source.lower().split(',')
+            queryset = queryset.filter(data_source__in=data_source)
+        if not self.request.query_params.get('show_all_places'):
+            queryset = queryset.filter(n_events__gt=0)
+        if not self.request.query_params.get('show_deleted'):
+            queryset = queryset.filter(deleted=False)
+
+        # Optionally filter places by filter parameter,
+        # can be used e.g. with typeahead.js
+        val = self.request.query_params.get('text') or self.request.query_params.get('filter')
+        if val:
+            queryset = queryset.filter(name__icontains=val)
         return queryset
 
     def get_serializer_context(self):
@@ -924,7 +944,7 @@ class EventSerializer(LinkedEventsSerializer, GeoModelAPIView):
     super_event = JSONLDRelatedField(serializer='EventSerializer', required=False, view_name='event-detail',
                                      queryset=Event.objects.all(), allow_null=True)
     event_status = EnumChoiceField(Event.STATUSES, required=False)
-    publication_status = EnumChoiceField(PUBLICATION_STATUSES)
+    publication_status = EnumChoiceField(PUBLICATION_STATUSES, required=False)
     external_links = EventLinkSerializer(many=True, required=False)
     offers = OfferSerializer(many=True, required=False)
     data_source = serializers.PrimaryKeyRelatedField(queryset=DataSource.objects.all(),
@@ -991,12 +1011,6 @@ def validate_id(self, value):
                            {'given': str(value), 'data_source': self.data_source}})
         return value
 
-    def validate_publication_status(self, value):
-        if not value:
-            raise serializers.ValidationError({'publication_status':
-                _("You must specify whether you wish to submit a draft or a public event.")})
-        return value
-
     def validate(self, data):
         # clean the html
         for k, v in data.items():
@@ -1005,6 +1019,9 @@ def validate(self, data):
                     data[k] = bleach.clean(v, settings.BLEACH_ALLOWED_TAGS)
         data = super().validate(data)
 
+        if 'publication_status' not in data:
+            data['publication_status'] = PublicationStatus.PUBLIC
+
         # if the event is a draft, no further validation is performed
         if data['publication_status'] == PublicationStatus.DRAFT:
             return data
diff --git a/events/migrations/0042_add_replaced_by_to_place.py b/events/migrations/0042_add_replaced_by_to_place.py
new file mode 100644
--- /dev/null
+++ b/events/migrations/0042_add_replaced_by_to_place.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.13 on 2017-08-22 12:53
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('events', '0041_rebuild_organization_mptt_index'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='place',
+            name='replaced_by',
+            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='aliases', to='events.Place'),
+        ),
+    ]
diff --git a/events/models.py b/events/models.py
--- a/events/models.py
+++ b/events/models.py
@@ -306,6 +306,7 @@ class Place(MPTTModel, BaseModel, SchemalessFieldMixin):
     address_country = models.CharField(verbose_name=_('Country'), max_length=2, null=True, blank=True)
 
     deleted = models.BooleanField(verbose_name=_('Deleted'), default=False)
+    replaced_by = models.ForeignKey('Place', related_name='aliases', null=True)
     divisions = models.ManyToManyField(AdministrativeDivision, verbose_name=_('Divisions'), related_name='places',
                                        blank=True)
 


Test patch:
diff --git a/events/tests/conftest.py b/events/tests/conftest.py
--- a/events/tests/conftest.py
+++ b/events/tests/conftest.py
@@ -118,7 +118,6 @@ def offer(event2):
 def minimal_event_dict(data_source, organization, location_id):
     return {
         'name': {'fi': TEXT},
-        'publication_status': 'public',
         'start_time': DATETIME,
         'location': {'@id': location_id},
         'keywords': [
@@ -371,7 +370,6 @@ def complex_event_dict(data_source, organization, location_id, languages):
         'headline': {'en': TEXT, 'sv': TEXT, 'fi': TEXT},
         'short_description': {'en': TEXT, 'sv': TEXT, 'fi': TEXT},
         'provider': {'en': TEXT, 'sv': TEXT, 'fi': TEXT},
-        'publication_status': 'public',
     }
 
 
diff --git a/events/tests/test_place_get.py b/events/tests/test_place_get.py
--- a/events/tests/test_place_get.py
+++ b/events/tests/test_place_get.py
@@ -9,6 +9,29 @@ def get_list(api_client, version='v1', data=None):
     return get(api_client, list_url, data=data)
 
 
+def get_detail(api_client, detail_pk, version='v1', data=None):
+    detail_url = reverse('place-detail', version=version, kwargs={'pk': detail_pk})
+    return get(api_client, detail_url, data=data)
+
+
+@pytest.mark.django_db
+def test_get_place_detail(api_client, place):
+    response = get_detail(api_client, place.pk)
+    assert response.data['id'] == place.id
+
+
+@pytest.mark.django_db
+def test_get_place_detail_check_redirect(api_client, place, place2):
+    place.replaced_by = place2
+    place.deleted = True
+    place.save()
+    url = reverse('place-detail', version='v1', kwargs={'pk': place.pk})
+    response = api_client.get(url, data=None, format='json')
+    assert response.status_code == 301
+    response2 = api_client.get(response.url, data=None, format='json')
+    assert response2.data['id'] == place2.id
+
+
 @pytest.mark.django_db
 def test_get_place_list_verify_division_filter(api_client, place, place2, place3, administrative_division,
                                                administrative_division2):