Skip to content

[WIP] Impact Notifications #11096

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 40 commits into
base: feature/pbs-25-07
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
ca91541
fixed gv for osf's docker compose, added envvar to fix initial postgr…
opaduchak Mar 24, 2025
b4d349f
bumped gv python to 3.13
opaduchak Mar 24, 2025
624f1e3
Add registration callback endpoint and tests
Ostap-Zherebetskyi Apr 3, 2025
940bc3c
add-trailing-comma
Ostap-Zherebetskyi Apr 3, 2025
178badb
fix test_view_classes_have_minimal_set_of_permissions_classes
Ostap-Zherebetskyi Apr 3, 2025
054ce01
remove old Flask registration_callback code and related decorators
Ostap-Zherebetskyi Apr 3, 2025
7507f66
Remove outdated tests
Ostap-Zherebetskyi Apr 14, 2025
d1c1db2
Merge pull request #11100 from CenterForOpenScience/feature/pbs-25-07
brianjgeiger Apr 18, 2025
6aef55d
Merge branch 'release/25.07.0'
brianjgeiger Apr 18, 2025
84e9997
Merge tag '25.07.0' into develop
brianjgeiger Apr 18, 2025
41207de
retry mint doi only if previous dois are minted
ihorsokhanexoft Apr 18, 2025
81f313f
added version filtering
ihorsokhanexoft Apr 18, 2025
1203ad7
flat guids
ihorsokhanexoft Apr 18, 2025
220ec73
simplified query
ihorsokhanexoft Apr 18, 2025
e2303c0
updated error text
ihorsokhanexoft Apr 21, 2025
38de16f
use minted doi for building metadata
ihorsokhanexoft Apr 21, 2025
21e9a1e
mint missing doi when build metadata
ihorsokhanexoft Apr 21, 2025
d8b5898
use newly built doi for previous versions
ihorsokhanexoft Apr 21, 2025
ac0c154
improved naming
ihorsokhanexoft Apr 21, 2025
3d7d244
Bind task for proper retrying
mfraezz Apr 21, 2025
d82f046
Merge branch 'hotfix/25.07.1' into develop
mfraezz Apr 21, 2025
b9e5e78
gdpr deletion shouldn't take into account deleted nodes (#11098)
ihorsokhanexoft Apr 21, 2025
2314b1f
Merge pull request #11095 from Ostap-Zherebetskyi/feature/outdated_code
Johnetordoff Apr 21, 2025
6a24f92
Merge pull request #11051 from opaduchak/fix/gv-local-setup
adlius Apr 22, 2025
7b3acf5
[ENG-7270] Enable Product Team to Force Archive Registrations in the …
antkryt Apr 22, 2025
b5f5d22
[ENG-7798] Parse versioned guid (#11104)
ihorsokhanexoft Apr 22, 2025
c43e24e
[ENG-7263] Fix/eng 7263 (#11090)
Vlad0n20 Apr 22, 2025
73b2a4a
Merge pull request #11089 from Ostap-Zherebetskyi/feature/registratio…
Johnetordoff Apr 23, 2025
a7a3bc6
[ENG-7503] Fix/eng 7503 (#11092)
Vlad0n20 Apr 24, 2025
b233bfe
delete sharev2 push [ENG-7387] (#11032)
aaxelb Apr 24, 2025
90d5b68
[ENG-7716] Allow for reinstatement of previous preprint versions (wit…
antkryt Apr 24, 2025
0d3698c
fix feature for non-contributor admin (#11111)
antkryt Apr 24, 2025
7e0c97b
[ENG-7263] Part 2 (#11110)
Vlad0n20 Apr 24, 2025
3f356a4
New Notification system proof of concept
Johnetordoff Apr 11, 2025
a746e7c
[ENG-7263] Fix/eng 7263 part 3 (#11119)
Vlad0n20 Apr 29, 2025
1c9e98b
[ENG-7716] Allow for reinstatement of previous preprint versions (wit…
antkryt Apr 29, 2025
3e83dec
Merge branch 'develop' into upstream/pbs-25-08
brianjgeiger Apr 30, 2025
0d4d9c5
Merge branch 'feature/pbs-25-07' of https://github.com/CenterForOpenS…
Johnetordoff May 1, 2025
03d7dd1
Merge branch 'feature/pbs-25-08' of https://github.com/CenterForOpenS…
Johnetordoff May 1, 2025
a4ca1c8
add impact Notification WIP
Johnetordoff May 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .docker-compose.gv.env
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ALLOWED_HOSTS="localhost,192.168.168.167"
ALLOWED_HOSTS="localhost,192.168.168.167,127.0.0.1"
CORS_ALLOWED_ORIGINS="http://localhost:5000,http://192.168.168.167:5000"
OSFDB_HOST=192.168.168.167
POSTGRES_HOST="$OSFDB_HOST"
Expand All @@ -7,5 +7,10 @@ OSF_API_BASE_URL="http://192.168.168.167:8000"
POSTGRES_USER="postgres"
POSTGRES_DB="gravyvalet"
SECRET_KEY="secret"
PYTHONUNBUFFERED=0 # This when set to 0 will allow print statements to be visible in the Docker logs
PYTHONUNBUFFERED=1 # This when set to 0 will allow print statements to be visible in the Docker logs
OSF_AUTH_COOKIE_NAME=osf
OSF_SENSITIVE_DATA_SECRET="TrainglesAre5Squares"
OSF_SENSITIVE_DATA_SALT="yusaltydough"
DEBUG=1


4 changes: 2 additions & 2 deletions README-docker-compose.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,12 +350,12 @@ docker compose run --rm --service-ports web

- Test a Specific Class
```bash
docker compose run --rm web python3 -m pytest tests/test_conferences.py::TestProvisionNode
docker compose run --rm web python3 -m pytest tests/test_node.py::TestExample:test_example
```

- Test a Specific Method
```bash
docker compose run --rm web python3 -m pytest tests/test_conferences.py::TestProvisionNode::test_upload
docker compose run --rm web python3 -m pytest tests/test_node.py::TestExample:test_example
```

## Managing Container State
Expand Down
40 changes: 22 additions & 18 deletions addons/base/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
from framework.sentry import log_exception
from framework.routing import proxy_url
from framework.transactions.handlers import no_auto_transaction
from website import mails
from website import settings
from addons.base import signals as file_signals
from addons.base.utils import format_last_known_metadata, get_mfr_url
Expand All @@ -54,7 +53,8 @@
DraftRegistration,
Guid,
FileVersionUserMetadata,
FileVersion
FileVersion,
NotificationType
)
from osf.metrics import PreprintView, PreprintDownload
from osf.utils import permissions
Expand Down Expand Up @@ -431,11 +431,7 @@ def _enqueue_metrics(file_version, file_node, action, auth, from_mfr=False):
def _construct_payload(auth, resource, credentials, waterbutler_settings):

if isinstance(resource, Registration):
callback_url = resource.api_url_for(
'registration_callbacks',
_absolute=True,
_internal=True
)
callback_url = resource.callbacks_url
else:
callback_url = resource.api_url_for(
'create_waterbutler_log',
Expand Down Expand Up @@ -583,17 +579,17 @@ def create_waterbutler_log(payload, **kwargs):
)

if payload.get('email') is True or payload.get('errors'):
mails.send_mail(
user.username,
mails.FILE_OPERATION_FAILED if payload.get('errors')
else mails.FILE_OPERATION_SUCCESS,
action=payload['action'],
source_node=source_node,
destination_node=destination_node,
source_path=payload['source']['materialized'],
source_addon=payload['source']['addon'],
destination_addon=payload['destination']['addon'],
osf_support_email=settings.OSF_SUPPORT_EMAIL
if payload.get('errors'):
notification_type = NotificationType.Type.FILE_OPERATION_FAILED
else:
notification_type = NotificationType.Type.FILE_OPERATION_SUCCESS

NotificationType.objects.get(
name=notification_type.value,
).emit(
user=user,
subscribed_object=destination_node or node,
event_context=payload,
)

if payload.get('errors'):
Expand All @@ -613,6 +609,14 @@ def create_waterbutler_log(payload, **kwargs):
with transaction.atomic():
file_signals.file_updated.send(target=node, user=user, event_type=action, payload=payload)

NotificationType.objects.get(
name=action,
).emit(
user=user,
subscribed_object=target_node or node,
event_context=metadata,
)

return {'status': 'success'}


Expand Down
60 changes: 0 additions & 60 deletions addons/wiki/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,66 +111,6 @@ def test_update_name_invalid(self):
with pytest.raises(NameInvalidError):
WikiPage.objects.create_for_node(self.project, invalid_name, 'more valid content', self.auth)

def test_update_wiki_updates_comments_and_user_comments_viewed_timestamp(self):
project = ProjectFactory(creator=self.user, is_public=True)
wiki_page = WikiFactory(node=project, page_name='test')
wiki = WikiVersionFactory(wiki_page=wiki_page)
comment = CommentFactory(node=project, target=Guid.load(wiki_page._id), user=UserFactory())

# user views comments -- sets user.comments_viewed_timestamp
url = project.api_url_for('update_comments_timestamp')
res = self.app.put(url, json={
'page': 'wiki',
'rootId': wiki_page._id
}, auth=self.user.auth)
assert res.status_code == 200
self.user.reload()
assert wiki_page._id in self.user.comments_viewed_timestamp

# user updates the wiki
wiki_page.update(self.user, 'Updating wiki')
comment.reload()
self.user.reload()
assert wiki_page._id in self.user.comments_viewed_timestamp
assert comment.target.referent._id == wiki_page._id

# Regression test for https://openscience.atlassian.net/browse/OSF-6138
def test_update_wiki_updates_contributor_comments_viewed_timestamp(self):
contributor = AuthUserFactory()
project = ProjectFactory(creator=self.user, is_public=True)
project.add_contributor(contributor)
project.save()
wiki_page = WikiFactory(node=project, page_name='test')
wiki = WikiVersionFactory(wiki_page=wiki_page)
comment = CommentFactory(node=project, target=Guid.load(wiki_page._id), user=self.user)

# user views comments -- sets user.comments_viewed_timestamp
url = project.api_url_for('update_comments_timestamp')
res = self.app.put(url, json={
'page': 'wiki',
'rootId': wiki_page._id
}, auth=self.user.auth)
assert res.status_code == 200
self.user.reload()
assert wiki_page._id in self.user.comments_viewed_timestamp

# contributor views comments -- sets contributor.comments_viewed_timestamp
res = self.app.put(url, json={
'page': 'wiki',
'rootId': wiki_page._id
}, auth=contributor.auth)
contributor.reload()
assert wiki_page._id in contributor.comments_viewed_timestamp

# user updates the wiki
wiki_page.update(self.user, 'Updating wiki')
comment.reload()
contributor.reload()

new_version_id = WikiVersion.objects.get_for_node(project, 'test')._id
assert wiki_page._id in contributor.comments_viewed_timestamp
assert comment.target.referent._id == wiki_page._id

# Regression test for https://openscience.atlassian.net/browse/OSF-8584
def test_no_read_more_when_less_than_400_character(self):
wiki_content = '1234567'
Expand Down
6 changes: 4 additions & 2 deletions admin/nodes/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@
re_path(r'^(?P<guid>[a-z0-9]+)/reindex_share_node/$', views.NodeReindexShare.as_view(), name='reindex-share-node'),
re_path(r'^(?P<guid>[a-z0-9]+)/reindex_elastic_node/$', views.NodeReindexElastic.as_view(),
name='reindex-elastic-node'),
re_path(r'^(?P<guid>[a-z0-9]+)/restart_stuck_registrations/$', views.RestartStuckRegistrationsView.as_view(),
name='restart-stuck-registrations'),
re_path(r'^(?P<guid>[a-z0-9]+)/remove_stuck_registrations/$', views.RemoveStuckRegistrationsView.as_view(),
name='remove-stuck-registrations'),
re_path(r'^(?P<guid>[a-z0-9]+)/check_archive_status/$', views.CheckArchiveStatusRegistrationsView.as_view(),
name='check-archive-status'),
re_path(r'^(?P<guid>[a-z0-9]+)/force_archive_registration/$', views.ForceArchiveRegistrationsView.as_view(),
name='force-archive-registration'),
re_path(r'^(?P<guid>[a-z0-9]+)/remove_user/(?P<user_id>[a-z0-9]+)/$', views.NodeRemoveContributorView.as_view(),
name='remove-user'),
re_path(r'^(?P<guid>[a-z0-9]+)/modify_storage_usage/$', views.NodeModifyStorageUsage.as_view(),
Expand Down
98 changes: 76 additions & 22 deletions admin/nodes/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytz
from enum import Enum
from datetime import datetime
from framework import status

Expand Down Expand Up @@ -26,7 +27,7 @@
from api.share.utils import update_share
from api.caching.tasks import update_storage_usage_cache

from osf.exceptions import NodeStateError
from osf.exceptions import NodeStateError, RegistrationStuckError
from osf.models import (
OSFUser,
NodeLog,
Expand Down Expand Up @@ -672,44 +673,97 @@ def post(self, request, *args, **kwargs):
return redirect(self.get_success_url())


class RestartStuckRegistrationsView(NodeMixin, TemplateView):
""" Allows an authorized user to restart a registrations archive process.
class RemoveStuckRegistrationsView(NodeMixin, View):
""" Allows an authorized user to remove a registrations if it's stuck in the archiving process.
"""
template_name = 'nodes/restart_registrations_modal.html'
permission_required = ('osf.view_node', 'osf.change_node')

def post(self, request, *args, **kwargs):
# Prevents circular imports that cause admin app to hang at startup
from osf.management.commands.force_archive import archive, verify
stuck_reg = self.get_object()
if verify(stuck_reg):
try:
archive(stuck_reg)
messages.success(request, 'Registration archive processes has restarted')
except Exception as exc:
messages.error(request, f'This registration cannot be unstuck due to {exc.__class__.__name__} '
f'if the problem persists get a developer to fix it.')
if Registration.find_failed_registrations().filter(id=stuck_reg.id).exists():
stuck_reg.delete_registration_tree(save=True)
messages.success(request, 'The registration has been deleted')
else:
messages.error(request, 'This registration may not technically be stuck,'
' if the problem persists get a developer to fix it.')

return redirect(self.get_success_url())


class RemoveStuckRegistrationsView(NodeMixin, TemplateView):
""" Allows an authorized user to remove a registrations if it's stuck in the archiving process.
class CheckArchiveStatusRegistrationsView(NodeMixin, View):
"""Allows an authorized user to check a registration archive status.
"""
permission_required = ('osf.view_node', 'osf.change_node')

def get(self, request, *args, **kwargs):
# Prevents circular imports that cause admin app to hang at startup
from osf.management.commands.force_archive import check

registration = self.get_object()

if registration.archived:
messages.success(request, f"Registration {registration._id} is archived.")
return redirect(self.get_success_url())

try:
archive_status = check(registration)
messages.success(request, archive_status)
except RegistrationStuckError as exc:
messages.error(request, str(exc))

return redirect(self.get_success_url())


class CollisionMode(Enum):
NONE: str = 'none'
SKIP: str = 'skip'
DELETE: str = 'delete'


class ForceArchiveRegistrationsView(NodeMixin, View):
"""Allows an authorized user to force archive registration.
"""
template_name = 'nodes/remove_registrations_modal.html'
permission_required = ('osf.view_node', 'osf.change_node')

def post(self, request, *args, **kwargs):
stuck_reg = self.get_object()
if Registration.find_failed_registrations().filter(id=stuck_reg.id).exists():
stuck_reg.delete_registration_tree(save=True)
messages.success(request, 'The registration has been deleted')
# Prevents circular imports that cause admin app to hang at startup
from osf.management.commands.force_archive import verify, archive, DEFAULT_PERMISSIBLE_ADDONS

registration = self.get_object()
force_archive_params = request.POST

collision_mode = force_archive_params.get('collision_mode', CollisionMode.NONE.value)
delete_collision = CollisionMode.DELETE.value == collision_mode
skip_collision = CollisionMode.SKIP.value == collision_mode

allow_unconfigured = force_archive_params.get('allow_unconfigured', False)

addons = set(force_archive_params.getlist('addons', []))
addons.update(DEFAULT_PERMISSIBLE_ADDONS)

try:
verify(registration, permissible_addons=addons, raise_error=True)
except ValidationError as exc:
messages.error(request, str(exc))
return redirect(self.get_success_url())

dry_mode = force_archive_params.get('dry_mode', False)

if dry_mode:
messages.success(request, f"Registration {registration._id} can be archived.")
else:
messages.error(request, 'This registration may not technically be stuck,'
' if the problem persists get a developer to fix it.')
try:
archive(
registration,
permissible_addons=addons,
allow_unconfigured=allow_unconfigured,
skip_collision=skip_collision,
delete_collision=delete_collision,
)
messages.success(request, 'Registration archive process has finished.')
except Exception as exc:
messages.error(request, f'This registration cannot be archived due to {exc.__class__.__name__}: {str(exc)}. '
f'If the problem persists get a developer to fix it.')

return redirect(self.get_success_url())

Expand Down
4 changes: 2 additions & 2 deletions admin/notifications/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from osf.models.notifications import NotificationSubscription
from osf.models.notification import NotificationSubscription
from django.db.models import Count

def delete_selected_notifications(selected_ids):
NotificationSubscription.objects.filter(id__in=selected_ids).delete()

def detect_duplicate_notifications(node_id=None):
query = NotificationSubscription.objects.values('_id').annotate(count=Count('_id')).filter(count__gt=1)
query = NotificationSubscription.objects.annotate(count=Count('id')).filter(count__gt=1)
if node_id:
query = query.filter(node_id=node_id)

Expand Down
2 changes: 2 additions & 0 deletions admin/preprints/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
re_path(r'^(?P<guid>\w+)/change_provider/$', views.PreprintProviderChangeView.as_view(), name='preprint-provider'),
re_path(r'^(?P<guid>\w+)/machine_state/$', views.PreprintMachineStateView.as_view(), name='preprint-machine-state'),
re_path(r'^(?P<guid>\w+)/reindex_share_preprint/$', views.PreprintReindexShare.as_view(), name='reindex-share-preprint'),
re_path(r'^(?P<guid>\w+)/reversion_preprint/$', views.PreprintReVersion.as_view(), name='re-version-preprint'),
re_path(r'^(?P<guid>\w+)/remove_user/(?P<user_id>[a-z0-9]+)/$', views.PreprintRemoveContributorView.as_view(), name='remove-user'),
re_path(r'^(?P<guid>\w+)/make_private/$', views.PreprintMakePrivate.as_view(), name='make-private'),
re_path(r'^(?P<guid>\w+)/fix_editing/$', views.PreprintFixEditing.as_view(), name='fix-editing'),
re_path(r'^(?P<guid>\w+)/make_public/$', views.PreprintMakePublic.as_view(), name='make-public'),
re_path(r'^(?P<guid>\w+)/remove/$', views.PreprintDeleteView.as_view(), name='remove'),
re_path(r'^(?P<guid>\w+)/restore/$', views.PreprintDeleteView.as_view(), name='restore'),
Expand Down
Loading
Loading