From 0fff53ee3c25abaa4f38d54941a31a1fe767249a Mon Sep 17 00:00:00 2001
From: Christophe Henry <contact@c-henry.fr>
Date: Wed, 6 Nov 2019 17:52:48 +0100
Subject: [PATCH] feat: Notifications management page (backend)
 (startinblox/applications/sib-app#255)

- feat: Add NotificationsSettings model (startinblox/djangoldp-packages/djangoldp-notifications#18)
- feat: Expose the model and expose user's subscriptions(startinblox/djangoldp-packages/djangoldp-notifications#19
---
 .gitlab-ci.yml                                |  19 ++-
 djangoldp_notification/djangoldp_urls.py      |  11 ++
 djangoldp_notification/middlewares.py         |  31 +++++
 .../migrations/0003_auto_20191114_1033.py     |  42 +++++++
 .../migrations/0004_auto_20191119_1540.py     |  29 +++++
 djangoldp_notification/models.py              | 110 +++++++++++++++---
 djangoldp_notification/permissions.py         |   6 +-
 djangoldp_notification/settings.py            |   2 +-
 djangoldp_notification/tests/conftest.py      |  15 +++
 djangoldp_notification/tests/factories.py     |  36 ++++++
 djangoldp_notification/tests/test_models.py   |  28 +++++
 djangoldp_notification/tests/test_views.py    |  78 +++++++++++++
 djangoldp_notification/tests/utils.py         |   5 +
 djangoldp_notification/views.py               |  25 ++++
 setup.cfg                                     |   5 +-
 15 files changed, 419 insertions(+), 23 deletions(-)
 create mode 100644 djangoldp_notification/djangoldp_urls.py
 create mode 100644 djangoldp_notification/middlewares.py
 create mode 100644 djangoldp_notification/migrations/0003_auto_20191114_1033.py
 create mode 100644 djangoldp_notification/migrations/0004_auto_20191119_1540.py
 create mode 100644 djangoldp_notification/tests/conftest.py
 create mode 100644 djangoldp_notification/tests/factories.py
 create mode 100644 djangoldp_notification/tests/test_models.py
 create mode 100644 djangoldp_notification/tests/test_views.py
 create mode 100644 djangoldp_notification/tests/utils.py
 create mode 100644 djangoldp_notification/views.py

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 137fb38..49d79db 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -5,10 +5,27 @@ stages:
   - test
   - release
 
+variables:
+  SIB_PROJECT_NAME: "sib_test"
+  SIB_PROJECT_PATH: "$CI_BUILD_DIR/SIB_PROJECT_NAME"
+  CI_BUILD_DIR: "/builds/startingblox"
+  DJANGO_SETTINGS_MODULE: "$SIB_PROJECT_NAME.settings"
+  PYTHONPATH: "$CI_BUILD_DIR/$SIB_PROJECT_NAME"
+
 test:
   stage: test
+  before_script:
+    - export PATH="$PATH:/root/.local/bin"
+    - mkdir -p $CI_BUILD_DIR
+    - cd $CI_BUILD_DIR
+    - pip install git+https://git.happy-dev.fr/startinblox/devops/sib.git@test-mode
+    - DJANGO_SETTINGS_MODULE="" sib startproject $SIB_PROJECT_NAME --test
+    - cd $CI_BUILD_DIR/$SIB_PROJECT_NAME
+    - sib install $SIB_PROJECT_NAME
+    - cd $CI_PROJECT_DIR
+    - pip install -e .[dev]
   script:
-    - echo 'Make your tests here !'
+    - pytest
   except:
     - master
   tags:
diff --git a/djangoldp_notification/djangoldp_urls.py b/djangoldp_notification/djangoldp_urls.py
new file mode 100644
index 0000000..1a82fbf
--- /dev/null
+++ b/djangoldp_notification/djangoldp_urls.py
@@ -0,0 +1,11 @@
+from django.conf.urls import url
+
+from djangoldp_notification.views import UserSubscriptionsViewset
+
+urlpatterns = [
+    url(
+        r'^user-subscriptions/(?P<slug>\w+)/$',
+        UserSubscriptionsViewset.as_view(UserSubscriptionsViewset.list_actions),
+        name="user-subscriptions"
+    ),
+]
diff --git a/djangoldp_notification/middlewares.py b/djangoldp_notification/middlewares.py
new file mode 100644
index 0000000..d67000a
--- /dev/null
+++ b/djangoldp_notification/middlewares.py
@@ -0,0 +1,31 @@
+from django.db.models import signals
+
+
+MODEL_MODIFICATION_USER_FIELD = 'modification_user'
+
+
+class CurrentUserMiddleware:
+    def __init__(self, get_response=None):
+        self.get_response = get_response
+
+    def __call__(self, request):
+        self.process_request(request)
+        response = self.get_response(request)
+        signals.pre_save.disconnect(dispatch_uid=request)
+        return response
+
+    def process_request(self, request):
+        if request.method in ('GET', 'HEAD', 'OPTION'):
+            # this request shouldn't update anything
+            # so no signal handler should be attached
+            return
+
+        if hasattr(request, 'user') and request.user.is_authenticated():
+            user = request.user
+        else:
+            user = None
+
+        def _update_users(sender, instance, **kwargs):
+            setattr(instance, MODEL_MODIFICATION_USER_FIELD, user)
+
+        signals.pre_save.connect(_update_users, dispatch_uid=request, weak=False)
diff --git a/djangoldp_notification/migrations/0003_auto_20191114_1033.py b/djangoldp_notification/migrations/0003_auto_20191114_1033.py
new file mode 100644
index 0000000..d3ec4c1
--- /dev/null
+++ b/djangoldp_notification/migrations/0003_auto_20191114_1033.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.25 on 2019-11-14 10:33
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import djangoldp.fields
+import djangoldp_notification.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('djangoldp_notification', '0002_auto_20190917_1107'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='NotificationSettings',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('urlid', djangoldp.fields.LDPUrlField(blank=True, null=True, unique=True)),
+                ('receive_by_email', models.BooleanField(default=True, verbose_name='I want to receive notifications by email')),
+                ('receive_for_direct_messages', models.BooleanField(default=True, verbose_name='I want to receive notifications for direct messages')),
+                ('receive_push', models.BooleanField(default=True, verbose_name='I want to receive push notifications')),
+                ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='notification_settings', to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                'verbose_name': 'notification settings',
+                'verbose_name_plural': 'notification settings',
+                'abstract': False,
+                'default_permissions': ('add', 'change', 'delete', 'view', 'control'),
+            },
+        ),
+        migrations.AlterField(
+            model_name='notification',
+            name='summary',
+            field=models.TextField(blank=True),
+        ),
+    ]
diff --git a/djangoldp_notification/migrations/0004_auto_20191119_1540.py b/djangoldp_notification/migrations/0004_auto_20191119_1540.py
new file mode 100644
index 0000000..b11302e
--- /dev/null
+++ b/djangoldp_notification/migrations/0004_auto_20191119_1540.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.25 on 2019-11-19 15:40
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations
+
+from djangoldp_notification.models import NotificationSettings
+
+
+def migrate_notification_settings(apps, schema_editor):
+    LDPUser = apps.get_model(settings.AUTH_USER_MODEL)
+
+    for user in LDPUser.objects.all():
+        NotificationSettings.create_default(user)
+
+
+def noop(apps, schema_editor):
+    pass
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('djangoldp_notification', '0003_auto_20191114_1033'),
+    ]
+
+    operations = [
+        migrations.RunPython(migrate_notification_settings, reverse_code=noop),
+    ]
diff --git a/djangoldp_notification/models.py b/djangoldp_notification/models.py
index 980dbfb..2049292 100644
--- a/djangoldp_notification/models.py
+++ b/djangoldp_notification/models.py
@@ -1,22 +1,26 @@
 import logging
