diff --git a/.gitignore b/.gitignore index 70d2c1202e645fc31260a0fdd6bac90fdd25e15a..a30a3fa095b66e018388937f3ffb79eed4692301 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,8 @@ htmlcov/ maintenance_mode_state.txt media/ package-lock.json + +# VSCode +.vscode/ +.history/ +*.code-workspace diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8a6b9273572106de9983bbd65662014781aa9535..d36458971660c0b5ab354ad1e4b03068fb46ebea 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,24 +1,6 @@ -stages: - - test - - build - - deploy - include: + - local: "/ci/general.yml" - local: "/ci/test.yml" - local: "/ci/build_dist.yml" - local: "/ci/build_docker.yml" - local: "/ci/deploy.yml" - -variables: - GIT_SUBMODULE_STRATEGY: recursive - PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" - FF_NETWORK_PER_BUILD: "true" - -cache: - key: - files: - - poetry.lock - - pyproject.toml - paths: - - .cache/pip - - .tox diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 2c24e62bb4d1acd60579eb0064c0a79c08748d7f..be12afc1552dc2b6d6cb398bc2a456392ef21b9d 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -48,7 +48,7 @@ Working with the Git repository The Git repository shall be used as a historic documentation of development and as change management. It is important that the Git commit history -describes waht was changed, by whom and why. +describes what was changed, by whom and why. Help and information on Git for beginners are available in the `Git guide`_ diff --git a/aleksis/core/templates/core/crud_events.html b/aleksis/core/templates/core/crud_events.html index 30c4ec464f29a8e99d7b6dd72c0bc824a9e9825f..d80fed9c93b37f5c9786d416038dbbb139176036 100644 --- a/aleksis/core/templates/core/crud_events.html +++ b/aleksis/core/templates/core/crud_events.html @@ -1,31 +1,33 @@ -{% load i18n %} +{% load i18n data_helpers %} <ul class="collection"> {% for event in obj.crud_events %} {% if no_m2m and event.event_type == event.M2M_CHANGE or event.event_type == event.M2M_CHANGE_REV %} {% else %} <li class="collection-item"> - {% if event.event_type == event.CREATE %} - {% blocktrans with person=event.user.person %} - Created by {{ person }} - {% endblocktrans %} - {% elif event.event_type == event.UPDATE %} - {% blocktrans with person=event.user.person %} - Updated by {{ person }} - {% endblocktrans %} - {% elif event.event_type == event.DELETE %} - {% blocktrans with person=event.user.person %} - Deleted by {{ person }} - {% endblocktrans %} - {% elif event.event_type == event.M2M_CHANGE %} - {% blocktrans with person=event.user.person %} - Updated by {{ person }} - {% endblocktrans %} - {% elif event.event_type == event.M2M_CHANGE_REV %} - {% blocktrans with person=event.user.person %} - Updated by {{ person }} - {% endblocktrans %} - {% endif %} + <strong> + {% if event.event_type == event.CREATE %} + {% blocktrans with person=event.user.person %} + Created by {{ person }} + {% endblocktrans %} + {% elif event.event_type == event.UPDATE %} + {% blocktrans with person=event.user.person %} + Updated by {{ person }} + {% endblocktrans %} + {% elif event.event_type == event.DELETE %} + {% blocktrans with person=event.user.person %} + Deleted by {{ person }} + {% endblocktrans %} + {% elif event.event_type == event.M2M_CHANGE %} + {% blocktrans with person=event.user.person %} + Updated by {{ person }} + {% endblocktrans %} + {% elif event.event_type == event.M2M_CHANGE_REV %} + {% blocktrans with person=event.user.person %} + Updated by {{ person }} + {% endblocktrans %} + {% endif %} + </strong> <div class="left" style="margin-right: 10px;"> {% if event.event_type == event.CREATE %} @@ -43,6 +45,17 @@ <div class="right"> {{ event.datetime }} </div> + {% parse_json event.changed_fields as changed_fields %} + {% if changed_fields %} + <ul> + {% for field, change in changed_fields.items %} + {% verbose_name "homework" "assignment" field as verbose_name %} + <li> + {{ verbose_name }}: <s>{{ change.0 }}</s> → {{ change.1 }} + </li> + {% endfor %} + </ul> + {% endif %} </li> {% endif %} {% endfor %} diff --git a/aleksis/core/templates/templated_email/notification.email b/aleksis/core/templates/templated_email/notification.email index bb39f5861876b1dc1c4c39639de649ec6d6590eb..8e61fd0df28b5bc6342c891921831e147e9cc8d9 100644 --- a/aleksis/core/templates/templated_email/notification.email +++ b/aleksis/core/templates/templated_email/notification.email @@ -2,21 +2,48 @@ {% block subject %} {% trans "New notification for" %} {{ notification_user }} {% endblock %} +{% block plain %} + {% blocktrans with notification_user=notification_user %}Dear {{ notification_user }},{% endblocktrans %} + + {% trans "we got a new notification for you:" %} + + {{ notification.title }} + + {{ notification.description }} + + {% if notification.link %} + {% trans "More information" %} → {{ notification.link }} + {% endif %} + + {% blocktrans with trans_sender=notification.sender trans_created_at=notification.created_at %} + Sent by {{ trans_sender }} at {{ trans_created_at }} + {% endblocktrans %} + + {% trans "Your AlekSIS team" %} +{% endblock %} + {% block html %} <main> - <p>{% trans "Dear" %} {{ notification_user }}, <br> - {% trans "we got a new notification for you:" %}</p> + <p>{% blocktrans with notification_user=notification_user %}Dear {{ notification_user }},{% endblocktrans %}</p> + + <p>{% trans "we got a new notification for you:" %}</p> + <blockquote> + <h5>{{ notification.title }}</h5> <p>{{ notification.description }}</p> {% if notification.link %} <a href="{{ notification.link }}">{% trans "More information" %} →</a> {% endif %} </blockquote> - {% blocktrans with trans_sender=notification.sender trans_created_at=notification.created_at %} - <p>By {{ trans_sender }} at {{ trans_created_at }}</p> + <p> + {% blocktrans with trans_sender=notification.sender trans_created_at=notification.created_at %} + Sent by {{ trans_sender }} at {{ trans_created_at }} + {% endblocktrans %} + </p> - <i>Your AlekSIS team</i> - {% endblocktrans %} + <p> + <i>{% trans "Your AlekSIS team" %}</i> + </p> </main> {% endblock %} diff --git a/aleksis/core/templatetags/data_helpers.py b/aleksis/core/templatetags/data_helpers.py index 19f286434db34eb9e74b88950b8d87ee854959ef..f7393c73fd86eba6f861f87e321bf8dc5cbce6fd 100644 --- a/aleksis/core/templatetags/data_helpers.py +++ b/aleksis/core/templatetags/data_helpers.py @@ -1,6 +1,8 @@ -from typing import Any +import json +from typing import Any, Optional, Union from django import template +from django.contrib.contenttypes.models import ContentType register = template.Library() @@ -16,3 +18,24 @@ def get_dict(value: Any, arg: Any) -> Any: return value[int(arg)] else: return None + + +@register.simple_tag +def verbose_name(app_label: str, model: str, field: Optional[str] = None) -> str: + """Get a verbose name of a model or a field by app label and model name.""" + ct = ContentType.objects.get(app_label=app_label, model=model).model_class() + + if field: + # Field + return ct._meta.get_field(field).verbose_name.title() + else: + # Whole model + return ct._meta.verbose_name.title() + + +@register.simple_tag +def parse_json(value: Optional[str] = None) -> Union[dict, None]: + """Template tag for parsing JSON from a string.""" + if not value: + return None + return json.loads(value) diff --git a/aleksis/core/tests/models/test_notification.py b/aleksis/core/tests/models/test_notification.py new file mode 100644 index 0000000000000000000000000000000000000000..1b1a6df054fe24f06bd363e825df3f1259af53e8 --- /dev/null +++ b/aleksis/core/tests/models/test_notification.py @@ -0,0 +1,34 @@ +import pytest + +from aleksis.core.models import Notification, Person + +pytestmark = pytest.mark.django_db + + +def test_email_notification(mailoutbox): + email = "doe@example.com" + recipient = Person.objects.create(first_name="Jane", last_name="Doe", email=email) + + sender = "Foo" + title = "There is happened something." + description = "Here you get some more information." + link = "https://aleksis.org/" + + notification = Notification( + sender=sender, recipient=recipient, title=title, description=description, link=link + ) + notification.save() + + assert notification.sent + + assert len(mailoutbox) == 1 + + mail = mailoutbox[0] + + assert email in mail.to + assert title in mail.body + assert description in mail.body + assert link in mail.body + assert sender in mail.body + assert recipient.addressing_name in mail.subject + assert recipient.addressing_name in mail.body diff --git a/aleksis/core/tests/templatetags/test_data_helpers.py b/aleksis/core/tests/templatetags/test_data_helpers.py index ce43e578dbd84f95ed96a6cf1426d615f7e9c816..176e08b22ecb252d5b0113583ab534f46e978072 100644 --- a/aleksis/core/tests/templatetags/test_data_helpers.py +++ b/aleksis/core/tests/templatetags/test_data_helpers.py @@ -1,4 +1,10 @@ -from aleksis.core.templatetags.data_helpers import get_dict +import json + +import pytest + +from aleksis.core.templatetags.data_helpers import get_dict, parse_json, verbose_name + +pytestmark = pytest.mark.django_db def test_get_dict_object(): @@ -24,3 +30,24 @@ def test_get_dict_invalid(): _foo = 12 assert get_dict(_foo, "bar") is None + + +def test_verbose_name_model(): + assert verbose_name("core", "person") == "Person" + + +def test_verbose_name_field(): + assert verbose_name("core", "person", "first_name") == "First Name" + + +def test_parse_json_json(): + foo = {"foo": 12, "bar": "12", "baz": []} + foo_json = json.dumps(foo) + + assert parse_json(foo_json) == foo + assert parse_json("{}") == {} + + +def test_parse_json_empty(): + assert parse_json(None) is None + assert parse_json("") is None diff --git a/aleksis/core/util/notifications.py b/aleksis/core/util/notifications.py index c9cbcb9f76700d3e26f9c448a5e02c5e103a4936..38d27057033752d6ae7cdefce9c53b4728447b80 100644 --- a/aleksis/core/util/notifications.py +++ b/aleksis/core/util/notifications.py @@ -73,13 +73,13 @@ def send_notification(notification: Union[int, "Notification"], resend: bool = F If resend is passed as True, the notification is sent even if it was previously marked as sent. """ - channels = lazy_preference("notification", "channels") - if isinstance(notification, int): - notification = apps.get_model("core", "Notification") - notification_ = notification.objects.get(pk=notification) + Notification = apps.get_model("core", "Notification") + notification = Notification.objects.get(pk=notification) + + channels = [notification.recipient.preferences["notification__channels"]] - if resend or not notification_.sent: + if resend or not notification.sent: for channel in channels: name, check, send = _CHANNELS_MAP[channel] if check(): diff --git a/aleksis/core/util/predicates.py b/aleksis/core/util/predicates.py index 0e452ddb0cb560ba4edbe174c560ade56c6261ed..975273d7d934ae41df8ecefb494b121ed69b2a94 100644 --- a/aleksis/core/util/predicates.py +++ b/aleksis/core/util/predicates.py @@ -98,6 +98,12 @@ def is_group_owner(user: User, group: Group) -> bool: return group.owners.filter(owners=user.person).exists() +@predicate +def is_group_member(user: User, group: Group) -> bool: + """Predicate which checks if the user is a member of the provided group.""" + return user.person in group.members.all() + + @predicate def is_notification_recipient(user: User, obj: Model) -> bool: """Check if is a notification recipient. diff --git a/ci/build_docker.yml b/ci/build_docker.yml index 83955938ce3f756d7947d18515cdf469a7bd92d8..92a1572c6ac4d1878110500f458262a6a7c5bdf4 100644 --- a/ci/build_docker.yml +++ b/ci/build_docker.yml @@ -1,9 +1,7 @@ -image: registry.edugit.org/teckids/team-sysadmin/docker-images/python-pimped:master - build_docker: stage: build image: - name: gcr.io/kaniko-project/executor:v0.22.0 + name: gcr.io/kaniko-project/executor:debug entrypoint: [""] script: - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" >/kaniko/.docker/config.json diff --git a/ci/general.yml b/ci/general.yml new file mode 100644 index 0000000000000000000000000000000000000000..58891983dbeda6b9c8f49dc07e490a021a4c31dd --- /dev/null +++ b/ci/general.yml @@ -0,0 +1,20 @@ +image: registry.edugit.org/teckids/team-sysadmin/docker-images/python-pimped:latest + +stages: + - test + - build + - deploy + +variables: + GIT_SUBMODULE_STRATEGY: recursive + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + FF_NETWORK_PER_BUILD: "true" + +cache: + key: + files: + - poetry.lock + - pyproject.toml + paths: + - .cache/pip + - .tox diff --git a/poetry.lock b/poetry.lock index cbd4fbb8216f54319da1281ed0cab5e2a3a1fb6c..0ead8c370fec4f3505f48da1f8af8bafe536ffd4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -665,7 +665,7 @@ description = "A pluggable framework for adding two-factor authentication to Dja name = "django-otp" optional = false python-versions = "*" -version = "0.9.0" +version = "0.9.1" [package.dependencies] django = ">=1.11" @@ -698,10 +698,6 @@ version = "3.0.1" Django = ">=1.11.3" babel = "*" -[package.dependencies.phonenumbers] -optional = true -version = ">=7.0.2" - [package.extras] phonenumbers = ["phonenumbers (>=7.0.2)"] phonenumberslite = ["phonenumberslite (>=7.0.2)"] @@ -1003,10 +999,10 @@ description = "flake8 plugin to call black as a code style validator" name = "flake8-black" optional = false python-versions = "*" -version = "0.1.2" +version = "0.2.0" [package.dependencies] -black = ">=19.3b0" +black = "*" flake8 = ">=3.0.0" [[package]] @@ -1792,7 +1788,7 @@ description = "A simple tool/library for working with SPDX license definitions." name = "spdx-license-list" optional = false python-versions = "*" -version = "0.4.0" +version = "0.5.0" [[package]] category = "dev" @@ -2114,7 +2110,7 @@ celery = ["Celery", "django-celery-results", "django-celery-beat", "django-celer ldap = ["django-auth-ldap"] [metadata] -content-hash = "4ed9748bdca432a11eca81cfeccd2c141b3d3d895c8382a70bc79c622fd023d6" +content-hash = "3e85d3bfff56719272c13ad591ca4ccd7a2b72324a33c2192be383536b66b5f8" python-versions = "^3.7" [metadata.files] @@ -2365,8 +2361,8 @@ django-middleware-global-request = [ {file = "django-middleware-global-request-0.1.2.tar.gz", hash = "sha256:f6490759bc9f7dbde4001709554e29ca715daf847f2222914b4e47117dca9313"}, ] django-otp = [ - {file = "django-otp-0.9.0.tar.gz", hash = "sha256:f5faa95a3e85391e70e433205509fa070ed25646f15fcafd2cd2fbd987c33262"}, - {file = "django_otp-0.9.0-py3-none-any.whl", hash = "sha256:334e2a0ece7e5d9de3263e17bd3b6aee2809d1f8d70555408d5bf8f0c33b13fb"}, + {file = "django-otp-0.9.1.tar.gz", hash = "sha256:f456639addace8b6d1eb77f9edaada1a53dbb4d6f3c19f17c476c4e3e4beb73f"}, + {file = "django_otp-0.9.1-py3-none-any.whl", hash = "sha256:0c67cf6f4bd6fca84027879ace9049309213b6ac81f88e954376a6b5535d96c4"}, ] django-otp-yubikey = [ {file = "django-otp-yubikey-0.5.2.tar.gz", hash = "sha256:f0b1881562fb42ee9f12c28d284cbdb90d1f0383f2d53a595373b080a19bc261"}, @@ -2454,7 +2450,7 @@ flake8-bandit = [ {file = "flake8_bandit-2.1.2.tar.gz", hash = "sha256:687fc8da2e4a239b206af2e54a90093572a60d0954f3054e23690739b0b0de3b"}, ] flake8-black = [ - {file = "flake8-black-0.1.2.tar.gz", hash = "sha256:b79d8d868bd42dc2c1f27469b92a984ecab3579ad285a8708ea5f19bf6c1f3a2"}, + {file = "flake8-black-0.2.0.tar.gz", hash = "sha256:10e7ff9f81f637a9471684e5624d6a32b11cba362b38df4e20fc8f761184440b"}, ] flake8-builtins = [ {file = "flake8-builtins-1.5.3.tar.gz", hash = "sha256:09998853b2405e98e61d2ff3027c47033adbdc17f9fe44ca58443d876eb00f3b"}, @@ -2889,8 +2885,8 @@ soupsieve = [ {file = "soupsieve-1.9.6.tar.gz", hash = "sha256:7985bacc98c34923a439967c1a602dc4f1e15f923b6fcf02344184f86cc7efaa"}, ] spdx-license-list = [ - {file = "spdx_license_list-0.4.0-py3-none-any.whl", hash = "sha256:e5c2d1efc4067ff83609a200c731db6c656fdfd26144ac8b50755d6c72515453"}, - {file = "spdx_license_list-0.4.0.tar.gz", hash = "sha256:f8b5eeda2a1c88d8ce15f6324d5a6128a462932a2e55b032f017ac9a0e61f1c7"}, + {file = "spdx_license_list-0.5.0-py3-none-any.whl", hash = "sha256:65c9f598dee3249d529300eb08800f8bf3d0d902868669146ada65192ecd0507"}, + {file = "spdx_license_list-0.5.0.tar.gz", hash = "sha256:40cd53ff16401bab7059e6d1ef61839196b12079929a2763a50145d3b6949bc1"}, ] sphinx = [ {file = "Sphinx-3.0.3-py3-none-any.whl", hash = "sha256:f5505d74cf9592f3b997380f9bdb2d2d0320ed74dd69691e3ee0644b956b8d83"}, diff --git a/pyproject.toml b/pyproject.toml index 103d35de097f7e1afe2c862ae62937be0d976d72..47ff4e661cf5237fb6f2245f84de728f97ff2b0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,7 @@ django-celery-beat = {version="^2.0.0", optional=true} django-celery-email = {version="^3.0.0", optional=true} django-jsonstore = "^0.4.1" django-polymorphic = "^2.1.2" -django-otp = "0.9.0" +django-otp = "0.9.1" django-colorfield = "^0.3.0" django-bleach = "^0.6.1" django-guardian = "^2.2.0" @@ -81,7 +81,7 @@ django-cache-memoize = "^0.1.6" django-haystack = {version="3.0b1", allow-prereleases = true} celery-haystack = {version="^0.10.0", optional=true} django-dbbackup = "^3.3.0" -spdx-license-list = "^0.4.0" +spdx-license-list = "^0.5.0" license-expression = "^1.2" django-reversion = "^3.0.7" django-favicon-plus-reloaded = "^1.0.4" @@ -109,7 +109,7 @@ flake8-builtins = "^1.4.1" flake8-docstrings = "^1.5.0" flake8-rst-docstrings = "^0.0.13" black = "^19.10b0" -flake8-black = "^0.1.1" +flake8-black = "^0.2.0" isort = "^4.3.21" flake8-isort = "^3.0.0" pytest-cov = "^2.8.1"