136 lines
4.4 KiB
Python
136 lines
4.4 KiB
Python
from typing import Optional
|
|
import hashlib
|
|
import io
|
|
|
|
from django import urls
|
|
from django.conf import settings
|
|
from django.contrib.admin.models import CHANGE, LogEntry
|
|
from django.contrib.sites.shortcuts import get_current_site
|
|
from django.urls import reverse
|
|
from django.utils.encoding import force_bytes
|
|
from urllib.parse import urlencode as urllib_urlencode
|
|
from urllib.parse import urljoin
|
|
import django.conf
|
|
import django.db.models
|
|
import django.utils.text
|
|
|
|
import yarl
|
|
|
|
|
|
def login_url_with_redirect(redirect_url: str) -> str:
|
|
"""
|
|
Construct a login URL which will redirect the user to the given URL after logging in.
|
|
|
|
:param redirect_url: The URL to redirect the user to after a successful login.
|
|
:return: The constructed login URL.
|
|
"""
|
|
return str(yarl.URL(urls.reverse('oauth:login')).with_query({'next': redirect_url}))
|
|
|
|
|
|
def compact_timesince(timesince):
|
|
"""Make timesince filter super compact."""
|
|
|
|
# Replace long words with letters. (2 days, 3 hours -> 2 d, 3 h)
|
|
timesince = (
|
|
timesince.replace('minutes', 'm')
|
|
.replace('minute', 'm')
|
|
.replace('hour', 'h')
|
|
.replace('hours', 'h')
|
|
)
|
|
timesince = timesince.replace('days', 'd').replace('day', 'd').replace('month', 'mon')
|
|
timesince = timesince.replace('months', 'mon').replace('weeks', 'w').replace('week', 'w')
|
|
|
|
# Remove space between digit and unit. (2 d, 3h -> 2d, 3h)
|
|
timesince = timesince.replace('\xa0', '')
|
|
|
|
# Take only the first, usually interesting part. (2d, 3h -> 2d)
|
|
timesince = timesince.split(',', 1)[0]
|
|
|
|
return timesince
|
|
|
|
|
|
def attach_log_entry(
|
|
instance: django.db.models.Model,
|
|
message: str,
|
|
action_flag: int = CHANGE,
|
|
user_id: Optional[int] = None,
|
|
) -> None:
|
|
"""Attach a log entry to this model, for in the admin 'history' page.
|
|
|
|
:param instance:
|
|
:param message:
|
|
:param action_flag: Either ADDITION, CHANGE, or DELETION, defaults to CHANGE.
|
|
:param user_id: Optionally, the user who performs the logged action.
|
|
Can be None when the action is performed by the system.
|
|
"""
|
|
from django.contrib.admin.models import LogEntry
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
if user_id is None:
|
|
user_id = django.conf.settings.SYSTEM_USER_ID
|
|
|
|
LogEntry.objects.log_action(
|
|
user_id=user_id,
|
|
content_type_id=ContentType.objects.get_for_model(type(instance)).pk,
|
|
object_id=instance.pk,
|
|
object_repr=str(instance),
|
|
action_flag=action_flag,
|
|
change_message=message,
|
|
)
|
|
|
|
|
|
def entries_for(instance: django.db.models.Model) -> 'django.db.models.QuerySet[LogEntry]':
|
|
"""Build a query for all log entries attached to an instance."""
|
|
|
|
from django.contrib.admin.models import LogEntry
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
return LogEntry.objects.filter(
|
|
content_type_id=ContentType.objects.get_for_model(type(instance)).pk, object_id=instance.pk
|
|
)
|
|
|
|
|
|
def get_sha256(file_obj):
|
|
"""Calculate a sha256 hash for `file_obj`.
|
|
|
|
`file_obj` must either be be an open file descriptor, in which case the
|
|
caller needs to take care of closing it properly, or a django File-like
|
|
object with a chunks() method to iterate over its contents.
|
|
"""
|
|
hash_ = hashlib.sha256()
|
|
if hasattr(file_obj, 'chunks') and callable(file_obj.chunks):
|
|
iterator = file_obj.chunks()
|
|
else:
|
|
iterator = iter(lambda: file_obj.read(io.DEFAULT_BUFFER_SIZE), b'')
|
|
for chunk in iterator:
|
|
hash_.update(chunk)
|
|
# This file might be read again by validation or other utilities
|
|
file_obj.seek(0)
|
|
return hash_.hexdigest()
|
|
|
|
|
|
def urlencode(items):
|
|
"""A Unicode-safe URLencoder."""
|
|
try:
|
|
return urllib_urlencode(items)
|
|
except UnicodeEncodeError:
|
|
return urllib_urlencode([(k, force_bytes(v)) for k, v in items])
|
|
|
|
|
|
def absolutify(url: str, request=None) -> str:
|
|
"""Return an absolute URL."""
|
|
if url and url.startswith(('http://', 'https://')):
|
|
return url
|
|
|
|
proto = 'http' if settings.DEBUG else 'https'
|
|
domain = get_current_site(request).domain
|
|
return urljoin(f'{proto}://{domain}', url)
|
|
|
|
|
|
def absolute_url(
|
|
view_name: str, args: Optional[tuple] = None, kwargs: Optional[dict] = None
|
|
) -> str:
|
|
"""Same as django.urls.reverse() but returned as an absolute URL."""
|
|
relative_url = reverse(view_name, args=args, kwargs=kwargs)
|
|
return absolutify(relative_url)
|