+from enum import Enum
 from threading import Thread
 
 import requests
+from django.apps import apps
 from django.conf import settings
-from django.contrib.admin.models import LogEntry
-from django.contrib.sessions.models import Session
+from django.core.exceptions import ValidationError
 from django.core.mail import send_mail
 from django.db import models
 from django.db.models.signals import post_save
 from django.dispatch import receiver
-from oidc_provider.models import Token
-from django.urls import NoReverseMatch
+from django.template import loader
+from django.urls import NoReverseMatch, reverse
+from django.utils.decorators import classonlymethod
+from django.utils.translation import ugettext_lazy as _
+
 
 from djangoldp.fields import LDPUrlField
 from djangoldp.models import Model
 
-from django.template import loader
-from .permissions import InboxPermissions
+from djangoldp_notification.middlewares import MODEL_MODIFICATION_USER_FIELD
+from djangoldp_notification.permissions import InboxPermissions, SubscriptionsPermissions
 
 
 class Notification(Model):
@@ -24,7 +28,7 @@ class Notification(Model):
     author = LDPUrlField()
     object = LDPUrlField()
     type = models.CharField(max_length=255)
-    summary = models.TextField()
+    summary = models.TextField(blank=True)
     date = models.DateTimeField(auto_now_add=True)
     unread = models.BooleanField(default=True)
 
