Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QGIS Hub API CRUD #17

Merged
merged 1 commit into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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