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
  • hansegucker/AlekSIS-Core
  • pinguin/AlekSIS-Core
  • AlekSIS/official/AlekSIS-Core
  • sunweaver/AlekSIS-Core
  • sggua/AlekSIS-Core
  • edward/AlekSIS-Core
  • magicfelix/AlekSIS-Core
7 results
Show changes
Commits on Source (93)
......@@ -13,6 +13,8 @@ include:
file: /ci/publish/pypi.yml
- project: "AlekSIS/official/AlekSIS"
file: /ci/docker/image.yml
- project: "AlekSIS/official/AlekSIS"
file: "/ci/deploy/review.yml"
- project: "AlekSIS/official/AlekSIS"
file: "/ci/deploy/trigger_dist.yml"
- project: "AlekSIS/official/AlekSIS"
......
FROM python:3.9-buster AS core
# Build arguments
ARG EXTRAS="ldap"
ARG EXTRAS="ldap,s3"
ARG APP_VERSION=""
# Configure Python to be nice inside Docker and pip to stfu
......@@ -47,21 +47,23 @@ RUN case ",$EXTRAS," in \
# Install core
RUN set -e; \
mkdir -p /var/lib/aleksis/media /usr/share/aleksis/static /var/lib/aleksis/backups; \
mkdir -p ${ALEKSIS_static__root} \
${ALEKSIS_media__root} \
${ALEKSIS_backup__location}; \
eatmydata pip install AlekSIS-Core\[$EXTRAS\]$APP_VERSION
# Declare a persistent volume for all data
VOLUME /var/lib/aleksis
# Define entrypoint and uWSGI running on port 8000
# Define entrypoint, volumes and uWSGI running on port 8000
EXPOSE 8000
VOLUME ${ALEKSIS_media__root} ${ALEKSIS_backup__location}
COPY docker-startup.sh /usr/local/bin/aleksis-docker-startup
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["/usr/local/bin/aleksis-docker-startup"]
# Install assets
FROM core as assets
RUN eatmydata aleksis-admin yarn install
RUN eatmydata aleksis-admin yarn install; \
eatmydata aleksis-admin collectstatic --no-input; \
rm -rf /usr/local/share/.cache
# Clean up build dependencies
FROM assets AS clean
......@@ -72,9 +74,33 @@ RUN set -e; \
libpq-dev \
libssl-dev \
libldap2-dev \
libsasl2-dev \
yarnpkg; \
libsasl2-dev; \
eatmydata apt-get autoremove --purge -y; \
apt-get clean -y; \
rm -f /var/lib/apt/lists/*_*; \
rm -rf /root/.cache
# Drop privileges for runtime to www-data
FROM clean AS unprivileged
WORKDIR /var/lib/aleksis
RUN chown -R www-data:www-data \
${ALEKSIS_static__root} \
${ALEKSIS_media__root} \
${ALEKSIS_backup__location}
USER 33:33
# Additional steps
ONBUILD ARG APPS
ONBUILD USER 0:0
ONBUILD RUN set -e; \
if [ -n "$APPS" ]; then \
eatmydata pip install $APPS; \
fi; \
eatmydata aleksis-admin yarn install; \
eatmydata aleksis-admin collectstatic --no-input; \
rm -rf /usr/local/share/.cache; \
eatmydata apt-get remove --purge -y yarnpkg; \
eatmydata apt-get autoremove --purge -y; \
apt-get clean -y; \
rm -f /var/lib/apt/lists/*_*; \
rm -rf /root/.cache
ONBUILD USER 33:33
from datetime import datetime, time
from typing import Callable, Dict, List, Sequence, Tuple
from django import forms
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.db.models import QuerySet
from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _
from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget, Select2Widget
......@@ -370,3 +373,144 @@ class DashboardWidgetOrderForm(ExtensibleForm):
DashboardWidgetOrderFormSet = forms.formset_factory(
form=DashboardWidgetOrderForm, max_num=0, extra=0
)
class ActionForm(forms.Form):
"""Generic form for executing actions on multiple items of a queryset.
This should be used together with a ``Table`` from django-tables2
which includes a ``SelectColumn``.
The queryset can be defined in two different ways:
You can use ``get_queryset`` or provide ``queryset`` as keyword argument
at the initialization of this form class.
If both are declared, it will use the keyword argument.
Any actions can be defined using the ``actions`` class attribute
or overriding the method ``get_actions``.
The actions use the same syntax like the Django Admin actions with one important difference:
Instead of the related model admin,
these actions will get the related ``ActionForm`` as first argument.
Here you can see an example for such an action:
.. code-block:: python
from django.utils.translation import gettext as _
def example_action(form, request, queryset):
# Do something with this queryset
example_action.short_description = _("Example action")
If you can include the ``ActionForm`` like any other form in your views,
but you must add the request as first argument.
When the form is valid, you should run ``execute``:
.. code-block:: python
from aleksis.core.forms import ActionForm
def your_view(request, ...):
# Something
action_form = ActionForm(request, request.POST or None, ...)
if request.method == "POST" and form.is_valid():
form.execute()
# Something
"""
layout = Layout("action")
actions = []
def get_actions(self) -> Sequence[Callable]:
"""Get all defined actions."""
return self.actions
def _get_actions_dict(self) -> Dict[str, Callable]:
"""Get all defined actions as dictionary."""
return {value.__name__: value for value in self.get_actions()}
def _get_action_choices(self) -> List[Tuple[str, str]]:
"""Get all defined actions as Django choices."""
return [
(value.__name__, getattr(value, "short_description", value.__name__))
for value in self.get_actions()
]
def get_queryset(self) -> QuerySet:
"""Get the related queryset."""
raise NotImplementedError("Queryset necessary.")
action = forms.ChoiceField(choices=[])
selected_objects = forms.ModelMultipleChoiceField(queryset=None)
def __init__(self, request: HttpRequest, *args, queryset: QuerySet = None, **kwargs):
self.request = request
self.queryset = queryset if isinstance(queryset, QuerySet) else self.get_queryset()
super().__init__(*args, **kwargs)
self.fields["selected_objects"].queryset = self.queryset
self.fields["action"].choices = self._get_action_choices()
def execute(self) -> bool:
"""Execute the selected action on all selected objects.
:return: If the form is not valid, it will return ``False``.
"""
if self.is_valid():
data = self.cleaned_data["selected_objects"]
action = self._get_actions_dict()[self.cleaned_data["action"]]
action(None, self.request, data)
return True
return False
class ListActionForm(ActionForm):
"""Generic form for executing actions on multiple items of a list of dictionaries.
Sometimes you want to implement actions for data from different sources
than querysets or even querysets from multiple models. For these cases,
you can use this form.
To provide an unique identification of each item, the dictionaries **must**
include the attribute ``pk``. This attribute has to be unique for the whole list.
If you don't mind this aspect, this will cause unexpected behavior.
Any actions can be defined as described in ``ActionForm``, but, of course,
the last argument won't be a queryset but a list of dictionaries.
For further information on usage, you can take a look at ``ActionForm``.
"""
selected_objects = forms.MultipleChoiceField(choices=[])
def get_queryset(self):
# Return None in order not to raise an unwanted exception
return None
def _get_dict(self) -> Dict[str, dict]:
"""Get the items sorted by pk attribute."""
return {item["pk"]: item for item in self.items}
def _get_choices(self) -> List[Tuple[str, str]]:
"""Get the items as Django choices."""
return [(item["pk"], item["pk"]) for item in self.items]
def _get_real_items(self, items: Sequence[dict]) -> List[dict]:
"""Get the real dictionaries from a list of pks."""
items_dict = self._get_dict()
real_items = []
for item in items:
if item not in items_dict:
raise ValidationError(_("No valid selection."))
real_items.append(items_dict[item])
return real_items
def clean_selected_objects(self) -> List[dict]:
data = self.cleaned_data["selected_objects"]
items = self._get_real_items(data)
return items
def __init__(self, request: HttpRequest, items, *args, **kwargs):
self.items = items
super().__init__(request, *args, **kwargs)
self.fields["selected_objects"].choices = self._get_choices()
......@@ -21,6 +21,7 @@ from django.views.generic.edit import DeleteView, ModelFormMixin
import reversion
from guardian.admin import GuardedModelAdmin
from guardian.core import ObjectPermissionChecker
from jsonstore.fields import IntegerField, JSONFieldMixin
from material.base import Layout, LayoutNode
from rules.contrib.admin import ObjectPermissionsModelAdmin
......@@ -332,6 +333,10 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
"""Dynamically add a new permission to a model."""
cls.extra_permissions.append((name, verbose_name))
def set_object_permission_checker(self, checker: ObjectPermissionChecker):
"""Annotate a ``ObjectPermissionChecker`` for use with permission system."""
self._permission_checker = checker
def save(self, *args, **kwargs):
"""Ensure all functionality of our extensions that needs saving gets it."""
# For auto-created remote syncable fields
......
......@@ -151,14 +151,14 @@ MIDDLEWARE = [
"django_prometheus.middleware.PrometheusBeforeMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"debug_toolbar.middleware.DebugToolbarMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django.middleware.http.ConditionalGetMiddleware",
"django_global_request.middleware.GlobalRequestMiddleware",
"django.contrib.sites.middleware.CurrentSiteMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"debug_toolbar.middleware.DebugToolbarMiddleware",
"django_otp.middleware.OTPMiddleware",
"impersonate.middleware.ImpersonateMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
......@@ -213,11 +213,11 @@ merge_app_settings("DATABASES", DATABASES, False)
REDIS_HOST = _settings.get("redis.host", "localhost")
REDIS_PORT = _settings.get("redis.port", 6379)
REDIS_DB = _settings.get("redis.database", 0)
REDIS_USER = _settings.get("redis.user", "default")
REDIS_PASSWORD = _settings.get("redis.password", None)
REDIS_USER = _settings.get("redis.user", None if REDIS_PASSWORD is None else "default")
REDIS_URL = (
f"redis://{REDIS_USER}{':'+REDIS_PASSWORD if REDIS_PASSWORD else ''}@"
f"redis://{REDIS_USER+':'+REDIS_PASSWORD+'@' if REDIS_USER else ''}"
f"{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}"
)
......@@ -401,6 +401,7 @@ MEDIA_ROOT = _settings.get("media.root", os.path.join(BASE_DIR, "media"))
NODE_MODULES_ROOT = _settings.get("node_modules.root", os.path.join(BASE_DIR, "node_modules"))
YARN_INSTALLED_APPS = [
"@fontsource/roboto",
"datatables",
"jquery",
"materialize-css",
......@@ -435,6 +436,7 @@ ANY_JS = {
},
"sortablejs": {"js_url": JS_URL + "/sortablejs/Sortable.min.js"},
"jquery-sortablejs": {"js_url": JS_URL + "/jquery-sortablejs/jquery-sortable.js"},
"Roboto": {"css_url": JS_URL + "/@fontsource/roboto/index.css"},
}
merge_app_settings("ANY_JS", ANY_JS, True)
......@@ -482,9 +484,10 @@ MAINTENANCE_MODE_IGNORE_IP_ADDRESSES = _settings.get(
)
MAINTENANCE_MODE_GET_CLIENT_IP_ADDRESS = "ipware.ip.get_ip"
MAINTENANCE_MODE_IGNORE_SUPERUSER = True
MAINTENANCE_MODE_STATE_FILE_PATH = _settings.get(
MAINTENANCE_MODE_STATE_FILE_NAME = _settings.get(
"maintenance.statefile", "maintenance_mode_state.txt"
)
MAINTENANCE_MODE_STATE_BACKEND = "maintenance_mode.backends.DefaultStorageBackend"
DBBACKUP_STORAGE = _settings.get("backup.storage", "django.core.files.storage.FileSystemStorage")
DBBACKUP_STORAGE_OPTIONS = {"location": _settings.get("backup.location", "/var/backups/aleksis")}
......@@ -781,3 +784,36 @@ DBBACKUP_CHECK_SECONDS = _settings.get("backup.database.check_seconds", 7200)
MEDIABACKUP_CHECK_SECONDS = _settings.get("backup.media.check_seconds", 7200)
PROMETHEUS_EXPORT_MIGRATIONS = False
if _settings.get("storage.s3.enabled", False):
INSTALLED_APPS.append("storages")
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
if _settings.get("storage.s3.static.enabled", False):
STATICFILES_STORAGE = "storages.backends.s3boto3.S3StaticStorage"
AWS_STORAGE_BUCKET_NAME_STATIC = _settings.get("storage.s3.static.bucket_name", "")
AWS_S3_MAX_AGE_SECONDS_CACHED_STATIC = _settings.get(
"storage.s3.static.max_age_seconds", 24 * 60 * 60
)
AWS_REGION = _settings.get("storage.s3.region", "")
AWS_ACCESS_KEY_ID = _settings.get("storage.s3.access_key_id", "")
AWS_SECRET_ACCESS_KEY = _settings.get("storage.s3.secret_key", "")
AWS_SESSION_TOKEN = _settings.get("storage.s3.session_token", "")
AWS_STORAGE_BUCKET_NAME = _settings.get("storage.s3.bucket_name", "")
AWS_S3_ADDRESSING_STYLE = _settings.get("storage.s3.addressing_style", "auto")
AWS_S3_ENDPOINT_URL = _settings.get("storage.s3.endpoint_url", "")
AWS_S3_KEY_PREFIX = _settings.get("storage.s3.key_prefix", "")
AWS_S3_BUCKET_AUTH = _settings.get("storage.s3.bucket_auth", True)
AWS_S3_MAX_AGE_SECONDS = _settings.get("storage.s3.max_age_seconds", 24 * 60 * 60)
AWS_S3_PUBLIC_URL = _settings.get("storage.s3.public_url", "")
AWS_S3_REDUCED_REDUNDANCY = _settings.get("storage.s3.reduced_redundancy", False)
AWS_S3_CONTENT_DISPOSITION = _settings.get("storage.s3.content_disposition", "")
AWS_S3_CONTENT_LANGUAGE = _settings.get("storage.s3.content_language", "")
AWS_S3_METADATA = _settings.get("storage.s3.metadata", {})
AWS_S3_ENCRYPT_KEY = _settings.get("storage.s3.encrypt_key", False)
AWS_S3_KMS_ENCRYPTION_KEY_ID = _settings.get("storage.s3.kms_encryption_key_id", "")
AWS_S3_GZIP = _settings.get("storage.s3.gzip", True)
AWS_S3_SIGNATURE_VERSION = _settings.get("storage.s3.signature_version", None)
AWS_S3_FILE_OVERWRITE = _settings.get("storage.s3.file_overwrite", False)
$(document).ready(function () {
$(".select--header-box").change(function () {
/*
If the top checkbox is checked, all sub checkboxes should be checked,
if it gets unchecked, all other ones should get unchecked.
*/
if ($(this).is(":checked")) {
$(this).closest("table").find('input[name="selected_objects"]').prop({
indeterminate: false,
checked: true,
});
} else {
$(this).closest("table").find('input[name="selected_objects"]').prop({
indeterminate: false,
checked: false,
});
}
});
$('input[name="selected_objects"]').change(function () {
/*
If a table checkbox changes, check the state of the other ones.
If all boxes are checked the box in the header should be checked,
if all boxes are unchecked the header box should be unchecked. If
only some boxes are checked the top one should be inderteminate.
*/
let checked = $(this).is(":checked");
let indeterminate = false;
let table = $(this).closest("table");
table.find('input[name="selected_objects"]').each(function () {
if ($(this).is(":checked") !== checked) {
/* Set the header box to indeterminate if the boxes are not the same */
table.find(".select--header-box").prop({
indeterminate: true,
})
indeterminate = true;
return false;
}
});
if (!(indeterminate)) {
/* All boxes are the same, set the header box to the same value */
table.find(".select--header-box").prop({
indeterminate: false,
checked: checked,
});
}
});
});
......@@ -400,7 +400,7 @@ th.orderable > a {
}
th.orderable > a::after {
@extend i.material-icons;
@extend .material-icons;
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
......
......@@ -18,6 +18,7 @@
{# CSS #}
{% include_css "material-design-icons" %}
{% include_css "Roboto" %}
<link rel="stylesheet" href="{% sass_src 'style.scss' %}">
{# Add JS URL resolver #}
......
......@@ -15,6 +15,7 @@
</title>
{% include_css "material-design-icons" %}
{% include_css "Roboto" %}
{% include_css "paper-css" %}
<link rel="stylesheet" href="{% sass_src 'style.scss' %}"/>
<link rel="stylesheet" href="{% static "print.css" %}"/>
......
from typing import Optional
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User
from django.db.models import Model
......@@ -7,6 +9,7 @@ from guardian.backends import ObjectPermissionBackend
from guardian.shortcuts import get_objects_for_user
from rules import predicate
from ..mixins import ExtensibleModel
from ..models import Group
from .core_helpers import get_content_type_by_perm, get_site_preferences
from .core_helpers import has_person as has_person_helper
......@@ -25,8 +28,20 @@ def check_global_permission(user: User, perm: str) -> bool:
return ModelBackend().has_perm(user, perm)
def check_object_permission(user: User, perm: str, obj: Model) -> bool:
"""Check whether a user has a permission on a object."""
def check_object_permission(
user: User, perm: str, obj: Model, checker_obj: Optional[ExtensibleModel] = None
) -> bool:
"""Check whether a user has a permission on an object.
You can provide a custom ``ObjectPermissionChecker`` for prefetching object permissions
by annotating an extensible model with ``set_object_permission_checker``.
This can be the provided object (``obj``) or a special object
which is only used to get the checker class (``checker_obj``).
"""
if not checker_obj:
checker_obj = obj
if hasattr(checker_obj, "_permission_checker"):
return checker_obj._permission_checker.has_perm(perm, obj)
return ObjectPermissionBackend().has_perm(user, perm, obj)
......
from django.utils.safestring import mark_safe
from django_tables2 import CheckBoxColumn
from django_tables2.utils import A, AttributeDict, computed_values
class MaterializeCheckboxColumn(CheckBoxColumn):
"""Checkbox column with Materialize support."""
empty_values = ()
@property
def header(self):
"""Render the header cell."""
default = {"type": "checkbox"}
general = self.attrs.get("input")
specific = self.attrs.get("th__input")
attrs = AttributeDict(default, **(specific or general or {}))
return mark_safe("<label><input %s/><span></span></label>" % attrs.as_html()) # noqa
def render(self, value, bound_column, record):
"""Render a data cell."""
default = {"type": "checkbox", "name": bound_column.name, "value": value}
if self.is_checked(value, record):
default.update({"checked": "checked"})
general = self.attrs.get("input")
specific = self.attrs.get("td__input")
attrs = dict(default, **(specific or general or {}))
attrs = computed_values(attrs, kwargs={"record": record, "value": value})
return mark_safe( # noqa
"<label><input %s/><span></span</label>" % AttributeDict(attrs).as_html()
)
class SelectColumn(MaterializeCheckboxColumn):
"""Column with a check box prepared for `ActionForm` forms."""
def __init__(self, *args, **kwargs):
kwargs["attrs"] = {
"td__input": {"name": "selected_objects"},
"th__input": {"class": "select--header-box"},
}
kwargs.setdefault("accessor", A("pk"))
super().__init__(*args, **kwargs)
#!/bin/bash
HTTP_PORT=${HTTP_PORT:8000}
if [[ -z $ALEKSIS_secret_key ]]; then
if [[ ! -e /var/lib/aleksis/secret_key ]]; then
touch /var/lib/aleksis/secret_key; chmod 600 /var/lib/aleksis/secret_key
LC_ALL=C tr -dc 'A-Za-z0-9!"#$%&'\''()*+,-./:;<=>?@[\]^_`{|}~' </dev/urandom | head -c 64 >/var/lib/aleksis/secret_key
fi
ALEKSIS_secret_key=$(</var/lib/aleksis/secret_key)
fi
echo -n "Waiting for database."
while ! aleksis-admin dbshell -- -c "SELECT 1" >/dev/null 2>&1; do
sleep 0.5
echo -n .
done
echo
aleksis-admin migrate
aleksis-admin createinitialrevisions
aleksis-admin compilescss
aleksis-admin collectstatic --no-input --clear
exec aleksis-admin runuwsgi -- --http-socket=:$HTTP_PORT
#!/bin/sh
#-
# Startup/entrypoint script for deployments based on Docker, vanilla or K8s
#
# Designed to be used in Kubernetes in a way such that this container is used
# in four places:
#
# 1. The app pod(s), setting PREPARE = 0
# 2. The celery-worker pod(s), setting PREPARE = 0 and RUN_MODE = celery-worker
# 3. One celery-beat pod, setting PREPARE = 0 and RUN_MODE = celery-beat
# 4. A post-deploy job, setting RUN_MODE = prepare
#
# To run as stand-alone Docker container, bundling all components, set
# ALEKSIS_dev__uwsgi__celery = true.
# Run mode to start container in
#
# uwsgi - application server
# celery-$foo - celery commands (e.g. worker or beat)
# * - Anything else to run arguments verbatim
RUN_MODE=${RUN_MODE:-uwsgi}
# HTTP port to let uWSGI bind to
HTTP_PORT=${HTTP_PORT:-8000}
# Run preparation steps before real command
PREPARE=${PREPARE:-1}
wait_migrations() {
# Wait for migrations to be applied from elsewhere, e.g. a K8s job
echo -n "Waiting for migrations to appear"
until aleksis-admin migrate --check >/dev/null 2>&1; do
sleep 0.5
echo -n .
done
echo
}
wait_database() {
# Wait for database to be reachable
echo -n "Waiting for database."
until aleksis-admin dbshell -- -c "SELECT 1" >/dev/null 2>&1; do
sleep 0.5
echo -n .
done
echo
}
prepare_database() {
# Migrate database; should only be run in app container or job
aleksis-admin migrate
aleksis-admin createinitialrevisions
}
# Wait for database to be reachable under all conditions
wait_database
case "$RUN_MODE" in
uwsgi)
# uWSGI app server mode
if [ $PREPARE = 1 ]; then
# Responsible for running migratiosn and preparing staticfiles
prepare_database
else
# Wait for migrations to be applied elsewhere
wait_migrations
fi
exec aleksis-admin runuwsgi -- --http-socket=:$HTTP_PORT
;;
celery-*)
# Celery command mode
if [ $PREPARE = 1 ]; then
# Responsible for running migrations
prepare_database
else
# Wait for migrations to be applied elsewhere
wait_migrations
fi
exec celery -A aleksis.core ${RUN_MODE#celery-}
;;
prepare)
# Preparation only mode
prepare_database
;;
*)
# Run arguments as command verbatim
exec "$@"
;;
esac
Storage
##########
Amazon S3
*********
AlekSIS allows you to configure an Amazon S3 endpoint for static and media
files. This is useful e.g. for loadbalancing with multiple AlekSIS
instances.
Configure an S3 endpoint
=======================
If you want to use an S3 endpoint to store files you have to configure the
endpoint in your configuration file (`/etc/aleksis/aleksis.toml`)::
# Default values
[storage.s3]
enabled = true
endpoint_url = "https://minio.example.com"
bucket_name = "aleksis-test"
access_key_id = "XXXXXXXXXXXXXX"
secret_key = "XXXXXXXXXXXXXXXXXXXXXX"
This diff is collapsed.
......@@ -36,9 +36,9 @@ secondary = true
python = "^3.7"
Django = "^3.1.7"
django-any-js = "^1.0"
django-debug-toolbar = "^2.0"
django-debug-toolbar = "^3.2"
django-middleware-global-request = "^0.1.2"
django-menu-generator-ng = "^1.2.0"
django-menu-generator-ng = "^1.2.3"
django-tables2 = "^2.1"
Pillow = "^8.0"
django-phonenumber-field = {version = "<5.1", extras = ["phonenumbers"]}
......@@ -48,7 +48,7 @@ colour = "^0.1.5"
dynaconf = {version = "^3.1", extras = ["yaml", "toml", "ini"]}
django-settings-context-processor = "^0.2"
django-auth-ldap = { version = "^2.2", optional = true }
django-maintenance-mode = "^0.15.0"
django-maintenance-mode = "^0.16.0"
django-ipware = "^3.0"
django-impersonate = "^1.4"
django-hattori = "^0.2"
......@@ -66,19 +66,19 @@ django-templated-email = "^2.3.0"
html2text = "^2020.0.0"
django-ckeditor = "^6.0.0"
django-js-reverse = "^0.9.1"
calendarweek = "^0.4.3"
calendarweek = "^0.5.0"
Celery = {version="^5.0.0", extras=["django", "redis"]}
django-celery-results = "^2.0.1"
django-celery-beat = "^2.2.0"
django-celery-email = "^3.0.0"
django-jsonstore = "^0.5.0"
django-polymorphic = "^3.0.0"
django-colorfield = "^0.3.0"
django-colorfield = "^0.4.0"
django-bleach = "^0.6.1"
django-guardian = "^2.2.0"
rules = "^2.2"
django-cache-memoize = "^0.1.6"
django-haystack = {version="3.0b1", allow-prereleases = true}
django-haystack = {version="3.0", allow-prereleases = true}
celery-haystack-ng = "^0.20"
django-dbbackup = "^3.3.0"
spdx-license-list = "^0.5.0"
......@@ -87,7 +87,7 @@ django-reversion = "^3.0.7"
django-favicon-plus-reloaded = "^1.0.4"
django-health-check = "^3.12.1"
psutil = "^5.7.0"
celery-progress = "^0.0.14"
celery-progress = "^0.1.0"
django-cachalot = "^2.3.2"
django-prometheus = "^2.1.0"
importlib-metadata = {version = "^3.0.0", python = "<3.9"}
......@@ -97,9 +97,12 @@ django-uwsgi-ng = "^1.1.0"
django-extensions = "^3.1.1"
ipython = "^7.20.0"
django-redis = "^4.12.1"
django-storages = {version = "^1.11.1", optional = true}
boto3 = {version = "^1.17.33", optional = true}
[tool.poetry.extras]
ldap = ["django-auth-ldap"]
s3 = ["boto3", "django-storages"]
[tool.poetry.dev-dependencies]
aleksis-builddeps = "*"
......