@@ -40,6 +44,50 @@ class Notification(Model):
         return '{}'.format(self.type)
 
 
+class NotificationSettings(Model):
+    user = models.OneToOneField(
+        settings.AUTH_USER_MODEL,
+        related_name="notification_settings",
+        on_delete=models.deletion.CASCADE
+    )
+    receive_by_email = models.BooleanField(_("I want to receive notifications by email"), default=True)
+    receive_for_direct_messages = models.BooleanField(
+        _("I want to receive notifications for direct messages"),
+        default=True
+    )
+    receive_push = models.BooleanField(_("I want to receive push notifications"), default=True)
+
+    class Meta(Model.Meta):
+        owner_field = "user"
+        permission_classes = [InboxPermissions]
+        authenticated_perms = ["inherit"]
+        owner_perms = ["view", "change", "control"]
+        verbose_name = "notification settings"
+        verbose_name_plural = "notification settings"
+        container_path = "notificationsettings"
+        serializer_fields = ["user", "receive_by_email", "receive_for_direct_messages", "receive_push", "subscriptions"]
+
+    def __str__(self):
+        return "{}<{}>".format(self.__class__.__name__, self.user)
+
+    def clean(self):
+        if self.pk:
+            old_user_pk = NotificationSettings.objects.filter(pk=self.pk).values("user").first().get("user")
+            new_user_pk = self.user.pk
+            if old_user_pk != new_user_pk:
+                raise ValidationError("Can't modify user field")
+        super().clean()
+
+    @property
+    def subscriptions(self):
+        return "{}{}".format(settings.BASE_URL, reverse("user-subscriptions", kwargs={'slug': self.user.slug}))
+
+    @classonlymethod
+    def create_default(cls, user):
+        LDPUser = apps.get_model(settings.AUTH_USER_MODEL)
+        NotificationSettings.objects.get_or_create(user=LDPUser.objects.get(pk=user.pk))
+
+
 class Subscription(Model):
     object = models.URLField()
     inbox = models.URLField()
@@ -47,32 +95,46 @@ class Subscription(Model):
     def __str__(self):
         return '{}'.format(self.object)
 
+    class Meta(Model.Meta):
+        pass
+        #  permission_classes = [SubscriptionsPermissions]
 
 # --- SUBSCRIPTION SYSTEM ---
 @receiver(post_save, dispatch_uid="callback_notif")
-def send_notification(sender, instance, **kwargs):
+def send_notification(sender, instance, created, **kwargs):
     if sender != Notification:
         threads = []
         try:
-            urlContainer = settings.BASE_URL + Model.container_id(instance)
-            urlResource = settings.BASE_URL + Model.resource_id(instance)
+            url_container = settings.BASE_URL + Model.container_id(instance)
+            url_resource = settings.BASE_URL + Model.resource_id(instance)
         except NoReverseMatch:
             return
 
