Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • djangoldp-packages/djangoldp
  • decentral1se/djangoldp
  • femmefaytale/djangoldp
  • jvtrudel/djangoldp
4 results
Show changes
Showing
with 1243 additions and 444 deletions
#!/usr/bin/env python
import os
import sys
from djangoldp.conf import settings as ldpsettings
from djangoldp.conf import ldpsettings
if __name__ == "__main__":
ldpsettings.configure()
ldpsettings.configure('settings.yml')
try:
from django.core.management import execute_from_command_line
......
......@@ -13,14 +13,14 @@ Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf.urls import url, include
from django.urls import include, path
from django.contrib import admin
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
url(r'^', include('djangoldp.urls')),
url(r'^admin/', admin.site.urls),
path('', include('djangoldp.urls')),
path('admin/', admin.site.urls),
]
if settings.DEBUG:
......
......@@ -11,9 +11,16 @@ import os
from django.core.wsgi import get_wsgi_application
from django.conf import settings as django_settings
from djangoldp.conf import settings as ldpsettings
from djangoldp.conf import ldpsettings
if not django_settings.configured:
ldpsettings.configure()
application = get_wsgi_application()
try:
from djangoldp.activities.services import ActivityQueueService
ActivityQueueService.start()
except:
pass
......@@ -13,15 +13,7 @@ server:
default:
ENGINE: django.db.backends.sqlite3
NAME: db.sqlite3
LDP_RDF_CONTEXT: https://cdn.startinblox.com/owl/context.jsonld
ROOT_URLCONF: server.urls
STATIC_ROOT: static
MEDIA_ROOT: media
LDP_RDF_CONTEXT: https://cdn.happy-dev.fr/owl/hdcontext.jsonld
PROSODY_HTTP_URL:
JABBER_DEFAULT_HOST:
ACCOUNT_ACTIVATION_DAYS: 10
# needs review (could be defined elsewhere)
ROOT_URLCONF: server.urls
USE_ETAGS: true
DEFAULT_CONTENT_TYPE: text/html
FILE_CHARSET: utf-8
import os
import sys
import yaml
import logging
from django.core.exceptions import ImproperlyConfigured
from django.conf import settings as django_settings
from pathlib import Path
from typing import Iterable
from . import global_settings
try:
from importlib import import_module
except ImportError:
from django.utils.importlib import import_module
logger = logging.getLogger(__name__)
def configure():
# ref: https://docs.djangoproject.com/fr/2.2/topics/settings/#custom-default-settings
settings = LDPSettings('config.yml')
django_settings.configure(settings) # gives a LazySettings
class LDPSettings(object):
"""Class managing the DjangoLDP configuration."""
def __init__(self, path):
if django_settings.configured:
raise ImproperlyConfigured('Settings have been configured already')
self.path = path
self._config = None
@property
def config(self):
"""Load configuration from file."""
if not self._config:
with open(self.path, 'r') as f:
self._config = yaml.safe_load(f)
return self._config
@config.setter
def config(self, value):
"""Set a dict has current configuration."""
self._config = value
def fetch(self, attributes):
"""
Explore packages looking for a list of attributes within the server configuration.
It returns all elements found and doesn't manage duplications or collisions.
"""
attr = []
for pkg in self.DJANGOLDP_PACKAGES:
try:
# import from an installed package
mod = import_module(f'{pkg}.djangoldp_settings')
logger.debug(f'Settings found for {pkg} in a installed package')
except (ModuleNotFoundError):
try:
# import from a local packages in a subfolder (same name the template is built this way)
mod = import_module(f'{pkg}.{pkg}.djangoldp_settings')
logger.debug(f'Settings found for {pkg} in a local package')
except (ModuleNotFoundError):
logger.debug(f'No settings found for {pkg}')
break
# looking for the attribute list in the module
try:
attr.extend(getattr(mod, attributes))
logger.debug(f'{attributes} found in local package {pkg}')
except (NameError):
logger.info(f'No {attributes} found for package {pkg}')
pass
return attr
@property
def DJANGOLDP_PACKAGES(self):
"""Returns the list of LDP packages configured."""
pkg = self.config.get('ldppackages', [])
return [] if pkg is None else pkg
@property
def INSTALLED_APPS(self):
"""Return the default installed apps and the LDP packages."""
# get default apps
apps = getattr(global_settings, 'INSTALLED_APPS')
# add ldp packages themselves (they are django apps)
apps.extend(self.DJANGOLDP_PACKAGES)
# add apps referenced in packages
apps.extend(self.fetch('INSTALLED_APPS'))
return apps
@property
def MIDDLEWARE(self):
"""
Return the default middlewares and the middlewares found in each LDP packages.
"""
# get default middlewares
middlewares = getattr(global_settings, 'MIDDLEWARE')
# explore packages looking for middleware to reference
middlewares.extend(self.fetch('MIDDLEWARE'))
return middlewares
def __getattr__(self, param):
"""
Look for the parameter in config and return the first value found.
Resolution order of the configuration:
1. YAML config file
2. Packages settings
3. Core default settings
"""
if not param.startswith('_') and param.isupper():
# look in config file
try:
value = self.config['server'][param]
logger.debug(f'{param} found in project config')
return value
except KeyError:
pass
# look in all packages config
for pkg in self.DJANGOLDP_PACKAGES:
try:
# import from local package
mod = import_module(f'{pkg}.{pkg}.djangoldp_settings')
value = getattr(mod, param)
logger.debug(f'{param} found in local package {pkg}')
return value
except (ModuleNotFoundError, NameError, AttributeError):
pass
try:
# import from installed package
mod = import_module(f'{pkg}.djangoldp_settings')
value = getattr(mod, param)
logger.debug(f'{param} found in installed package {pkg}')
return value
except (ModuleNotFoundError, NameError, AttributeError):
pass
# look in default settings
try:
value = getattr(global_settings, param)
logger.debug(f'{param} found in core default config')
return value
except AttributeError:
pass
# raise the django exception for inexistent parameter
raise AttributeError(f'no "{param}" parameter found in settings')
from django.conf import settings
from django.db.models import Q
from rest_framework.filters import BaseFilterBackend
from djangoldp.models import Model
from djangoldp.utils import check_client_ip
class OwnerFilterBackend(BaseFilterBackend):
"""Adds the objects owned by the user"""
def filter_queryset(self, request, queryset, view):
if request.user.is_superuser:
return queryset
if request.user.is_anonymous:
return queryset.none()
if getattr(view.model._meta, 'owner_field', None):
return queryset.filter(**{view.model._meta.owner_field: request.user})
if getattr(view.model._meta, 'owner_urlid_field', None):
return queryset.filter(**{view.model._meta.owner_urlid_field: request.user.urlid})
if getattr(view.model._meta, 'auto_author', None):
return queryset.filter(**{view.model._meta.auto_author: request.user})
return queryset
class PublicFilterBackend(BaseFilterBackend):
"""
Public filter applied.
This class can be applied on models which bears a is_public boolean field, to filter objects that are public.
"""
def filter_queryset(self, request, queryset, view):
public_field = queryset.model._meta.public_field
return queryset.filter(**{public_field: True})
class ActiveFilterBackend(BaseFilterBackend):
"""
Filter which removes inactive objects from the queryset, useful for user for instance and configurable using the active_field meta
"""
def filter_queryset(self, request, queryset, view):
if (hasattr(queryset.model._meta, 'active_field')):
is_active_field = queryset.model._meta.active_field
return queryset.filter(**{is_active_field :True})
return queryset
class NoFilterBackend(BaseFilterBackend):
"""
No filter applied.
This class is useful for permission classes that don't filter objects, so that they can be chained with other
"""
def filter_queryset(self, request, queryset, view):
return queryset
class LocalObjectFilterBackend(BaseFilterBackend):
"""
Filter which removes external objects (federated backlinks) from the queryset
For querysets which should only include local objects
"""
def filter_queryset(self, request, queryset, view):
internal_ids = [x.pk for x in queryset if not Model.is_external(x)]
return queryset.filter(pk__in=internal_ids)
return queryset.filter(urlid__startswith=settings.SITE_URL)
class LocalObjectOnContainerPathBackend(LocalObjectFilterBackend):
......@@ -18,6 +61,94 @@ class LocalObjectOnContainerPathBackend(LocalObjectFilterBackend):
is the model container path
"""
def filter_queryset(self, request, queryset, view):
from djangoldp.models import Model
if issubclass(view.model, Model) and request.path_info == view.model.get_container_path():
return super(LocalObjectOnContainerPathBackend, self).filter_queryset(request, queryset, view)
return queryset
class SearchByQueryParamFilterBackend(BaseFilterBackend):
"""
Applies search fields in the request query params to the queryset
"""
def filter_queryset(self, request, queryset, view):
# the model fields on which to perform the search
search_fields = request.GET.get('search-fields', None)
# the terms to search the fields for
search_terms = request.GET.get('search-terms', None)
# the method of search to apply to the model fields
search_method = request.GET.get('search-method', "basic")
# union or intersection
search_policy = request.GET.get('search-policy', 'union')
# check if there is indeed a search requested
if search_fields is None or search_terms is None:
return queryset
def _construct_search_query(search):
'''Utility function pipes many Django Query objects'''
search_query = []
for idx, s in enumerate(search):
if idx > 0:
# the user has indicated to make a union of all query results
if search_policy == "union":
search_query = search_query | Q(**s)
# the user has indicated to make an intersection of all query results
else:
search_query = search_query & Q(**s)
continue
search_query = Q(**s)
return search_query
search_fields = search_fields.split(',')
if search_method == "basic":
search = []
for s in search_fields:
query = {}
query["{}__contains".format(s)] = search_terms
search.append(query)
queryset = queryset.filter(_construct_search_query(search))
elif search_method == "ibasic":
# NOTE: to use, see https://stackoverflow.com/questions/54071944/fielderror-unsupported-lookup-unaccent-for-charfield-or-join-on-the-field-not
unaccent_extension = getattr(settings, 'SEARCH_UNACCENT_EXTENSION', False) and 'django.contrib.postgres' in settings.INSTALLED_APPS
middle_term = '__unaccent' if unaccent_extension else ''
search = []
for s in search_fields:
query = {}
query["{}{}__icontains".format(s, middle_term)] = search_terms
search.append(query)
queryset = queryset.filter(_construct_search_query(search))
elif search_method == "exact":
search = []
for s in search_fields:
query = {}
query["{}__exact".format(s)] = search_terms
search.append(query)
queryset = queryset.filter(_construct_search_query(search))
return queryset
class IPFilterBackend(BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
if check_client_ip(request):
return queryset
else:
return queryset.none()
\ No newline at end of file
'''
DjangoLDP Check Integrity Command & Importer
Usage `./manage.py check_integrity --help`
This command does import every `check_integrity.py` of every DJANGOLDP_PACKAGES
Allowed methods:
`add_arguments(parser)`: Allows to create new arguments to this command
`check_integrity(options)`: Do your own checks on the integrity
Examples on `djangoldp/check_integrity.py`
'''
from django.core.management.base import BaseCommand
from django.conf import settings
from importlib import import_module
import requests
class Command(BaseCommand):
help = "Check the datas integrity"
def add_arguments(self, parser):
import_module('djangoldp.check_integrity').add_arguments(parser)
for package in settings.DJANGOLDP_PACKAGES:
try:
import_module('{}.check_integrity'.format(package)).add_arguments(parser)
except:
pass
def handle(self, *args, **options):
import_module('djangoldp.check_integrity').check_integrity(options)
for package in settings.DJANGOLDP_PACKAGES:
try:
import_module('{}.check_integrity'.format(package)).check_integrity(options)
except:
pass
exit(0)
......@@ -8,6 +8,14 @@ class Command(BaseCommand):
help = 'Initialize the DjangoLDP backend'
def add_arguments(self, parser):
"""Define the same arguments as the ones in CLI."""
parser.add_argument('--with-admin', nargs='?', type=str, help='Create an administrator user.')
parser.add_argument('--email', nargs='?', type=str, help='Provide an email for administrator.')
parser.add_argument('--with-dummy-admin', action='store_true', help='Create a default "admin" user with "admin" password.')
def handle(self, *args, **options):
"""Wrapper command around default django initialization commands."""
......@@ -17,26 +25,27 @@ class Command(BaseCommand):
management.call_command('migrate', interactive=False)
except CommandError as e:
setf.stdout.write(self.style.ERROR(f'Data migration failed: {e}'))
self.stdout.write(self.style.ERROR(f'Data migration failed: {e}'))
try:
if settings.DEBUG:
if options['with_dummy_admin']:
try:
# create a default super user
from django.contrib.auth import get_user_model
User = get_user_model()
User.objects.create_superuser('admin', 'admin@example.org', 'admin')
else:
# call default createsuperuser command
management.call_command('createsuperuser', interactive=True)
except (ValidationError, IntegrityError):
self.stdout.write('User "admin" already exists. Skipping...')
pass
except (ValidationError, IntegrityError):
self.stdout.write('User "admin" already exists. Skipping...')
pass
elif options['with_admin']:
try:
# call default createsuperuser command
management.call_command('createsuperuser', '--noinput', '--username', options['with_admin'], '--email', options['email'])
except CommandError as e:
self.stdout.write(self.style.ERROR(f'Superuser creation failed: {e}'))
pass
except CommandError as e:
self.stdout.write(self.style.ERROR(f'Superuser {e}'))
pass
#try:
# # creatersakey
......
import argparse
from django.core.management.base import BaseCommand
from django.conf import settings
from djangoldp.models import LDPSource
def isstring(target):
if isinstance(target, str):
return target
return False
def list_models():
# Improve me using apps.get_models()
return {
"circles": "/circles/",
"circlesjoinable": "/circles/joinable/",
"communities": "/communities/",
"opencommunities": "/open-communities/",
"communitiesaddresses": "/community-addresses/",
"dashboards": "/dashboards/",
"events": "/events/",
"eventsfuture": "/events/future/",
"eventspast": "/events/past/",
"typeevents": "/typeevents/",
"resources": "/resources/",
"keywords": "/keywords/",
"types": "/types/",
"joboffers": "/job-offers/current/",
"polls": "/polls/",
"projects": "/projects/",
"projectsjoinable": "/projects/joinable/",
"skills": "/skills/",
"users": "/users/"
}
class Command(BaseCommand):
help = 'Add another server to this one sources'
def add_arguments(self, parser):
parser.add_argument(
"--target",
action="store",
default=False,
type=isstring,
help="Targeted server, format protocol://domain",
)
parser.add_argument(
"--delete",
default=False,
nargs='?',
const=True,
help="Remove targeted source",
)
def handle(self, *args, **options):
target = options["target"]
models = list_models()
if not target:
target = settings.SITE_URL
error_counter = 0
if(options["delete"]):
for attr, value in models.items():
try:
LDPSource.objects.filter(urlid=target+value, federation=attr).delete()
except:
error_counter += 1
if error_counter > 0:
self.stdout.write(self.style.ERROR("Can't remove: "+target))
exit(2)
else:
self.stdout.write(self.style.SUCCESS("Successfully removed sources for "+target))
exit(0)
else:
for attr, value in models.items():
try:
LDPSource.objects.create(urlid=target+value, federation=attr)
except:
error_counter += 1
if error_counter > 0:
self.stdout.write(self.style.WARNING("Source aleady exists for "+target+"\nIgnored "+str(error_counter)+"/"+str(len(models))))
if error_counter == len(models):
exit(1)
exit(0)
else:
self.stdout.write(self.style.SUCCESS("Successfully created sources for "+target))
exit(0)
import os
import json
import requests
from django.core.management.base import BaseCommand
from django.conf import settings
from django.apps import apps
from urllib.parse import urlparse, urljoin
class StaticContentGenerator:
def __init__(self, stdout, style):
self.stdout = stdout
self.style = style
self.base_uri = getattr(settings, 'BASE_URL', '')
self.max_depth = getattr(settings, 'MAX_RECURSION_DEPTH', 5)
self.request_timeout = getattr(settings, 'SSR_REQUEST_TIMEOUT', 10)
self.regenerated_urls = set()
self.failed_urls = set()
self.output_dir = 'ssr'
self.output_dir_filtered = 'ssr_filtered'
def generate_content(self):
self._create_output_directory()
for model in self._get_static_models():
self._process_model(model)
def _create_output_directory(self):
os.makedirs(self.output_dir, exist_ok=True)
os.makedirs(self.output_dir_filtered, exist_ok=True)
def _get_static_models(self):
return [model for model in apps.get_models() if hasattr(model._meta, 'static_version')]
def _process_model(self, model):
self.stdout.write(f"Generating content for model: {model}")
url = self._build_url(model)
if url not in self.regenerated_urls and url not in self.failed_urls:
self._fetch_and_save_content(model, url, self.output_dir)
else:
self.stdout.write(self.style.WARNING(f'Skipping {url} as it has already been fetched'))
if hasattr(model._meta, 'static_params'):
url = self._build_url(model, True)
if url not in self.regenerated_urls and url not in self.failed_urls:
self._fetch_and_save_content(model, url, self.output_dir_filtered)
else:
self.stdout.write(self.style.WARNING(f'Skipping {url} as it has already been fetched'))
def _build_url(self, model, use_static_params=False):
container_path = model.get_container_path()
url = urljoin(self.base_uri, container_path)
if hasattr(model._meta, 'static_params') and use_static_params:
url += '?' + '&'.join(f'{k}={v}' for k, v in model._meta.static_params.items())
return url
def _fetch_and_save_content(self, model, url, output_dir):
try:
response = requests.get(url, timeout=self.request_timeout)
if response.status_code == 200:
content = self._update_ids_and_fetch_associated(response.text)
self._save_content(model, url, content, output_dir)
else:
self.stdout.write(self.style.ERROR(f'Failed to fetch content from {url}: HTTP {response.status_code}'))
except requests.exceptions.RequestException as e:
self.stdout.write(self.style.ERROR(f'Error fetching content from {url}: {str(e)}'))
def _save_content(self, model, url, content, output_dir):
relative_path = urlparse(url).path.strip('/')
file_path = os.path.join(output_dir, relative_path)
if file_path.endswith('/'):
file_path = file_path[:-1]
if not file_path.endswith('.jsonld'):
file_path += '.jsonld'
os.makedirs(os.path.dirname(file_path), exist_ok=True)
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
self.stdout.write(self.style.SUCCESS(f'Successfully saved content for {model._meta.model_name} from {url} to {file_path}'))
except IOError as e:
self.stdout.write(self.style.ERROR(f'Error saving content for {model._meta.model_name}: {str(e)}'))
def _update_ids_and_fetch_associated(self, content, depth=0):
if depth > self.max_depth:
return content
try:
data = json.loads(content)
self._process_data(data, depth)
return json.dumps(data)
except json.JSONDecodeError as e:
self.stdout.write(self.style.ERROR(f'Failed to decode JSON: {e}'))
return content
def _process_data(self, data, depth):
if isinstance(data, dict):
self._process_item(data, depth)
elif isinstance(data, list):
for item in data:
if isinstance(item, dict):
self._process_item(item, depth)
def _process_item(self, item, depth):
if '@id' in item:
self._update_and_fetch_content(item, depth)
for value in item.values():
if isinstance(value, (dict, list)):
self._process_data(value, depth)
def _update_and_fetch_content(self, item, depth):
original_id = item['@id']
parsed_url = urlparse(original_id)
if not parsed_url.netloc:
original_id = urljoin(self.base_uri, original_id)
parsed_url = urlparse(original_id)
path = parsed_url.path
if path.startswith(urlparse(self.base_uri).path):
path = path[len(urlparse(self.base_uri).path):]
new_id = f'/ssr{path}'
item['@id'] = urljoin(self.base_uri, new_id)
self._fetch_and_save_associated_content(original_id, path, depth)
def _rewrite_ids_before_saving(self, data):
if isinstance(data, dict):
if '@id' in data:
original_id = data['@id']
parsed_url = urlparse(data['@id'])
if not parsed_url.netloc:
content_id = urljoin(self.base_uri, original_id)
parsed_url = urlparse(content_id)
if 'ssr/' not in data['@id']:
path = parsed_url.path
if path.startswith(urlparse(self.base_uri).path):
path = path[len(urlparse(self.base_uri).path):]
new_id = f'/ssr{path}'
data['@id'] = urljoin(self.base_uri, new_id)
for value in data.values():
if isinstance(value, (dict, list)):
self._rewrite_ids_before_saving(value)
elif isinstance(data, list):
for item in data:
self._rewrite_ids_before_saving(item)
return data
def _fetch_and_save_associated_content(self, url, new_path, depth):
if url in self.regenerated_urls:
self.stdout.write(self.style.WARNING(f'Skipping {url} as it has already been fetched'))
return
if url in self.failed_urls:
self.stdout.write(self.style.WARNING(f'Skipping {url} as it has already been tried and failed'))
return
file_path = os.path.join(self.output_dir, new_path.strip('/'))
if file_path.endswith('/'):
file_path = file_path[:-1]
if not file_path.endswith('.jsonld'):
file_path += '.jsonld'
os.makedirs(os.path.dirname(file_path), exist_ok=True)
try:
response = requests.get(url, timeout=self.request_timeout)
if response.status_code == 200:
updated_content = json.loads(self._update_ids_and_fetch_associated(response.text, depth + 1))
updated_content = self._rewrite_ids_before_saving(updated_content)
with open(file_path, 'w', encoding='utf-8') as f:
f.write(json.dumps(updated_content))
self.regenerated_urls.add(url)
self.stdout.write(self.style.SUCCESS(f'Successfully fetched and saved associated content from {url} to {file_path}'))
else:
self.failed_urls.add(url)
self.stdout.write(self.style.ERROR(f'Failed to fetch associated content from {url}: HTTP {response.status_code}'))
except requests.exceptions.RequestException as e:
self.stdout.write(self.style.ERROR(f'Error fetching associated content from {url}: {str(e)}'))
except IOError as e:
self.stdout.write(self.style.ERROR(f'Error saving associated content from {url}: {str(e)}'))
class Command(BaseCommand):
help = 'Generate static content for models having the static_version meta attribute set to 1/true'
def handle(self, *args, **options):
generator = StaticContentGenerator(self.stdout, self.style)
generator.generate_content()
\ No newline at end of file
from django.conf import settings
from django.utils.http import is_safe_url
from django.utils.http import url_has_allowed_host_and_scheme
from django.shortcuts import redirect
from djangoldp.models import Model
class AllowOnlySiteUrl:
......@@ -9,7 +10,26 @@ class AllowOnlySiteUrl:
def __call__(self, request):
response = self.get_response(request)
if(is_safe_url(request.get_raw_uri(), allowed_hosts=settings.SITE_URL) or response.status_code != 200):
if(url_has_allowed_host_and_scheme(request.get_raw_uri(), allowed_hosts=settings.SITE_URL) or response.status_code != 200):
return response
else:
return redirect('{}{}'.format(settings.SITE_URL, request.path), permanent=True)
class AllowRequestedCORSMiddleware:
'''A CORS Middleware which allows the domains requested by the request'''
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
response["Access-Control-Allow-Origin"] = request.headers.get('origin')
response["Access-Control-Allow-Methods"] = "GET,POST,PUT,PATCH,DELETE,OPTIONS,HEAD"
response["Access-Control-Allow-Headers"] = \
getattr(settings, 'OIDC_ACCESS_CONTROL_ALLOW_HEADERS',
"authorization, Content-Type, if-match, accept, DPoP, cache-control, prefer")
response["Access-Control-Expose-Headers"] = "Location, User"
response["Access-Control-Allow-Credentials"] = 'true'
return response
\ No newline at end of file
# Generated by Django 2.2 on 2020-09-09 22:06
from django.db import migrations, models
import django.db.models.deletion
import djangoldp.fields
class Migration(migrations.Migration):
dependencies = [
('djangoldp', '0013_auto_20200624_1709'),
]
operations = [
migrations.CreateModel(
name='ScheduledActivity',
fields=[
('activity_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='djangoldp.Activity')),
('failed_attempts', models.PositiveIntegerField(default=0, help_text='a log of how many failed retries have been made sending the activity')),
],
options={
'abstract': False,
'default_permissions': ('add', 'change', 'delete', 'view', 'control'),
},
bases=('djangoldp.activity',),
),
migrations.AlterField(
model_name='activity',
name='created_at',
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name='activity',
name='local_id',
field=djangoldp.fields.LDPUrlField(help_text='/inbox or /outbox url (local - this server)'),
),
migrations.AlterField(
model_name='activity',
name='payload',
field=models.BinaryField(),
),
migrations.AddField(
model_name='activity',
name='success',
field=models.BooleanField(default=False,
help_text='set to True when an Activity is successfully delivered'),
),
migrations.AddField(
model_name='activity',
name='type',
field=models.CharField(blank=True, help_text='the ActivityStreams type of the Activity', max_length=64,
null=True),
),
migrations.AddField(
model_name='activity',
name='is_finished',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='activity',
name='response_code',
field=models.CharField(blank=True, help_text='Response code sent by receiver', max_length=8, null=True),
),
migrations.AddField(
model_name='activity',
name='response_location',
field=djangoldp.fields.LDPUrlField(blank=True, help_text='Location saved activity can be found', null=True),
),
migrations.AddField(
model_name='activity',
name='response_body',
field=models.BinaryField(null=True),
),
migrations.AddField(
model_name='activity',
name='external_id',
field=djangoldp.fields.LDPUrlField(help_text='the /inbox or /outbox url (from the sender or receiver)',
null=True),
),
migrations.RemoveField(
model_name='activity',
name='aid',
),
]
# Generated by Django 2.2.17 on 2021-01-25 18:47
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('djangoldp', '0014_auto_20200909_2206'),
]
operations = [
migrations.AlterModelOptions(
name='activity',
options={'default_permissions': ['add', 'change', 'delete', 'view', 'control']},
),
migrations.AlterModelOptions(
name='follower',
options={'default_permissions': ['add', 'change', 'delete', 'view', 'control']},
),
migrations.AlterModelOptions(
name='ldpsource',
options={'default_permissions': ['add', 'change', 'delete', 'view', 'control'], 'ordering': ('federation',)},
),
migrations.AlterModelOptions(
name='scheduledactivity',
options={'default_permissions': ['add', 'change', 'delete', 'view', 'control']},
),
]
# Generated by Django 4.2.3 on 2023-08-31 15:27
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('djangoldp', '0015_auto_20210125_1847'),
]
operations = [
migrations.AlterModelOptions(
name='activity',
options={'default_permissions': {'delete', 'change', 'view', 'add', 'control'}},
),
migrations.AlterModelOptions(
name='follower',
options={'default_permissions': {'delete', 'change', 'view', 'add', 'control'}},
),
migrations.AlterModelOptions(
name='ldpsource',
options={'default_permissions': {'delete', 'change', 'view', 'add', 'control'}, 'ordering': ('federation',)},
),
migrations.AlterModelOptions(
name='scheduledactivity',
options={'default_permissions': {'delete', 'change', 'view', 'add', 'control'}},
),
]
# Generated by Django 4.2.3 on 2023-09-03 20:26
from django.db import migrations
import djangoldp.fields
class Migration(migrations.Migration):
dependencies = [
('djangoldp', '0016_alter_activity_options_alter_follower_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='activity',
name='urlid',
field=djangoldp.fields.LDPUrlField(blank=True, db_index=True, null=True, unique=True),
),
migrations.AlterField(
model_name='follower',
name='urlid',
field=djangoldp.fields.LDPUrlField(blank=True, db_index=True, null=True, unique=True),
),
migrations.AlterField(
model_name='ldpsource',
name='urlid',
field=djangoldp.fields.LDPUrlField(blank=True, db_index=True, null=True, unique=True),
),
]
# Generated by Django 4.2.3 on 2023-10-17 19:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('djangoldp', '0017_alter_activity_urlid_alter_follower_urlid_and_more'),
]
operations = [
migrations.AlterField(
model_name='activity',
name='payload',
field=models.TextField(),
),
migrations.AlterField(
model_name='activity',
name='response_body',
field=models.TextField(null=True),
),
]
import json
import logging
import uuid
from urllib.parse import urlparse
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.core.exceptions import ObjectDoesNotExist, ValidationError, FieldDoesNotExist
from django.db import models
from django.db.models import BinaryField, DateField
from django.db.models.base import ModelBase
from django.db.models.signals import post_save
from django.db.models.signals import post_save, pre_save, pre_delete, m2m_changed
from django.dispatch import receiver
from django.urls import reverse_lazy, get_resolver
from django.urls import get_resolver
from django.utils.datastructures import MultiValueDictKeyError
from django.utils.decorators import classonlymethod
from guardian.shortcuts import assign_perm
from rest_framework.utils import model_meta
from djangoldp.fields import LDPUrlField
from djangoldp.permissions import LDPPermissions
import logging
from djangoldp.permissions import DEFAULT_DJANGOLDP_PERMISSIONS, OwnerPermissions, InheritPermissions, ReadOnly
logger = logging.getLogger('djangoldp')
Group._meta.serializer_fields = ['name', 'user_set']
Group._meta.rdf_type = 'foaf:Group'
# Group._meta.rdf_context = {'user_set': 'foaf:member'}
Group._meta.permission_classes = [(OwnerPermissions&ReadOnly)|InheritPermissions]
Group._meta.owner_field = 'user'
Group._meta.inherit_permissions = []
class LDPModelManager(models.Manager):
def local(self):
......@@ -29,44 +34,21 @@ class LDPModelManager(models.Manager):
internal_ids = [x.pk for x in queryset if not Model.is_external(x)]
return queryset.filter(pk__in=internal_ids)
def nested_fields(self):
'''parses the relations on the model, and returns a list of nested field names'''
nested_fields = set()
# include all many-to-many relations
for field_name, relation_info in model_meta.get_field_info(self.model).relations.items():
if relation_info.to_many:
if field_name is not None:
nested_fields.add(field_name)
# include all nested fields explicitly included on the model
nested_fields.update(set(Model.get_meta(self.model, 'nested_fields', set())))
# exclude anything marked explicitly to be excluded
nested_fields = nested_fields.difference(set(Model.get_meta(self.model, 'nested_fields_exclude', set())))
return list(nested_fields)
def fields(self):
return self.nested_fields()
class Model(models.Model):
urlid = LDPUrlField(blank=True, null=True, unique=True)
urlid = LDPUrlField(blank=True, null=True, unique=True, db_index=True)
is_backlink = models.BooleanField(default=False, help_text='set automatically to indicate the Model is a backlink')
allow_create_backlink = models.BooleanField(default=True,
help_text='set to False to disable backlink creation after Model save')
objects = LDPModelManager()
nested = LDPModelManager()
class Meta:
default_permissions = DEFAULT_DJANGOLDP_PERMISSIONS
abstract = True
depth = 0
def __init__(self, *args, **kwargs):
super(Model, self).__init__(*args, **kwargs)
@classmethod
def get_view_set(cls):
'''returns the view_set defined in the model Meta or the LDPViewSet class'''
view_set = getattr(cls._meta, 'view_set', getattr(cls.Meta, 'view_set', None))
if view_set is None:
from djangoldp.views import LDPViewSet
view_set = LDPViewSet
return view_set
@classmethod
def get_container_path(cls):
'''returns the url path which is used to access actions on this model (e.g. /users/)'''
......@@ -81,7 +63,7 @@ class Model(models.Model):
@classonlymethod
def absolute_url(cls, instance_or_model):
if isinstance(instance_or_model, ModelBase) or instance_or_model.urlid is None or instance_or_model.urlid == '':
if isinstance(instance_or_model, ModelBase) or not instance_or_model.urlid:
return '{}{}'.format(settings.SITE_URL, Model.resource(instance_or_model))
else:
return instance_or_model.urlid
......@@ -104,16 +86,24 @@ class Model(models.Model):
@classonlymethod
def slug_field(cls, instance_or_model):
if isinstance(instance_or_model, ModelBase):
object_name = instance_or_model.__name__.lower()
model = instance_or_model
else:
object_name = instance_or_model._meta.object_name.lower()
model = type(instance_or_model)
# Use cached value if present
if hasattr(model, "_slug_field"):
return model._slug_field
object_name = model.__name__.lower()
view_name = '{}-detail'.format(object_name)
try:
slug_field = '/{}'.format(get_resolver().reverse_dict[view_name][0][0][1][0])
except MultiValueDictKeyError:
slug_field = Model.get_meta(instance_or_model, 'lookup_field', 'pk')
slug_field = getattr(model._meta, 'lookup_field', 'pk')
if slug_field.startswith('/'):
slug_field = slug_field[1:]
model._slug_field = slug_field
return slug_field
@classonlymethod
......@@ -128,11 +118,6 @@ class Model(models.Model):
return path
class Meta:
default_permissions = ('add', 'change', 'delete', 'view', 'control')
abstract = True
depth = 0
@classonlymethod
def resolve_id(cls, id):
'''
......@@ -152,7 +137,7 @@ class Model(models.Model):
@classonlymethod
def resolve_parent(cls, path):
split = path.strip('/').split('/')
parent_path = "/".join(split[0:len(split) - 1])
parent_path = "/".join(split[:-1])
return Model.resolve_id(parent_path)
@classonlymethod
......@@ -169,8 +154,8 @@ class Model(models.Model):
:param path: a URL path to check
:return: the container model and resolved id in a tuple
'''
if settings.BASE_URL in path:
path = path[len(settings.BASE_URL):]
if path.startswith(settings.BASE_URL):
path = path.replace(settings.BASE_URL, '')
container = cls.resolve_container(path)
try:
resolve_id = cls.resolve_id(path)
......@@ -198,7 +183,6 @@ class Model(models.Model):
:raises Exception: if the object does not exist, but the data passed is invalid
'''
try:
logger.debug('[get_or_create] ' + str(model) + ' backlink ' + str(urlid))
rval = model.objects.get(urlid=urlid)
if update:
for field in field_tuples.keys():
......@@ -206,7 +190,6 @@ class Model(models.Model):
rval.save()
return rval
except ObjectDoesNotExist:
logger.debug('[get_or_create] creating..')
if model is get_user_model():
field_tuples['username'] = str(uuid.uuid4())
return model.objects.create(urlid=urlid, is_backlink=True, **field_tuples)
......@@ -221,46 +204,19 @@ class Model(models.Model):
raise ObjectDoesNotExist
return Model.get_or_create(model, urlid, **kwargs)
@classonlymethod
def get_model_rdf_type(cls, model):
if model is get_user_model():
return "foaf:user"
else:
return Model.get_meta(model, "rdf_type")
@classonlymethod
def get_subclass_with_rdf_type(cls, type):
#TODO: deprecate
'''returns Model subclass with Meta.rdf_type matching parameterised type, or None'''
if type == 'foaf:user':
return get_user_model()
for subcls in Model.__subclasses__():
if Model.get_meta(subcls, 'rdf_type') == type:
if getattr(subcls._meta, "rdf_type", None) == type:
return subcls
return None
@classonlymethod
def get_permission_classes(cls, related_model, default_permissions_classes):
'''returns the permission_classes set in the models Meta class'''
return cls.get_meta(related_model, 'permission_classes', default_permissions_classes)
@classonlymethod
def get_meta(cls, model_class, meta_name, default=None):
'''returns the models Meta class'''
if hasattr(model_class, 'Meta'):
meta = getattr(model_class.Meta, meta_name, default)
else:
meta = default
return getattr(model_class._meta, meta_name, meta)
@staticmethod
def get_permissions(obj_or_model, user_or_group, filter):
permissions = filter
for permission_class in Model.get_permission_classes(obj_or_model, [LDPPermissions]):
permissions = permission_class().filter_user_perms(user_or_group, obj_or_model, permissions)
return [{'mode': {'@type': name.split('_')[0]}} for name in permissions]
@classmethod
def is_external(cls, value):
'''
......@@ -268,19 +224,24 @@ class Model(models.Model):
:return: True if the urlid is external to the server, False otherwise
'''
try:
if not value:
return False
if not isinstance(value, str):
value = value.urlid
return value is not None and not value.startswith(settings.SITE_URL)
# This expects all @ids to start with http which mlight not be universal. Maybe needs a fix.
return value.startswith('http') and not value.startswith(settings.SITE_URL)
except:
return False
#TODO: this breaks the serializer, which probably assumes that traditional models don't have a urlid.
# models.Model.urlid = property(lambda self: '{}{}'.format(settings.SITE_URL, Model.resource(self)))
class LDPSource(Model):
federation = models.CharField(max_length=255)
class Meta(Model.Meta):
rdf_type = 'ldp:Container'
rdf_type = 'sib:federatedContainer'
ordering = ('federation',)
container_path = 'sources'
lookup_field = 'federation'
......@@ -291,17 +252,41 @@ class LDPSource(Model):
class Activity(Model):
'''Models an ActivityStreams Activity'''
aid = LDPUrlField(null=True) # activity id
local_id = LDPUrlField() # /inbox or /outbox full url
payload = BinaryField()
created_at = DateField(auto_now_add=True)
local_id = LDPUrlField(help_text='/inbox or /outbox url (local - this server)') # /inbox or /outbox full url
external_id = LDPUrlField(null=True, help_text='the /inbox or /outbox url (from the sender or receiver)')
payload = models.TextField()
response_location = LDPUrlField(null=True, blank=True, help_text='Location saved activity can be found')
response_code = models.CharField(null=True, blank=True, help_text='Response code sent by receiver', max_length=8)
response_body = models.TextField(null=True)
type = models.CharField(null=True, blank=True, help_text='the ActivityStreams type of the Activity',
max_length=64)
is_finished = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
success = models.BooleanField(default=False, help_text='set to True when an Activity is successfully delivered')
class Meta(Model.Meta):
container_path = "activities"
rdf_type = 'as:Activity'
disable_url = True
def to_activitystream(self):
return json.loads(self.payload.tobytes())
return json.loads(self.payload)
def response_to_json(self):
return self.to_activitystream()
# temporary database-side storage used for scheduled tasks in the ActivityQueue
class ScheduledActivity(Activity):
failed_attempts = models.PositiveIntegerField(default=0,
help_text='a log of how many failed retries have been made sending the activity')
def save(self, *args, **kwargs):
self.is_finished = False
super(ScheduledActivity, self).save(*args, **kwargs)
class Meta(Model.Meta):
disable_url = True
class Follower(Model):
......@@ -313,32 +298,74 @@ class Follower(Model):
def __str__(self):
return 'Inbox ' + str(self.inbox) + ' on ' + str(self.object)
def save(self, *args, **kwargs):
if self.pk is None:
logger.debug('[Follower] saving Follower ' + self.__str__())
super(Follower, self).save(*args, **kwargs)
class Meta(Model.Meta):
disable_url = True
class DynamicNestedField:
'''
Used to define a method as a nested_field.
Usage:
LDPUser.circles = lambda self: Circle.objects.filter(members__user=self)
LDPUser.circles.field = DynamicNestedField(Circle, 'circles')
'''
related_query_name = None
one_to_many = False
many_to_many = True
many_to_one = False
one_to_one = False
read_only = True
name = ''
def __init__(self, model:models.Model|None, remote_name:str, name:str='', remote:object|None=None) -> None:
self.model = model
self.name = name
if remote:
self.remote_field = remote
else:
self.remote_field = DynamicNestedField(None, '', remote_name, self)
@receiver([post_save])
def auto_urlid(sender, instance, **kwargs):
if isinstance(instance, Model):
changed = False
if getattr(instance, Model.slug_field(instance), None) is None:
setattr(instance, Model.slug_field(instance), instance.pk)
instance.save()
if (instance.urlid is None or instance.urlid == '' or 'None' in instance.urlid):
changed = True
if (not instance.urlid or 'None' in instance.urlid):
instance.urlid = instance.get_absolute_url()
changed = True
if changed:
instance.save()
@receiver(post_save)
def create_role_groups(sender, instance, created, **kwargs):
if created:
for name, params in getattr(instance._meta, 'permission_roles', {}).items():
group, x = Group.objects.get_or_create(name=f'LDP_{instance._meta.model_name}_{name}_{instance.id}')
setattr(instance, name, group)
instance.save()
if params.get('add_author'):
assert hasattr(instance._meta, 'auto_author'), "add_author requires to also define auto_author"
author = getattr(instance, instance._meta.auto_author)
if author:
group.user_set.add(author)
for permission in params.get('perms', []):
assign_perm(f'{permission}_{instance._meta.model_name}', group, instance)
if 'djangoldp_account' not in settings.DJANGOLDP_PACKAGES:
def webid(self):
# an external user should have urlid set
webid = getattr(self, 'urlid', None)
if webid is not None and urlparse(settings.BASE_URL).netloc != urlparse(webid).netloc:
webid = self.urlid
# local user use user-detail URL with primary key
else:
webid = '{0}{1}'.format(settings.BASE_URL, reverse_lazy('user-detail', kwargs={'pk': self.pk}))
return webid
get_user_model().webid = webid
def invalidate_cache_if_has_entry(entry):
from djangoldp.serializers import GLOBAL_SERIALIZER_CACHE
if GLOBAL_SERIALIZER_CACHE.has(entry):
GLOBAL_SERIALIZER_CACHE.invalidate(entry)
def invalidate_model_cache_if_has_entry(model):
entry = getattr(model._meta, 'label', None)
invalidate_cache_if_has_entry(entry)
@receiver([pre_save, pre_delete])
def invalidate_caches(sender, instance, **kwargs):
invalidate_model_cache_if_has_entry(sender)
@receiver([m2m_changed])
def invalidate_caches_m2m(sender, instance, action, *args, **kwargs):
invalidate_model_cache_if_has_entry(kwargs['model'])
\ No newline at end of file
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response
class LDPOffsetPagination(LimitOffsetPagination):
def get_paginated_response(self, data):
next_url = self.get_next_link()
previous_url = self.get_previous_link()
links = []
for url, label in ((previous_url, 'prev'), (next_url, 'next')):
if url is not None:
links.append('<{}>; rel="{}"'.format(url, label))
headers = {'Link': ', '.join(links)} if links else {}
return Response(data, headers=headers)
class LDPPagination(PageNumberPagination):
page_query_param = 'p'
page_size_query_param = 'limit'
class LDPPagination(LimitOffsetPagination):
def get_paginated_response(self, data):
next_url = self.get_next_link()
previous_url = self.get_previous_link()
......
This diff is collapsed.
from rest_framework.utils import model_meta
def get_prefetch_fields(model, serializer, depth, prepend_string=''):
'''
This method should then be used with queryset.prefetch_related, to auto-fetch joined resources (to speed up nested serialization)
This can speed up ModelViewSet and LDPViewSet alike by as high a factor as 2
:param model: the model to be analysed
:param serializer: an LDPSerializer instance. Used to extract the fields for each nested model
:param depth: the depth at which to stop the recursion (should be set to the configured depth of the ViewSet)
:param prepend_string: should be set to the default. Used in recursive calls
:return: set of strings to prefetch for a given model. Including serialized nested fields and foreign keys recursively
called on many-to-many fields until configured depth reached
'''
# the objective is to build a list of fields and nested fields which should be prefetched for the optimisation
# of database queries
fields = set()
# get a list of all fields which would be serialized on this model
# TODO: dynamically generating serializer fields is necessary to retrieve many-to-many fields at depth > 0,
# but the _all_ default has issues detecting reverse many-to-many fields
# meta_args = {'model': model, 'depth': 0, 'fields': getattr(model._meta, 'serializer_fields', '__all__')}
# meta_class = type('Meta', (), meta_args)
# serializer = (type(LDPSerializer)('TestSerializer', (LDPSerializer,), {'Meta': meta_class}))()
serializer_fields = set([f for f in serializer.get_fields()])
empty_containers = getattr(model._meta, 'empty_containers', [])
# we are only interested in foreign keys (and many-to-many relationships)
model_relations = model_meta.get_field_info(model).relations
for field_name, relation_info in model_relations.items():
# foreign keys can be added without fuss
if not relation_info.to_many:
fields.add((prepend_string + field_name))
continue
# nested fields should be added if serialized
if field_name in serializer_fields and field_name not in empty_containers:
fields.add((prepend_string + field_name))
# and they should also have their immediate foreign keys prefetched if depth not reached
if depth >= 0:
new_prepend_str = prepend_string + field_name + '__'
fields = fields.union(get_prefetch_fields(relation_info.related_model, serializer, depth - 1, new_prepend_str))
return fields