Skip to content

Commit

Permalink
QGIS Hub API CRUD
Browse files Browse the repository at this point in the history
  • Loading branch information
Xpirix committed Nov 15, 2024
1 parent cc17e6a commit 4455cfb
Show file tree
Hide file tree
Showing 17 changed files with 1,393 additions and 11 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ dockerize/webroot
# test cache
qgis-app/*/tests/*/cache
qgis-app/api/tests/*/
!qgis-app/wavefronts/tests/wavefrontfiles/*.zip

# whoosh_index
qgis-app/whoosh_index/
Expand Down
52 changes: 52 additions & 0 deletions HUB_API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
API URL Configuration
# QGIS Resources Hub API Documentation

The `urlpatterns` list routes URLs to views. For more information please see:
[https://docs.djangoproject.com/en/3.2/topics/http/urls/](https://docs.djangoproject.com/en/3.2/topics/http/urls/)

## Endpoints

### Resources
- **URL:** `/resources/`
- **Method:** `GET`
- **View:** `ResourceAPIList.as_view()`
- **Name:** `resource-list`
- **Description:** Retrieves a list of all resources.

### Resource by UUID
- **URL:** `/resource/<uuid:uuid>/`
- **Method:** `GET`
- **View:** `ResourceAPIDownload.as_view()`
- **Name:** `resource-download`
- **Description:** Downloads a specific resource identified by UUID.

### Create Resource
- **URL:** `/resource/create`
- **Method:** `POST`
- **View:** `ResourceCreateView.as_view()`
- **Name:** `resource-create`
- **Description:** Creates a new resource.
- **Request example with cURL:**
```sh
curl --location 'http://localhost:62202/api/v1/resource/create' \
--header 'Authorization: Bearer <my_token>' \
--form 'file=@"path/to/the/file.zip"' \
--form 'thumbnail_full=@"path/to/the/thumbnail.png"' \
--form 'name="My model"' \
--form 'description="Little description"' \
--form 'tags="notag"' \
--form 'resource_type="model"'
```

### Resource Detail
- **URL:** `/resource/<str:resource_type>/<uuid:uuid>/`
- **Methods:** `GET`, `PUT`, `DELETE`
- **View:** `ResourceDetailView.as_view()`
- **Name:** `resource-detail`
- **Description:** Handles the detailed display, update, and deletion of a specific resource based on its type and UUID.
- **Example:**
To access the details of a resource with type 'style' and UUID '123e4567-e89b-12d3-a456-426614174000':
```sh
GET /resource/style/123e4567-e89b-12d3-a456-426614174000/
```
- **Permissions:** Ensure that the user has the necessary permissions (staff or creator) to view, update, or delete the resource details.
14 changes: 14 additions & 0 deletions qgis-app/api/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from django.forms import CharField, ModelForm
from api.models import UserOutstandingToken


class UserTokenForm(ModelForm):
"""
Form for token description editing
"""

class Meta:
model = UserOutstandingToken
fields = (
"description",
)
30 changes: 30 additions & 0 deletions qgis-app/api/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 4.2.16 on 2024-09-12 07:16

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


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('token_blacklist', '0012_alter_outstandingtoken_user'),
]

operations = [
migrations.CreateModel(
name='UserOutstandingToken',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_blacklisted', models.BooleanField(default=False)),
('is_newly_created', models.BooleanField(default=False)),
('description', models.CharField(blank=True, help_text="Describe this token so that it's easier to remember where you're using it.", max_length=512, null=True, verbose_name='Description')),
('last_used_on', models.DateTimeField(blank=True, null=True, verbose_name='Last used on')),
('token', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='token_blacklist.outstandingtoken')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
33 changes: 32 additions & 1 deletion qgis-app/api/models.py
Original file line number Diff line number Diff line change
@@ -1 +1,32 @@
# Create your models here.
from base.models.processing_models import Resource
from django.db import models
from django.utils.translation import gettext_lazy as _
from rest_framework_simplejwt.token_blacklist.models import OutstandingToken
from django.contrib.auth.models import User

class UserOutstandingToken(models.Model):
"""
Hub outstanding token
"""
user = models.ForeignKey(
User,
on_delete=models.CASCADE
)
token = models.ForeignKey(
OutstandingToken,
on_delete=models.CASCADE
)
is_blacklisted = models.BooleanField(default=False)
is_newly_created = models.BooleanField(default=False)
description = models.CharField(
verbose_name=_("Description"),
help_text=_("Describe this token so that it's easier to remember where you're using it."),
max_length=512,
blank=True,
null=True,
)
last_used_on = models.DateTimeField(
verbose_name=_("Last used on"),
blank=True,
null=True
)
101 changes: 96 additions & 5 deletions qgis-app/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@
from geopackages.models import Geopackage
from models.models import Model
from rest_framework import serializers
from sorl_thumbnail_serializer.fields import HyperlinkedSorlImageField
from styles.models import Style
from styles.models import Style, StyleType
from layerdefinitions.models import LayerDefinition
from wavefronts.models import Wavefront
from wavefronts.models import WAVEFRONTS_STORAGE_PATH, Wavefront
from sorl.thumbnail import get_thumbnail
from django.conf import settings
from os.path import exists
from os.path import exists, join
from django.templatetags.static import static
from wavefronts.validator import WavefrontValidator

from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from styles.file_handler import read_xml_style, validator as style_validator
from layerdefinitions.file_handler import get_provider, get_url_datasource, validator as layer_validator
import tempfile

class ResourceBaseSerializer(serializers.ModelSerializer):
creator = serializers.ReadOnlyField(source="get_creator_name")
Expand Down Expand Up @@ -83,17 +89,102 @@ class StyleSerializer(ResourceBaseSerializer):
class Meta(ResourceBaseSerializer.Meta):
model = Style

def validate(self, attrs):
"""
Validate a style file.
We need to check if the uploaded file is a valid XML file.
Then, we upload the file to a temporary file, validate it
and check if the style type is defined.
"""
attrs = super().validate(attrs)
file = attrs.get("file")

if not file:
raise ValidationError(_("File is required."))

if file.size == 0:
raise ValidationError(_("Uploaded file is empty."))
try:
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
for chunk in file.chunks():
temp_file.write(chunk)
temp_file.flush()

with open(temp_file.name, 'rb') as xml_file:
style = style_validator(xml_file)
xml_parse = read_xml_style(xml_file)
if xml_parse:
self.style_type, created = StyleType.objects.get_or_create(
symbol_type=xml_parse["type"],
defaults={
"name": xml_parse["type"].title(),
"description": "Automatically created from '"
"'an uploaded Style file",
}
)

if not style:
raise ValidationError(
_("Undefined style type. Please register your style type.")
)
finally:
import os
if temp_file and os.path.exists(temp_file.name):
os.remove(temp_file.name)

return attrs

class LayerDefinitionSerializer(ResourceBaseSerializer):
class Meta(ResourceBaseSerializer.Meta):
model = LayerDefinition

def get_resource_subtype(self, obj):
return None

def validate(self, attrs):
"""
Validate a qlr file.
We need to check if the uploaded file is a valid QLR file.
Then, we upload the file to a temporary file and validate it
"""
attrs = super().validate(attrs)
file = attrs.get("file")

if not file:
raise ValidationError(_("File is required."))

if file.size == 0:
raise ValidationError(_("Uploaded file is empty."))
try:
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
for chunk in file.chunks():
temp_file.write(chunk)
temp_file.flush()

with open(temp_file.name, 'rb') as qlr_file:
layer_validator(qlr_file)
self.url_datasource = get_url_datasource(qlr_file)
self.provider = get_provider(qlr_file)


finally:
import os
if temp_file and os.path.exists(temp_file.name):
os.remove(temp_file.name)

return attrs

class WavefrontSerializer(ResourceBaseSerializer):
class Meta(ResourceBaseSerializer.Meta):
model = Wavefront

def get_resource_subtype(self, obj):
return None
return None

def validate(self, attrs):
attrs = super().validate(attrs)
file = attrs.get("file")
if file and file.name.endswith('.zip'):
valid_3dmodel = WavefrontValidator(file).validate_wavefront()
self.new_filepath = join(WAVEFRONTS_STORAGE_PATH, valid_3dmodel)
return attrs
22 changes: 22 additions & 0 deletions qgis-app/api/templates/user_token_base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{% extends BASE_TEMPLATE %}{% load i18n %}
{% block app_title %}
<h2 xmlns="http://www.w3.org/1999/html">{{ title }}</h2>
{% endblock %}

{% block menu %}
{{ block.super }}
<form method="post" action="{% url "user_token_create"%}">{% csrf_token %}
<div class="field">
<h2 class="title is-4">
<button type="submit" name="user_token_create" id="user_token_create"
class="button is-success is-medium has-text-weight-medium">
<span class="icon is-small">
<i class="fas fa-plus"></i>
</span>
<span>{% trans "Generate a New Token" %}</span>
</button>
</h2>
</div>
</form>

{% endblock %}
19 changes: 19 additions & 0 deletions qgis-app/api/templates/user_token_delete.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% extends 'user_token_base.html' %}{% load i18n %}
{% block content %}
<h3 class="title is-3">Delete token of "{{ username }}"</h3>
<form action="" method="post">{% csrf_token %}
<div class="notification is-danger is-light">
<p>{% trans "You asked to delete a token.<br />It will be permanently deleted and this action cannot be undone.<br />Please confirm." %}</p>
</div>
<div class="buttons">
<button type="submit" class="button is-danger" name="delete_confirm">
<span class="icon"><i class="fas fa-check"></i></span>
<span>{% trans "Ok" %}</span>
</button>
<a class="button is-light" href="javascript:history.back()">
<span class="icon"><i class="fas fa-times"></i></span>
<span>{% trans "Cancel" %}</span>
</a>
</div>
</form>
{% endblock %}
Loading

0 comments on commit 4455cfb

Please sign in to comment.