summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorÉtienne Loks <etienne.loks@iggdrasil.net>2021-10-06 18:36:16 +0200
committerÉtienne Loks <etienne.loks@iggdrasil.net>2022-12-12 12:20:58 +0100
commita1a1b524fd02a57bd514ed95580fea8b67e1cede (patch)
tree0327937f9c376ae95b0777faea227bb628528dab
parent14c5ccd235d963457485cd907712b43672c5e400 (diff)
downloadIshtar-a1a1b524fd02a57bd514ed95580fea8b67e1cede.tar.bz2
Ishtar-a1a1b524fd02a57bd514ed95580fea8b67e1cede.zip
Syndication - docs, api permissions
Permissions by token, IP and by model.
-rw-r--r--archaeological_finds/urls.py5
-rw-r--r--archaeological_finds/views.py39
-rw-r--r--archaeological_finds/views_api.py40
-rw-r--r--archaeological_operations/tests.py68
-rw-r--r--archaeological_operations/urls.py5
-rw-r--r--archaeological_operations/views_api.py7
-rw-r--r--docs/fr/source/media-src/ishtar-syndication.drawio1
-rw-r--r--docs/fr/source/media-src/ishtar-syndication.drawio.pngbin0 -> 37844 bytes
-rw-r--r--ishtar_common/admin.py45
-rw-r--r--ishtar_common/migrations/0217_auto_20211006_1526.py177
-rw-r--r--ishtar_common/models.py5
-rw-r--r--ishtar_common/models_rest.py32
-rw-r--r--ishtar_common/rest.py31
13 files changed, 413 insertions, 42 deletions
diff --git a/archaeological_finds/urls.py b/archaeological_finds/urls.py
index bc67139d9..532e22ab0 100644
--- a/archaeological_finds/urls.py
+++ b/archaeological_finds/urls.py
@@ -22,6 +22,7 @@ from django.conf.urls import url
from ishtar_common.utils import check_rights, get_urls_for_model
from archaeological_finds import views
+from archaeological_finds import views_api
from archaeological_operations.views import administrativeactfile_document
from archaeological_finds import models
@@ -593,7 +594,9 @@ urlpatterns = [
),
name="autocomplete-findbasket-write",
),
- url(r"api/public/find/$", views.PublicFindAPI.as_view(), name="api-public-find"),
+ url(
+ r"api/public/find/$", views_api.PublicFindAPI.as_view(), name="api-public-find"
+ ),
url(
r"api/ishtar/base-finds/geo/polygons$",
check_rights(["view_find", "view_own_find"])(
diff --git a/archaeological_finds/views.py b/archaeological_finds/views.py
index 4f5e64475..db8c4b04e 100644
--- a/archaeological_finds/views.py
+++ b/archaeological_finds/views.py
@@ -20,10 +20,6 @@
from collections import OrderedDict
import json
-from rest_framework import authentication, permissions
-from rest_framework.views import APIView
-from rest_framework.response import Response
-
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.db.models import Q
@@ -35,8 +31,6 @@ from ishtar_common.utils import ugettext_lazy as _
from django.views.generic import TemplateView
from django.views.generic.edit import CreateView, FormView
-from ishtar_common.serializers import PublicSerializer
-
from ishtar_common.models import IshtarUser, get_current_profile
from archaeological_operations.models import AdministrativeAct, Operation
from archaeological_context_records.models import ContextRecord
@@ -1311,39 +1305,6 @@ class QAFindLockView(QABaseLockView):
base_url = "find-qa-lock"
-class PublicFindAPI(APIView):
- authentication_classes = (authentication.TokenAuthentication,)
- permission_classes = (permissions.IsAuthenticated,)
-
- def get_queryset(self):
- empty = models.Find.objects.filter(pk=None)
- basket_slug = self.request.GET.get("basket", None)
- if not basket_slug:
- return empty
- try:
- basket = models.FindBasket.objects.get(slug=basket_slug, public=True)
- except models.FindBasket.DoesNotExist:
- return empty
- q = (
- models.FindBasket.items.through.objects.filter(findbasket_id=basket.id)
- .values("find_id")
- .order_by("id")
- )
- id_list = [bi["find_id"] for bi in q]
- clauses = " ".join(
- "WHEN id=%s THEN %s" % (pk, i) for i, pk in enumerate(id_list)
- )
-
- ordering = "CASE {} END".format(clauses)
- return models.Find.objects.filter(id__in=id_list).extra(
- select={"ordering": ordering}, order_by=("ordering",)
- )
-
- def get(self, request, format=None):
- serializer = PublicSerializer(self.get_queryset(), many=True)
- return Response(serializer.data)
-
-
def get_geo_items(request, get_polygons, current_right=None):
operation_pk = request.GET.get("operation_pk")
context_record_pk = request.GET.get("context_record_pk")
diff --git a/archaeological_finds/views_api.py b/archaeological_finds/views_api.py
new file mode 100644
index 000000000..66cadeb5e
--- /dev/null
+++ b/archaeological_finds/views_api.py
@@ -0,0 +1,40 @@
+from rest_framework import authentication, permissions
+from rest_framework.views import APIView
+from rest_framework.response import Response
+
+from ishtar_common.serializers import PublicSerializer
+
+from archaeological_finds import models
+
+
+class PublicFindAPI(APIView):
+ authentication_classes = (authentication.TokenAuthentication,)
+ permission_classes = (permissions.IsAuthenticated,)
+
+ def get_queryset(self):
+ empty = models.Find.objects.filter(pk=None)
+ basket_slug = self.request.GET.get("basket", None)
+ if not basket_slug:
+ return empty
+ try:
+ basket = models.FindBasket.objects.get(slug=basket_slug, public=True)
+ except models.FindBasket.DoesNotExist:
+ return empty
+ q = (
+ models.FindBasket.items.through.objects.filter(findbasket_id=basket.id)
+ .values("find_id")
+ .order_by("id")
+ )
+ id_list = [bi["find_id"] for bi in q]
+ clauses = " ".join(
+ "WHEN id=%s THEN %s" % (pk, i) for i, pk in enumerate(id_list)
+ )
+
+ ordering = "CASE {} END".format(clauses)
+ return models.Find.objects.filter(id__in=id_list).extra(
+ select={"ordering": ordering}, order_by=("ordering",)
+ )
+
+ def get(self, request, format=None):
+ serializer = PublicSerializer(self.get_queryset(), many=True)
+ return Response(serializer.data)
diff --git a/archaeological_operations/tests.py b/archaeological_operations/tests.py
index e70b701c4..58915dce9 100644
--- a/archaeological_operations/tests.py
+++ b/archaeological_operations/tests.py
@@ -39,6 +39,9 @@ from django.utils.text import slugify
from django.contrib.auth.models import User, Permission
from django.utils.translation import ugettext_lazy as _, pgettext, pgettext_lazy
+from rest_framework.test import APITestCase
+from rest_framework.authtoken.models import Token
+
from . import models
from ishtar_common.views import document_deletion_steps
@@ -77,6 +80,8 @@ from ishtar_common.models import (
Document,
ValueFormater,
Regexp,
+ ApiUser,
+ ApiSearchModel,
)
from ishtar_common.models_imports import ImporterDefault, ImporterDefaultValues
from archaeological_files.models import File, FileType
@@ -4419,3 +4424,66 @@ class SeleniumTestsOperations(SeleniumTests):
for pk, xpath in from_table:
slug_pk = slug + "-" + str(pk)
self._test_operation(xpath, slug_pk, copy.deepcopy(geojsons))
+
+
+class ApiTest(OperationInitTest, APITestCase):
+ fixtures = FILE_FIXTURES
+
+ def setUp(self):
+ IshtarSiteProfile.objects.get_or_create(slug="default", active=True)
+ self.username, self.password, self.user = create_superuser()
+ self.orgas = self.create_orgas(self.user)
+ self.create_operation(self.user, self.orgas[0])
+ self.create_operation(self.user, self.orgas[0])
+ self.create_operation(self.user, self.orgas[0])
+ self.create_operation(self.user, self.orgas[0])
+ self.auth_token = "Token " + Token.objects.create(user=self.user).key
+ self.api_user = ApiUser.objects.create(user_ptr=self.user, ip="127.0.0.1")
+
+ def create_api_search_model(self):
+ return ApiSearchModel.objects.create(
+ user=self.api_user,
+ content_type=ContentType.objects.get(
+ app_label="archaeological_operations",
+ model="operation"
+ ))
+
+ def test_permissions(self):
+ url = reverse("api-search-operation")
+ response = self.client.get(url, format="json")
+ # nothing OK
+ self.assertEqual(response.status_code, 401)
+ # token + IP
+ response = self.client.get(
+ url, format="json", HTTP_AUTHORIZATION=self.auth_token
+ )
+ self.assertEqual(response.status_code, 403)
+ api_search_model = self.create_api_search_model()
+ content_type_id = api_search_model.content_type.id
+ api_search_model.content_type = ContentType.objects.get(
+ app_label="archaeological_operations",
+ model="archaeologicalsite"
+ )
+ api_search_model.save()
+ # token + IP + bad model
+ response = self.client.get(
+ url, format="json", HTTP_AUTHORIZATION=self.auth_token
+ )
+ self.assertEqual(response.status_code, 403)
+ api_search_model.content_type_id = content_type_id
+ api_search_model.save()
+ # token + IP + good model
+ response = self.client.get(
+ url, format="json", HTTP_AUTHORIZATION=self.auth_token
+ )
+ self.assertEqual(response.status_code, 200)
+ # token + bad IP + good model
+ self.api_user.ip = "8.8.8.8"
+ self.api_user.save()
+ response = self.client.get(
+ url, format="json", HTTP_AUTHORIZATION=self.auth_token
+ )
+ self.assertEqual(response.status_code, 403)
+ self.api_user.ip = "127.0.0.1"
+ self.api_user.save()
+
diff --git a/archaeological_operations/urls.py b/archaeological_operations/urls.py
index 85a650216..4aee195f4 100644
--- a/archaeological_operations/urls.py
+++ b/archaeological_operations/urls.py
@@ -21,6 +21,7 @@ from django.conf.urls import url
from ishtar_common.utils import check_rights
from archaeological_operations import views
+from archaeological_operations import views_api
from archaeological_operations import models
# be carreful: each check_rights must be relevant with ishtar_menu
@@ -356,4 +357,8 @@ urlpatterns = [
views.GenerateStatsOperation.as_view(),
name="generate-stats-operation",
),
+ url(
+ r"api/search/operation/$", views_api.SearchOperationAPI.as_view(),
+ name="api-search-operation"
+ ),
]
diff --git a/archaeological_operations/views_api.py b/archaeological_operations/views_api.py
new file mode 100644
index 000000000..48127ec4b
--- /dev/null
+++ b/archaeological_operations/views_api.py
@@ -0,0 +1,7 @@
+from ishtar_common.rest import SearchAPIView
+from archaeological_operations import models
+
+
+class SearchOperationAPI(SearchAPIView):
+ model = models.Operation
+
diff --git a/docs/fr/source/media-src/ishtar-syndication.drawio b/docs/fr/source/media-src/ishtar-syndication.drawio
new file mode 100644
index 000000000..d1addf4c1
--- /dev/null
+++ b/docs/fr/source/media-src/ishtar-syndication.drawio
@@ -0,0 +1 @@
+<mxfile host="Electron" modified="2021-10-06T17:03:12.966Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/15.4.0 Chrome/91.0.4472.164 Electron/13.5.0 Safari/537.36" etag="8bPWOHmZ-piZMJ_4vZ0y" version="15.4.0" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">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</diagram></mxfile> \ No newline at end of file
diff --git a/docs/fr/source/media-src/ishtar-syndication.drawio.png b/docs/fr/source/media-src/ishtar-syndication.drawio.png
new file mode 100644
index 000000000..fc5c7fc81
--- /dev/null
+++ b/docs/fr/source/media-src/ishtar-syndication.drawio.png
Binary files differ
diff --git a/ishtar_common/admin.py b/ishtar_common/admin.py
index 07750ffe7..0b73f5708 100644
--- a/ishtar_common/admin.py
+++ b/ishtar_common/admin.py
@@ -554,8 +554,12 @@ admin_site.register(models.Person, PersonAdmin)
class AuthorAdmin(admin.ModelAdmin):
list_display = ["person", "author_type"]
list_filter = ("author_type",)
- search_fields = ("person__name", "person__surname", "person__attached_to__name",
- "author_type__label")
+ search_fields = (
+ "person__name",
+ "person__surname",
+ "person__attached_to__name",
+ "author_type__label",
+ )
model = models.Author
autocomplete_fields = ["person"]
@@ -2082,3 +2086,40 @@ class DocumentTemplateAdmin(admin.ModelAdmin):
admin_site.register(models.DocumentTemplate, DocumentTemplateAdmin)
+
+class ApiUserAdmin(admin.ModelAdmin):
+ list_display = ("user_ptr", "ip")
+
+
+admin_site.register(models.ApiUser, ApiUserAdmin)
+
+
+def get_main_content_types_query():
+ CONTENT_TYPES = (
+ ("archaeological_operations", "operation"),
+ )
+ pks = []
+ for app_label, model_name in CONTENT_TYPES:
+ try:
+ ct = ContentType.objects.get(app_label=app_label, model=model_name)
+ pks.append(ct.pk)
+ except ContentType.DoesNotExist:
+ pass
+ return ContentType.objects.filter(pk__in=pks)
+
+
+class ApiSearchModelAdminForm(forms.ModelForm):
+ class Meta:
+ model = models.ApiUser
+ exclude = []
+ content_type = forms.ModelChoiceField(
+ label=_("Content type"), queryset=get_main_content_types_query()
+ )
+
+
+class ApiSearchModelAdmin(admin.ModelAdmin):
+ form = ApiSearchModelAdminForm
+ list_display = ("user", "content_type")
+
+
+admin_site.register(models.ApiSearchModel, ApiSearchModelAdmin)
diff --git a/ishtar_common/migrations/0217_auto_20211006_1526.py b/ishtar_common/migrations/0217_auto_20211006_1526.py
new file mode 100644
index 000000000..33299b4c3
--- /dev/null
+++ b/ishtar_common/migrations/0217_auto_20211006_1526.py
@@ -0,0 +1,177 @@
+# Generated by Django 2.2.24 on 2021-10-06 15:26
+
+from django.conf import settings
+import django.contrib.postgres.fields.jsonb
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('contenttypes', '0002_remove_content_type_name'),
+ ('auth', '0011_update_proxy_permissions'),
+ ('ishtar_common', '0216_auto_20210805_1703'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ApiUser',
+ fields=[
+ ('user_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='apiuser', serialize=False, to=settings.AUTH_USER_MODEL)),
+ ('ip', models.GenericIPAddressField(verbose_name='IP')),
+ ],
+ options={
+ 'verbose_name': 'Api - User',
+ 'verbose_name_plural': 'Api - Users',
+ },
+ ),
+ migrations.AlterField(
+ model_name='author',
+ name='author_type',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.AuthorType', verbose_name='Author type'),
+ ),
+ migrations.AlterField(
+ model_name='document',
+ name='data',
+ field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
+ ),
+ migrations.AlterField(
+ model_name='document',
+ name='history_m2m',
+ field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
+ ),
+ migrations.AlterField(
+ model_name='document',
+ name='language',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.Language', verbose_name='Language'),
+ ),
+ migrations.AlterField(
+ model_name='document',
+ name='publisher',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='publish', to='ishtar_common.Organization', verbose_name='Publisher'),
+ ),
+ migrations.AlterField(
+ model_name='document',
+ name='source',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='ishtar_common.Document', verbose_name='Source'),
+ ),
+ migrations.AlterField(
+ model_name='historicalorganization',
+ name='data',
+ field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
+ ),
+ migrations.AlterField(
+ model_name='historicalorganization',
+ name='history_m2m',
+ field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
+ ),
+ migrations.AlterField(
+ model_name='historicalperson',
+ name='data',
+ field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
+ ),
+ migrations.AlterField(
+ model_name='historicalperson',
+ name='history_m2m',
+ field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
+ ),
+ migrations.AlterField(
+ model_name='ishtarsiteprofile',
+ name='default_language',
+ field=models.ForeignKey(blank=True, help_text='If set, by default the selected language will be set for localized documents.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.Language', verbose_name='Default language for documentation'),
+ ),
+ migrations.AlterField(
+ model_name='ishtarsiteprofile',
+ name='display_srs',
+ field=models.ForeignKey(blank=True, help_text='Spatial Reference System used for display when no SRS is defined', null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.SpatialReferenceSystem', verbose_name='Spatial Reference System for display'),
+ ),
+ migrations.AlterField(
+ model_name='itemkey',
+ name='group',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.TargetKeyGroup'),
+ ),
+ migrations.AlterField(
+ model_name='itemkey',
+ name='importer',
+ field=models.ForeignKey(blank=True, help_text='Specific key to an import', null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.Import'),
+ ),
+ migrations.AlterField(
+ model_name='itemkey',
+ name='user',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.IshtarUser'),
+ ),
+ migrations.AlterField(
+ model_name='organization',
+ name='data',
+ field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
+ ),
+ migrations.AlterField(
+ model_name='organization',
+ name='history_m2m',
+ field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
+ ),
+ migrations.AlterField(
+ model_name='organization',
+ name='organization_type',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.OrganizationType', verbose_name='Type'),
+ ),
+ migrations.AlterField(
+ model_name='organization',
+ name='precise_town',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.Town', verbose_name='Town (precise)'),
+ ),
+ migrations.AlterField(
+ model_name='person',
+ name='data',
+ field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
+ ),
+ migrations.AlterField(
+ model_name='person',
+ name='history_m2m',
+ field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
+ ),
+ migrations.AlterField(
+ model_name='person',
+ name='precise_town',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.Town', verbose_name='Town (precise)'),
+ ),
+ migrations.AlterField(
+ model_name='statscache',
+ name='values',
+ field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
+ ),
+ migrations.AlterField(
+ model_name='targetkey',
+ name='associated_group',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.TargetKeyGroup'),
+ ),
+ migrations.AlterField(
+ model_name='targetkey',
+ name='associated_import',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.Import'),
+ ),
+ migrations.AlterField(
+ model_name='targetkey',
+ name='associated_user',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ishtar_common.IshtarUser'),
+ ),
+ migrations.AlterField(
+ model_name='userprofile',
+ name='profile_type',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='ishtar_common.ProfileType', verbose_name='Profile type'),
+ ),
+ migrations.CreateModel(
+ name='ApiSearchModel',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('limit_query', models.TextField(blank=True, help_text='Search query add to each request', null=True, verbose_name='Limit query')),
+ ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ishtar_common.ApiUser')),
+ ],
+ options={
+ 'verbose_name': 'Api - Search model',
+ 'verbose_name_plural': 'Api - Search models',
+ },
+ ),
+ ]
diff --git a/ishtar_common/models.py b/ishtar_common/models.py
index 6783a89b5..59248fa43 100644
--- a/ishtar_common/models.py
+++ b/ishtar_common/models.py
@@ -113,6 +113,11 @@ from ishtar_common.models_imports import (
TargetKeyGroup,
ValueFormater,
)
+from ishtar_common.models_rest import (
+ ApiUser,
+ ApiSearchModel
+)
+
from ishtar_common.utils import (
get_cache,
create_slug,
diff --git a/ishtar_common/models_rest.py b/ishtar_common/models_rest.py
new file mode 100644
index 000000000..7d321ca92
--- /dev/null
+++ b/ishtar_common/models_rest.py
@@ -0,0 +1,32 @@
+from django.contrib.auth.models import User
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.gis.db import models
+
+from ishtar_common.utils import ugettext_lazy as _
+
+
+class ApiUser(models.Model):
+ user_ptr = models.OneToOneField(
+ User, primary_key=True, related_name="apiuser", on_delete=models.CASCADE
+ )
+ ip = models.GenericIPAddressField(verbose_name=_("IP"))
+
+ class Meta:
+ verbose_name = _("Api - User")
+ verbose_name_plural = _("Api - Users")
+
+ def __str__(self):
+ return self.user_ptr.username
+
+
+class ApiSearchModel(models.Model):
+ user = models.ForeignKey(ApiUser, on_delete=models.CASCADE)
+ content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
+ limit_query = models.TextField(
+ verbose_name=_("Limit query"), blank=True, null=True,
+ help_text=_("Search query add to each request")
+ )
+
+ class Meta:
+ verbose_name = _("Api - Search model")
+ verbose_name_plural = _("Api - Search models")
diff --git a/ishtar_common/rest.py b/ishtar_common/rest.py
new file mode 100644
index 000000000..9354a943d
--- /dev/null
+++ b/ishtar_common/rest.py
@@ -0,0 +1,31 @@
+from rest_framework import authentication, permissions
+from rest_framework.response import Response
+from rest_framework.views import APIView
+
+from ishtar_common.models import ApiSearchModel
+
+
+class IpModelPermission(permissions.BasePermission):
+ def has_permission(self, request, view):
+ if not request.user or not getattr(request.user, "apiuser", None):
+ return False
+ ip_addr = request.META['REMOTE_ADDR']
+ q = ApiSearchModel.objects.filter(
+ user=request.user.apiuser,
+ user__ip=ip_addr,
+ content_type__app_label=view.model._meta.app_label,
+ content_type__model=view.model._meta.model_name)
+ return bool(q.count())
+
+
+class SearchAPIView(APIView):
+ model = None
+ authentication_classes = (authentication.TokenAuthentication,)
+ permission_classes = (permissions.IsAuthenticated, IpModelPermission)
+
+ def __init__(self, **kwargs):
+ assert self.model is not None
+ super(SearchAPIView, self).__init__(**kwargs)
+
+ def get(self, request, format=None):
+ return Response({})