-        for subscription in Subscription.objects.filter(models.Q(object=urlResource)|models.Q(object=urlContainer)):
-            process = Thread(target=send_request, args=[subscription.inbox, urlResource])
+        for subscription in Subscription.objects.filter(models.Q(object=url_resource) | models.Q(object=url_container)):
+            process = Thread(target=send_request, args=[subscription.inbox, url_resource, instance, created])
             process.start()
             threads.append(process)
 
 
-def send_request(target, object_iri):
+def send_request(target, object_iri, instance, created):
+    unknown = _("Unknown author")
+    author = getattr(getattr(instance, MODEL_MODIFICATION_USER_FIELD, unknown), "urlid", unknown)
+    request_type = "creation" if created else "update"
+
     try:
-        req = requests.post(target,
-                            json={"@context": "https://cdn.happy-dev.fr/owl/hdcontext.jsonld",
-                                  "object": object_iri, "type": "update"},
-                            headers={"Content-Type": "application/ld+json"})
-    except:
-        logging.error('Djangoldp_notifications: Error with request')
+        if target.startswith(settings.SITE_URL):
+            user = Model.resolve_parent(target.replace(settings.SITE_URL, ''))
+            Notification.objects.create(user=user, object=object_iri, type=request_type, author=author)
+        else:
+            json = {
+                "@context": settings.LDP_RDF_CONTEXT,
+                "object": object_iri,
+                "author": author,
+                "type": request_type
+            }
+            requests.post(target, json=json, headers={"Content-Type": "application/ld+json"})
+    except Exception as e:
+        logging.error('Djangoldp_notifications: Error with request: {}'.format(e))
     return True
 
 
@@ -118,3 +180,13 @@ def send_email_on_notification(sender, instance, created, **kwargs):
             fail_silently=True,
             html_message=html_message
         )
+
+
+@receiver(post_save, sender=settings.AUTH_USER_MODEL)
+def create_notification_settings(sender, instance, created, **kwargs):
+    if created:
+        NotificationSettings.objects.get_or_create(user=instance)
+    else:
+        user_settings = NotificationSettings.objects.filter(user__pk=instance.pk)
+        if len(user_settings) == 0:
+            NotificationSettings.objects.get_or_create(user=instance)
diff --git a/djangoldp_notification/permissions.py b/djangoldp_notification/permissions.py
index f013752..b2a239b 100644
--- a/djangoldp_notification/permissions.py
+++ b/djangoldp_notification/permissions.py
@@ -35,4 +35,8 @@ class InboxPermissions(LDPPermissions):
             if not perm.split('.')[1].split('_')[0] in self.user_permissions(request.user, model, obj):
                 return False
 
