Skip to content

Commit

Permalink
Merge f249420 into eae32f3
Browse files Browse the repository at this point in the history
  • Loading branch information
willemarcel committed Jun 8, 2020
2 parents eae32f3 + f249420 commit c7e609b
Show file tree
Hide file tree
Showing 19 changed files with 750 additions and 1 deletion.
5 changes: 5 additions & 0 deletions config/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
'osmchadjango.feature',
'osmchadjango.supervise',
'osmchadjango.frontend',
'osmchadjango.roulette_integration',
)

# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
Expand Down Expand Up @@ -369,6 +370,10 @@
# Version or any valid git branch tag of front-end code
OSMCHA_FRONTEND_VERSION = env('OSMCHA_FRONTEND_VERSION', default='oh-pages')

# MapRoulette API CONFIG
MAP_ROULETTE_API_KEY = env('MAP_ROULETTE_API_KEY', default=None)
MAP_ROULETTE_API_URL = env('MAP_ROULETTE_API_URL', default="https://maproulette.org/api/v2/")

# Define the URL to where the user will be redirected after the authentication
# in OSM website
OAUTH_REDIRECT_URI = env(
Expand Down
4 changes: 4 additions & 0 deletions config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
'{}'.format(API_BASE_URL),
include("osmchadjango.users.urls", namespace="users")
),
path(
'{}'.format(API_BASE_URL),
include("osmchadjango.roulette_integration.urls", namespace="challenge")
),
]

