diff --git a/config/settings/base.py b/config/settings/base.py
index bb8df71ebc3ede1a11ef5c34dd86977352033fe5..e6a773ad7a8439558199e161a998535d95ae553c 100644
--- a/config/settings/base.py
+++ b/config/settings/base.py
@@ -78,6 +78,7 @@ THIRD_PARTY_APPS = [
"shibboleth",
"taggit",
"django_elasticsearch_dsl",
+ "mptt",
]
LOCAL_APPS = [
@@ -353,3 +354,11 @@ RUNSCRIPT_SCRIPT_DIR = "djangoscripts"
BLEACH_ALLOWED_TAGS = []
BLEACH_STRIP_TAGS = False
BLEACH_STRIP_COMMENTS = True
+
+# actstream
+ACTSTREAM_SETTINGS = {
+ "MANAGER": "core.managers.DiscussDataActionManager",
+ "FETCH_RELATIONS": True,
+ "USE_JSONFIELD": False,
+ "GFK_FETCH_DEPTH": 1,
+}
diff --git a/discuss_data/core/managers.py b/discuss_data/core/managers.py
new file mode 100644
index 0000000000000000000000000000000000000000..955c39af92e72973559f75c220bd47ca7366f66f
--- /dev/null
+++ b/discuss_data/core/managers.py
@@ -0,0 +1,68 @@
+from datetime import datetime
+
+from django.contrib.contenttypes.models import ContentType
+
+from actstream.managers import ActionManager, stream
+from actstream.registry import check
+
+from django.apps import apps
+from django.db.models import Q
+
+
+class DiscussDataActionManager(ActionManager):
+ @stream
+ def mystream(self, obj, verb="posted", time=None):
+ if time is None:
+ time = datetime.now()
+ return obj.actor_actions.filter(verb=verb, timestamp__lte=time)
+
+ @stream
+ def dataset_access_requests_sent(self, sender, recipient=None):
+ notification_content_type = ContentType.objects.get(
+ app_label="ddcomments", model="notification"
+ )
+ dataset_content_type = ContentType.objects.get(
+ app_label="dddatasets", model="dataset"
+ )
+ filters = {"action_object_content_type": notification_content_type.id}
+ filters["target_content_type"] = dataset_content_type.id
+ if recipient: # optional recipient
+ filters["target"] = recipient
+ # Actions where sender is the actor with extra filters applied
+ return sender.actor_actions.filter(**filters)
+
+ @stream
+ def access_requests_received(self, datasets):
+ notification_content_type = ContentType.objects.get(
+ app_label="ddcomments", model="notification"
+ )
+ dataset_content_type = ContentType.objects.get(
+ app_label="dddatasets", model="dataset"
+ )
+
+ return self.filter(
+ action_object_content_type=notification_content_type.id,
+ target_content_type=dataset_content_type.id,
+ target_object_id__in=datasets,
+ )
+
+ def everything(self, *args, **kwargs):
+ """
+ Return all actions (public=True and False)
+ """
+ return self.filter(*args, **kwargs)
+
+ @stream
+ def any_everything(self, obj, **kwargs):
+ """
+ Stream of most recent actions where obj is the actor OR target OR action_object.
+ Includes also Actions which are not public
+ """
+ check(obj)
+ ctype = ContentType.objects.get_for_model(obj)
+ return self.everything(
+ Q(actor_content_type=ctype, actor_object_id=obj.pk,)
+ | Q(target_content_type=ctype, target_object_id=obj.pk,)
+ | Q(action_object_content_type=ctype, action_object_object_id=obj.pk,),
+ **kwargs,
+ )
diff --git a/discuss_data/core/templatetags/core_tags.py b/discuss_data/core/templatetags/core_tags.py
index f0b013b0972e5410999d002ad88120f44eb7e4d5..bc72ea07de373bfdf63b9555bfd06f6c1177ff75 100644
--- a/discuss_data/core/templatetags/core_tags.py
+++ b/discuss_data/core/templatetags/core_tags.py
@@ -1,6 +1,15 @@
from django.urls import resolve
from django import template
+
from discuss_data.ddusers.models import User
+from discuss_data.dddatasets.models import DataSet
+
+from discuss_data.ddcomments.forms import (
+ NotificationForm,
+ CommentForm,
+)
+
+from discuss_data.ddcomments.models import Notification
register = template.Library()
DEFAULT_USER_IMAGE = "/static/images/user_default.png"
@@ -51,3 +60,94 @@ def get_value(dictionary, key):
return dictionary[key]
except Exception:
return None
+
+
+@register.filter
+def get_file_icon(mime_str):
+ """
+ Returns the fa icon string for the mime group
+ """
+ if mime_str == "application/zip":
+ return "fa-file"
+ if mime_str == "archive/pdf" or mime_str == "application/pdf":
+ return "fa-file-pdf"
+
+ try:
+ kind, ext = mime_str.split("/")
+ return "fa-file-" + kind
+ except Exception:
+ return "fa-file"
+
+
+@register.filter
+def user_has_restricted_access(dataset, user):
+ return dataset.user_has_ra_view_right(user)
+
+
+@register.filter
+def user_has_admin_right(dataset, user):
+ return dataset.user_has_admin_right(user)
+
+
+@register.filter
+def prepare_notification_form(comment, user):
+ if comment.notification_type == Notification.ACCESS_REQUEST:
+ recipient_uuid = comment.owner.uuid
+ else:
+ if comment.owner:
+ recipient_uuid = comment.recipient.uuid
+ else:
+ recipient_uuid = comment.owner.uuid
+ parent_uuid = None
+ if comment.level > 0:
+ if not comment.get_next_sibling():
+ if comment.parent:
+ parent_uuid = comment.parent.uuid
+ initial = {
+ "recipient_uuid": recipient_uuid,
+ "parent_uuid": parent_uuid,
+ }
+ return NotificationForm(initial=initial)
+ else:
+ if comment.is_leaf_node():
+ parent_uuid = comment.uuid
+ initial = {
+ "recipient_uuid": recipient_uuid,
+ "parent_uuid": parent_uuid,
+ }
+ return NotificationForm(initial=initial)
+ return None
+
+
+@register.filter
+def prepare_comment_form(comment, user):
+ parent_uuid = None
+ if comment.level > 0:
+ if not comment.get_next_sibling():
+ if comment.parent:
+ parent_uuid = comment.parent.uuid
+ initial = {
+ "parent_uuid": parent_uuid,
+ }
+ return CommentForm(initial=initial)
+ else:
+ if comment.is_leaf_node():
+ parent_uuid = comment.uuid
+ initial = {
+ "parent_uuid": parent_uuid,
+ }
+ return CommentForm(initial=initial)
+ return None
+
+
+@register.filter
+def get_parent_comment(comment):
+ parent = None
+ if comment.level > 0:
+ if not comment.get_next_sibling():
+ if comment.parent:
+ parent = comment.parent
+ else:
+ if comment.is_leaf_node():
+ parent = comment
+ return parent
diff --git a/discuss_data/core/views.py b/discuss_data/core/views.py
index c5ddb6b7dcaac448a297c150e7b91853ec6305a3..fbe5bbaa96cd22413c822d1771f9f7c3dfbdadf6 100644
--- a/discuss_data/core/views.py
+++ b/discuss_data/core/views.py
@@ -131,7 +131,7 @@ def core_search_view(request, search_index, objects_on_page):
else:
# default search index is user_index
- template = "ddusers/search_results.html"
+ template = "ddusers/_search_results.html"
queryset = index_search(
"user_index", query, countries=countries, categories=categories
)
@@ -218,7 +218,6 @@ def landing_page(request):
def check_perms(permission, user, dd_obj):
- logger.debug("start checks_perms")
try:
if dd_obj.owner == user:
logger.debug("is owner")
@@ -234,6 +233,7 @@ def check_perms(permission, user, dd_obj):
else:
logger.debug("%s lacks perm %s on %s" % (user, permission, dd_obj))
return False
+ return False
def check_perms_403(permission, user, dd_obj):
diff --git a/discuss_data/ddcomments/apps.py b/discuss_data/ddcomments/apps.py
index 9e33001529deb4a7a26473180e5119beb9c1f4c7..6aa0e9133fc0a4a6e5aa4097b4bbbe03a2d03c06 100644
--- a/discuss_data/ddcomments/apps.py
+++ b/discuss_data/ddcomments/apps.py
@@ -8,3 +8,4 @@ class DdcommentsConfig(AppConfig):
from actstream import registry
registry.register(self.get_model("Comment"))
+ registry.register(self.get_model("Notification"))
diff --git a/discuss_data/ddcomments/forms.py b/discuss_data/ddcomments/forms.py
index 119cd57d48b390344486f539337b304fb2534855..f52bbdee7cb5df45bb1e5ed9df2f818a82e49da3 100644
--- a/discuss_data/ddcomments/forms.py
+++ b/discuss_data/ddcomments/forms.py
@@ -1,3 +1,5 @@
+from django import forms
+from django.contrib import messages
from django.forms import ModelForm
from crispy_forms.helper import FormHelper
@@ -13,14 +15,26 @@ from crispy_forms.layout import (
)
from crispy_forms.bootstrap import FormActions, TabHolder, Tab
-from .models import Comment
+from .models import (
+ Comment,
+ Notification,
+)
-UPDATED_MSG = """
- {% if updated %}
- {{ target|title }} saved
{% endif %}
- {% if err %}
- Error! {{ target|title }} could not be saved. Error encountered in fields {% for field in err %} {{ field }}, {% endfor %}
{% endif %}"""
+MSG = """
+{% if messages %}
+ {% for message in messages %}
+ {% if message.level == DEFAULT_MESSAGE_LEVELS.SUCCESS %}
+
+ {{ message }}
+ {% endif %}
+ {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
+
+ Error! {{ target|title }} could not be saved. Error encountered in fields {% for field in err %} {{ field }}, {% endfor %}
+ {% endif %}
+ {% endfor %}
+{% endif %}
+"""
ADD_EDIT_HEADING = (
"{% if new %}Add{% else %}Edit{% endif %} {{ target|title }} "
@@ -39,10 +53,51 @@ class CommentForm(ModelForm):
helper.form_tag = False
helper.form_show_labels = False
+ helper.layout = Layout(
+ Div(Div("text", css_class="col-md-12", rows="3",), css_class="row",),
+ )
+
+
+class NotificationForm(ModelForm):
+ recipient_uuid = forms.UUIDField()
+ parent_uuid = forms.UUIDField(required=False)
+
+ class Meta:
+ model = Notification
+ fields = [
+ "text",
+ "recipient_uuid",
+ "parent_uuid",
+ ]
+
+ helper = FormHelper()
+ helper.form_tag = False
+ helper.form_show_labels = False
+
+ helper.layout = Layout(
+ Field("recipient_uuid", type="hidden"),
+ Field("parent_uuid", type="hidden"),
+ Div(
+ Div("text", css_class="col-md-12 notification", rows="3",), css_class="row",
+ ),
+ FormActions(Submit("save", "{{ btn_text }}")),
+ )
+
+
+class AccessRequestForm(ModelForm):
+ class Meta:
+ model = Notification
+ fields = [
+ "text",
+ ]
+
+ helper = FormHelper()
+ helper.form_tag = False
+ helper.form_show_labels = False
+
helper.layout = Layout(
Div(
- Div("text", css_class="col-md-12", rows="3",),
- css_class="row",
- # Field('text', css_class='col-md-12 row', rows='5', ),
+ Div("text", css_class="col-md-12 notification", rows="3",), css_class="row",
),
+ FormActions(Submit("save", "{{ btn_text }}"),),
)
diff --git a/discuss_data/ddcomments/migrations/0009_notification.py b/discuss_data/ddcomments/migrations/0009_notification.py
new file mode 100644
index 0000000000000000000000000000000000000000..4773ff4c22084291019c06a338614ad79e05c449
--- /dev/null
+++ b/discuss_data/ddcomments/migrations/0009_notification.py
@@ -0,0 +1,26 @@
+# Generated by Django 2.2.11 on 2020-05-12 07:05
+
+from django.db import migrations, models
+import django_bleach.models
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ddcomments', '0008_auto_20200506_1402'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Notification',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('uuid', models.UUIDField(default=uuid.uuid4, editable=False)),
+ ('text', django_bleach.models.BleachField()),
+ ('read', models.BooleanField()),
+ ('deleted', models.BooleanField()),
+ ('notification_type', models.CharField(choices=[('USER', 'user'), ('CUR', 'curator')], default='USER', max_length=4)),
+ ],
+ ),
+ ]
diff --git a/discuss_data/ddcomments/migrations/0010_auto_20200513_1225.py b/discuss_data/ddcomments/migrations/0010_auto_20200513_1225.py
new file mode 100644
index 0000000000000000000000000000000000000000..78273a63c75dae5b6a0ce1232bc5c854c15f4202
--- /dev/null
+++ b/discuss_data/ddcomments/migrations/0010_auto_20200513_1225.py
@@ -0,0 +1,23 @@
+# Generated by Django 2.2.11 on 2020-05-13 12:25
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ddcomments', '0009_notification'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='notification',
+ name='deleted',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AlterField(
+ model_name='notification',
+ name='read',
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/discuss_data/ddcomments/migrations/0011_ddcomment.py b/discuss_data/ddcomments/migrations/0011_ddcomment.py
new file mode 100644
index 0000000000000000000000000000000000000000..cf498da8795fef29633dfec7ebbf998f0b5f5dd2
--- /dev/null
+++ b/discuss_data/ddcomments/migrations/0011_ddcomment.py
@@ -0,0 +1,44 @@
+# Generated by Django 2.2.11 on 2020-05-18 09:45
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django_bleach.models
+import mptt.fields
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('contenttypes', '0002_remove_content_type_name'),
+ ('ddcomments', '0010_auto_20200513_1225'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='DDComment',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('uuid', models.UUIDField(default=uuid.uuid4, editable=False)),
+ ('comment_type', models.CharField(choices=[('PUB', 'public'), ('PRI', 'private'), ('CUR', 'curator'), ('PERM', 'permanent'), ('AR', 'access request')], default='PUB', max_length=4)),
+ ('doi', models.CharField(blank=True, max_length=200)),
+ ('text', django_bleach.models.BleachField()),
+ ('date_added', models.DateTimeField(auto_now_add=True)),
+ ('date_edited', models.DateTimeField(auto_now=True)),
+ ('object_id', models.IntegerField()),
+ ('lft', models.PositiveIntegerField(editable=False)),
+ ('rght', models.PositiveIntegerField(editable=False)),
+ ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
+ ('level', models.PositiveIntegerField(editable=False)),
+ ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='contenttypes.ContentType')),
+ ('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='comment_owner', to=settings.AUTH_USER_MODEL)),
+ ('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='ddcomments.DDComment')),
+ ('recipient', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='comment_recipient', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ ]
diff --git a/discuss_data/ddcomments/migrations/0012_auto_20200520_0802.py b/discuss_data/ddcomments/migrations/0012_auto_20200520_0802.py
new file mode 100644
index 0000000000000000000000000000000000000000..6c68f37b283e04826dd3c31d57a2985492535ead
--- /dev/null
+++ b/discuss_data/ddcomments/migrations/0012_auto_20200520_0802.py
@@ -0,0 +1,23 @@
+# Generated by Django 2.2.11 on 2020-05-20 08:02
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ddcomments', '0011_ddcomment'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='comment',
+ name='deleted',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name='comment',
+ name='read',
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/discuss_data/ddcomments/migrations/0013_delete_notification.py b/discuss_data/ddcomments/migrations/0013_delete_notification.py
new file mode 100644
index 0000000000000000000000000000000000000000..7a4d803b2222fe7127737da7b6fb700461efd718
--- /dev/null
+++ b/discuss_data/ddcomments/migrations/0013_delete_notification.py
@@ -0,0 +1,16 @@
+# Generated by Django 2.2.11 on 2020-05-20 09:13
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ddcomments', '0012_auto_20200520_0802'),
+ ]
+
+ operations = [
+ migrations.DeleteModel(
+ name='Notification',
+ ),
+ ]
diff --git a/discuss_data/ddcomments/migrations/0014_auto_20200520_0917.py b/discuss_data/ddcomments/migrations/0014_auto_20200520_0917.py
new file mode 100644
index 0000000000000000000000000000000000000000..27d3717d0159eae63235769a5b87ed54e0db524b
--- /dev/null
+++ b/discuss_data/ddcomments/migrations/0014_auto_20200520_0917.py
@@ -0,0 +1,20 @@
+# Generated by Django 2.2.11 on 2020-05-20 09:17
+
+from django.conf import settings
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('contenttypes', '0002_remove_content_type_name'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('ddcomments', '0013_delete_notification'),
+ ]
+
+ operations = [
+ migrations.RenameModel(
+ old_name='DDComment',
+ new_name='Notification',
+ ),
+ ]
diff --git a/discuss_data/ddcomments/migrations/0015_auto_20200520_0919.py b/discuss_data/ddcomments/migrations/0015_auto_20200520_0919.py
new file mode 100644
index 0000000000000000000000000000000000000000..c0c9711c42a2a201805654613fce06d72c508f7d
--- /dev/null
+++ b/discuss_data/ddcomments/migrations/0015_auto_20200520_0919.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.11 on 2020-05-20 09:19
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ddcomments', '0014_auto_20200520_0917'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='notification',
+ old_name='comment_type',
+ new_name='notification_type',
+ ),
+ ]
diff --git a/discuss_data/ddcomments/migrations/0016_notification_permanent.py b/discuss_data/ddcomments/migrations/0016_notification_permanent.py
new file mode 100644
index 0000000000000000000000000000000000000000..beb2868a4a7965ab8e286c20a3c99f81f9fabfcb
--- /dev/null
+++ b/discuss_data/ddcomments/migrations/0016_notification_permanent.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.11 on 2020-05-20 11:33
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ddcomments', '0015_auto_20200520_0919'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='notification',
+ name='permanent',
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/discuss_data/ddcomments/migrations/0017_auto_20200520_1134.py b/discuss_data/ddcomments/migrations/0017_auto_20200520_1134.py
new file mode 100644
index 0000000000000000000000000000000000000000..48ba5b13c41054a158eb478343383ea346baa561
--- /dev/null
+++ b/discuss_data/ddcomments/migrations/0017_auto_20200520_1134.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.11 on 2020-05-20 11:34
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ddcomments', '0016_notification_permanent'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='notification',
+ name='notification_type',
+ field=models.CharField(choices=[('PUB', 'public'), ('PRI', 'private'), ('CUR', 'curator'), ('AR', 'access request')], default='PUB', max_length=4),
+ ),
+ ]
diff --git a/discuss_data/ddcomments/migrations/0018_remove_notification_doi.py b/discuss_data/ddcomments/migrations/0018_remove_notification_doi.py
new file mode 100644
index 0000000000000000000000000000000000000000..148352109224f6943712a159223979ca6bda969c
--- /dev/null
+++ b/discuss_data/ddcomments/migrations/0018_remove_notification_doi.py
@@ -0,0 +1,17 @@
+# Generated by Django 2.2.11 on 2020-05-20 11:35
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ddcomments', '0017_auto_20200520_1134'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='notification',
+ name='doi',
+ ),
+ ]
diff --git a/discuss_data/ddcomments/migrations/0019_auto_20200520_1139.py b/discuss_data/ddcomments/migrations/0019_auto_20200520_1139.py
new file mode 100644
index 0000000000000000000000000000000000000000..26c3e1b836a459dd2feac3600334a9c42bc7d95a
--- /dev/null
+++ b/discuss_data/ddcomments/migrations/0019_auto_20200520_1139.py
@@ -0,0 +1,59 @@
+# Generated by Django 2.2.11 on 2020-05-20 11:39
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django_bleach.models
+import mptt.fields
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('contenttypes', '0002_remove_content_type_name'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('ddcomments', '0018_remove_notification_doi'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='notification',
+ name='owner',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='notification_owner', to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.AlterField(
+ model_name='notification',
+ name='parent',
+ field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notification_children', to='ddcomments.Notification'),
+ ),
+ migrations.AlterField(
+ model_name='notification',
+ name='recipient',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='notification_recipient', to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.CreateModel(
+ name='Comment2',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('uuid', models.UUIDField(default=uuid.uuid4, editable=False)),
+ ('doi', models.CharField(blank=True, max_length=200)),
+ ('notification_type', models.CharField(choices=[('PUB', 'public'), ('PRI', 'private'), ('CUR', 'curator'), ('PERM', 'permanent')], default='PUB', max_length=4)),
+ ('text', django_bleach.models.BleachField()),
+ ('date_added', models.DateTimeField(auto_now_add=True)),
+ ('date_edited', models.DateTimeField(auto_now=True)),
+ ('permanent', models.BooleanField(default=False)),
+ ('object_id', models.IntegerField()),
+ ('lft', models.PositiveIntegerField(editable=False)),
+ ('rght', models.PositiveIntegerField(editable=False)),
+ ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
+ ('level', models.PositiveIntegerField(editable=False)),
+ ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='contenttypes.ContentType')),
+ ('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='comment_owner', to=settings.AUTH_USER_MODEL)),
+ ('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='comment_children', to='ddcomments.Comment2')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ ]
diff --git a/discuss_data/ddcomments/migrations/0020_auto_20200520_1410.py b/discuss_data/ddcomments/migrations/0020_auto_20200520_1410.py
new file mode 100644
index 0000000000000000000000000000000000000000..cd5b57c6a045c64a98b681bcac5e46840e0ea134
--- /dev/null
+++ b/discuss_data/ddcomments/migrations/0020_auto_20200520_1410.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.11 on 2020-05-20 14:10
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ddcomments', '0019_auto_20200520_1139'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='comment2',
+ old_name='notification_type',
+ new_name='comment_type',
+ ),
+ ]
diff --git a/discuss_data/ddcomments/migrations/0021_delete_comment.py b/discuss_data/ddcomments/migrations/0021_delete_comment.py
new file mode 100644
index 0000000000000000000000000000000000000000..ef4c40041e087608d1b61cf07223f277d4f06ad9
--- /dev/null
+++ b/discuss_data/ddcomments/migrations/0021_delete_comment.py
@@ -0,0 +1,16 @@
+# Generated by Django 2.2.11 on 2020-05-20 14:33
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ddcomments', '0020_auto_20200520_1410'),
+ ]
+
+ operations = [
+ migrations.DeleteModel(
+ name='Comment',
+ ),
+ ]
diff --git a/discuss_data/ddcomments/migrations/0022_auto_20200520_1436.py b/discuss_data/ddcomments/migrations/0022_auto_20200520_1436.py
new file mode 100644
index 0000000000000000000000000000000000000000..0263df0ccc1a5fa264f03c4a924c84f387bbfb58
--- /dev/null
+++ b/discuss_data/ddcomments/migrations/0022_auto_20200520_1436.py
@@ -0,0 +1,20 @@
+# Generated by Django 2.2.11 on 2020-05-20 14:36
+
+from django.conf import settings
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('contenttypes', '0002_remove_content_type_name'),
+ ('ddcomments', '0021_delete_comment'),
+ ]
+
+ operations = [
+ migrations.RenameModel(
+ old_name='Comment2',
+ new_name='Comment',
+ ),
+ ]
diff --git a/discuss_data/ddcomments/migrations/0023_comment_deleted.py b/discuss_data/ddcomments/migrations/0023_comment_deleted.py
new file mode 100644
index 0000000000000000000000000000000000000000..7a5745241ecc4fa8b541a025588b37d3b5cda66d
--- /dev/null
+++ b/discuss_data/ddcomments/migrations/0023_comment_deleted.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.11 on 2020-05-20 14:55
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ddcomments', '0022_auto_20200520_1436'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='comment',
+ name='deleted',
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/discuss_data/ddcomments/models.py b/discuss_data/ddcomments/models.py
index 44df8924d589d825be4d3ec60e08dd372801e5ff..5af5d4495da183bf7206450d41f94a536407e065 100644
--- a/discuss_data/ddcomments/models.py
+++ b/discuss_data/ddcomments/models.py
@@ -5,12 +5,85 @@ from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _
+from django.utils import timezone
+
+from django.contrib.contenttypes.fields import GenericRelation
+
+from mptt.models import MPTTModel, TreeForeignKey
from django_bleach.models import BleachField
+from actstream.models import Action
+
+
+class Notification(MPTTModel):
+ """
+ Notification with tree-traversal support for threads
+ """
+
+ PUBLIC = "PUB"
+ PRIVATE = "PRI"
+ CURATOR = "CUR"
+ ACCESS_REQUEST = "AR"
+
+ NOTIFICATION_TYPE_CHOICES = [
+ (PUBLIC, _("public")),
+ (PRIVATE, _("private")),
+ (CURATOR, _("curator")),
+ (ACCESS_REQUEST, _("access request")),
+ ]
+
+ uuid = models.UUIDField(default=uuid.uuid4, editable=False)
+ notification_type = models.CharField(
+ max_length=4, choices=NOTIFICATION_TYPE_CHOICES, default=PUBLIC,
+ )
+ text = BleachField()
+ date_added = models.DateTimeField(auto_now_add=True)
+ date_edited = models.DateTimeField(auto_now=True)
+ owner = models.ForeignKey(
+ "ddusers.User", related_name="notification_owner", on_delete=models.PROTECT
+ )
+ recipient = models.ForeignKey(
+ "ddusers.User",
+ related_name="notification_recipient",
+ on_delete=models.PROTECT,
+ null=True,
+ blank=True,
+ )
+ permanent = models.BooleanField(default=False)
+
+ parent = TreeForeignKey(
+ "self",
+ on_delete=models.CASCADE,
+ null=True,
+ blank=True,
+ related_name="notification_children",
+ )
+
+ # mandatory fields for generic relation
+ content_type = models.ForeignKey(ContentType, on_delete=models.PROTECT)
+ object_id = models.IntegerField()
+ content_object = GenericForeignKey()
+
+ def __str__(self):
+ return "[%s] %s %s (%s), %s" % (
+ self.get_notification_type_display(),
+ self.content_type,
+ self.content_object,
+ self.owner,
+ self.date_added,
+ )
+
+ def class_name(self):
+ return self.__class__.__name__
+
@reversion.register()
-class Comment(models.Model):
+class Comment(MPTTModel):
+ """
+ Comment with tree-traversal support for threads
+ """
+
PUBLIC = "PUB"
PRIVATE = "PRI"
CURATOR = "CUR"
@@ -24,16 +97,25 @@ class Comment(models.Model):
]
uuid = models.UUIDField(default=uuid.uuid4, editable=False)
+ doi = models.CharField(max_length=200, blank=True)
comment_type = models.CharField(
max_length=4, choices=COMMENT_TYPE_CHOICES, default=PUBLIC,
)
- doi = models.CharField(max_length=200, blank=True)
text = BleachField()
date_added = models.DateTimeField(auto_now_add=True)
date_edited = models.DateTimeField(auto_now=True)
- owner = models.ForeignKey("ddusers.User", on_delete=models.PROTECT)
- reply_to = models.ForeignKey(
- "Comment", on_delete=models.PROTECT, blank=True, null=True
+ owner = models.ForeignKey(
+ "ddusers.User", related_name="comment_owner", on_delete=models.PROTECT
+ )
+ permanent = models.BooleanField(default=False)
+ deleted = models.BooleanField(default=False)
+
+ parent = TreeForeignKey(
+ "self",
+ on_delete=models.CASCADE,
+ null=True,
+ blank=True,
+ related_name="comment_children",
)
# mandatory fields for generic relation
@@ -42,9 +124,20 @@ class Comment(models.Model):
content_object = GenericForeignKey()
def __str__(self):
- return "%s %s, %s (%s)" % (
+ return "[%s] %s %s (%s), %s" % (
+ self.get_comment_type_display(),
self.content_type,
self.content_object,
self.owner,
self.date_added,
)
+
+ def class_name(self):
+ return self.__class__.__name__
+
+ def set_delete(self):
+ delete_date = timezone.now().strftime("%d.%m.%Y, %H:%M:%S")
+ delete_text = _("This comment has been deleted at")
+ self.text = "{} {}".format(delete_text, delete_date)
+ self.deleted = True
+ self.save()
diff --git a/discuss_data/dddatasets/models.py b/discuss_data/dddatasets/models.py
index b2d3e494b2c8bfca788f965c84335e478a4989bb..d0b033b79f12bf891f51583771f856f814404274 100644
--- a/discuss_data/dddatasets/models.py
+++ b/discuss_data/dddatasets/models.py
@@ -1,13 +1,13 @@
import logging
import os
import uuid
+import filetype
from actstream import action
from datetime import datetime
from dcxml import simpledc
from PIL import Image
-
from taggit.managers import TaggableManager
from taggit.models import (
TagBase,
@@ -61,6 +61,8 @@ logger = logging.getLogger(__name__)
# provide a storage independent of 'media' file storage
datafile_storage = FileSystemStorage(location=settings.DATA_ROOT)
+
+
class Category(models.Model):
name = models.CharField(max_length=400)
slug = models.CharField(max_length=400)
@@ -94,6 +96,11 @@ class Sponsor(models.Model):
class DataFile(models.Model):
+ FILE_FORMATS = {
+ "png": "image",
+ "jpeg": "image",
+ "jpg": "image",
+ }
DATA_FILE_TYPES = (
("DAT", _("data")),
("MET", _("metadata")),
@@ -106,7 +113,7 @@ class DataFile(models.Model):
dataset = models.ForeignKey(
"DataSet", related_name="datafile_dataset_dataset", on_delete=models.CASCADE,
)
- #better: provide an upload directory with a datestamp to prevent overflow
+ # better: provide an upload directory with a datestamp to prevent overflow
file = models.FileField(upload_to="datasets/", storage=datafile_storage)
data_file_type = models.CharField(
max_length=3, choices=DATA_FILE_TYPES, default="DAT",
@@ -118,10 +125,21 @@ class DataFile(models.Model):
repository_file_id = models.CharField(max_length=200, default="not set")
repository = models.CharField(max_length=200, default="dariah-repository")
+ def get_download_file_name(self):
+ return "DiscussData-{}-{}".format(
+ self.dataset.title.replace(" ", "_"), self.file
+ )
+
def extension(self):
extension = os.path.splitext(self.file.name)
return extension
+ def get_file_format(self):
+ file_format = filetype.guess(self.file)
+ if file_format is None:
+ return "other"
+ return file_format.mime
+
def get_user(self):
return self.dataset.owner
@@ -132,16 +150,15 @@ class DataFile(models.Model):
return self.name
def clone(self, new_ds):
- # orig_id = self.id
self.save()
new_file = ContentFile(self.file.read())
new_file.name = self.file.name
df = self
df.pk = None
+ df.uuid = uuid.uuid4()
df.file = new_file
df.dataset = new_ds
df.save()
- # print("cloned datafile " + str(df.id))
return df
class Meta:
@@ -314,10 +331,13 @@ class DataSetExternalLink(models.Model):
class DataSet(models.Model):
+ OPEN_ACCESS = "OA"
+ METADATA_ONLY = "MO"
+ RESTRICTED_ACCESS = "RA"
DATA_ACCESS_CHOICES = [
- ("OA", _("Open Access")),
- ("MO", _("Metadata only")),
- ("RA", _("Restricted access")),
+ (OPEN_ACCESS, _("Open Access")),
+ (METADATA_ONLY, _("Metadata only")),
+ (RESTRICTED_ACCESS, _("Restricted access")),
]
uuid = models.UUIDField(
@@ -467,6 +487,12 @@ class DataSet(models.Model):
# get all users with permissions, but exclude owner
return get_users_with_perms(self).exclude(uuid=self.owner.uuid)
+ def user_has_ra_view_right(self, user):
+ return user.has_perm("ra_view_dataset", self)
+
+ def user_has_admin_right(self, user):
+ return user.has_perm("admin_dsmo", self.dataset_management_object)
+
def class_name(self):
return self.__class__.__name__
@@ -514,12 +540,24 @@ class DataSet(models.Model):
return dataset_get_users(self)
- def get_prep_comments(self):
- # return prep comments in reverse date order
- return self.comments.order_by("-date_added")
+ def get_prep_comments_all(self):
+ # return all prep comments in reverse date order
+ ct = ContentType.objects.get_for_model(DataSet)
+ return Comment.objects.filter(content_type=ct, object_id=self.id,).order_by(
+ "-date_added"
+ )
+
+ def get_prep_comments_root(self):
+ # return prep comments root nodes in reverse date order
+ ct = ContentType.objects.get_for_model(DataSet)
+ return (
+ Comment.objects.root_nodes()
+ .filter(content_type=ct, object_id=self.id,)
+ .order_by("-date_added")
+ )
- def get_public_comments(self):
- # return public comments in reverse date order
+ def get_public_comments_all(self):
+ # return all public comments in reverse date order
ct = ContentType.objects.get_for_model(DataSet)
return Comment.objects.filter(
content_type=ct,
@@ -527,6 +565,19 @@ class DataSet(models.Model):
comment_type__in=(Comment.PUBLIC, Comment.PERMANENT),
).order_by("-date_added")
+ def get_public_comments_root(self):
+ # return public comments root nodes in reverse date order
+ ct = ContentType.objects.get_for_model(DataSet)
+ return (
+ Comment.objects.root_nodes()
+ .filter(
+ content_type=ct,
+ object_id=self.id,
+ comment_type__in=(Comment.PUBLIC, Comment.PERMANENT),
+ )
+ .order_by("-date_added")
+ )
+
def get_pdf_file_name(self):
return "DiscussData-%s-description.pdf" % (self.title.replace(" ", "_"))
@@ -670,10 +721,7 @@ class DataSet(models.Model):
assign_perm(perm, group, self)
def save(self, *args, **kwargs):
- # print(self.id)
- # print(self.__dict__)
if not self.dataset_management_object:
- # print('creatin dsmo')
dsmo = DataSetManagementObject()
dsmo.owner = self.owner
dsmo.save()
@@ -707,6 +755,9 @@ class DataSet(models.Model):
def get_datafiles(self):
return DataFile.objects.filter(dataset=self)
+ def get_datafiles_count(self):
+ return self.get_datafiles().count()
+
def dublin_core(self):
dc_dict = dict(
titles=[self.title_en, self.subtitle_en],
diff --git a/discuss_data/dddatasets/urls.py b/discuss_data/dddatasets/urls.py
index ef529de01bb1fa0a599c100ca982a63885281df4..40ee8b648e4ebccc91c51cff440bace0d9b17a49 100644
--- a/discuss_data/dddatasets/urls.py
+++ b/discuss_data/dddatasets/urls.py
@@ -12,6 +12,12 @@ urlpatterns = [
path("public//", views.dataset_detail, name="detail"),
path("public//follow/", views.dataset_follow, name="follow"),
path("public//metadata/", views.dataset_metadata, name="metadata"),
+ path("public//access/", views.dataset_access, name="access"),
+ path(
+ "public//access/request/",
+ views.dataset_access_request,
+ name="access_request",
+ ),
path("public//files/", views.dataset_files, name="files"),
path("public//files/pdf/", views.dataset_files_pdf, name="files_pdf"),
path("public//files/zip/", views.dataset_files_zip, name="files_zip"),
@@ -42,6 +48,11 @@ urlpatterns = [
views.public_dataset_edit_comment,
name="edit_comment",
),
+ path(
+ "public//edit/comment//reply/",
+ views.public_dataset_edit_comment_reply,
+ name="edit_comment_reply",
+ ),
path(
"public//edit/comment/",
views.public_dataset_edit_comment,
@@ -228,4 +239,9 @@ urlpatterns += [
views_prep.prep_dataset_edit_comment,
name="prep_edit_comment",
),
+ path(
+ "prep//edit/comment//reply/",
+ views_prep.prep_dataset_edit_comment_reply,
+ name="prep_edit_comment_reply",
+ ),
]
diff --git a/discuss_data/dddatasets/utils.py b/discuss_data/dddatasets/utils.py
index 314ae0824bf25545e577b8ef6ea1782ef08e7665..7d2f298d293b41d16540ea916115b6dab0097620 100644
--- a/discuss_data/dddatasets/utils.py
+++ b/discuss_data/dddatasets/utils.py
@@ -1,12 +1,22 @@
+from datetime import datetime
+
from django.http import Http404
-from django.shortcuts import render
+from django.shortcuts import (
+ render,
+ redirect,
+)
from django.contrib.contenttypes.models import ContentType
+from django.utils.translation import gettext as _
from weasyprint import HTML
from actstream import action
-from discuss_data.dddatasets.models import DataSet, DataSetManagementObject, DataFile
+from discuss_data.dddatasets.models import (
+ DataSet,
+ DataSetManagementObject,
+ DataFile,
+)
from discuss_data.pages.models import ManualPage
@@ -106,28 +116,43 @@ def dataset_comments(request, ds_uuid, view_type):
if view_type != "public":
check_perms_403("edit_dsmo", request.user, ds.dataset_management_object)
- comments = ds.get_prep_comments()
+ comments = ds.get_prep_comments_root()
add_url = "dddatasets:prep_edit_comment"
list_url = "dddatasets:prep_comments"
+ reply_url = "dddatasets:prep_edit_comment_reply"
+ root_count = comments.count()
+ all_count = ds.get_prep_comments_all().count()
else:
- comments = ds.get_public_comments()
+ comments = ds.get_public_comments_root()
add_url = "dddatasets:edit_comment"
list_url = "dddatasets:comments"
+ reply_url = "dddatasets:edit_comment_reply"
+ root_count = comments.count()
+ all_count = ds.get_public_comments_all().count()
# add http header to response to resume polling (which could have been canceled in prep_dataset_edit_comment)
response = render(
request,
"ddcomments/_comments_list.html",
- {"ds": ds, "comments": comments, "add_url": add_url, "list_url": list_url},
+ {
+ "ds": ds,
+ "comments": comments,
+ "add_url": add_url,
+ "list_url": list_url,
+ "reply_url": reply_url,
+ "root_count": root_count,
+ "all_count": all_count,
+ },
)
response["X-IC-ResumePolling"] = "true"
return response
-def dataset_edit_comment(request, ds_uuid, view_type, co_uuid):
+def dataset_edit_comment(request, ds_uuid, view_type, co_uuid, reply=None):
"""
view to add, edit or delete public/prep comments
to/from a dataset
"""
+
if view_type == "public" or view_type == "prep":
pass
else:
@@ -137,26 +162,31 @@ def dataset_edit_comment(request, ds_uuid, view_type, co_uuid):
if view_type == "public":
add_url = "dddatasets:edit_comment"
list_url = "dddatasets:comments"
+ reply_url = "dddatasets:edit_comment_reply"
else:
add_url = "dddatasets:prep_edit_comment"
list_url = "dddatasets:prep_comments"
+ reply_url = "dddatasets:prep_edit_comment_reply"
if view_type != "public":
check_perms_403("edit_dsmo", request.user, ds.dataset_management_object)
+ parent = None
- # DELETE
+ # DELETE: delete not the whole Comment object, but mark the text as "deleted"
if request.method == "DELETE":
co_uuid = request.DELETE["objectid"]
comment = Comment.objects.get(uuid=co_uuid)
check_perms_403("delete_comment", request.user, comment)
- comment.delete()
+ comment.set_delete()
+ comment.save()
+
if view_type == "public":
return dataset_comments(request, ds.uuid, "public")
else:
return dataset_comments(request, ds.uuid, "prep")
if request.method == "POST":
- if co_uuid:
+ if co_uuid and not reply:
# Edit
comment = Comment.objects.get(uuid=co_uuid)
check_perms_403("edit_comment", request.user, comment)
@@ -170,7 +200,7 @@ def dataset_edit_comment(request, ds_uuid, view_type, co_uuid):
else:
err = form.errors.as_data()
else:
- # Add
+ # Add new and reply
comment = Comment()
form = CommentForm(request.POST, instance=comment)
if form.is_valid():
@@ -182,24 +212,35 @@ def dataset_edit_comment(request, ds_uuid, view_type, co_uuid):
comment.comment_type = comment.PUBLIC
action.send(
request.user,
- verb="posted comment",
+ verb="commented dataset",
target=ds,
action_object=comment,
+ public=True,
)
else:
comment.comment_type = comment.PRIVATE
+ action.send(
+ request.user,
+ verb="commented dataset",
+ target=ds,
+ action_object=comment,
+ public=False,
+ )
comment = form.save()
+ if reply:
+ comment.parent = Comment.objects.get(uuid=co_uuid)
+ comment.save()
return render(
request,
"ddcomments/_comment_block_load.html",
- {"object_uuid": ds.uuid, "add_url": add_url, "list_url": list_url},
+ {"ds": ds, "add_url": add_url, "list_url": list_url},
)
else:
err = form.errors.as_data()
else:
# Get
- if co_uuid:
+ if co_uuid and not reply:
# Edit form
comment = Comment.objects.get(uuid=co_uuid)
check_perms_403("edit_comment", request.user, comment)
@@ -209,32 +250,45 @@ def dataset_edit_comment(request, ds_uuid, view_type, co_uuid):
request,
"ddcomments/_comment_add.html",
{
- "dsuuid": ds.uuid,
+ "ds": ds,
"comment": comment,
"comment_form": form,
"err": err,
"edit": True,
"add_url": add_url,
"list_url": list_url,
+ "reply_url": reply_url,
+ "form": form,
+ "ictarget": "#comments-list",
},
)
response["X-IC-CancelPolling"] = "true"
return response
+ elif reply:
+ # Reply form
+ parent = Comment.objects.get(uuid=co_uuid)
+ form = CommentForm()
else:
# Add form
- comment = None
form = CommentForm()
# updated = False
# form_err = None
- return render(
+ response = render(
request,
"ddcomments/_comment_add.html",
{
- "dsuuid": ds.uuid,
- "comment": comment,
+ "ds": ds,
+ "parent": parent,
"comment_form": form,
"err": err,
"add_url": add_url,
"list_url": list_url,
+ "reply_url": reply_url,
+ "form": form,
+ "reply": reply,
+ "ictarget": "#comment-block",
},
)
+ if reply:
+ response["X-IC-CancelPolling"] = "true"
+ return response
diff --git a/discuss_data/dddatasets/views.py b/discuss_data/dddatasets/views.py
index fd31f7dc2a355c0772f17c674370b78997410d66..b95f7c0afb8acbfa9d00fbe78e65ca7163b267ce 100644
--- a/discuss_data/dddatasets/views.py
+++ b/discuss_data/dddatasets/views.py
@@ -5,18 +5,28 @@ from django.http import (
Http404,
HttpResponse,
FileResponse,
+ HttpResponseForbidden,
)
from django.shortcuts import render
from django.utils.translation import gettext as _
from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect
+from django.contrib import messages
+from django.contrib.contenttypes.models import ContentType
+from actstream import action
from actstream.actions import (
follow,
unfollow,
)
from actstream.models import followers
+from discuss_data.core.views import (
+ check_perms,
+ check_perms_403,
+ get_help_texts,
+)
+
from discuss_data.dddatasets.models import (
DataSet,
DataSetManagementObject,
@@ -32,6 +42,12 @@ from discuss_data.dddatasets.utils import (
from discuss_data.core.views import core_search_view
+from discuss_data.ddcomments.models import Notification
+from discuss_data.ddcomments.forms import (
+ NotificationForm,
+ AccessRequestForm,
+)
+
@login_required
def dataset_search(request):
@@ -62,6 +78,7 @@ def dataset_search(request):
# )
+@login_required
def public_dataset_comments(request, ds_uuid):
"""
calls dataset_comments for "public" comments
@@ -70,6 +87,7 @@ def public_dataset_comments(request, ds_uuid):
return response
+@login_required
def public_dataset_edit_comment(request, ds_uuid, co_uuid=None):
"""
calls dataset_edit_comment for a "public" comment
@@ -78,6 +96,15 @@ def public_dataset_edit_comment(request, ds_uuid, co_uuid=None):
return response
+@login_required
+def public_dataset_edit_comment_reply(request, ds_uuid, co_uuid):
+ """
+ calls dataset_edit_comment_reply for a "public" comment
+ """
+ response = dataset_edit_comment(request, ds_uuid, "public", co_uuid, reply=True)
+ return response
+
+
@login_required
def datafile_download(request, df_uuid):
"""
@@ -121,11 +148,76 @@ def dataset_metadata(request, uuid):
)
+@login_required
+def dataset_access(request, uuid):
+ ds = check_published(uuid)
+ return render(
+ request,
+ "dddatasets/access.html",
+ {"ds": ds, "updated": True, "type": "access"},
+ )
+
+
+@login_required
+def dataset_access_request(request, uuid):
+ ds = check_published(uuid)
+ # check restricted access view permissions for the dataset
+ if check_perms("ra_view_dataset", request.user, ds):
+ # user has already access
+ return HttpResponse(
+ '{} '.format(
+ _("You have access to this Dataset")
+ )
+ )
+
+ if request.method == "POST":
+ form = AccessRequestForm(request.POST)
+ if form.is_valid():
+ comment = form.save(commit=False)
+ comment.owner = request.user
+ comment.notification_type = Notification.ACCESS_REQUEST
+ comment.content_type = ContentType.objects.get_for_model(ds)
+ comment.object_id = ds.id
+ comment.save()
+
+ return HttpResponse(
+ '{} '.format(
+ _("Access request sent to the Datasets administrators")
+ )
+ )
+ else:
+ messages.warning(request, form.errors)
+ else:
+ initial = {"text": "Please grant me access to this Dataset"}
+ form = AccessRequestForm(initial=initial)
+ return render(
+ request,
+ "ddcomments/_notification_add.html",
+ {
+ "form": form,
+ "add_url": "dddatasets:access_request",
+ "object": ds,
+ "btn_text": "Request Access",
+ "access_request": True,
+ },
+ )
+
+
@login_required
def dataset_files(request, uuid):
ds = check_published(uuid)
+ datafiles = list()
+ for datafile in ds.get_datafiles():
+ # check restricted access view permissions for the dataset
+ if check_perms("ra_view_dataset", request.user, ds):
+ datafiles.append(datafile)
+ else:
+ datafiles.append("RA")
+
return render(
- request, "dddatasets/files.html", {"ds": ds, "updated": True, "type": "files"},
+ request,
+ "dddatasets/files.html",
+ {"ds": ds, "updated": True, "type": "files", "datafiles": datafiles},
)
@@ -146,13 +238,14 @@ def dataset_files_pdf(request, uuid):
@login_required
def dataset_files_zip(request, uuid):
- # TODO: no rights management whatsoever implemented yet!
ds = DataSet.objects.get(uuid=uuid)
in_memory = BytesIO()
zipfile = ZipFile(in_memory, "a")
zipfile.writestr(ds.get_pdf_file_name(), create_pdf(request, ds))
for file in ds.get_datafiles():
- zipfile.writestr(file.name, file.file.read())
+ # check restricted access view permissions for the dataset
+ if check_perms("ra_view_dataset", request.user, ds):
+ zipfile.writestr(file.name, file.file.read())
# fix for Linux zip files read in Windows
for file in zipfile.filelist:
file.create_system = 0
@@ -170,11 +263,17 @@ def dataset_files_zip(request, uuid):
@login_required
def dataset_files_download(request, uuid, df_uuid):
- # TODO: no rights management whatsoever implemented yet!
- datafile = DataFile.objects.get(uuid=df_uuid).file
- response = FileResponse(datafile)
- response["Content-Disposition"] = "attachment; filename=%s" % (datafile,)
- return response
+ datafile = DataFile.objects.get(uuid=df_uuid, dataset__uuid=uuid)
+ download_name = datafile.get_download_file_name()
+ # check restricted access view permissions for the dataset
+ if check_perms("ra_view_dataset", request.user, datafile.dataset):
+ response = FileResponse(datafile.file)
+ response["Content-Disposition"] = "attachment; filename={}".format(
+ download_name,
+ )
+ return response
+ else:
+ return HttpResponseForbidden()
@login_required
diff --git a/discuss_data/dddatasets/views_prep.py b/discuss_data/dddatasets/views_prep.py
index dcf738bb758f4cf1ad88dcc8892309057be7fc62..319d57dabf2fa26d54652b3383bf5046a090b660 100644
--- a/discuss_data/dddatasets/views_prep.py
+++ b/discuss_data/dddatasets/views_prep.py
@@ -77,7 +77,10 @@ from discuss_data.dddatasets.utils import (
from discuss_data.ddpublications.forms import PublicationForm
from discuss_data.ddpublications.models import Publication
from discuss_data.ddusers.models import Country, User
-from discuss_data.ddusers.views import user_search
+from discuss_data.ddusers.views import (
+ user_search,
+ user_notifications,
+)
from discuss_data.pages.models import LicensePage
@@ -138,6 +141,7 @@ def prep_dataset_add(request):
ds.owner = request.user
ds.save()
form.save_m2m()
+
return redirect("dddatasets:prep_edit", ds_uuid=ds.uuid)
else:
None
@@ -452,7 +456,7 @@ def prep_dataset_edit_metadata(request, ds_uuid):
@login_required
def prep_dataset_edit_collaboration(request, ds_uuid):
"""
- Prep area tab to edit access
+ Prep area tab to edit collaborators
"""
ds = DataSet.objects.get(uuid=ds_uuid)
dsmo = ds.dataset_management_object
@@ -503,7 +507,7 @@ def prep_dataset_edit_access_user_search(request, ds_uuid):
return render(
request,
- "ddusers/search_results.html",
+ "ddusers/_search_results.html",
{"object_list": qs, "ds": ds, "search_type": search_type},
)
@@ -518,17 +522,36 @@ def prep_dataset_edit_access_user(request, ds_uuid):
userid = request.POST["userid"]
user = User.objects.get(uuid=userid)
perm_code = request.POST["perm"]
+ feed = request.POST.get("requestsrc")
if perm_code == "clr":
- print("clearing perms for", user)
+ logger.debug("clearing perms for", user)
ds.clear_user_permissions(user)
+ ds.save()
+ action.send(
+ request.user,
+ verb="revoked access",
+ action_object=ds,
+ target=user,
+ public=False,
+ )
elif perm_code == "acc":
- print("assigning perms for", user)
+ logger.debug("assigning perms for", user)
ds.assign_user_permissions(user, "ra_view_dataset")
+ action.send(
+ request.user,
+ verb="granted access",
+ action_object=ds,
+ target=user,
+ public=False,
+ )
+ ds.save()
else:
logger.error("requested non existing perm_code", perm_code, exc_info=1)
-
- return prep_dataset_edit_publish(request, ds_uuid)
+ if feed:
+ return user_notifications(request)
+ else:
+ return prep_dataset_edit_publish_access(request, ds_uuid)
@login_required
@@ -541,7 +564,7 @@ def prep_dataset_edit_collaboration_user_search(request, ds_uuid):
return render(
request,
- "ddusers/search_results.html",
+ "ddusers/_search_results.html",
{"object_list": qs, "ds": ds, "search_type": search_type},
)
@@ -591,6 +614,15 @@ def prep_dataset_edit_comment(request, ds_uuid, co_uuid=None):
return response
+@login_required
+def prep_dataset_edit_comment_reply(request, ds_uuid, co_uuid):
+ """
+ calls dataset_edit_comment_reply for a "prep" comment
+ """
+ response = dataset_edit_comment(request, ds_uuid, "prep", co_uuid, reply=True)
+ return response
+
+
@login_required
def prep_dataset_edit_datafiles(request, ds_uuid):
"""
@@ -648,14 +680,12 @@ def prep_dataset_edit_datafile(request, ds_uuid, df_uuid=None):
if request.method == "POST":
if df_uuid:
# Edit
- print("Edit DF")
form = DataFileUploadForm(request.POST, request.FILES, instance=datafile)
if form.is_valid():
form, form_err, updated = edit_datafile(request, datafile, form)
return prep_dataset_edit_datafiles(request, ds_uuid)
else:
# Add
- print("Add DF")
form = DataFileUploadForm(request.POST, request.FILES)
form, form_err, updated = edit_datafile(request, datafile, form)
return prep_dataset_edit_datafiles(request, ds_uuid)
@@ -663,10 +693,8 @@ def prep_dataset_edit_datafile(request, ds_uuid, df_uuid=None):
# Get
if df_uuid:
# Edit
- print("GET: Edit DF")
form = DataFileUploadForm(instance=datafile)
else:
- print("GET: Add DF")
form = DataFileUploadForm()
updated = False
form_err = None
@@ -708,7 +736,6 @@ def prep_dataset_edit_publication(request, ds_uuid, ds_pub_uuid=None):
if request.method == "POST":
if ds_pub_uuid:
- print("Edit ds_pub")
# Edit
form = PublicationForm(
request.POST, instance=publication, prefix="publication"
@@ -741,13 +768,11 @@ def prep_dataset_edit_publication(request, ds_uuid, ds_pub_uuid=None):
form_err = form.errors.as_data()
else:
# Get
- print("GET EDIT")
if ds_pub_uuid:
# Edit
form = PublicationForm(instance=publication, prefix="publication")
sub_form = DataSetPublicationForm(instance=ds_pub, prefix="ds_pub")
else:
- print("GET ADD")
form = PublicationForm(prefix="publication")
sub_form = DataSetPublicationForm(prefix="ds_pub")
updated = False
diff --git a/discuss_data/ddusers/models.py b/discuss_data/ddusers/models.py
index d4d9d2160207f31b7204e1d728ac9d3871c59bda..659ff9b427a92977a022854c355e2458cbd66b34 100644
--- a/discuss_data/ddusers/models.py
+++ b/discuss_data/ddusers/models.py
@@ -13,6 +13,9 @@ from actstream.models import (
followers,
user_stream,
)
+
+from guardian.shortcuts import get_objects_for_user
+
from django_bleach.models import BleachField
from taggit.managers import TaggableManager
@@ -142,9 +145,9 @@ class User(AbstractUser):
def get_affiliations(self):
return self.affiliation_user.all()
- # def get_datasets(self):
- # datasets = get_objects_for_user(self, 'view_dataset')
- # return datasets #DataSet.objects.filter(user=self).order_by('title')
+ def get_published_admin_datasets(self):
+ model = apps.get_model("dddatasets", "dataset")
+ return get_objects_for_user(self, "admin_dsmo", model).filter(published=True)
def get_published_datasets(self):
# use get model to avoid circular import
diff --git a/discuss_data/ddusers/urls.py b/discuss_data/ddusers/urls.py
index ff5a48867a4b667f8eae041742ccbe05656e2115..110464c2abe2272ec509351d2640db972a2c1257 100644
--- a/discuss_data/ddusers/urls.py
+++ b/discuss_data/ddusers/urls.py
@@ -6,28 +6,25 @@ from discuss_data.ddusers import views
app_name = "ddusers"
-# public URLs
+
urlpatterns = [
- path("public/search/", views.search, name="search"),
- path("public/list/", views.user_list, name="list"),
- path("public//", views.user_detail, name="detail"),
+ # public URLs
+ path("search/", views.search, name="search"),
+ path("list/", views.user_list, name="list"),
+ path("/", views.user_detail, name="detail"),
path("follow//", views.user_follow, name="follow"),
path("follow/", views.user_follow_status, name="follow_status"),
- path("follow/userfeed/", views.user_act_feed, name="user_feed"),
- path("follow/datasetfeed/", views.dataset_act_feed, name="dataset_feed"),
+ path("feed/users/", views.user_act_feed, name="user_feed"),
+ path("feed/datasets/", views.dataset_act_feed, name="dataset_feed"),
+ path("feed/notifications", views.notification_act_feed, name="notification_feed"),
# path("follow/otherfeed/", views.other_feed, name="other_feed"),
- path(
- "public//affiliations/",
- views.user_affiliations,
- name="affiliations",
- ),
- path("public//projects/", views.user_projects, name="projects"),
- path(
- "public//publications/",
- views.user_publications,
- name="publications",
- ),
- path("public//datasets/", views.user_datasets, name="datasets"),
+ path("/affiliations/", views.user_affiliations, name="affiliations",),
+ path("/projects/", views.user_projects, name="projects"),
+ path("/publications/", views.user_publications, name="publications",),
+ path("/datasets/", views.user_datasets, name="datasets"),
+ path("notifications/", views.user_notifications, name="notifications"),
+ path("notifications/add/", views.user_notification, name="notification"),
+ # edit urls
# users search for dataset access control
path("edit/search/", views.user_search, name="user_search"),
# user profile: edit pages
diff --git a/discuss_data/ddusers/views.py b/discuss_data/ddusers/views.py
index b59174fc3a8522461bd87c64975a7a4752ac6906..8fead868869893f67fb5e4e787afde1acc732c6c 100644
--- a/discuss_data/ddusers/views.py
+++ b/discuss_data/ddusers/views.py
@@ -6,7 +6,11 @@ from django.http import Http404
from django.shortcuts import render, redirect
from django.utils.translation import gettext as _
from django.contrib.auth.decorators import login_required
+from django.apps import apps
+from django.contrib import messages
+from django.shortcuts import get_object_or_404
+from actstream import action
from actstream.actions import (
follow,
unfollow,
@@ -16,6 +20,8 @@ from actstream.models import (
Follow,
followers,
user_stream,
+ Action,
+ model_stream,
)
from discuss_data.core.models import KeywordTagged
@@ -40,6 +46,9 @@ from discuss_data.ddusers.models import (
User,
)
+from discuss_data.ddcomments.models import Notification # , Notification
+from discuss_data.ddcomments.forms import NotificationForm
+
logger = logging.getLogger(__name__)
@@ -89,16 +98,16 @@ def user_list(request, page=1):
def format_act_feed(request, feed, object_list=None):
action_list = list()
- for action in feed:
- if action.target.class_name() == "User":
- action_list.append(action)
- if action.target.class_name() == "DataSet" and action.target.published:
- action_list.append(action)
+ for act in feed:
+ if act.target.class_name() == "User":
+ action_list.append(act)
+ if act.target.class_name() == "DataSet" and act.target.published:
+ action_list.append(act)
if (
- action.target.class_name() == "DataSetManagementObject"
- and action.target.published
+ act.target.class_name() == "DataSetManagementObject"
+ and act.target.published
):
- action_list.append(action)
+ action_list.append(act)
paginator = Paginator(action_list, 5)
page = request.GET.get("page")
feed_actions = paginator.get_page(page)
@@ -126,23 +135,103 @@ def following_act_feed(request):
raise Http404("Page not found.")
+@login_required
+def notification_act_feed(request):
+ """
+ Deprecated, to be replaced by user_notifications()
+ """
+ # list of ids of datasets where user has admin rights
+ user_admin_datasets = list(
+ request.user.get_published_admin_datasets().values_list("id", flat=True)
+ )
+
+ # queries for actions with notification as action object and one of the users datasets as target
+ actions_ar = Action.objects.access_requests_received(user_admin_datasets)
+
+ notification_content_type = ContentType.objects.get(
+ app_label="ddcomments", model="comment"
+ )
+ actions_notifications = Action.objects.any_everything(request.user).filter(
+ action_object_content_type=notification_content_type.id,
+ )
+
+ actions = actions_ar | actions_notifications
+ feed_actions, paginator_range = format_act_feed(request, actions)
+ feed_type = "notification-act-feed"
+
+ return render(
+ request,
+ "ddusers/dashboard.html",
+ {
+ "feed_actions": feed_actions,
+ "paginator_range": paginator_range,
+ "feed_type": feed_type,
+ },
+ )
+
+
+@login_required
+def user_notifications(request):
+ user_notifications_to = Notification.objects.root_nodes().filter(
+ recipient=request.user
+ )
+ user_notifications_from = Notification.objects.root_nodes().filter(
+ owner=request.user
+ )
+
+ user_admin_datasets = list(
+ request.user.get_published_admin_datasets().values_list("id", flat=True)
+ )
+ dataset_content_type = ContentType.objects.get(
+ app_label="dddatasets", model="dataset"
+ )
+ datasets_notifications = Notification.objects.root_nodes().filter(
+ content_type=dataset_content_type.id, object_id__in=user_admin_datasets
+ )
+
+ notifications = (
+ user_notifications_to | user_notifications_from | datasets_notifications
+ )
+
+ feed_type = "notifications"
+
+ paginator = Paginator(notifications.order_by("-date_added"), 5)
+ page = request.GET.get("page")
+ feed_actions = paginator.get_page(page)
+ paginator_range = range(1, paginator.num_pages + 1)
+
+ form = NotificationForm()
+
+ return render(
+ request,
+ "ddusers/dashboard.html",
+ {
+ "feed_actions": feed_actions,
+ "paginator_range": paginator_range,
+ "feed_type": feed_type,
+ "form": form,
+ "add_url": "ddusers:notification",
+ "btn_text": "Reply",
+ },
+ )
+
+
@login_required
def user_act_feed(request):
- if request.user.is_authenticated:
- feed_actions, paginator_range = format_act_feed(
- request, actor_stream(request.user)
- )
- feed_type = "user-act-feed"
+ actions = Action.objects.any_everything(request.user)
- return render(
- request,
- "ddusers/dashboard.html",
- {
- "feed_actions": feed_actions,
- "paginator_range": paginator_range,
- "feed_type": feed_type,
- },
- )
+ feed_actions, paginator_range = format_act_feed(request, actions)
+
+ feed_type = "user-act-feed"
+ return render(
+ request,
+ "ddusers/dashboard.html",
+ {
+ "feed_actions": feed_actions,
+ "paginator_range": paginator_range,
+ "feed_type": feed_type,
+ },
+ )
@login_required
@@ -191,11 +280,63 @@ def user_follow_status(request):
@login_required
def user_detail(request, us_uuid):
- try:
- user = User.objects.get(uuid=us_uuid)
- except Exception:
- raise Http404("Page not found.")
- return render(request, "ddusers/detail.html", {"user": user})
+ user = get_object_or_404(User, uuid=us_uuid)
+ initial = {
+ "recipient_uuid": user.uuid,
+ "parent_uuid": None,
+ }
+
+ return render(
+ request,
+ "ddusers/detail.html",
+ {
+ "form": NotificationForm(initial=initial),
+ "add_url": "ddusers:notification",
+ "object": user,
+ "ictarget": "#ds-content",
+ "btn_text": _("Send message"),
+ "user": user,
+ },
+ )
+
+
+@login_required
+def user_notification(request):
+ if request.method == "POST":
+ form = NotificationForm(request.POST)
+ if form.is_valid():
+ comment = form.save(commit=False)
+ comment.owner = request.user
+ recipient_uuid = form.cleaned_data.get("recipient_uuid")
+ user = User.objects.get(uuid=recipient_uuid)
+ comment.recipient = user
+ parent_uuid = form.cleaned_data.get("parent_uuid")
+ if parent_uuid:
+ comment.parent = Notification.objects.get(uuid=parent_uuid)
+ comment.notification_type = Notification.PRIVATE
+ comment.content_type = ContentType.objects.get_for_model(user)
+ comment.object_id = user.id
+ comment.save()
+
+ # # send an action
+ # action.send(
+ # request.user,
+ # verb=_("sent message to"),
+ # target=user,
+ # action_object=comment,
+ # public=False,
+ # )
+ messages.success(request, _("Message sent user_notification()"))
+ return user_notifications(request)
+ else:
+ messages.warning(request, form.errors)
+ else:
+ form = NotificationForm()
+ return render(
+ request,
+ "ddcomments/_notification_add.html",
+ {"form": form, "add_url": "ddusers:notification", "ictarget": "#ds-content"},
+ )
@login_required
diff --git a/discuss_data/static/js/project.js b/discuss_data/static/js/project.js
index 4d65684028ce4431fa8105154dfc89200775d3a0..bb61950deab599a34d4f05bec156c3f90eca998f 100644
--- a/discuss_data/static/js/project.js
+++ b/discuss_data/static/js/project.js
@@ -91,6 +91,14 @@ function docucardActive(element) {
}
};
+/*
+ * Remove modal manually in cases where an intercooler request conflicts
+ * with bootstraps data-dismiss="modal".
+ * Called like this: ic-on-beforeSend="removeModal()"
+ */
+function removeModal() {
+ $(".modal").modal('hide');
+};
/*
* disable buttons prior to submit and show spinner
@@ -114,4 +122,5 @@ $(document).on('beforeSend.ic', function (event, el, data) {
el.addClass('form-submitted');
}
}
-});
\ No newline at end of file
+});
+
diff --git a/discuss_data/static/sass/project.scss b/discuss_data/static/sass/project.scss
index 726a4d941e4e71fa134a0d979e00b24872e9439f..526680960d5a74564f4b5515196af299d200415b 100644
--- a/discuss_data/static/sass/project.scss
+++ b/discuss_data/static/sass/project.scss
@@ -289,6 +289,27 @@ body {
border: 1px solid theme-color("border-gray");
}
+.border-restricted {
+ border-color: theme-color("danger");
+}
+
+.fileicon {
+ font-size: xx-large;
+ color: theme-color("primary");
+}
+
+/*
+ * Notifications
+ */
+
+.notification textarea.form-control {
+ height: 5rem;
+}
+
+.notification .form-control {
+ width: 80%;
+}
+
/*
* Sidebar
*/
@@ -345,6 +366,10 @@ body {
* Comments
*/
+.comments-head {
+ margin-top: 1rem;
+}
+
.comment-name {
font-weight: 700;
}
@@ -364,6 +389,18 @@ body {
/*
* Comment badges
*/
+.badge-deleted {
+ background-color: theme-color("dark");
+ color: theme-color("light");
+ font-weight: 500;
+}
+
+.badge-access-request {
+ background-color: theme-color("tertiary");
+ color: theme-color("light");
+ font-weight: 500;
+}
+
.badge-public {
background-color: theme-color("success");
diff --git a/discuss_data/templates/actstream/action.html b/discuss_data/templates/actstream/action.html
index e0e18e1b6a7b6649e986600bbe52aa0964651985..1844b045a47ecd33753cc2e9f0ef34b27d27d5c5 100644
--- a/discuss_data/templates/actstream/action.html
+++ b/discuss_data/templates/actstream/action.html
@@ -3,12 +3,19 @@
{% if action.actor.get_absolute_url %}{{ action.actor }}
{% else %}{{ action.actor }} {% endif %}
-{% if action.verb %}{{ action.verb }}{% endif %}
+{% if action.verb %}
+ {{ action.verb }}
+{% endif %}
{% if action.action_object %}
+ {% if action.action_object.class_name == 'Notification' %}
+ {% else %}
{{ action.action_object }}
+ {% endif %}
+{% endif %}
+{% if action.action_object and action.target %}
+ {% if action.target.class_name == 'DataSet' %}for Dataset{% else %}to{% endif %}
{% endif %}
-{% if action.action_object and action.target %}to{% endif %}
{% if action.target %}
{{ action.target }}
{% endif %}
diff --git a/discuss_data/templates/base.html b/discuss_data/templates/base.html
index 144c441f2b114433fc3ca037d27460521a1440b1..c2a9167789a652d1d0f28445143800633f6c35c5 100644
--- a/discuss_data/templates/base.html
+++ b/discuss_data/templates/base.html
@@ -53,12 +53,6 @@
{# sidebar nav #}
{% include 'nav-sidebar.html' with editmode=editmode %}
- {% if messages %}
- {% for message in messages %}
- {{ message }}×
- {% endfor %}
- {% endif %}
-
{% block content %}
{% block ic-content %}
diff --git a/discuss_data/templates/core/_confirm_modal.html b/discuss_data/templates/core/_confirm_modal.html
index 9c9afcbe452003d7fc0bc51a3ca7c35dc44546bf..2bbcfe7a45aba1fe55f3cf8eb5075b31e0547ac7 100644
--- a/discuss_data/templates/core/_confirm_modal.html
+++ b/discuss_data/templates/core/_confirm_modal.html
@@ -2,7 +2,7 @@
-