-        return True
\ No newline at end of file
+        return True
+
+
+class SubscriptionsPermissions:
+    pass
diff --git a/djangoldp_notification/settings.py b/djangoldp_notification/settings.py
index 3428c3a..65ef3c0 100644
--- a/djangoldp_notification/settings.py
+++ b/djangoldp_notification/settings.py
@@ -1 +1 @@
-USER_NESTED_FIELDS = ['inbox']
\ No newline at end of file
+USER_NESTED_FIELDS = ['inbox', 'notification_settings']
diff --git a/djangoldp_notification/tests/conftest.py b/djangoldp_notification/tests/conftest.py
new file mode 100644
index 0000000..b1beb8b
--- /dev/null
+++ b/djangoldp_notification/tests/conftest.py
@@ -0,0 +1,15 @@
+from importlib import import_module
+from os import environ
+
+from pytest import fail
+
+settings_module = environ.get("DJANGO_SETTINGS_MODULE")
+if settings_module is None or len(settings_module) == 0:
+    fail("DJANGO_SETTINGS_MODULE needs to be defined and point to your SIB app installation settings")
+
+try:
+    import_module(settings_module)
+except ImportError:
+    initial_module = [token for token in settings_module.split(".") if len(token) > 0][0]
+    fail("Unable to import {}. Try to configure PYTHONPATH to point the "
+         "directory containing the {} module".format(settings_module, initial_module))
diff --git a/djangoldp_notification/tests/factories.py b/djangoldp_notification/tests/factories.py
new file mode 100644
index 0000000..52a9fa9
--- /dev/null
+++ b/djangoldp_notification/tests/factories.py
@@ -0,0 +1,36 @@
+import factory
+
+from django.db.models.signals import post_save
+from djangoldp.factories import UserFactory
+from djangoldp_circle.models import Circle
+
+from djangoldp_notification.models import NotificationSettings
+
+
+@factory.django.mute_signals(post_save)
+class CircleFactory(factory.django.DjangoModelFactory):
+    class Meta:
+        model = Circle
+
+    urlid = factory.Faker("url")
+    name = factory.Faker('word')
+    description = factory.Faker('text', max_nb_chars=250)
+    owner = factory.SubFactory(UserFactory)
+    jabberID = factory.Faker('email')
+    jabberRoom = True
+
+    @factory.post_generation
+    def members(self, create, extracted, **kwargs):
+        if not create:
+            return
+
+        if extracted:
+            for member in extracted:
+                self.team.add(member)
+
+
+class NotificationSettingsFactory(factory.django.DjangoModelFactory):
+    user = factory.SubFactory(UserFactory)
+
+    class Meta:
+        model = NotificationSettings
diff --git a/djangoldp_notification/tests/test_models.py b/djangoldp_notification/tests/test_models.py
new file mode 100644
index 0000000..24b451d
--- /dev/null
+++ b/djangoldp_notification/tests/test_models.py
@@ -0,0 +1,28 @@
+from djangoldp.factories import UserFactory
+
+from djangoldp_notification.models import Subscription
+from djangoldp_notification.tests.factories import CircleFactory, NotificationSettingsFactory
+from djangoldp_notification.tests.utils import DjangoLDPTestCase
+
+
+class TestNotificationOrigin(DjangoLDPTestCase):
+    user_settings = None
+    user = None
+    circle = None
+
+    def setUp(self):
+        self.user = user = UserFactory(username="karl_marx")
+        self.circle1 = CircleFactory(owner=user)
+        self.user_settings = NotificationSettingsFactory(user=user)
+        Subscription.objects.create(object=self.circle1.urlid, inbox="https://localhost/users/karl_marx/inbox/")
+
+    def test_serialises(self):
+        result = dict(self.get("/notificationsettings/1/").data)
+        expected = {
+            'receive_by_email': True,
+            'receive_for_direct_messages': True,
+            'receive_push': True,
+            'subscriptions': 'http://localhost:8000/user-subscriptions/karl_marx/'
+        }
+        [result.pop(k) for k in ['user', '@id', 'permissions', '@context']]
+        self.assertDictEqual(result, expected)
diff --git a/djangoldp_notification/tests/test_views.py b/djangoldp_notification/tests/test_views.py
new file mode 100644
index 0000000..ce8e2da
--- /dev/null
+++ b/djangoldp_notification/tests/test_views.py
@@ -0,0 +1,78 @@
+from django.urls import reverse
+from djangoldp.factories import UserFactory
+
+from djangoldp_notification.models import Subscription
+from djangoldp_notification.tests.factories import CircleFactory, NotificationSettingsFactory
+from djangoldp_notification.tests.utils import DjangoLDPTestCase
+
+
+class TestUserSubscriptionsViewset(DjangoLDPTestCase):
+    user_settings = None
+    user1 = None
+    user2 = None
+    circle = None
+
+    def setUp(self):
+        self.user1 = UserFactory(username="karl_marx", password="password")
+        self.user2 = UserFactory(username="peter_kropotkin", password="password")
+        self.circle1 = CircleFactory(owner=self.user1, urlid='http://green.org/')
+        self.user_settings = NotificationSettingsFactory(user=self.user1)
+        Subscription.objects.create(object=self.circle1.urlid, inbox="https://localhost/users/karl_marx/inbox/")
+
+    def test_get_queryset(self):
+        """
+        Asserts that `user-subscriptions` API URL returns a list of the current logged user's usbscriptions
+        """
+        user_subscription_url = reverse("user-subscriptions", kwargs={'slug': self.user1.slug})
+
+        with self.login(self.user1):
+            result = dict(self.get(user_subscription_url).data)
+            result['ldp:contains'] = [dict(item) for item in result['ldp:contains']]
+
+            expected = {
+                '@context': 'https://cdn.happy-dev.fr/owl/hdcontext.jsonld',
+                '@id': 'http://localhost:8000/user-subscriptions/karl_marx/',
+                '@type': 'ldp:Container',
+                'ldp:contains': [
+                    {
+                        '@id': 'http://localhost:8000/subscriptions/1/',
+                        'object': 'http://green.org/',
+                        'inbox': 'https://localhost/users/karl_marx/inbox/',
+                        'permissions': [{'mode': {'@type': 'view'}}]
+                    }
+                ],
+                'permissions': [{'mode': {'@type': 'view'}}]
+            }
+            self.assertDictEqual(result, expected)
+
+    def test_get_queryset_anonyous(self):
+        """
+        Asserts that `user-subscriptions` API URL returns an list of usbscriptions for the anonymous user
+        or if currently logged user queries another user's subscriptions.
+        """
+        user_subscription_url = reverse("user-subscriptions", kwargs={'slug': self.user2.slug})
+
+        # Logged with user2
+        with self.login(self.user1):
+            result = dict(self.get(user_subscription_url).data)
+
+            expected = {
+                '@context': 'https://cdn.happy-dev.fr/owl/hdcontext.jsonld',
+                '@id': 'http://localhost:8000/user-subscriptions/peter_kropotkin/',
+                '@type': 'ldp:Container',
+                'ldp:contains': [],
+                'permissions': [{'mode': {'@type': 'view'}}]
+            }
+            self.assertDictEqual(result, expected)
+
+        # Anonymous scenario
+        result = dict(self.get(user_subscription_url).data)
+
+        expected = {
+            '@context': 'https://cdn.happy-dev.fr/owl/hdcontext.jsonld',
+                '@id': 'http://localhost:8000/user-subscriptions/peter_kropotkin/',
+            '@type': 'ldp:Container',
+            'ldp:contains': [],
+            'permissions': [{'mode': {'@type': 'view'}}]
+        }
+        self.assertDictEqual(result, expected)
diff --git a/djangoldp_notification/tests/utils.py b/djangoldp_notification/tests/utils.py
new file mode 100644
index 0000000..6260eb9
--- /dev/null
+++ b/djangoldp_notification/tests/utils.py
@@ -0,0 +1,5 @@
+from test_plus import TestCase
+
+
+class DjangoLDPTestCase(TestCase):
+    pass
diff --git a/djangoldp_notification/views.py b/djangoldp_notification/views.py
new file mode 100644
index 0000000..f1439e7
--- /dev/null
+++ b/djangoldp_notification/views.py
@@ -0,0 +1,25 @@
+import logging
+
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import AnonymousUser
+from django.urls import reverse, NoReverseMatch
+from djangoldp.views import LDPViewSet
+
+from djangoldp_notification.models import Subscription
+
+
+class UserSubscriptionsViewset(LDPViewSet):
+    model = Subscription
+
+    def get_queryset(self):
+        user = self.request.user
+        slug = self.kwargs.get("slug")
+        if isinstance(user, AnonymousUser) or self.request.user.slug != slug:
+            return Subscription.objects.none()
+
+        try:
+            user_inbox = reverse("ldpuser-notification-list", kwargs={'slug': slug})
+            return super().get_queryset().filter(inbox__endswith=user_inbox).all()
+        except NoReverseMatch as e:
+            logging.error("Error while trying to get subscriptions for user %s: %s", self.request.user.username, e)
+            return super().get_queryset().none()
diff --git a/setup.cfg b/setup.cfg
index ee79f41..e3d0e1b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -15,7 +15,10 @@ install_requires =
 [options.extras_require]
 include_package_data = True
 dev =
-    factory_boy>=2.11.0
+    factory_boy>=2.12.0
+    pytest==5.1.1
+    pytest-django==3.5.1
+    django-test-plus==1.3.1
 
 [semantic_release]
 version_source = tag
-- 
GitLab