schema_view = get_schema_view(
Expand Down
193 changes: 193 additions & 0 deletions osmchadjango/changeset/tests/test_add_feature_views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import sys
import json
from datetime import datetime
from unittest import mock, skipIf
import requests

from django.test import override_settings
from django.urls import reverse
from django.conf import settings

Expand All @@ -9,6 +13,9 @@
from rest_framework.test import APITestCase

from ...users.models import User
from ...roulette_integration.models import ChallengeIntegration
from ...roulette_integration.utils import format_challenge_task_payload

from ..models import SuspicionReasons, Tag, Changeset
from .modelfactories import ChangesetFactory

Expand Down Expand Up @@ -417,3 +424,189 @@ def test_create_feature_two_times_with_different_reasons(self):

self.assertEqual(len(changeset.new_features[0].get('reasons')), 3)
self.assertEqual(SuspicionReasons.objects.count(), 3)

@skipIf(
sys.version_info < (3,6),
"Python 3.5 has a different dict ordering that makes this test to fail"
)
@override_settings(MAP_ROULETTE_API_KEY='xyz')
@override_settings(MAP_ROULETTE_API_URL='https://maproulette.org/api/v2')
@mock.patch.object(requests, 'post')
def test_maproulette_integration(self, mocked_post):
"""Only one Challenge and only one suspicion reason assigned to it,
so it should trigger only one request.
"""
reason_1 = SuspicionReasons.objects.create(name="new mapper edits")
integration_1 = ChallengeIntegration.objects.create(
challenge_id=1234, user=self.user
)
integration_1.reasons.add(reason_1)

class MockResponse():
status_code = 200
mocked_post.return_value = MockResponse

self.client.post(
reverse('changeset:add-feature-v1'),
data=json.dumps(self.fixture),
content_type="application/json",
HTTP_AUTHORIZATION='Token {}'.format(self.token.key)
)
mocked_post.assert_called_once_with(
'https://maproulette.org/api/v2/task',
headers={
"Content-Type": "application/json",
"apiKey": "xyz"
},
data=format_challenge_task_payload(
{
"type": "Feature",
"geometry": self.fixture.get('geometry'),
"properties": self.fixture.get('properties')
},
1234,
169218447,
["new mapper edits", "moved an object a significant amount"]
)
)

@skipIf(
sys.version_info < (3,6),
"Python 3.5 has a different dict ordering that makes this test to fail"
)
@override_settings(MAP_ROULETTE_API_KEY='xyz')
@override_settings(MAP_ROULETTE_API_URL='https://maproulette.org/api/v2')
@mock.patch.object(requests, 'post')
def test_maproulette_integration_with_two_reasons(self, mocked_post):
"""Two suspicion reasons assigned to one Challenge,
so it should trigger only one request.
"""
reason_1 = SuspicionReasons.objects.create(name="new mapper edits")
reason_2 = SuspicionReasons.objects.create(
name="moved an object a significant amount"
)
integration_1 = ChallengeIntegration.objects.create(
challenge_id=1234, user=self.user
)
integration_1.reasons.add(reason_1)
integration_1.reasons.add(reason_2)

class MockResponse():
status_code = 200
mocked_post.return_value = MockResponse

self.client.post(
reverse('changeset:add-feature-v1'),
data=json.dumps(self.fixture),
content_type="application/json",
HTTP_AUTHORIZATION='Token {}'.format(self.token.key)
)
mocked_post.assert_called_once_with(
'https://maproulette.org/api/v2/task',
headers={
"Content-Type": "application/json",
"apiKey": "xyz"
},
data=format_challenge_task_payload(
{
"type": "Feature",
"geometry": self.fixture.get('geometry'),
"properties": self.fixture.get('properties')
},
1234,
169218447,
["new mapper edits", "moved an object a significant amount"]
)
)

@skipIf(
sys.version_info < (3,6),
"Python 3.5 has a different dict ordering that makes this test to fail"
)
@override_settings(MAP_ROULETTE_API_KEY='xyz')
@override_settings(MAP_ROULETTE_API_URL='https://maproulette.org/api/v2')
@mock.patch.object(requests, 'post')
def test_maproulette_integration_called_twice(self, mocked_post):
"""If a feature has two suspicion reasons and each of them are assigned
to different Challenges, it should make two requests to MapRoulette API.
"""
reason_1 = SuspicionReasons.objects.create(name="new mapper edits")
reason_2 = SuspicionReasons.objects.create(
name="moved an object a significant amount"
)
integration_1 = ChallengeIntegration.objects.create(
challenge_id=1234, user=self.user
)
integration_1.reasons.add(reason_1)
integration_2 = ChallengeIntegration.objects.create(
challenge_id=4321, user=self.user
)
integration_2.reasons.add(reason_2)

class MockResponse():
status_code = 200
mocked_post.return_value = MockResponse

self.client.post(
reverse('changeset:add-feature-v1'),
data=json.dumps(self.fixture),
content_type="application/json",
HTTP_AUTHORIZATION='Token {}'.format(self.token.key)
)
mocked_post.assert_has_calls(
[
mock.call(
'https://maproulette.org/api/v2/task',
headers={
"Content-Type": "application/json",
"apiKey": "xyz"
},
data=format_challenge_task_payload(
{
"type": "Feature",
"geometry": self.fixture.get('geometry'),
"properties": self.fixture.get('properties')
},
1234,
169218447,
["new mapper edits", "moved an object a significant amount"]
)
),
mock.call(
'https://maproulette.org/api/v2/task',
headers={
"Content-Type": "application/json",
"apiKey": "xyz"
},
data=format_challenge_task_payload(
{
"type": "Feature",
"geometry": self.fixture.get('geometry'),
"properties": self.fixture.get('properties')
},
4321,
169218447,
["new mapper edits", "moved an object a significant amount"]
)
)
],
any_order=True
)

@mock.patch.object(requests, 'post')
def test_maproulette_integration_not_called(self, mocked_post):
reason = SuspicionReasons.objects.create(name="new mapper edits")
integration = ChallengeIntegration.objects.create(challenge_id=1234, user=self.user)
integration.reasons.add(reason)

class MockResponse():
status_code = 200
mocked_post.return_value = MockResponse

self.client.post(
reverse('changeset:add-feature-v1'),
data=json.dumps(self.fixture),
content_type="application/json",
HTTP_AUTHORIZATION='Token {}'.format(self.token.key)
)
mocked_post.assert_not_called()
19 changes: 18 additions & 1 deletion osmchadjango/changeset/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
)
from .tasks import ChangesetCommentAPI
from .throttling import NonStaffUserThrottle
from ..roulette_integration.utils import push_feature_to_maproulette
from ..roulette_integration.models import ChallengeIntegration


class StandardResultsSetPagination(GeoJsonPagination):
Expand Down Expand Up @@ -748,13 +750,28 @@ def add_feature_v1(request):
reasons.add(reason)
if reason.is_visible:
has_visible_features = True

except (ValueError, SuspicionReasons.DoesNotExist):
reason, created = SuspicionReasons.objects.get_or_create(
name=suspicion
)
reasons.add(reason)
if reason.is_visible:
has_visible_features = True
challenges = ChallengeIntegration.objects.filter(
active=True
).filter(reasons__in=reasons).distinct()
for challenge in challenges:
push_feature_to_maproulette(
{
"type": "Feature",
"geometry": request.data.get('geometry'),
"properties": request.data.get('properties')
},
challenge.challenge_id,
feature.get('osm_id'),
[r.name for r in reasons]
)

changeset_defaults = {
'is_suspect': has_visible_features
Expand Down Expand Up @@ -820,5 +837,5 @@ def add_reasons_to_changeset(changeset, reasons):
# since what we wanted inserted has already been done through
# a separate web request.
print('IntegrityError with changeset %s' % changeset.id)
except ValueError as e:
except ValueError:
print('ValueError with changeset %s' % changeset.id)
Empty file.
3 changes: 3 additions & 0 deletions osmchadjango/roulette_integration/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.contrib import admin

# Register your models here.
5 changes: 5 additions & 0 deletions osmchadjango/roulette_integration/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class RouletteIntegrationConfig(AppConfig):
name = 'roulette_integration'
34 changes: 34 additions & 0 deletions osmchadjango/roulette_integration/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 2.2.11 on 2020-06-01 01:08

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = [
('changeset', '0050_auto_20181008_1001'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='ChallengeIntegration',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('challenge_id', models.IntegerField(db_index=True, unique=True)),
('active', models.BooleanField(db_index=True, default=True)),
('created', models.DateTimeField(auto_now_add=True)),
('reasons', models.ManyToManyField(related_name='challenges', to='changeset.SuspicionReasons')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='challenges', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Challenge Integration',
'verbose_name_plural': 'Challenge Integrations',
'ordering': ['id'],
},
),
]
Empty file.
22 changes: 22 additions & 0 deletions osmchadjango/roulette_integration/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.db import models
from django.contrib.auth import get_user_model

from ..changeset.models import SuspicionReasons

User = get_user_model()


class ChallengeIntegration(models.Model):
challenge_id = models.IntegerField(unique=True, db_index=True)
reasons = models.ManyToManyField(SuspicionReasons, related_name='challenges')
active = models.BooleanField(default=True, db_index=True)
created = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(User, related_name="challenges", on_delete=models.CASCADE)

def __str__(self):
return 'Challenge {}'.format(self.challenge_id)

class Meta:
ordering = ['id']
verbose_name = 'Challenge Integration'
verbose_name_plural = 'Challenge Integrations'
20 changes: 20 additions & 0 deletions osmchadjango/roulette_integration/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from rest_framework.fields import HiddenField, CurrentUserDefault, ReadOnlyField
from rest_framework.serializers import ModelSerializer, PrimaryKeyRelatedField

from ..changeset.serializers import SuspicionReasonsSerializer
from .models import ChallengeIntegration


class ChallengeIntegrationSerializer(ModelSerializer):
user = HiddenField(default=CurrentUserDefault())
owner = ReadOnlyField(source='user.username', default=None)

# def create(self, validated_data):
# import ipdb; ipdb.set_trace()
# reasons_data = [i.get('id') for i in validated_data.pop('reasons')]
# obj = ChallengeIntegration.objects.create(**validated_data)

class Meta:
model = ChallengeIntegration
fields = ['id', 'challenge_id', 'reasons', 'user', 'active', 'created', 'owner']
read_only_fields = ('created', 'owner')
Empty file.

0 comments on commit c7e609b

Please sign in to comment.