Repository: rdmorganiser/rdmo (language: python)

Issue:
translation of empty fields for title or help text of a question
### Rationale / Begründung

Currently, when a field is left empty (`''`) and translated by the [`TranslationMixin`](https://github.com/rdmorganiser/rdmo/blob/89961b02fe5a9bfd8ce58c347406d053e7b26270/rdmo/core/models.py#LL30C7-L30C23), another language with a non-empty field is returned. 
In some cases this can be desirable but in others not. For example, when the help text of a question is defined for one language and missing for another..
[This line](https://github.com/rdmorganiser/rdmo/blob/89961b02fe5a9bfd8ce58c347406d053e7b26270/rdmo/core/models.py#LL38C9-L38C10) a makes empty fields return `None` instead of `''` and skips to find other language fields. Is this a bug or feature?
```py
 r = getattr(self, '%s_%s' % (field, lang_field)) or None
```

### Affected 

Users

### Minimal functionality

* return an empty string when a field for a certain language is left empty

### Nice-to-have functionality

* have a setting to return empty fields or find next non-empty language field

### References / Verweise

* https://github.com/rdmorganiser/rdmo/blob/89961b02fe5a9bfd8ce58c347406d053e7b26270/rdmo/core/models.py#LL38C9-L38C10

Add invite api (#540)
This PR adds an API endpoint for the invite functionality. Invites can be created, retrieved, updated (only the role), or deleted for a project. There is not "silent" version of an invite, since the same behavior can be achieved by adding a membership over the existing membership AIP. The other difference to the existing invite feature is, that *either* user (via the `user_id`) or email are specified. 
show all projects using a certain catalog
### Rationale / Begründung

For catalog management it would be helpful to get a list of projects that are using a certain catalog. This could be done, e.g. by extending the all projects view.

### Affected 

Users, Managers

### Minimal functionality

a list of all projects which can be filtered by catalog

### References / Verweise

An extension to "Sorting and filter functions for "View all projects" window" https://github.com/rdmorganiser/rdmo/issues/355


Discussion / hints:
I think the idea was to have "something" if no language specific content is available. Maybe it was a bad idea. I think we should talk about the this with the content people (tagging @Zack-83 ).

I think the line was on purpose, that if the field is `''`, it is set to `None` and another is searched.



Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 00e85f80e7..c9e512e9f7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
 # Changelog
 
+## RDMO 1.11.0 (Aug 1, 2023)
+
+* Refactor Shibboleth setup, add LOGIN_FORM, SHIBBOLETH_LOGIN_URL
+* Add filter for catalogs to site_projects view
+* Add API for project Invites
+* Add catalog, site, and rdmo version to views
+* Enable PROJECT_QUESTIONS_AUTOSAVE by default
+* Remove skip button when PROJECT_QUESTIONS_AUTOSAVE is True, move back button
+* Remove automatic replacement of missing translations, unless REPLACE_MISSING_TRANSLATION is True
+* Hide html metadata tag in views
+* Update django-allauth requirement
+
 ## RDMO 1.10.0 (Apr 27, 2023)
 
 * Allow users to create API access tokens (if ACCOUNT_ALLOW_USER_TOKEN is set)
diff --git a/rdmo/accounts/settings.py b/rdmo/accounts/settings.py
index e6ccbd0546..b531de743b 100644
--- a/rdmo/accounts/settings.py
+++ b/rdmo/accounts/settings.py
@@ -133,5 +133,9 @@
       ('projects', 'integration', 'change_integration'),
       ('projects', 'integration', 'delete_integration'),
       ('projects', 'integration', 'view_integration'),
+      ('projects', 'invite', 'add_invite'),
+      ('projects', 'invite', 'change_invite'),
+      ('projects', 'invite', 'delete_invite'),
+      ('projects', 'invite', 'view_invite'),
   ))
 )
diff --git a/rdmo/accounts/templates/account/login_form.html b/rdmo/accounts/templates/account/login_form.html
index 7250b1df2b..b672375724 100644
--- a/rdmo/accounts/templates/account/login_form.html
+++ b/rdmo/accounts/templates/account/login_form.html
@@ -2,12 +2,23 @@
 
 {% if settings.SHIBBOLETH %}
 
-    <a class="btn btn-success extend" href="{{ settings.LOGIN_URL }}">
+{% if settings.SHIBBOLETH_LOGIN_URL %}
+<p>
+    <a class="btn btn-success extend" href="{% url 'shibboleth_login' %}">
         {% trans 'Login with Shibboleth' %}
     </a>
-
+</p>
 {% else %}
+<p>
+    <a class="btn btn-success extend" href="{% url 'account_login' %}">
+        {% trans 'Login with Shibboleth' %}
+    </a>
+</p>
+{% endif %}
 
+{% endif %}
+
+{% if settings.LOGIN_FORM %}
 <form method="post" action="{% url 'account_login' %}">
     {% csrf_token %}
 
@@ -21,7 +32,6 @@
 
     <input type="submit" class="btn btn-primary" value="{% trans 'Login' %}" />
 </form>
-
 {% endif %}
 
 {% if settings.ACCOUNT %}
diff --git a/rdmo/accounts/urls/__init__.py b/rdmo/accounts/urls/__init__.py
index 7574394317..3ae49cf1b0 100644
--- a/rdmo/accounts/urls/__init__.py
+++ b/rdmo/accounts/urls/__init__.py
@@ -2,7 +2,8 @@
 from django.contrib.auth import views as auth_views
 from django.urls import include, re_path
 
-from ..views import profile_update, remove_user, terms_of_use, token
+from ..views import (profile_update, remove_user, shibboleth_login,
+                     shibboleth_logout, terms_of_use, token)
 
 urlpatterns = [
     # edit own profile
@@ -20,16 +21,23 @@
     urlpatterns += [
         re_path(r'^', include('allauth.urls'))
     ]
-elif settings.SHIBBOLETH:
-    urlpatterns += [
-        re_path('^logout/', auth_views.LogoutView.as_view(next_page=settings.SHIBBOLETH_LOGOUT_URL), name='account_logout'),
-    ]
 else:
     urlpatterns += [
         re_path('^login/', auth_views.LoginView.as_view(template_name='account/login.html'), name='account_login'),
         re_path('^logout/', auth_views.LogoutView.as_view(next_page=settings.LOGIN_REDIRECT_URL), name='account_logout'),
     ]
 
+if settings.SHIBBOLETH:
+    if settings.SHIBBOLETH_LOGIN_URL:
+        urlpatterns += [
+            re_path('^shibboleth/login/', shibboleth_login, name='shibboleth_login'),
+            re_path('^shibboleth/logout/', shibboleth_logout, name='shibboleth_logout'),
+        ]
+    else:
+        urlpatterns += [
+            re_path('^logout/', auth_views.LogoutView.as_view(next_page=settings.SHIBBOLETH_LOGOUT_URL), name='account_logout'),
+        ]
+
 if settings.ACCOUNT_ALLOW_USER_TOKEN:
     urlpatterns += [
         re_path(r'^token/$', token, name='account_token')
diff --git a/rdmo/accounts/views.py b/rdmo/accounts/views.py
index c99dbcc30b..eeebe58123 100644
--- a/rdmo/accounts/views.py
+++ b/rdmo/accounts/views.py
@@ -1,10 +1,12 @@
 import logging
+import re
 
 from django.conf import settings
 from django.contrib.auth import logout
 from django.contrib.auth.decorators import login_required
 from django.http import HttpResponseRedirect
 from django.shortcuts import render
+from django.urls import reverse
 from rest_framework.authtoken.models import Token
 
 from rdmo.core.utils import get_next, get_referer_path_info
@@ -46,7 +48,7 @@ def remove_user(request):
         return render(request, 'profile/profile_remove_closed.html')
     form = RemoveForm(request.POST or None, request=request)
     log.debug('Remove user form initialized for "%s"' % request.user.username)
-    
+
     if request.method == 'POST':
         if 'cancel' in request.POST:
             log.info('User %s removal cancelled', str(request.user))
@@ -58,8 +60,8 @@ def remove_user(request):
 
         if form.is_valid():
             user_is_deleted = delete_user(user=request.user,
-                                       email=request.POST['email'],
-                                       password=request.POST.get('password', None))
+                                          email=request.POST['email'],
+                                          password=request.POST.get('password', None))
 
             if user_is_deleted:
                 logout(request)
@@ -77,6 +79,7 @@ def remove_user(request):
 def terms_of_use(request):
     return render(request, 'account/terms_of_use.html')
 
+
 @login_required()
 def token(request):
     if request.method == 'POST':
@@ -89,3 +92,19 @@ def token(request):
     return render(request, 'account/account_token.html', {
         'token': token
     })
+
+
+def shibboleth_login(request):
+    if request.user.is_authenticated:
+        return HttpResponseRedirect(reverse('projects'))
+    else:
+        login_url = settings.SHIBBOLETH_LOGIN_URL + f'?target={request.path}'
+        return HttpResponseRedirect(login_url)
+
+
+def shibboleth_logout(request):
+    logout_url = reverse('account_logout')
+    if settings.SHIBBOLETH_USERNAME_PATTERN is None \
+            or re.search(settings.SHIBBOLETH_USERNAME_PATTERN, request.user.username):
+        logout_url += f'?next={settings.SHIBBOLETH_LOGOUT_URL}'
+    return HttpResponseRedirect(logout_url)
diff --git a/rdmo/core/models.py b/rdmo/core/models.py
index d04cd27428..9ac75d4cc4 100644
--- a/rdmo/core/models.py
+++ b/rdmo/core/models.py
@@ -1,5 +1,6 @@
 import logging
 
+from django.conf import settings
 from django.db import models
 from django.utils.timezone import now
 from django.utils.translation import get_language
@@ -38,7 +39,7 @@ def trans(self, field):
                 r = getattr(self, '%s_%s' % (field, lang_field)) or None
                 if r is not None:
                     return r
-                else:
+                elif settings.REPLACE_MISSING_TRANSLATION:
                     for i in range(1, 6):
                         r = getattr(self, '%s_%s' % (field, 'lang' + str(i))) or None
                         if r is not None:
diff --git a/rdmo/core/settings.py b/rdmo/core/settings.py
index 45bfc22294..1ca8bd42d4 100644
--- a/rdmo/core/settings.py
+++ b/rdmo/core/settings.py
@@ -85,6 +85,8 @@
 
 MULTISITE = False
 
+LOGIN_FORM = True
+
 PROFILE_UPDATE = True
 PROFILE_DELETE = True
 
@@ -95,7 +97,9 @@
 SOCIALACCOUNT = False
 
 SHIBBOLETH = False
+SHIBBOLETH_LOGIN_URL = '/Shibboleth.sso/Login'
 SHIBBOLETH_LOGOUT_URL = '/Shibboleth.sso/Logout'
+SHIBBOLETH_USERNAME_PATTERN = None
 
 ACCOUNT_SIGNUP_FORM_CLASS = 'rdmo.accounts.forms.SignupForm'
 ACCOUNT_USER_DISPLAY = 'rdmo.accounts.utils.get_full_name'
@@ -176,6 +180,7 @@
     'SITE_ID',
     'LOGIN_URL',
     'LOGOUT_URL',
+    'LOGIN_FORM',
     'ACCOUNT',
     'ACCOUNT_SIGNUP',
     'ACCOUNT_TERMS_OF_USE',
@@ -184,6 +189,7 @@
     'PROFILE_UPDATE',
     'PROFILE_DELETE',
     'SHIBBOLETH',
+    'SHIBBOLETH_LOGIN_URL',
     'MULTISITE',
     'EXPORT_FORMATS',
     'PROJECT_ISSUES',
@@ -274,7 +280,7 @@
 
 PROJECT_IMPORTS_LIST = []
 
-PROJECT_QUESTIONS_AUTOSAVE = False
+PROJECT_QUESTIONS_AUTOSAVE = True
 
 PROJECT_QUESTIONS_CYCLE_SETS = False
 
@@ -307,6 +313,8 @@
 
 DEFAULT_URI_PREFIX = 'http://example.com/terms'
 
+REPLACE_MISSING_TRANSLATION = False
+
 VENDOR_CDN = True
 
 VENDOR = {
diff --git a/rdmo/core/static/core/css/base.scss b/rdmo/core/static/core/css/base.scss
index 408e9487d2..a824fc22c5 100644
--- a/rdmo/core/static/core/css/base.scss
+++ b/rdmo/core/static/core/css/base.scss
@@ -120,6 +120,10 @@ summary {
     cursor: pointer;
 }
 
+metadata {
+    display: none;
+}
+
 /* navbar */
 
 .navbar-default {
diff --git a/rdmo/core/views.py b/rdmo/core/views.py
index c42c0934d8..b709cf0a4e 100644
--- a/rdmo/core/views.py
+++ b/rdmo/core/views.py
@@ -28,17 +28,18 @@ def home(request):
     if request.user.is_authenticated:
         return HttpResponseRedirect(reverse('projects'))
     else:
-        if settings.SHIBBOLETH:
-            return render(request, 'core/home.html')
-        elif settings.ACCOUNT or settings.SOCIALACCOUNT:
-            from allauth.account.forms import LoginForm
-            return render(request, 'core/home.html', {
-                'form': LoginForm(),
-                'signup_url': reverse("account_signup")
-            })
+        if settings.LOGIN_FORM:
+            if settings.ACCOUNT or settings.SOCIALACCOUNT:
+                from allauth.account.forms import LoginForm
+                return render(request, 'core/home.html', {
+                    'form': LoginForm(),
+                    'signup_url': reverse("account_signup")
+                })
+            else:
+                from django.contrib.auth.forms import AuthenticationForm
+                return render(request, 'core/home.html', {'form': AuthenticationForm()})
         else:
-            from django.contrib.auth.forms import AuthenticationForm
-            return render(request, 'core/home.html', {'form': AuthenticationForm()})
+            return render(request, 'core/home.html')
 
 
 @login_required
diff --git a/rdmo/locale/de/LC_MESSAGES/django.mo b/rdmo/locale/de/LC_MESSAGES/django.mo
index 2ed86c5fb6..9a8158fd99 100644
Binary files a/rdmo/locale/de/LC_MESSAGES/django.mo and b/rdmo/locale/de/LC_MESSAGES/django.mo differ
diff --git a/rdmo/locale/de/LC_MESSAGES/django.po b/rdmo/locale/de/LC_MESSAGES/django.po
index c694777b83..ee92222895 100644
--- a/rdmo/locale/de/LC_MESSAGES/django.po
+++ b/rdmo/locale/de/LC_MESSAGES/django.po
@@ -2,8 +2,8 @@ msgid ""
 msgstr ""
 "Project-Id-Version: RDMO\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-04-27 16:01+0200\n"
-"PO-Revision-Date: 2023-04-27 16:11+0200\n"
+"POT-Creation-Date: 2023-07-31 16:18+0200\n"
+"PO-Revision-Date: 2023-07-31 16:19+0200\n"
 "Last-Translator: Jochen Klar <jklar@aip.de>\n"
 "Language-Team: RDMO <rdmo-team@listserv.dfn.de>\n"
 "Language: de\n"
@@ -25,7 +25,7 @@ msgstr "Vorname"
 msgid "Last name"
 msgstr "Nachname"
 
-#: accounts/forms.py:100 core/constants.py:20 projects/models/invite.py:27
+#: accounts/forms.py:100 core/constants.py:20 projects/models/invite.py:30
 msgid "E-mail"
 msgstr "E-Mail"
 
@@ -86,7 +86,7 @@ msgstr "Der Text für das zusätzliche Feld (fünfte Sprache)."
 
 #: accounts/models.py:47 questions/models/catalog.py:84
 #: questions/models/question.py:77 questions/models/questionset.py:102
-#: views/models.py:91
+#: views/models.py:93
 msgid "Help (primary)"
 msgstr "Help (erste Sprache)"
 
@@ -99,7 +99,7 @@ msgstr ""
 
 #: accounts/models.py:52 questions/models/catalog.py:89
 #: questions/models/question.py:82 questions/models/questionset.py:107
-#: views/models.py:96
+#: views/models.py:98
 msgid "Help (secondary)"
 msgstr "Help (zweite Sprache)"
 
@@ -112,7 +112,7 @@ msgstr ""
 
 #: accounts/models.py:57 questions/models/catalog.py:94
 #: questions/models/question.py:87 questions/models/questionset.py:112
-#: views/models.py:101
+#: views/models.py:103
 msgid "Help (tertiary)"
 msgstr "Help (dritte Sprache)"
 
@@ -125,7 +125,7 @@ msgstr ""
 
 #: accounts/models.py:62 questions/models/catalog.py:99
 #: questions/models/question.py:92 questions/models/questionset.py:117
-#: views/models.py:106
+#: views/models.py:108
 msgid "Help (quaternary)"
 msgstr "Help (vierte Sprache)"
 
@@ -138,7 +138,7 @@ msgstr ""
 
 #: accounts/models.py:67 questions/models/catalog.py:104
 #: questions/models/question.py:97 questions/models/questionset.py:122
-#: views/models.py:111
+#: views/models.py:113
 msgid "Help (quinary)"
 msgstr "Help (fünfte Sprache)"
 
@@ -198,7 +198,7 @@ msgstr "Manager"
 msgid "The sites for which this user is manager."
 msgstr "Die Seiten bei denen dieser Benutzer Manager ist."
 
-#: accounts/models.py:143 projects/models/invite.py:32
+#: accounts/models.py:143 projects/models/invite.py:35
 #: projects/models/membership.py:32
 #: projects/templates/projects/project_detail_invites.html:16
 #: projects/templates/projects/project_detail_memberships.html:21
@@ -350,14 +350,15 @@ msgstr ""
 "href=\"%(email_url)s\">fordern sie eine neue Betätigungs-E-Mail an</a>."
 
 #: accounts/templates/account/login.html:6
-#: accounts/templates/account/login_form.html:22
+#: accounts/templates/account/login_form.html:33
 #: accounts/templates/account/password_reset_from_key_done.html:14
 #: core/templates/core/base_navigation.html:162
 #: core/templates/core/home.html:55
 msgid "Login"
 msgstr "Anmelden"
 
-#: accounts/templates/account/login_form.html:6
+#: accounts/templates/account/login_form.html:8
+#: accounts/templates/account/login_form.html:14
 msgid "Login with Shibboleth"
 msgstr "Mit Shibboleth einloggen"
 
@@ -758,7 +759,7 @@ msgstr ""
 
 #: accounts/templatetags/accounts_tags.py:19
 #: projects/templates/projects/projects.html:125
-#: projects/templates/projects/site_projects.html:49
+#: projects/templates/projects/site_projects.html:64
 msgid "Name"
 msgstr "Name"
 
@@ -803,7 +804,7 @@ msgstr "Bedingungen"
 #: questions/templates/questions/catalogs_element.html:56
 #: questions/templates/questions/catalogs_sidebar_showuri_checkboxes.html:8
 #: tasks/models.py:21 tasks/templates/tasks/tasks_export.html:15
-#: views/models.py:21 views/templates/views/views_export.html:15
+#: views/models.py:23 views/templates/views/views_export.html:15
 msgid "URI"
 msgstr "URI"
 
@@ -815,7 +816,7 @@ msgstr ""
 #: conditions/models.py:39 domain/models.py:18 options/models.py:20
 #: options/models.py:120 questions/models/catalog.py:24
 #: questions/models/question.py:27 questions/models/questionset.py:26
-#: questions/models/section.py:20 tasks/models.py:26 views/models.py:26
+#: questions/models/section.py:20 tasks/models.py:26 views/models.py:28
 msgid "URI Prefix"
 msgstr "URI Prefix"
 
@@ -827,7 +828,7 @@ msgstr "Das Präfix für die URI dieser Bedingung."
 #: options/models.py:125 projects/models/integration.py:60
 #: questions/models/catalog.py:29 questions/models/question.py:32
 #: questions/models/questionset.py:31 questions/models/section.py:25
-#: tasks/models.py:31 views/models.py:31
+#: tasks/models.py:31 views/models.py:33
 msgid "Key"
 msgstr "Schlüssel"
 
@@ -846,7 +847,7 @@ msgstr "Der interne Bezeichner dieser Bedingung."
 #: questions/templates/questions/catalog_tree.html:65
 #: questions/templates/questions/catalog_tree.html:84
 #: questions/templates/questions/catalog_tree.html:149 tasks/models.py:36
-#: tasks/templates/tasks/tasks_export.html:21 views/models.py:36
+#: tasks/templates/tasks/tasks_export.html:21 views/models.py:38
 #: views/templates/views/views_export.html:21
 msgid "Comment"
 msgstr "Kommentar"
@@ -858,7 +859,7 @@ msgstr "Zusätzliche interne Informationen über diese Bedingung."
 #: conditions/models.py:54 domain/models.py:38 options/models.py:35
 #: options/models.py:140 questions/models/catalog.py:39
 #: questions/models/question.py:47 questions/models/questionset.py:46
-#: questions/models/section.py:40 tasks/models.py:41 views/models.py:41
+#: questions/models/section.py:40 tasks/models.py:41 views/models.py:43
 msgid "Locked"
 msgstr "Gesperrt"
 
@@ -949,7 +950,7 @@ msgstr "Alle URI Prefixe"
 #: projects/templates/projects/project_detail_sidebar.html:11
 #: projects/templates/projects/project_view.html:42
 #: projects/templates/projects/projects.html:25
-#: projects/templates/projects/site_projects.html:33
+#: projects/templates/projects/site_projects.html:48
 #: questions/templates/questions/catalog_tree.html:104
 #: questions/templates/questions/catalogs_sidebar.html:78
 #: questions/templates/questions/catalogs_sidebar_showuri_checkboxes.html:26
@@ -1118,7 +1119,7 @@ msgstr "Standard URI Prefix einsetzen"
 #: options/templates/options/options_modal_form_conditions.html:34
 #: options/templates/options/options_modal_form_options.html:182
 #: options/templates/options/options_modal_form_optionsets.html:118
-#: projects/templates/projects/project_questions_buttons.html:27
+#: projects/templates/projects/project_questions_buttons.html:36
 #: projects/templates/projects/project_questions_modal_form_valuesets.html:32
 #: questions/templates/questions/catalogs_modal_form_catalogs.html:216
 #: questions/templates/questions/catalogs_modal_form_questions.html:464
@@ -1183,14 +1184,14 @@ msgid "Tasks"
 msgstr "Aufgaben"
 
 #: conditions/validators.py:14 domain/validators.py:15 options/validators.py:14
-#: options/validators.py:26 projects/forms.py:333 projects/forms.py:354
+#: options/validators.py:26 projects/forms.py:338 projects/forms.py:359
 #: questions/validators.py:16 questions/validators.py:28
 #: questions/validators.py:46 questions/validators.py:64 tasks/validators.py:14
 #: views/validators.py:14
 msgid "This field is required."
 msgstr "Dieses Feld wird benötigt."
 
-#: core/constants.py:14 core/settings.py:296
+#: core/constants.py:14 core/settings.py:302
 #: options/templates/options/options_export.html:39
 #: options/templates/options/options_modal_form_options.html:163
 #: projects/models/value.py:64 questions/constants.py:4
@@ -1236,99 +1237,99 @@ msgstr "Option"
 msgid "File"
 msgstr "Datei"
 
-#: core/models.py:15
+#: core/models.py:16
 msgid "created"
 msgstr "erstellt"
 
-#: core/models.py:16
+#: core/models.py:17
 msgid "updated"
 msgstr "zuletzt geändert"
 
-#: core/settings.py:124
+#: core/settings.py:128
 msgid "English"
 msgstr "Englisch"
 
-#: core/settings.py:125
+#: core/settings.py:129
 msgid "German"
 msgstr "Deutsch"
 
-#: core/settings.py:239
+#: core/settings.py:245
 msgid "PDF"
 msgstr "PDF"
 
-#: core/settings.py:240
+#: core/settings.py:246
 msgid "Rich Text Format"
 msgstr "Rich Text Format"
 
-#: core/settings.py:241
+#: core/settings.py:247
 msgid "Open Office"
 msgstr "Open Office"
 
-#: core/settings.py:242
+#: core/settings.py:248
 msgid "Microsoft Office"
 msgstr "Microsoft Office"
 
-#: core/settings.py:243
+#: core/settings.py:249
 msgid "HTML"
 msgstr "HTML"
 
-#: core/settings.py:244
+#: core/settings.py:250
 msgid "Markdown"
 msgstr "Markdown"
 
-#: core/settings.py:245
+#: core/settings.py:251
 msgid "mediawiki"
 msgstr "mediawiki"
 
-#: core/settings.py:246
+#: core/settings.py:252
 msgid "LaTeX"
 msgstr "LaTeX"
 
-#: core/settings.py:266 core/settings.py:272
+#: core/settings.py:272 core/settings.py:278
 msgid "RDMO XML"
 msgstr "RDMO XML"
 
-#: core/settings.py:267 domain/templates/domain/domain.html:74
+#: core/settings.py:273 domain/templates/domain/domain.html:74
 msgid "CSV comma separated"
 msgstr "CSV durch Komma getrennt"
 
-#: core/settings.py:268 domain/templates/domain/domain.html:79
+#: core/settings.py:274 domain/templates/domain/domain.html:79
 msgid "CSV semicolon separated"
 msgstr "CSV durch Semikolon getrennt"
 
-#: core/settings.py:297 questions/constants.py:5
+#: core/settings.py:303 questions/constants.py:5
 msgid "Textarea"
 msgstr "Textfeld"
 
-#: core/settings.py:298 questions/constants.py:6
+#: core/settings.py:304 questions/constants.py:6
 msgid "Yes/No"
 msgstr "Ja/Nein"
 
-#: core/settings.py:299 questions/constants.py:7
+#: core/settings.py:305 questions/constants.py:7
 msgid "Checkboxes"
 msgstr "Ankreuzfelder"
 
-#: core/settings.py:300 questions/constants.py:8
+#: core/settings.py:306 questions/constants.py:8
 msgid "Radio buttons"
 msgstr "Radio Buttons"
 
-#: core/settings.py:301 questions/constants.py:9
+#: core/settings.py:307 questions/constants.py:9
 msgid "Select drop-down"
 msgstr "Select-Dropdown-Menü"
 
-#: core/settings.py:302 questions/constants.py:10
+#: core/settings.py:308 questions/constants.py:10
 msgid "Autocomplete"
 msgstr "Autocomplete"
 
-#: core/settings.py:303 questions/constants.py:11
+#: core/settings.py:309 questions/constants.py:11
 msgid "Range slider"
 msgstr "Schieberegler"
 
-#: core/settings.py:304 questions/constants.py:12
+#: core/settings.py:310 questions/constants.py:12
 msgid "Date picker"
 msgstr "Datumspicker"
 
-#: core/settings.py:305 questions/constants.py:13
+#: core/settings.py:311 questions/constants.py:13
 msgid "File upload"
 msgstr "Dateien-Upload"
 
@@ -1388,10 +1389,10 @@ msgstr "Management"
 msgid "Domain"
 msgstr "Domäne"
 
-#: core/templates/core/base_navigation.html:61 projects/forms.py:309
+#: core/templates/core/base_navigation.html:61 projects/forms.py:314
 #: projects/models/project.py:64
 #: projects/templates/projects/project_detail_views.html:11 views/apps.py:7
-#: views/models.py:123 views/templates/views/views.html:91
+#: views/models.py:125 views/templates/views/views.html:91
 #: views/templates/views/views_export.html:6 views/views.py:43
 msgid "Views"
 msgstr "Ansichten"
@@ -1685,8 +1686,9 @@ msgid "%(foreign_model)s %(foreign_uri)s does not exist."
 msgstr "%(foreign_uri)s %(foreign_model)s existiert nicht."
 
 #: management/templates/management/import.html:70
-#: projects/templates/projects/project_questions_buttons.html:15
-#: projects/templates/projects/project_questions_buttons.html:48
+#: projects/templates/projects/project_questions_buttons.html:19
+#: projects/templates/projects/project_questions_buttons.html:32
+#: projects/templates/projects/project_questions_buttons.html:58
 msgid "Back"
 msgstr "Zurück"
 
@@ -2034,8 +2036,8 @@ msgstr "Dieses Feld darf nicht Null sein."
 msgid "Overlays"
 msgstr "Einblendungen"
 
-#: overlays/models.py:11 projects/forms.py:176
-#: projects/models/continuation.py:18 projects/models/invite.py:22
+#: overlays/models.py:11 projects/forms.py:181
+#: projects/models/continuation.py:18 projects/models/invite.py:25
 #: projects/models/membership.py:27 projects/models/project.py:34
 #: projects/templates/projects/project_detail_invites.html:14
 #: projects/templates/projects/project_detail_memberships.html:19
@@ -2062,7 +2064,7 @@ msgstr "Url-Name"
 msgid "The url_name for this overlay."
 msgstr "Der url_name für diese Einblendung."
 
-#: overlays/models.py:26 projects/forms.py:321
+#: overlays/models.py:26 projects/forms.py:326
 #: projects/templates/projects/project_answers.html:13
 #: projects/templates/projects/project_view.html:28
 msgid "Current"
@@ -2115,19 +2117,19 @@ msgstr "Autor"
 msgid "Guest"
 msgstr "Gast"
 
-#: projects/forms.py:175
+#: projects/forms.py:180
 msgid "Username or e-mail"
 msgstr "Benutzername oder E-Mail"
 
-#: projects/forms.py:177
+#: projects/forms.py:182
 msgid "The username or e-mail of the new user."
 msgstr "Der Benutzername oder die E-Mail des neuen Benutzers."
 
-#: projects/forms.py:189
+#: projects/forms.py:194
 msgid "Add member silently"
 msgstr "Mitglied stillschweigend hinzufügen"
 
-#: projects/forms.py:190
+#: projects/forms.py:195
 msgid ""
 "As site manager or admin, you can directly add users without notifying them "
 "via e-mail, when you check the following checkbox."
@@ -2135,11 +2137,11 @@ msgstr ""
 "Als Site-Manager oder Admin können Sie Benutzer direkt hinzufügen, ohne sie "
 "per E-Mail zu benachrichtigen, wenn Sie das folgende Kästchen aktivieren."
 
-#: projects/forms.py:203
+#: projects/forms.py:208 projects/serializers/v1/__init__.py:147
 msgid "The user is already a member of the project."
 msgstr "Der Benutzer ist bereits ein Mitglied dieses Projektes."
 
-#: projects/forms.py:215
+#: projects/forms.py:220
 msgid ""
 "A user with this username or e-mail was not found. Only registered users can "
 "be invited."
@@ -2147,51 +2149,51 @@ msgstr ""
 "Ein Benutzer mit diesem Benutzernamen oder dieser E-Mail wurde nicht "
 "gefunden. Nur registrierte Benutzer können eingeladen werden."
 
-#: projects/forms.py:219
+#: projects/forms.py:224
 msgid "Only existing users can be added silently."
 msgstr "Nur bestehende Benutzer können stillschweigend hinzugefügt werden."
 
-#: projects/forms.py:287 projects/forms.py:291
+#: projects/forms.py:292 projects/forms.py:296
 #, python-format
 msgid "Attach %s"
 msgstr "%s hinzufügen"
 
-#: projects/forms.py:297
+#: projects/forms.py:302
 msgid "Subject"
 msgstr "Betreff"
 
-#: projects/forms.py:298
+#: projects/forms.py:303
 msgid "Message"
 msgstr "Nachricht"
 
-#: projects/forms.py:305
+#: projects/forms.py:310
 msgid "Answers"
 msgstr "Antworten"
 
-#: projects/forms.py:306
+#: projects/forms.py:311
 msgid "Attach the output of \"View answers\"."
 msgstr "Die Ausgabe von \"Antworten anzeigen\" anhängen."
 
-#: projects/forms.py:313
+#: projects/forms.py:318
 msgid "Files"
 msgstr "Dateien"
 
-#: projects/forms.py:320 projects/models/snapshot.py:33
+#: projects/forms.py:325 projects/models/snapshot.py:33
 #: projects/models/value.py:38
 #: projects/templates/projects/project_detail_snapshots.html:20
 #: projects/templates/projects/project_import.html:93
 msgid "Snapshot"
 msgstr "Snapshot"
 
-#: projects/forms.py:324
+#: projects/forms.py:329
 msgid "Format"
 msgstr "Format"
 
-#: projects/forms.py:339 projects/forms.py:344
+#: projects/forms.py:344 projects/forms.py:349
 msgid "Recipients"
 msgstr "Empfänger"
 
-#: projects/forms.py:345
+#: projects/forms.py:350
 msgid "Enter recipients line by line"
 msgstr "Empfänger Zeile für Zeile eingeben"
 
@@ -2238,7 +2240,7 @@ msgid "Files of this type cannot be imported."
 msgstr "Dateien dieses Typs können nicht importiert werden."
 
 #: projects/models/continuation.py:13 projects/models/integration.py:16
-#: projects/models/invite.py:17 projects/models/issue.py:28
+#: projects/models/invite.py:20 projects/models/issue.py:28
 #: projects/models/membership.py:22 projects/models/project.py:70
 #: projects/models/snapshot.py:17 projects/models/value.py:32
 #: projects/templates/projects/project_questions_overview.html:5
@@ -2330,43 +2332,43 @@ msgstr "Integrations-Option"
 msgid "Integration options"
 msgstr "Integrations-Optionen"
 
-#: projects/models/invite.py:18
+#: projects/models/invite.py:21
 msgid "The project for this invite."
 msgstr "Das Projekt für diese Einladung."
 
-#: projects/models/invite.py:23 projects/models/membership.py:28
+#: projects/models/invite.py:26 projects/models/membership.py:28
 msgid "The user for this membership."
 msgstr "Der Benutzer für diese Mitgliedschaft."
 
-#: projects/models/invite.py:28
+#: projects/models/invite.py:31
 msgid "The e-mail for this membership."
 msgstr "Die E-Mail für diese Mitgliedschaft."
 
-#: projects/models/invite.py:33
+#: projects/models/invite.py:36
 msgid "The role for this invite."
 msgstr "Die Rolle für diese Einladung."
 
-#: projects/models/invite.py:37
+#: projects/models/invite.py:40
 msgid "Token"
 msgstr "Token"
 
-#: projects/models/invite.py:38
+#: projects/models/invite.py:41
 msgid "The token for this invite."
 msgstr "Der Token für diese Einladung."
 
-#: projects/models/invite.py:41
+#: projects/models/invite.py:44
 msgid "Timestamp"
 msgstr "Zeitstempel"
 
-#: projects/models/invite.py:42
+#: projects/models/invite.py:45
 msgid "The timestamp for this invite."
 msgstr "Der Zeitstempel für diese Einladung."
 
-#: projects/models/invite.py:47
+#: projects/models/invite.py:50
 msgid "Invite"
 msgstr "Einladung"
 
-#: projects/models/invite.py:48
+#: projects/models/invite.py:51
 #: projects/templates/projects/project_detail_invites.html:10
 msgid "Invites"
 msgstr "Einladungen"
@@ -2706,6 +2708,19 @@ msgstr "Das GitLab-Repository an das Aufgaben versendet werden."
 msgid "The secret for a GitLab webhook to close a task."
 msgstr "Das Geheimnis für einen GitLab-Webhook zum Schließen einer Aufgabe."
 
+#: projects/serializers/v1/__init__.py:152
+msgid "A user with that e-mail is already a member of the project."
+msgstr ""
+"Ein Benutzer mit dieser E-Mail-Adresse ist bereits Mitglied des Projekts."
+
+#: projects/serializers/v1/__init__.py:160
+msgid "Either user or e-mail needs to be provided."
+msgstr "Es muss entweder ein Benutzer oder eine E-Mail angegeben werden."
+
+#: projects/serializers/v1/__init__.py:162
+msgid "User and e-mail are mutually exclusive."
+msgstr "Benutzer und E-Mail schließen einander aus."
+
 #: projects/serializers/v1/questionset.py:94
 msgid "item"
 msgstr "Eintrag"
@@ -3172,7 +3187,7 @@ msgstr "Projekt direkt importieren"
 #: projects/templates/projects/project_detail_snapshots.html:22
 #: projects/templates/projects/project_import.html:34
 #: projects/templates/projects/project_import.html:104
-#: projects/templates/projects/site_projects.html:50
+#: projects/templates/projects/site_projects.html:65
 msgid "Created"
 msgstr "Erstellt"
 
@@ -3203,7 +3218,7 @@ msgstr ""
 "Snapshot zum Erstellen von Ansichten verwendet werden, und das Projekt kann "
 "bei Bedarf auch auf einen früheren Snapshot zurückgesetzt werden."
 
-#: projects/templates/projects/project_detail_views.html:19 views/models.py:122
+#: projects/templates/projects/project_detail_views.html:19 views/models.py:124
 #: views/templates/views/views.html:127
 msgid "View"
 msgstr "Ansicht"
@@ -3349,17 +3364,18 @@ msgstr ""
 "%(name_plural)s über die grüne Schaltfläche hinzufügen und über das blaue "
 "Kreuz (&times;) entfernen."
 
-#: projects/templates/projects/project_questions_buttons.html:9
-#: projects/templates/projects/project_questions_buttons.html:52
-msgid "Skip"
-msgstr "Überspringen"
-
-#: projects/templates/projects/project_questions_buttons.html:32
-#: projects/templates/projects/project_questions_buttons.html:40
+#: projects/templates/projects/project_questions_buttons.html:10
+#: projects/templates/projects/project_questions_buttons.html:41
+#: projects/templates/projects/project_questions_buttons.html:49
 msgid "Proceed"
 msgstr "Fortfahren"
 
-#: projects/templates/projects/project_questions_buttons.html:34
+#: projects/templates/projects/project_questions_buttons.html:12
+#: projects/templates/projects/project_questions_buttons.html:62
+msgid "Skip"
+msgstr "Überspringen"
+
+#: projects/templates/projects/project_questions_buttons.html:43
 msgid "Save and proceed"
 msgstr "Sichern und fortfahren"
 
@@ -3459,7 +3475,7 @@ msgstr ""
 "hidden=\"true\"></i> können aufgrund Ihrer Eingabe übersprungen werden."
 
 #: projects/templates/projects/project_questions_overview.html:14
-#: projects/templates/projects/site_projects.html:37
+#: projects/templates/projects/site_projects.html:52
 msgid "Back to my projects"
 msgstr "Zurück zu meinen Projekten"
 
@@ -3532,7 +3548,7 @@ msgid "Navigation"
 msgstr "Navigation"
 
 #: projects/templates/projects/project_view.html:90
-#: projects/views/project.py:198
+#: projects/views/project.py:201
 msgid "Error"
 msgstr "Fehler"
 
@@ -3578,7 +3594,7 @@ msgid "Click on one of the links to join the projects."
 msgstr "Klicken Sie auf einen der Links, um an den Projekten teilzunehmen."
 
 #: projects/templates/projects/projects.html:127
-#: projects/templates/projects/site_projects.html:51
+#: projects/templates/projects/site_projects.html:66
 msgid "Last changed"
 msgstr "Letzte Änderung"
 
@@ -3597,7 +3613,11 @@ msgstr "Seite %(number)s von %(num_pages)s."
 msgid "Next"
 msgstr "Nächste"
 
-#: projects/templates/projects/site_projects.html:44
+#: projects/templates/projects/site_projects.html:32
+msgid "All catalogs"
+msgstr "Alle Kataloge"
+
+#: projects/templates/projects/site_projects.html:59
 #, python-format
 msgid "All projects on %(site)s"
 msgstr "Alle Projekte auf %(site)s"
@@ -3623,20 +3643,20 @@ msgstr "Zurücksetzen"
 msgid "Set"
 msgstr "Set"
 
-#: projects/validators.py:22 projects/viewsets.py:318
+#: projects/validators.py:22 projects/viewsets.py:350
 msgid "You reached the file quota for this project."
 msgstr "Sie haben die Quota für dieses Projekt erreicht."
 
-#: projects/views/project.py:178
+#: projects/views/project.py:181
 msgid "Sorry, your invitation has been expired."
 msgstr "Entschuldigung, Ihre Einladung ist abgelaufen."
 
-#: projects/views/project.py:181
+#: projects/views/project.py:184
 #, python-format
 msgid "Sorry, but this invitation is for the user \"%s\"."
 msgstr "Entschuldigung, aber diese Einladung ist für den Benutzer \"%s\"."
 
-#: projects/views/project.py:195
+#: projects/views/project.py:198
 msgid "Sorry, the invitation link is not valid."
 msgstr "Der Einladungslink ist leider nicht gültig."
 
@@ -3674,7 +3694,7 @@ msgstr "Die Position von diesem Katalog in Listen."
 #: questions/templates/questions/catalogs_modal_form_catalogs.html:196
 #: tasks/models.py:51 tasks/templates/tasks/tasks.html:144
 #: tasks/templates/tasks/tasks_modal_form_tasks.html:144
-#: tasks/templates/tasks/tasks_modal_form_tasks.html:212 views/models.py:51
+#: tasks/templates/tasks/tasks_modal_form_tasks.html:212 views/models.py:53
 #: views/templates/views/views.html:141
 #: views/templates/views/views_modal_form_views.html:143
 #: views/templates/views/views_modal_form_views.html:210
@@ -3685,7 +3705,7 @@ msgstr "Standorte"
 msgid "The sites this catalog belongs to (in a multi site setup)."
 msgstr "Die Seiten zu denen dieser Katalog gehört."
 
-#: questions/models/catalog.py:54 tasks/models.py:56 views/models.py:56
+#: questions/models/catalog.py:54 tasks/models.py:56 views/models.py:58
 msgid "Group"
 msgstr "Gruppe"
 
@@ -3694,7 +3714,7 @@ msgid "The groups for which this catalog is active."
 msgstr "Die Gruppen für die dieser Katalog aktiv ist."
 
 #: questions/models/catalog.py:59 questions/models/questionset.py:77
-#: questions/models/section.py:55 tasks/models.py:61 views/models.py:66
+#: questions/models/section.py:55 tasks/models.py:61 views/models.py:68
 msgid "Title (primary)"
 msgstr "Titel (erste Sprache)"
 
@@ -3703,7 +3723,7 @@ msgid "The title for this catalog in the primary language."
 msgstr "Der Titel für diesen Katalog (erste Sprache)."
 
 #: questions/models/catalog.py:64 questions/models/questionset.py:82
-#: questions/models/section.py:60 tasks/models.py:66 views/models.py:71
+#: questions/models/section.py:60 tasks/models.py:66 views/models.py:73
 msgid "Title (secondary)"
 msgstr "Titel (zweite Sprache)"
 
@@ -3712,7 +3732,7 @@ msgid "The title for this catalog in the secondary language."
 msgstr "Der Titel für diesen Katalog (zweite Sprache)."
 
 #: questions/models/catalog.py:69 questions/models/questionset.py:87
-#: questions/models/section.py:65 tasks/models.py:71 views/models.py:76
+#: questions/models/section.py:65 tasks/models.py:71 views/models.py:78
 msgid "Title (tertiary)"
 msgstr "Titel (dritte Sprache)"
 
@@ -3721,7 +3741,7 @@ msgid "The title for this catalog in the tertiary language."
 msgstr "Der Titel für diesen Katalog (dritte Sprache)."
 
 #: questions/models/catalog.py:74 questions/models/questionset.py:92
-#: questions/models/section.py:70 tasks/models.py:76 views/models.py:81
+#: questions/models/section.py:70 tasks/models.py:76 views/models.py:83
 msgid "Title (quaternary)"
 msgstr "Titel (vierte Sprache)"
 
@@ -3730,7 +3750,7 @@ msgid "The title for this catalog in the quaternary language."
 msgstr "Der Titel für diesen Katalog (vierte Sprache)."
 
 #: questions/models/catalog.py:79 questions/models/questionset.py:97
-#: questions/models/section.py:75 tasks/models.py:81 views/models.py:86
+#: questions/models/section.py:75 tasks/models.py:81 views/models.py:88
 msgid "Title (quinary)"
 msgstr "Titel (fünfte Sprache)"
 
@@ -3758,7 +3778,7 @@ msgstr "Der Hilfetext für diesen Katalog (vierte Sprache)."
 msgid "The help text for this catalog in the quinary language."
 msgstr "Der Hilfetext für diesen Katalog (fünfte Sprache)."
 
-#: questions/models/catalog.py:109 tasks/models.py:136 views/models.py:116
+#: questions/models/catalog.py:109 tasks/models.py:136 views/models.py:118
 msgid "Available"
 msgstr "Verfügbar"
 
@@ -3768,7 +3788,7 @@ msgstr "Legt fest, ob dieser Katalog für Projekte verfügbar ist."
 
 #: questions/models/catalog.py:116 tasks/models.py:46
 #: tasks/templates/tasks/tasks_modal_form_tasks.html:123
-#: tasks/templates/tasks/tasks_modal_form_tasks.html:187 views/models.py:46
+#: tasks/templates/tasks/tasks_modal_form_tasks.html:187 views/models.py:48
 #: views/templates/views/views_modal_form_views.html:122
 #: views/templates/views/views_modal_form_views.html:185
 msgid "Catalogs"
@@ -4929,27 +4949,27 @@ msgstr ""
 "Eine leere Liste bedeutet, dass die Aufgabe mit jedem Katalog verwendet "
 "werden kann."
 
-#: views/models.py:22
+#: views/models.py:24
 msgid "The Uniform Resource Identifier of this view (auto-generated)."
 msgstr "Der Uniform Resource Identifier dieser Ansicht (automatisch erstellt)."
 
-#: views/models.py:27
+#: views/models.py:29
 msgid "The prefix for the URI of this view."
 msgstr "Das Präfix für die URI dieser Ansicht."
 
-#: views/models.py:32
+#: views/models.py:34
 msgid "The internal identifier of this view."
 msgstr "Der interne Bezeichner für diese Ansicht."
 
-#: views/models.py:37
+#: views/models.py:39
 msgid "Additional internal information about this view."
 msgstr "Zusätzliche interne Informationen über diese Ansicht."
 
-#: views/models.py:42
+#: views/models.py:44
 msgid "Designates whether this view can be changed."
 msgstr "Bezeichnet, ob diese Ansicht geändert werden kann."
 
-#: views/models.py:47
+#: views/models.py:49
 msgid ""
 "The catalogs this view can be used with. An empty list implies that this "
 "view can be used with every catalog."
@@ -4957,66 +4977,66 @@ msgstr ""
 "Die Kataloge, mit denen diese Ansicht verwendet werden kann. Eine leere "
 "Liste bedeutet, dass diese Ansicht mit jedem Katalog verwendet werden kann."
 
-#: views/models.py:52
+#: views/models.py:54
 msgid "The sites this view belongs to (in a multi site setup)."
 msgstr ""
 "Die Standorte, zu denen diese Ansicht gehört (in einem Multi-Standort-Setup)."
 
-#: views/models.py:57
+#: views/models.py:59
 msgid "The groups for which this view is active."
 msgstr "Die Gruppen, für die diese Ansicht aktiv ist."
 
-#: views/models.py:61 views/templates/views/views_export.html:35
+#: views/models.py:63 views/templates/views/views_export.html:35
 #: views/templates/views/views_modal_form_templates.html:11
 msgid "Template"
 msgstr "Vorlage"
 
-#: views/models.py:62
+#: views/models.py:64
 msgid "The template for this view, written in Django template language."
 msgstr ""
 "Das Template für diese Ansicht, in der Django-Template-Language verfasst."
 
-#: views/models.py:67
+#: views/models.py:69
 msgid "The title for this view in the primary language."
 msgstr "Der Titel für diese Ansicht (erste Sprache)."
 
-#: views/models.py:72
+#: views/models.py:74
 msgid "The title for this view in the secondary language."
 msgstr "Der Titel für diese Ansicht (zweite Sprache)."
 
-#: views/models.py:77
+#: views/models.py:79
 msgid "The title for this view in the tertiary language."
 msgstr "Der Titel für diese Ansicht (dritte Sprache)."
 
-#: views/models.py:82
+#: views/models.py:84
 msgid "The title for this view in the quaternary language."
 msgstr "Der Titel für diese Ansicht (vierte Sprache)."
 
-#: views/models.py:87
+#: views/models.py:89
 msgid "The title for this view in the quinary language."
 msgstr "Der Titel für diese Ansicht (fünfte Sprache)."
 
-#: views/models.py:92
+#: views/models.py:94
 msgid "The help text for this view in the primary language."
 msgstr "Der Hilfetext für diese Ansicht (erste Sprache)."
 
-#: views/models.py:97
+#: views/models.py:99
 msgid "The help text for this view in the secondary language."
 msgstr "Der Hilfetext für diese Ansicht (zweite Sprache)."
 
-#: views/models.py:102
+#: views/models.py:104
 msgid "The help text for this view in the tertiary language."
 msgstr "Der Hilfetext für diese Ansicht (dritte Sprache)."
 
-#: views/models.py:107
+#: views/models.py:109
 msgid "The help text for this view in the quaternary language."
 msgstr "Der Hilfetext für diese Ansicht (vierte Sprache)."
 
-#: views/models.py:112
+#: views/models.py:114
 msgid "The help text for this view in the quinary language."
 msgstr "Der Hilfetext für diese Ansicht (fünfte Sprache)."
 
-#: views/models.py:117
+#: views/models.py:119
 msgid "Designates whether this view is generally available for projects."
 msgstr "Legt fest, ob diese Ansicht für Projekte verfügbar ist."
 
diff --git a/rdmo/locale/es/LC_MESSAGES/django.mo b/rdmo/locale/es/LC_MESSAGES/django.mo
index b62b484cfd..526ca32d0f 100644
Binary files a/rdmo/locale/es/LC_MESSAGES/django.mo and b/rdmo/locale/es/LC_MESSAGES/django.mo differ
diff --git a/rdmo/locale/es/LC_MESSAGES/django.po b/rdmo/locale/es/LC_MESSAGES/django.po
index 83c8104e07..c36943801e 100644
--- a/rdmo/locale/es/LC_MESSAGES/django.po
+++ b/rdmo/locale/es/LC_MESSAGES/django.po
@@ -7,8 +7,8 @@ msgid ""
 msgstr ""
 "Project-Id-Version: \n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-04-27 16:01+0200\n"
-"PO-Revision-Date: 2023-04-27 16:08+0200\n"
+"POT-Creation-Date: 2023-07-31 16:18+0200\n"
+"PO-Revision-Date: 2023-07-31 13:31+0200\n"
 "Last-Translator: DAVID MARTINEZ <dmartinezmu@cem.es>\n"
 "Language-Team: \n"
 "Language: es\n"
@@ -30,7 +30,7 @@ msgstr "Nombre"
 msgid "Last name"
 msgstr "Apellidos"
 
-#: accounts/forms.py:100 core/constants.py:20 projects/models/invite.py:27
+#: accounts/forms.py:100 core/constants.py:20 projects/models/invite.py:30
 msgid "E-mail"
 msgstr "E-mail"
 
@@ -93,7 +93,7 @@ msgstr "Texto para esta campo adicional en el lenguaje quinario."
 
 #: accounts/models.py:47 questions/models/catalog.py:84
 #: questions/models/question.py:77 questions/models/questionset.py:102
-#: views/models.py:91
+#: views/models.py:93
 msgid "Help (primary)"
 msgstr "Ayuda (primario)"
 
@@ -107,7 +107,7 @@ msgstr ""
 
 #: accounts/models.py:52 questions/models/catalog.py:89
 #: questions/models/question.py:82 questions/models/questionset.py:107
-#: views/models.py:96
+#: views/models.py:98
 msgid "Help (secondary)"
 msgstr "Ayuda (secundario)"
 
@@ -121,7 +121,7 @@ msgstr ""
 
 #: accounts/models.py:57 questions/models/catalog.py:94
 #: questions/models/question.py:87 questions/models/questionset.py:112
-#: views/models.py:101
+#: views/models.py:103
 msgid "Help (tertiary)"
 msgstr "Ayuda (terciario)"
 
@@ -135,7 +135,7 @@ msgstr ""
 
 #: accounts/models.py:62 questions/models/catalog.py:99
 #: questions/models/question.py:92 questions/models/questionset.py:117
-#: views/models.py:106
+#: views/models.py:108
 msgid "Help (quaternary)"
 msgstr "Ayuda (cuaternario)"
 
@@ -149,7 +149,7 @@ msgstr ""
 
 #: accounts/models.py:67 questions/models/catalog.py:104
 #: questions/models/question.py:97 questions/models/questionset.py:122
-#: views/models.py:111
+#: views/models.py:113
 msgid "Help (quinary)"
 msgstr "Ayuda (quinario)"
 
@@ -210,7 +210,7 @@ msgstr "Gestor"
 msgid "The sites for which this user is manager."
 msgstr "Sitios para los cuales este usuario es gestor."
 
-#: accounts/models.py:143 projects/models/invite.py:32
+#: accounts/models.py:143 projects/models/invite.py:35
 #: projects/models/membership.py:32
 #: projects/templates/projects/project_detail_invites.html:16
 #: projects/templates/projects/project_detail_memberships.html:21
@@ -360,14 +360,15 @@ msgstr ""
 "mail</a>."
 
 #: accounts/templates/account/login.html:6
-#: accounts/templates/account/login_form.html:22
+#: accounts/templates/account/login_form.html:33
 #: accounts/templates/account/password_reset_from_key_done.html:14
 #: core/templates/core/base_navigation.html:162
 #: core/templates/core/home.html:55
 msgid "Login"
 msgstr "Login"
 
-#: accounts/templates/account/login_form.html:6
+#: accounts/templates/account/login_form.html:8
+#: accounts/templates/account/login_form.html:14
 msgid "Login with Shibboleth"
 msgstr "Login con Shibboleth"
 
@@ -772,7 +773,7 @@ msgstr ""
 
 #: accounts/templatetags/accounts_tags.py:19
 #: projects/templates/projects/projects.html:125
-#: projects/templates/projects/site_projects.html:49
+#: projects/templates/projects/site_projects.html:64
 msgid "Name"
 msgstr "Nombre"
 
@@ -817,7 +818,7 @@ msgstr "Condiciones"
 #: questions/templates/questions/catalogs_element.html:56
 #: questions/templates/questions/catalogs_sidebar_showuri_checkboxes.html:8
 #: tasks/models.py:21 tasks/templates/tasks/tasks_export.html:15
-#: views/models.py:21 views/templates/views/views_export.html:15
+#: views/models.py:23 views/templates/views/views_export.html:15
 msgid "URI"
 msgstr "URI"
 
@@ -829,7 +830,7 @@ msgstr ""
 #: conditions/models.py:39 domain/models.py:18 options/models.py:20
 #: options/models.py:120 questions/models/catalog.py:24
 #: questions/models/question.py:27 questions/models/questionset.py:26
-#: questions/models/section.py:20 tasks/models.py:26 views/models.py:26
+#: questions/models/section.py:20 tasks/models.py:26 views/models.py:28
 msgid "URI Prefix"
 msgstr "Prefijo URI"
 
@@ -841,7 +842,7 @@ msgstr "EL prefijo para el URI de esta condición."
 #: options/models.py:125 projects/models/integration.py:60
 #: questions/models/catalog.py:29 questions/models/question.py:32
 #: questions/models/questionset.py:31 questions/models/section.py:25
-#: tasks/models.py:31 views/models.py:31
+#: tasks/models.py:31 views/models.py:33
 msgid "Key"
 msgstr "Clave"
 
@@ -860,7 +861,7 @@ msgstr "Identificador interno de esta condición."
 #: questions/templates/questions/catalog_tree.html:65
 #: questions/templates/questions/catalog_tree.html:84
 #: questions/templates/questions/catalog_tree.html:149 tasks/models.py:36
-#: tasks/templates/tasks/tasks_export.html:21 views/models.py:36
+#: tasks/templates/tasks/tasks_export.html:21 views/models.py:38
 #: views/templates/views/views_export.html:21
 msgid "Comment"
 msgstr "Comentar"
@@ -872,7 +873,7 @@ msgstr "Información interna adicional sobre esta condición."
 #: conditions/models.py:54 domain/models.py:38 options/models.py:35
 #: options/models.py:140 questions/models/catalog.py:39
 #: questions/models/question.py:47 questions/models/questionset.py:46
-#: questions/models/section.py:40 tasks/models.py:41 views/models.py:41
+#: questions/models/section.py:40 tasks/models.py:41 views/models.py:43
 msgid "Locked"
 msgstr "Bloqueado"
 
@@ -963,7 +964,7 @@ msgstr "Todos los prefijos del URI"
 #: projects/templates/projects/project_detail_sidebar.html:11
 #: projects/templates/projects/project_view.html:42
 #: projects/templates/projects/projects.html:25
-#: projects/templates/projects/site_projects.html:33
+#: projects/templates/projects/site_projects.html:48
 #: questions/templates/questions/catalog_tree.html:104
 #: questions/templates/questions/catalogs_sidebar.html:78
 #: questions/templates/questions/catalogs_sidebar_showuri_checkboxes.html:26
@@ -1133,7 +1134,7 @@ msgstr "Inserte el prefijo URI por defecto"
 #: options/templates/options/options_modal_form_conditions.html:34
 #: options/templates/options/options_modal_form_options.html:182
 #: options/templates/options/options_modal_form_optionsets.html:118
-#: projects/templates/projects/project_questions_buttons.html:27
+#: projects/templates/projects/project_questions_buttons.html:36
 #: projects/templates/projects/project_questions_modal_form_valuesets.html:32
 #: questions/templates/questions/catalogs_modal_form_catalogs.html:216
 #: questions/templates/questions/catalogs_modal_form_questions.html:464
@@ -1198,14 +1199,14 @@ msgid "Tasks"
 msgstr "Tareas"
 
 #: conditions/validators.py:14 domain/validators.py:15 options/validators.py:14
-#: options/validators.py:26 projects/forms.py:333 projects/forms.py:354
+#: options/validators.py:26 projects/forms.py:338 projects/forms.py:359
 #: questions/validators.py:16 questions/validators.py:28
 #: questions/validators.py:46 questions/validators.py:64 tasks/validators.py:14
 #: views/validators.py:14
 msgid "This field is required."
 msgstr "Este campo es obligatorio."
 
-#: core/constants.py:14 core/settings.py:296
+#: core/constants.py:14 core/settings.py:302
 #: options/templates/options/options_export.html:39
 #: options/templates/options/options_modal_form_options.html:163
 #: projects/models/value.py:64 questions/constants.py:4
@@ -1251,99 +1252,99 @@ msgstr "Opción"
 msgid "File"
 msgstr "Archivo"
 
-#: core/models.py:15
+#: core/models.py:16
 msgid "created"
 msgstr "creado"
 
-#: core/models.py:16
+#: core/models.py:17
 msgid "updated"
 msgstr "actualizado"
 
-#: core/settings.py:124
+#: core/settings.py:128
 msgid "English"
 msgstr "Inglés"
 
-#: core/settings.py:125
+#: core/settings.py:129
 msgid "German"
 msgstr "Alemán"
 
-#: core/settings.py:239
+#: core/settings.py:245
 msgid "PDF"
 msgstr "PDF"
 
-#: core/settings.py:240
+#: core/settings.py:246
 msgid "Rich Text Format"
 msgstr "Rich Text Format"
 
-#: core/settings.py:241
+#: core/settings.py:247
 msgid "Open Office"
 msgstr "Open Office"
 
-#: core/settings.py:242
+#: core/settings.py:248
 msgid "Microsoft Office"
 msgstr "Microsoft Office"
 
-#: core/settings.py:243
+#: core/settings.py:249
 msgid "HTML"
 msgstr "HTML"
 
-#: core/settings.py:244
+#: core/settings.py:250
 msgid "Markdown"
 msgstr "Markdown"
 
-#: core/settings.py:245
+#: core/settings.py:251
 msgid "mediawiki"
 msgstr "mediawiki"
 
-#: core/settings.py:246
+#: core/settings.py:252
 msgid "LaTeX"
 msgstr "LaTeX"
 
-#: core/settings.py:266 core/settings.py:272
+#: core/settings.py:272 core/settings.py:278
 msgid "RDMO XML"
 msgstr "RDMO XML"
 
-#: core/settings.py:267 domain/templates/domain/domain.html:74
+#: core/settings.py:273 domain/templates/domain/domain.html:74
 msgid "CSV comma separated"
 msgstr "CSV separado por comas"
 
-#: core/settings.py:268 domain/templates/domain/domain.html:79
+#: core/settings.py:274 domain/templates/domain/domain.html:79
 msgid "CSV semicolon separated"
 msgstr "CSV separado por punto y coma"
 
-#: core/settings.py:297 questions/constants.py:5
+#: core/settings.py:303 questions/constants.py:5
 msgid "Textarea"
 msgstr "Área de texto"
 
-#: core/settings.py:298 questions/constants.py:6
+#: core/settings.py:304 questions/constants.py:6
 msgid "Yes/No"
 msgstr "Sí/No"
 
-#: core/settings.py:299 questions/constants.py:7
+#: core/settings.py:305 questions/constants.py:7
 msgid "Checkboxes"
 msgstr "Casillas de comprobación"
 
-#: core/settings.py:300 questions/constants.py:8
+#: core/settings.py:306 questions/constants.py:8
 msgid "Radio buttons"
 msgstr "Botones de radio"
 
-#: core/settings.py:301 questions/constants.py:9
+#: core/settings.py:307 questions/constants.py:9
 msgid "Select drop-down"
 msgstr "Menú desplegable de selección"
 
-#: core/settings.py:302 questions/constants.py:10
+#: core/settings.py:308 questions/constants.py:10
 msgid "Autocomplete"
 msgstr "Auto-completar"
 
-#: core/settings.py:303 questions/constants.py:11
+#: core/settings.py:309 questions/constants.py:11
 msgid "Range slider"
 msgstr "Deslizador de rango"
 
-#: core/settings.py:304 questions/constants.py:12
+#: core/settings.py:310 questions/constants.py:12
 msgid "Date picker"
 msgstr "Selección de fecha"
 
-#: core/settings.py:305 questions/constants.py:13
+#: core/settings.py:311 questions/constants.py:13
 msgid "File upload"
 msgstr "Subida de archivo"
 
@@ -1402,10 +1403,10 @@ msgstr "Gestión"
 msgid "Domain"
 msgstr "Dominio"
 
-#: core/templates/core/base_navigation.html:61 projects/forms.py:309
+#: core/templates/core/base_navigation.html:61 projects/forms.py:314
 #: projects/models/project.py:64
 #: projects/templates/projects/project_detail_views.html:11 views/apps.py:7
-#: views/models.py:123 views/templates/views/views.html:91
+#: views/models.py:125 views/templates/views/views.html:91
 #: views/templates/views/views_export.html:6 views/views.py:43
 msgid "Views"
 msgstr "Vistas"
@@ -1695,8 +1696,9 @@ msgid "%(foreign_model)s %(foreign_uri)s does not exist."
 msgstr "%(foreign_model)s %(foreign_uri)s no existen."
 
 #: management/templates/management/import.html:70
-#: projects/templates/projects/project_questions_buttons.html:15
-#: projects/templates/projects/project_questions_buttons.html:48
+#: projects/templates/projects/project_questions_buttons.html:19
+#: projects/templates/projects/project_questions_buttons.html:32
+#: projects/templates/projects/project_questions_buttons.html:58
 msgid "Back"
 msgstr "Atrás"
 
@@ -2046,8 +2048,8 @@ msgstr "Este campo no puede ser nulo."
 msgid "Overlays"
 msgstr "Superposiciones"
 
-#: overlays/models.py:11 projects/forms.py:176
-#: projects/models/continuation.py:18 projects/models/invite.py:22
+#: overlays/models.py:11 projects/forms.py:181
+#: projects/models/continuation.py:18 projects/models/invite.py:25
 #: projects/models/membership.py:27 projects/models/project.py:34
 #: projects/templates/projects/project_detail_invites.html:14
 #: projects/templates/projects/project_detail_memberships.html:19
@@ -2074,7 +2076,7 @@ msgstr "Nombre del URL"
 msgid "The url_name for this overlay."
 msgstr "El url_name para esta superposición."
 
-#: overlays/models.py:26 projects/forms.py:321
+#: overlays/models.py:26 projects/forms.py:326
 #: projects/templates/projects/project_answers.html:13
 #: projects/templates/projects/project_view.html:28
 msgid "Current"
@@ -2126,19 +2128,19 @@ msgstr "Autor"
 msgid "Guest"
 msgstr "Invitado"
 
-#: projects/forms.py:175
+#: projects/forms.py:180
 msgid "Username or e-mail"
 msgstr "Nombre de usuario o e-mail"
 
-#: projects/forms.py:177
+#: projects/forms.py:182
 msgid "The username or e-mail of the new user."
 msgstr "Nombre o e-mail del nuevo usuario."
 
-#: projects/forms.py:189
+#: projects/forms.py:194
 msgid "Add member silently"
 msgstr "Añadir miembro silenciosamente"
 
-#: projects/forms.py:190
+#: projects/forms.py:195
 msgid ""
 "As site manager or admin, you can directly add users without notifying them "
 "via e-mail, when you check the following checkbox."
@@ -2146,11 +2148,11 @@ msgstr ""
 "Como gestor o administrador del sitio, usted puede añadir directament nuevos "
 "usuarios sin notificárselo via e-mail, marcando el siguiente checkbox."
 
-#: projects/forms.py:203
+#: projects/forms.py:208 projects/serializers/v1/__init__.py:147
 msgid "The user is already a member of the project."
 msgstr "El usuario ya es miembro del proyecto."
 
-#: projects/forms.py:215
+#: projects/forms.py:220
 msgid ""
 "A user with this username or e-mail was not found. Only registered users can "
 "be invited."
@@ -2158,51 +2160,51 @@ msgstr ""
 "No se ha encontrado un usuario con este nombre o e-mail. Solo los usuarios "
 "registrados pueden ser invitados."
 
-#: projects/forms.py:219
+#: projects/forms.py:224
 msgid "Only existing users can be added silently."
 msgstr "Solo se pueden añadir silenciosamente usuarios ya existentes."
 
-#: projects/forms.py:287 projects/forms.py:291
+#: projects/forms.py:292 projects/forms.py:296
 #, python-format
 msgid "Attach %s"
 msgstr "Adjuntar %s"
 
-#: projects/forms.py:297
+#: projects/forms.py:302
 msgid "Subject"
 msgstr "Asunto"
 
-#: projects/forms.py:298
+#: projects/forms.py:303
 msgid "Message"
 msgstr "Mensaje"
 
-#: projects/forms.py:305
+#: projects/forms.py:310
 msgid "Answers"
 msgstr "Respuestas"
 
-#: projects/forms.py:306
+#: projects/forms.py:311
 msgid "Attach the output of \"View answers\"."
 msgstr "Adjuntar la salida de \"Ver respuestas\"."
 
-#: projects/forms.py:313
+#: projects/forms.py:318
 msgid "Files"
 msgstr "Archivos"
 
-#: projects/forms.py:320 projects/models/snapshot.py:33
+#: projects/forms.py:325 projects/models/snapshot.py:33
 #: projects/models/value.py:38
 #: projects/templates/projects/project_detail_snapshots.html:20
 #: projects/templates/projects/project_import.html:93
 msgid "Snapshot"
 msgstr "Instantátanea"
 
-#: projects/forms.py:324
+#: projects/forms.py:329
 msgid "Format"
 msgstr "Formato"
 
-#: projects/forms.py:339 projects/forms.py:344
+#: projects/forms.py:344 projects/forms.py:349
 msgid "Recipients"
 msgstr "Destinatarios"
 
-#: projects/forms.py:345
+#: projects/forms.py:350
 msgid "Enter recipients line by line"
 msgstr "Introducir destinatarios línea por línea"
 
@@ -2249,7 +2251,7 @@ msgid "Files of this type cannot be imported."
 msgstr "No se pueden importar archivos de este tipo."
 
 #: projects/models/continuation.py:13 projects/models/integration.py:16
-#: projects/models/invite.py:17 projects/models/issue.py:28
+#: projects/models/invite.py:20 projects/models/issue.py:28
 #: projects/models/membership.py:22 projects/models/project.py:70
 #: projects/models/snapshot.py:17 projects/models/value.py:32
 #: projects/templates/projects/project_questions_overview.html:5
@@ -2343,43 +2345,43 @@ msgstr "Opción de integración"
 msgid "Integration options"
 msgstr "Opciones de integración"
 
-#: projects/models/invite.py:18
+#: projects/models/invite.py:21
 msgid "The project for this invite."
 msgstr "El proyecto para esta invitación."
 
-#: projects/models/invite.py:23 projects/models/membership.py:28
+#: projects/models/invite.py:26 projects/models/membership.py:28
 msgid "The user for this membership."
 msgstr "El usuario para esta afiliación."
 
-#: projects/models/invite.py:28
+#: projects/models/invite.py:31
 msgid "The e-mail for this membership."
 msgstr "El e-mail para esta afiliación."
 
-#: projects/models/invite.py:33
+#: projects/models/invite.py:36
 msgid "The role for this invite."
 msgstr "El rol para esta invitación."
 
-#: projects/models/invite.py:37
+#: projects/models/invite.py:40
 msgid "Token"
 msgstr "Testigo"
 
-#: projects/models/invite.py:38
+#: projects/models/invite.py:41
 msgid "The token for this invite."
 msgstr "El testigo de esta invitación."
 
-#: projects/models/invite.py:41
+#: projects/models/invite.py:44
 msgid "Timestamp"
 msgstr "Sello de tiempo"
 
-#: projects/models/invite.py:42
+#: projects/models/invite.py:45
 msgid "The timestamp for this invite."
 msgstr "El sello de tiempo para esta invitación."
 
-#: projects/models/invite.py:47
+#: projects/models/invite.py:50
 msgid "Invite"
 msgstr "Invitación"
 
-#: projects/models/invite.py:48
+#: projects/models/invite.py:51
 #: projects/templates/projects/project_detail_invites.html:10
 msgid "Invites"
 msgstr "Invitaciones"
@@ -2718,6 +2720,18 @@ msgstr "El repositorio GitLab al que enviar asuntos."
 msgid "The secret for a GitLab webhook to close a task."
 msgstr "El secreto para el webhook GitLab para cerrar una tarea."
 
+#: projects/serializers/v1/__init__.py:152
+msgid "A user with that e-mail is already a member of the project."
+msgstr "Un usuario con ese e-mail ya es miembro del proyecto."
+
+#: projects/serializers/v1/__init__.py:160
+msgid "Either user or e-mail needs to be provided."
+msgstr "Es necesario indicar el usuario o el correo electrónico."
+
+#: projects/serializers/v1/__init__.py:162
+msgid "User and e-mail are mutually exclusive."
+msgstr "Usuario y correo electrónico se excluyen mutuamente."
+
 #: projects/serializers/v1/questionset.py:94
 msgid "item"
 msgstr "artículo"
@@ -3181,7 +3195,7 @@ msgstr "Importar directamente"
 #: projects/templates/projects/project_detail_snapshots.html:22
 #: projects/templates/projects/project_import.html:34
 #: projects/templates/projects/project_import.html:104
-#: projects/templates/projects/site_projects.html:50
+#: projects/templates/projects/site_projects.html:65
 msgid "Created"
 msgstr "Creado"
 
@@ -3212,7 +3226,7 @@ msgstr ""
 "adelante, la instantánea puede ser utilizada para crear vistas, y el "
 "proyecto puede ser restablecido a una instantánea previa si es necesario."
 
-#: projects/templates/projects/project_detail_views.html:19 views/models.py:122
+#: projects/templates/projects/project_detail_views.html:19 views/models.py:124
 #: views/templates/views/views.html:127
 msgid "View"
 msgstr "Vista"
@@ -3359,17 +3373,18 @@ msgstr ""
 "%(name_plural)s utilizando el botón verde y eliminarlos utilizando la cruz "
 "azul (&times;)."
 
-#: projects/templates/projects/project_questions_buttons.html:9
-#: projects/templates/projects/project_questions_buttons.html:52
-msgid "Skip"
-msgstr "Saltar"
-
-#: projects/templates/projects/project_questions_buttons.html:32
-#: projects/templates/projects/project_questions_buttons.html:40
+#: projects/templates/projects/project_questions_buttons.html:10
+#: projects/templates/projects/project_questions_buttons.html:41
+#: projects/templates/projects/project_questions_buttons.html:49
 msgid "Proceed"
 msgstr "Proseguir"
 
-#: projects/templates/projects/project_questions_buttons.html:34
+#: projects/templates/projects/project_questions_buttons.html:12
+#: projects/templates/projects/project_questions_buttons.html:62
+msgid "Skip"
+msgstr "Saltar"
+
+#: projects/templates/projects/project_questions_buttons.html:43
 msgid "Save and proceed"
 msgstr "Guardar y proseguir"
 
@@ -3469,7 +3484,7 @@ msgstr ""
 "hidden=\"true\"></i> podrían ser ignoradas según lo que ha introducido."
 
 #: projects/templates/projects/project_questions_overview.html:14
-#: projects/templates/projects/site_projects.html:37
+#: projects/templates/projects/site_projects.html:52
 msgid "Back to my projects"
 msgstr "Volver a mis proyectos"
 
@@ -3541,7 +3556,7 @@ msgid "Navigation"
 msgstr "Navegación"
 
 #: projects/templates/projects/project_view.html:90
-#: projects/views/project.py:198
+#: projects/views/project.py:201
 msgid "Error"
 msgstr "Error"
 
@@ -3586,7 +3601,7 @@ msgid "Click on one of the links to join the projects."
 msgstr "Pulse en uno de los enlaces para unirse a los proyectos."
 
 #: projects/templates/projects/projects.html:127
-#: projects/templates/projects/site_projects.html:51
+#: projects/templates/projects/site_projects.html:66
 msgid "Last changed"
 msgstr "Modificado por última vez"
 
@@ -3605,7 +3620,11 @@ msgstr "Página %(number)s de %(num_pages)s."
 msgid "Next"
 msgstr "Siguiente"
 
-#: projects/templates/projects/site_projects.html:44
+#: projects/templates/projects/site_projects.html:32
+msgid "All catalogs"
+msgstr "Todos los catálogos"
+
+#: projects/templates/projects/site_projects.html:59
 #, python-format
 msgid "All projects on %(site)s"
 msgstr "Todos los proyectos en %(site)s"
@@ -3631,20 +3650,20 @@ msgstr "Retroceder"
 msgid "Set"
 msgstr "Establecer"
 
-#: projects/validators.py:22 projects/viewsets.py:318
+#: projects/validators.py:22 projects/viewsets.py:350
 msgid "You reached the file quota for this project."
 msgstr "Usted ha alcanzado la quota de archivado para este proyecto."
 
-#: projects/views/project.py:178
+#: projects/views/project.py:181
 msgid "Sorry, your invitation has been expired."
 msgstr "Lo sentimos, su invitación ha expirado."
 
-#: projects/views/project.py:181
+#: projects/views/project.py:184
 #, python-format
 msgid "Sorry, but this invitation is for the user \"%s\"."
 msgstr "Lo sentimos, pero esta invitación es para el usuario \"%s\"."
 
-#: projects/views/project.py:195
+#: projects/views/project.py:198
 msgid "Sorry, the invitation link is not valid."
 msgstr "Lo sentimos, la invitación no es válida."
 
@@ -3681,7 +3700,7 @@ msgstr "La posición de este catálogo en listas."
 #: questions/templates/questions/catalogs_modal_form_catalogs.html:196
 #: tasks/models.py:51 tasks/templates/tasks/tasks.html:144
 #: tasks/templates/tasks/tasks_modal_form_tasks.html:144
-#: tasks/templates/tasks/tasks_modal_form_tasks.html:212 views/models.py:51
+#: tasks/templates/tasks/tasks_modal_form_tasks.html:212 views/models.py:53
 #: views/templates/views/views.html:141
 #: views/templates/views/views_modal_form_views.html:143
 #: views/templates/views/views_modal_form_views.html:210
@@ -3694,7 +3713,7 @@ msgstr ""
 "Los sitios a los que pertenece este catálogo (en una configuración multi "
 "sitios)."
 
-#: questions/models/catalog.py:54 tasks/models.py:56 views/models.py:56
+#: questions/models/catalog.py:54 tasks/models.py:56 views/models.py:58
 msgid "Group"
 msgstr "Grupo"
 
@@ -3703,7 +3722,7 @@ msgid "The groups for which this catalog is active."
 msgstr "Los grupos para los cuales este catálogo está activo."
 
 #: questions/models/catalog.py:59 questions/models/questionset.py:77
-#: questions/models/section.py:55 tasks/models.py:61 views/models.py:66
+#: questions/models/section.py:55 tasks/models.py:61 views/models.py:68
 msgid "Title (primary)"
 msgstr "Título (primario)"
 
@@ -3712,7 +3731,7 @@ msgid "The title for this catalog in the primary language."
 msgstr "El título para este catálogo en el lenguaje primario."
 
 #: questions/models/catalog.py:64 questions/models/questionset.py:82
-#: questions/models/section.py:60 tasks/models.py:66 views/models.py:71
+#: questions/models/section.py:60 tasks/models.py:66 views/models.py:73
 msgid "Title (secondary)"
 msgstr "Título (secundario)"
 
@@ -3721,7 +3740,7 @@ msgid "The title for this catalog in the secondary language."
 msgstr "El título para este catálogo en el lenguaje secundario."
 
 #: questions/models/catalog.py:69 questions/models/questionset.py:87
-#: questions/models/section.py:65 tasks/models.py:71 views/models.py:76
+#: questions/models/section.py:65 tasks/models.py:71 views/models.py:78
 msgid "Title (tertiary)"
 msgstr "Título (terciario)"
 
@@ -3730,7 +3749,7 @@ msgid "The title for this catalog in the tertiary language."
 msgstr "El título para este catálogo en el lenguaje terciario."
 
 #: questions/models/catalog.py:74 questions/models/questionset.py:92
-#: questions/models/section.py:70 tasks/models.py:76 views/models.py:81
+#: questions/models/section.py:70 tasks/models.py:76 views/models.py:83
 msgid "Title (quaternary)"
 msgstr "Título (cuaternario)"
 
@@ -3739,7 +3758,7 @@ msgid "The title for this catalog in the quaternary language."
 msgstr "El título para este catálogo en el lenguaje cuaternario."
 
 #: questions/models/catalog.py:79 questions/models/questionset.py:97
-#: questions/models/section.py:75 tasks/models.py:81 views/models.py:86
+#: questions/models/section.py:75 tasks/models.py:81 views/models.py:88
 msgid "Title (quinary)"
 msgstr "Título (quinario)"
 
@@ -3767,7 +3786,7 @@ msgstr "El texto de ayuda para este catálogo en el lenguaje cuaternario."
 msgid "The help text for this catalog in the quinary language."
 msgstr "El texto de ayuda para este catálogo en el lenguaje quinario."
 
-#: questions/models/catalog.py:109 tasks/models.py:136 views/models.py:116
+#: questions/models/catalog.py:109 tasks/models.py:136 views/models.py:118
 msgid "Available"
 msgstr "Disponible"
 
@@ -3777,7 +3796,7 @@ msgstr "Designa si este catálogo está a disposición general de los proyectos.
 
 #: questions/models/catalog.py:116 tasks/models.py:46
 #: tasks/templates/tasks/tasks_modal_form_tasks.html:123
-#: tasks/templates/tasks/tasks_modal_form_tasks.html:187 views/models.py:46
+#: tasks/templates/tasks/tasks_modal_form_tasks.html:187 views/models.py:48
 #: views/templates/views/views_modal_form_views.html:122
 #: views/templates/views/views_modal_form_views.html:185
 msgid "Catalogs"
@@ -4962,27 +4981,27 @@ msgid "An empty list implies that the task can be used with every catalog."
 msgstr ""
 "Una lista vacía implica que la tarea podrá utilizarse con cualquier catálogo."
 
-#: views/models.py:22
+#: views/models.py:24
 msgid "The Uniform Resource Identifier of this view (auto-generated)."
 msgstr "El Identificador de Recurso Uniforme de esta vista (auto-generado)."
 
-#: views/models.py:27
+#: views/models.py:29
 msgid "The prefix for the URI of this view."
 msgstr "El prefijo para el URI de esta vista."
 
-#: views/models.py:32
+#: views/models.py:34
 msgid "The internal identifier of this view."
 msgstr "El identificador interno de esta vista."
 
-#: views/models.py:37
+#: views/models.py:39
 msgid "Additional internal information about this view."
 msgstr "Información interna adicional para esta vista."
 
-#: views/models.py:42
+#: views/models.py:44
 msgid "Designates whether this view can be changed."
 msgstr "Designa si esta vista puede o no ser modificada."
 
-#: views/models.py:47
+#: views/models.py:49
 msgid ""
 "The catalogs this view can be used with. An empty list implies that this "
 "view can be used with every catalog."
@@ -4990,66 +5009,66 @@ msgstr ""
 "Los catálogos con los que esta vista podrá utilizarse. Una lista vacía "
 "implica que esta vista puede ser utilizada con todos los catálogos."
 
-#: views/models.py:52
+#: views/models.py:54
 msgid "The sites this view belongs to (in a multi site setup)."
 msgstr ""
 "Los sites a los que esta vista pertenece (en una configuración multi-sitio)."
 
-#: views/models.py:57
+#: views/models.py:59
 msgid "The groups for which this view is active."
 msgstr "Los grupos para los cuales esta vista está activa."
 
-#: views/models.py:61 views/templates/views/views_export.html:35
+#: views/models.py:63 views/templates/views/views_export.html:35
 #: views/templates/views/views_modal_form_templates.html:11
 msgid "Template"
 msgstr "Plantilla"
 
-#: views/models.py:62
+#: views/models.py:64
 msgid "The template for this view, written in Django template language."
 msgstr ""
 "La plantilla para esta vista, escrita en el leguaje de plantillas Django."
 
-#: views/models.py:67
+#: views/models.py:69
 msgid "The title for this view in the primary language."
 msgstr "El título para esta vista en el lenguaje primario."
 
-#: views/models.py:72
+#: views/models.py:74
 msgid "The title for this view in the secondary language."
 msgstr "El título para esta vista en el lenguaje secundario."
 
-#: views/models.py:77
+#: views/models.py:79
 msgid "The title for this view in the tertiary language."
 msgstr "El título para esta vista en el lenguaje terciario."
 
-#: views/models.py:82
+#: views/models.py:84
 msgid "The title for this view in the quaternary language."
 msgstr "El título para esta vista en el lenguaje cuaternario."
 
-#: views/models.py:87
+#: views/models.py:89
 msgid "The title for this view in the quinary language."
 msgstr "El título para esta vista en el lenguaje quinario."
 
-#: views/models.py:92
+#: views/models.py:94
 msgid "The help text for this view in the primary language."
 msgstr "El texto de ayuda para esta vista en el lenguaje primario."
 
-#: views/models.py:97
+#: views/models.py:99
 msgid "The help text for this view in the secondary language."
 msgstr "El texto de ayuda para esta vista en el lenguaje secundario."
 
-#: views/models.py:102
+#: views/models.py:104
 msgid "The help text for this view in the tertiary language."
 msgstr "El texto de ayuda para esta vista en el lenguaje terciario."
 
-#: views/models.py:107
+#: views/models.py:109
 msgid "The help text for this view in the quaternary language."
 msgstr "El texto de ayuda para esta vista en el lenguaje cuaternario."
 
-#: views/models.py:112
+#: views/models.py:114
 msgid "The help text for this view in the quinary language."
 msgstr "El texto de ayuda para esta vista en el lenguaje quinario."
 
-#: views/models.py:117
+#: views/models.py:119
 msgid "Designates whether this view is generally available for projects."
 msgstr ""
 "Designa si esta vista estará generalmente disponible para todos los "
diff --git a/rdmo/locale/fr/LC_MESSAGES/django.mo b/rdmo/locale/fr/LC_MESSAGES/django.mo
index 1866da670f..28ce63167b 100644
Binary files a/rdmo/locale/fr/LC_MESSAGES/django.mo and b/rdmo/locale/fr/LC_MESSAGES/django.mo differ
diff --git a/rdmo/locale/fr/LC_MESSAGES/django.po b/rdmo/locale/fr/LC_MESSAGES/django.po
index 3abfd4de78..01537d3e2a 100644
--- a/rdmo/locale/fr/LC_MESSAGES/django.po
+++ b/rdmo/locale/fr/LC_MESSAGES/django.po
@@ -2,8 +2,8 @@ msgid ""
 msgstr ""
 "Project-Id-Version: RDMO\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-04-27 16:01+0200\n"
-"PO-Revision-Date: 2023-04-27 16:09+0200\n"
+"POT-Creation-Date: 2023-07-31 16:18+0200\n"
+"PO-Revision-Date: 2023-07-31 16:19+0200\n"
 "Last-Translator: Yonny Cardenas <cardenas@cc.in2p3.fr>\n"
 "Language-Team: RDMO <cardenas@cc.in2p3.fr>\n"
 "Language: fr\n"
@@ -25,7 +25,7 @@ msgstr "Prénom"
 msgid "Last name"
 msgstr "Nom de famille"
 
-#: accounts/forms.py:100 core/constants.py:20 projects/models/invite.py:27
+#: accounts/forms.py:100 core/constants.py:20 projects/models/invite.py:30
 msgid "E-mail"
 msgstr "E-mail"
 
@@ -86,7 +86,7 @@ msgstr "Le texte de ce champ supplémentaire dans la langue quinaire."
 
 #: accounts/models.py:47 questions/models/catalog.py:84
 #: questions/models/question.py:77 questions/models/questionset.py:102
-#: views/models.py:91
+#: views/models.py:93
 msgid "Help (primary)"
 msgstr "Aide (primaire)"
 
@@ -100,7 +100,7 @@ msgstr ""
 
 #: accounts/models.py:52 questions/models/catalog.py:89
 #: questions/models/question.py:82 questions/models/questionset.py:107
-#: views/models.py:96
+#: views/models.py:98
 msgid "Help (secondary)"
 msgstr "Aide (secondaire)"
 
@@ -114,7 +114,7 @@ msgstr ""
 
 #: accounts/models.py:57 questions/models/catalog.py:94
 #: questions/models/question.py:87 questions/models/questionset.py:112
-#: views/models.py:101
+#: views/models.py:103
 msgid "Help (tertiary)"
 msgstr "Aide (tertiaire)"
 
@@ -128,7 +128,7 @@ msgstr ""
 
 #: accounts/models.py:62 questions/models/catalog.py:99
 #: questions/models/question.py:92 questions/models/questionset.py:117
-#: views/models.py:106
+#: views/models.py:108
 msgid "Help (quaternary)"
 msgstr "Aide (quaternaire)"
 
@@ -142,7 +142,7 @@ msgstr ""
 
 #: accounts/models.py:67 questions/models/catalog.py:104
 #: questions/models/question.py:97 questions/models/questionset.py:122
-#: views/models.py:111
+#: views/models.py:113
 msgid "Help (quinary)"
 msgstr "Aide (quinaire)"
 
@@ -203,7 +203,7 @@ msgstr "Directeur"
 msgid "The sites for which this user is manager."
 msgstr "Les sites dont cet utilisateur est le gestionnaire."
 
-#: accounts/models.py:143 projects/models/invite.py:32
+#: accounts/models.py:143 projects/models/invite.py:35
 #: projects/models/membership.py:32
 #: projects/templates/projects/project_detail_invites.html:16
 #: projects/templates/projects/project_detail_memberships.html:21
@@ -305,9 +305,10 @@ msgid ""
 "%(site_domain)s will be reset. It can be safely ignored if you did not "
 "request a password reset. Click the link below to reset your password."
 msgstr ""
-"Vous recevez ce message car vous avez demandé que votre mot de passe dans "
-"%(site_domain) soit réinitialisé. Il peut être ignoré en toute sécurité si "
-"vous n'avez pas fait une demande de réinitialisation du mot de passe. "
+"Vous recevez cet e-mail parce que vous avez demandé la réinitialisation de "
+"votre mot de passe sur %(site_domain)s. Vous pouvez l'ignorer si vous n'avez "
+"pas demandé la réinitialisation de votre mot de passe. Vous pouvez l'ignorer "
+"si vous n'avez pas demandé la réinitialisation de votre mot de passe. "
 "Cliquez sur le lien ci-dessous pour réinitialiser votre mot de passe."
 
 #: accounts/templates/account/email/password_reset_key_message.txt:11
@@ -354,14 +355,15 @@ msgstr ""
 "mail </a>."
 
 #: accounts/templates/account/login.html:6
-#: accounts/templates/account/login_form.html:22
+#: accounts/templates/account/login_form.html:33
 #: accounts/templates/account/password_reset_from_key_done.html:14
 #: core/templates/core/base_navigation.html:162
 #: core/templates/core/home.html:55
 msgid "Login"
 msgstr "S'identifier"
 
-#: accounts/templates/account/login_form.html:6
+#: accounts/templates/account/login_form.html:8
+#: accounts/templates/account/login_form.html:14
 msgid "Login with Shibboleth"
 msgstr "Connectez-vous avec Shibboleth"
 
@@ -775,7 +777,7 @@ msgstr ""
 
 #: accounts/templatetags/accounts_tags.py:19
 #: projects/templates/projects/projects.html:125
-#: projects/templates/projects/site_projects.html:49
+#: projects/templates/projects/site_projects.html:64
 msgid "Name"
 msgstr "Nom"
 
@@ -820,7 +822,7 @@ msgstr "Conditions"
 #: questions/templates/questions/catalogs_element.html:56
 #: questions/templates/questions/catalogs_sidebar_showuri_checkboxes.html:8
 #: tasks/models.py:21 tasks/templates/tasks/tasks_export.html:15
-#: views/models.py:21 views/templates/views/views_export.html:15
+#: views/models.py:23 views/templates/views/views_export.html:15
 msgid "URI"
 msgstr "URI"
 
@@ -833,7 +835,7 @@ msgstr ""
 #: conditions/models.py:39 domain/models.py:18 options/models.py:20
 #: options/models.py:120 questions/models/catalog.py:24
 #: questions/models/question.py:27 questions/models/questionset.py:26
-#: questions/models/section.py:20 tasks/models.py:26 views/models.py:26
+#: questions/models/section.py:20 tasks/models.py:26 views/models.py:28
 msgid "URI Prefix"
 msgstr "Préfixe URI"
 
@@ -845,7 +847,7 @@ msgstr "Préfixe de l'URI de cette condition."
 #: options/models.py:125 projects/models/integration.py:60
 #: questions/models/catalog.py:29 questions/models/question.py:32
 #: questions/models/questionset.py:31 questions/models/section.py:25
-#: tasks/models.py:31 views/models.py:31
+#: tasks/models.py:31 views/models.py:33
 msgid "Key"
 msgstr "Clé"
 
@@ -864,7 +866,7 @@ msgstr "Identificateur interne de cette condition."
 #: questions/templates/questions/catalog_tree.html:65
 #: questions/templates/questions/catalog_tree.html:84
 #: questions/templates/questions/catalog_tree.html:149 tasks/models.py:36
-#: tasks/templates/tasks/tasks_export.html:21 views/models.py:36
+#: tasks/templates/tasks/tasks_export.html:21 views/models.py:38
 #: views/templates/views/views_export.html:21
 msgid "Comment"
 msgstr "Commentaire"
@@ -876,7 +878,7 @@ msgstr "Informations internes supplémentaires sur cette condition."
 #: conditions/models.py:54 domain/models.py:38 options/models.py:35
 #: options/models.py:140 questions/models/catalog.py:39
 #: questions/models/question.py:47 questions/models/questionset.py:46
-#: questions/models/section.py:40 tasks/models.py:41 views/models.py:41
+#: questions/models/section.py:40 tasks/models.py:41 views/models.py:43
 msgid "Locked"
 msgstr "Verrouillé"
 
@@ -968,7 +970,7 @@ msgstr "Tous les préfixes URI"
 #: projects/templates/projects/project_detail_sidebar.html:11
 #: projects/templates/projects/project_view.html:42
 #: projects/templates/projects/projects.html:25
-#: projects/templates/projects/site_projects.html:33
+#: projects/templates/projects/site_projects.html:48
 #: questions/templates/questions/catalog_tree.html:104
 #: questions/templates/questions/catalogs_sidebar.html:78
 #: questions/templates/questions/catalogs_sidebar_showuri_checkboxes.html:26
@@ -1138,7 +1140,7 @@ msgstr "Insérer le préfixe URI par défaut"
 #: options/templates/options/options_modal_form_conditions.html:34
 #: options/templates/options/options_modal_form_options.html:182
 #: options/templates/options/options_modal_form_optionsets.html:118
-#: projects/templates/projects/project_questions_buttons.html:27
+#: projects/templates/projects/project_questions_buttons.html:36
 #: projects/templates/projects/project_questions_modal_form_valuesets.html:32
 #: questions/templates/questions/catalogs_modal_form_catalogs.html:216
 #: questions/templates/questions/catalogs_modal_form_questions.html:464
@@ -1203,14 +1205,14 @@ msgid "Tasks"
 msgstr "Tâches"
 
 #: conditions/validators.py:14 domain/validators.py:15 options/validators.py:14
-#: options/validators.py:26 projects/forms.py:333 projects/forms.py:354
+#: options/validators.py:26 projects/forms.py:338 projects/forms.py:359
 #: questions/validators.py:16 questions/validators.py:28
 #: questions/validators.py:46 questions/validators.py:64 tasks/validators.py:14
 #: views/validators.py:14
 msgid "This field is required."
 msgstr "Ce champ est obligatoire."
 
-#: core/constants.py:14 core/settings.py:296
+#: core/constants.py:14 core/settings.py:302
 #: options/templates/options/options_export.html:39
 #: options/templates/options/options_modal_form_options.html:163
 #: projects/models/value.py:64 questions/constants.py:4
@@ -1256,99 +1258,99 @@ msgstr "Option"
 msgid "File"
 msgstr "Fichier"
 
-#: core/models.py:15
+#: core/models.py:16
 msgid "created"
 msgstr "créé"
 
-#: core/models.py:16
+#: core/models.py:17
 msgid "updated"
 msgstr "mise à jour"
 
-#: core/settings.py:124
+#: core/settings.py:128
 msgid "English"
 msgstr "Anglais"
 
-#: core/settings.py:125
+#: core/settings.py:129
 msgid "German"
 msgstr "Allemand"
 
-#: core/settings.py:239
+#: core/settings.py:245
 msgid "PDF"
 msgstr "PDF"
 
-#: core/settings.py:240
+#: core/settings.py:246
 msgid "Rich Text Format"
 msgstr "Rich Text Format"
 
-#: core/settings.py:241
+#: core/settings.py:247
 msgid "Open Office"
 msgstr "Open Office"
 
-#: core/settings.py:242
+#: core/settings.py:248
 msgid "Microsoft Office"
 msgstr "Microsoft Office"
 
-#: core/settings.py:243
+#: core/settings.py:249
 msgid "HTML"
 msgstr "HTML"
 
-#: core/settings.py:244
+#: core/settings.py:250
 msgid "Markdown"
 msgstr "Markdown"
 
-#: core/settings.py:245
+#: core/settings.py:251
 msgid "mediawiki"
 msgstr "mediawiki"
 
-#: core/settings.py:246
+#: core/settings.py:252
 msgid "LaTeX"
 msgstr "LaTeX"
 
-#: core/settings.py:266 core/settings.py:272
+#: core/settings.py:272 core/settings.py:278
 msgid "RDMO XML"
 msgstr "RDMO XML"
 
-#: core/settings.py:267 domain/templates/domain/domain.html:74
+#: core/settings.py:273 domain/templates/domain/domain.html:74
 msgid "CSV comma separated"
 msgstr "CSV séparées par des virgules"
 
-#: core/settings.py:268 domain/templates/domain/domain.html:79
+#: core/settings.py:274 domain/templates/domain/domain.html:79
 msgid "CSV semicolon separated"
 msgstr "CSV point-virgule séparé"
 
-#: core/settings.py:297 questions/constants.py:5
+#: core/settings.py:303 questions/constants.py:5
 msgid "Textarea"
 msgstr "Zone de texte"
 
-#: core/settings.py:298 questions/constants.py:6
+#: core/settings.py:304 questions/constants.py:6
 msgid "Yes/No"
 msgstr "Oui/Non"
 
-#: core/settings.py:299 questions/constants.py:7
+#: core/settings.py:305 questions/constants.py:7
 msgid "Checkboxes"
 msgstr "Cases à cocher"
 
-#: core/settings.py:300 questions/constants.py:8
+#: core/settings.py:306 questions/constants.py:8
 msgid "Radio buttons"
 msgstr "Boutons radio"
 
-#: core/settings.py:301 questions/constants.py:9
+#: core/settings.py:307 questions/constants.py:9
 msgid "Select drop-down"
 msgstr "Sélectionnez le menu déroulant"
 
-#: core/settings.py:302 questions/constants.py:10
+#: core/settings.py:308 questions/constants.py:10
 msgid "Autocomplete"
 msgstr "Autocomplétion"
 
-#: core/settings.py:303 questions/constants.py:11
+#: core/settings.py:309 questions/constants.py:11
 msgid "Range slider"
 msgstr "Curseur de gamme"
 
-#: core/settings.py:304 questions/constants.py:12
+#: core/settings.py:310 questions/constants.py:12
 msgid "Date picker"
 msgstr "Sélecteur de date"
 
-#: core/settings.py:305 questions/constants.py:13
+#: core/settings.py:311 questions/constants.py:13
 msgid "File upload"
 msgstr "Téléchargement de fichiers"
 
@@ -1407,10 +1409,10 @@ msgstr "Gestion"
 msgid "Domain"
 msgstr "Domaine"
 
-#: core/templates/core/base_navigation.html:61 projects/forms.py:309
+#: core/templates/core/base_navigation.html:61 projects/forms.py:314
 #: projects/models/project.py:64
 #: projects/templates/projects/project_detail_views.html:11 views/apps.py:7
-#: views/models.py:123 views/templates/views/views.html:91
+#: views/models.py:125 views/templates/views/views.html:91
 #: views/templates/views/views_export.html:6 views/views.py:43
 msgid "Views"
 msgstr "Vues"
@@ -1704,8 +1706,9 @@ msgid "%(foreign_model)s %(foreign_uri)s does not exist."
 msgstr "%(foreign_model)s %(foreign_uri)s n'existe pas."
 
 #: management/templates/management/import.html:70
-#: projects/templates/projects/project_questions_buttons.html:15
-#: projects/templates/projects/project_questions_buttons.html:48
+#: projects/templates/projects/project_questions_buttons.html:19
+#: projects/templates/projects/project_questions_buttons.html:32
+#: projects/templates/projects/project_questions_buttons.html:58
 msgid "Back"
 msgstr "Retour"
 
@@ -2057,8 +2060,8 @@ msgstr "Ce champ ne doit pas être nul."
 msgid "Overlays"
 msgstr "Superpositions"
 
-#: overlays/models.py:11 projects/forms.py:176
-#: projects/models/continuation.py:18 projects/models/invite.py:22
+#: overlays/models.py:11 projects/forms.py:181
+#: projects/models/continuation.py:18 projects/models/invite.py:25
 #: projects/models/membership.py:27 projects/models/project.py:34
 #: projects/templates/projects/project_detail_invites.html:14
 #: projects/templates/projects/project_detail_memberships.html:19
@@ -2085,7 +2088,7 @@ msgstr "Nom de l'URL"
 msgid "The url_name for this overlay."
 msgstr "Le nom de l'url pour cette superposition."
 
-#: overlays/models.py:26 projects/forms.py:321
+#: overlays/models.py:26 projects/forms.py:326
 #: projects/templates/projects/project_answers.html:13
 #: projects/templates/projects/project_view.html:28
 msgid "Current"
@@ -2138,19 +2141,19 @@ msgstr "Auteur"
 msgid "Guest"
 msgstr "Invité"
 
-#: projects/forms.py:175
+#: projects/forms.py:180
 msgid "Username or e-mail"
 msgstr "Nom d'utilisateur ou e-mail"
 
-#: projects/forms.py:177
+#: projects/forms.py:182
 msgid "The username or e-mail of the new user."
 msgstr "Le nom d'utilisateur ou l'adresse électronique du nouvel utilisateur."
 
-#: projects/forms.py:189
+#: projects/forms.py:194
 msgid "Add member silently"
 msgstr "Ajouter un membre en silence"
 
-#: projects/forms.py:190
+#: projects/forms.py:195
 msgid ""
 "As site manager or admin, you can directly add users without notifying them "
 "via e-mail, when you check the following checkbox."
@@ -2159,11 +2162,11 @@ msgstr ""
 "directement des utilisateurs sans les avertir par courrier électronique, "
 "lorsque vous cochez la case suivante."
 
-#: projects/forms.py:203
+#: projects/forms.py:208 projects/serializers/v1/__init__.py:147
 msgid "The user is already a member of the project."
 msgstr "L'utilisateur est déjà membre du projet."
 
-#: projects/forms.py:215
+#: projects/forms.py:220
 msgid ""
 "A user with this username or e-mail was not found. Only registered users can "
 "be invited."
@@ -2171,51 +2174,51 @@ msgstr ""
 "Un utilisateur avec ce nom d'utilisateur ou ce courriel n'a pas été trouvé. "
 "Seuls les utilisateurs enregistrés peuvent être invités."
 
-#: projects/forms.py:219
+#: projects/forms.py:224
 msgid "Only existing users can be added silently."
 msgstr "Seuls les utilisateurs existants peuvent être ajoutés en silence."
 
-#: projects/forms.py:287 projects/forms.py:291
+#: projects/forms.py:292 projects/forms.py:296
 #, python-format
 msgid "Attach %s"
 msgstr "Joindre le %s"
 
-#: projects/forms.py:297
+#: projects/forms.py:302
 msgid "Subject"
 msgstr "Sujet"
 
-#: projects/forms.py:298
+#: projects/forms.py:303
 msgid "Message"
 msgstr "Message"
 
-#: projects/forms.py:305
+#: projects/forms.py:310
 msgid "Answers"
 msgstr "Afficher les réponses"
 
-#: projects/forms.py:306
+#: projects/forms.py:311
 msgid "Attach the output of \"View answers\"."
 msgstr "Joignez la sortie de \"Voir les réponses\"."
 
-#: projects/forms.py:313
+#: projects/forms.py:318
 msgid "Files"
 msgstr "Fichiers"
 
-#: projects/forms.py:320 projects/models/snapshot.py:33
+#: projects/forms.py:325 projects/models/snapshot.py:33
 #: projects/models/value.py:38
 #: projects/templates/projects/project_detail_snapshots.html:20
 #: projects/templates/projects/project_import.html:93
 msgid "Snapshot"
 msgstr "Instantané"
 
-#: projects/forms.py:324
+#: projects/forms.py:329
 msgid "Format"
 msgstr "Format"
 
-#: projects/forms.py:339 projects/forms.py:344
+#: projects/forms.py:344 projects/forms.py:349
 msgid "Recipients"
 msgstr "Recipients"
 
-#: projects/forms.py:345
+#: projects/forms.py:350
 msgid "Enter recipients line by line"
 msgstr "Indiquer les destinataires ligne par ligne"
 
@@ -2262,7 +2265,7 @@ msgid "Files of this type cannot be imported."
 msgstr "Les fichiers de ce type ne peuvent pas être importés."
 
 #: projects/models/continuation.py:13 projects/models/integration.py:16
-#: projects/models/invite.py:17 projects/models/issue.py:28
+#: projects/models/invite.py:20 projects/models/issue.py:28
 #: projects/models/membership.py:22 projects/models/project.py:70
 #: projects/models/snapshot.py:17 projects/models/value.py:32
 #: projects/templates/projects/project_questions_overview.html:5
@@ -2355,43 +2358,43 @@ msgstr "Option d'intégration"
 msgid "Integration options"
 msgstr "Options d'intégration"
 
-#: projects/models/invite.py:18
+#: projects/models/invite.py:21
 msgid "The project for this invite."
 msgstr "Le projet pour cette invitation."
 
-#: projects/models/invite.py:23 projects/models/membership.py:28
+#: projects/models/invite.py:26 projects/models/membership.py:28
 msgid "The user for this membership."
 msgstr "L'utilisateur de cette adhésion."
 
-#: projects/models/invite.py:28
+#: projects/models/invite.py:31
 msgid "The e-mail for this membership."
 msgstr "Le courriel pour cette adhésion."
 
-#: projects/models/invite.py:33
+#: projects/models/invite.py:36
 msgid "The role for this invite."
 msgstr "Le rôle de cette invitation."
 
-#: projects/models/invite.py:37
+#: projects/models/invite.py:40
 msgid "Token"
 msgstr "Token"
 
-#: projects/models/invite.py:38
+#: projects/models/invite.py:41
 msgid "The token for this invite."
 msgstr "Le jeton pour cette invitation."
 
-#: projects/models/invite.py:41
+#: projects/models/invite.py:44
 msgid "Timestamp"
 msgstr "Horodatage"
 
-#: projects/models/invite.py:42
+#: projects/models/invite.py:45
 msgid "The timestamp for this invite."
 msgstr "L'horodatage de cette invitation."
 
-#: projects/models/invite.py:47
+#: projects/models/invite.py:50
 msgid "Invite"
 msgstr "Inviter"
 
-#: projects/models/invite.py:48
+#: projects/models/invite.py:51
 #: projects/templates/projects/project_detail_invites.html:10
 msgid "Invites"
 msgstr "Invites"
@@ -2733,6 +2736,18 @@ msgstr "Le référentiel GitLab vers lequel envoyer les problèmes."
 msgid "The secret for a GitLab webhook to close a task."
 msgstr "Le secret d'un webhook GitLab pour fermer une tâche."
 
+#: projects/serializers/v1/__init__.py:152
+msgid "A user with that e-mail is already a member of the project."
+msgstr "Un utilisateur avec cet e-mail est déjà membre du projet."
+
+#: projects/serializers/v1/__init__.py:160
+msgid "Either user or e-mail needs to be provided."
+msgstr "L'utilisateur ou l'e-mail doit être indiqué."
+
+#: projects/serializers/v1/__init__.py:162
+msgid "User and e-mail are mutually exclusive."
+msgstr "L'utilisateur et le courrier électronique s'excluent mutuellement."
+
 #: projects/serializers/v1/questionset.py:94
 msgid "item"
 msgstr "article"
@@ -3199,7 +3214,7 @@ msgstr "Importation directe"
 #: projects/templates/projects/project_detail_snapshots.html:22
 #: projects/templates/projects/project_import.html:34
 #: projects/templates/projects/project_import.html:104
-#: projects/templates/projects/site_projects.html:50
+#: projects/templates/projects/site_projects.html:65
 msgid "Created"
 msgstr "Créé"
 
@@ -3230,7 +3245,7 @@ msgstr ""
 "l'instantané peut être utilisé pour créer des vues, et le projet peut "
 "également être réinitialisé à un instantané précédent si nécessaire."
 
-#: projects/templates/projects/project_detail_views.html:19 views/models.py:122
+#: projects/templates/projects/project_detail_views.html:19 views/models.py:124
 #: views/templates/views/views.html:127
 msgid "View"
 msgstr "Vue"
@@ -3376,17 +3391,18 @@ msgstr ""
 "%(name_plural)s en utilisant le bouton vert et les supprimer en utilisant la "
 "croix bleue (&times;)."
 
-#: projects/templates/projects/project_questions_buttons.html:9
-#: projects/templates/projects/project_questions_buttons.html:52
-msgid "Skip"
-msgstr "Sauter"
-
-#: projects/templates/projects/project_questions_buttons.html:32
-#: projects/templates/projects/project_questions_buttons.html:40
+#: projects/templates/projects/project_questions_buttons.html:10
+#: projects/templates/projects/project_questions_buttons.html:41
+#: projects/templates/projects/project_questions_buttons.html:49
 msgid "Proceed"
 msgstr "Procéder"
 
-#: projects/templates/projects/project_questions_buttons.html:34
+#: projects/templates/projects/project_questions_buttons.html:12
+#: projects/templates/projects/project_questions_buttons.html:62
+msgid "Skip"
+msgstr "Sauter"
+
+#: projects/templates/projects/project_questions_buttons.html:43
 msgid "Save and proceed"
 msgstr "Enregistrez et continuez"
 
@@ -3487,7 +3503,7 @@ msgstr ""
 "hidden=\"true\"></i> peuvent être ignorées en fonction de votre saisie."
 
 #: projects/templates/projects/project_questions_overview.html:14
-#: projects/templates/projects/site_projects.html:37
+#: projects/templates/projects/site_projects.html:52
 msgid "Back to my projects"
 msgstr "Retour à mes projets"
 
@@ -3560,7 +3576,7 @@ msgid "Navigation"
 msgstr "Navigation"
 
 #: projects/templates/projects/project_view.html:90
-#: projects/views/project.py:198
+#: projects/views/project.py:201
 msgid "Error"
 msgstr "Erreur"
 
@@ -3606,7 +3622,7 @@ msgid "Click on one of the links to join the projects."
 msgstr "Cliquez sur l'un des liens pour rejoindre les projets."
 
 #: projects/templates/projects/projects.html:127
-#: projects/templates/projects/site_projects.html:51
+#: projects/templates/projects/site_projects.html:66
 msgid "Last changed"
 msgstr "Dernière modification"
 
@@ -3625,7 +3641,11 @@ msgstr "Page %(number)s sur %(num_pages)s."
 msgid "Next"
 msgstr "Suivant"
 
-#: projects/templates/projects/site_projects.html:44
+#: projects/templates/projects/site_projects.html:32
+msgid "All catalogs"
+msgstr "Tous les catalogues"
+
+#: projects/templates/projects/site_projects.html:59
 #, python-format
 msgid "All projects on %(site)s"
 msgstr "Tous les projets sur %(site)s"
@@ -3651,21 +3671,21 @@ msgstr "Retour en arriere"
 msgid "Set"
 msgstr "Ensemble"
 
-#: projects/validators.py:22 projects/viewsets.py:318
+#: projects/validators.py:22 projects/viewsets.py:350
 msgid "You reached the file quota for this project."
 msgstr ""
 "Le titre de ce projet.Vous avez atteint le quota de dossiers pour ce projet."
 
-#: projects/views/project.py:178
+#: projects/views/project.py:181
 msgid "Sorry, your invitation has been expired."
 msgstr "Désolé, votre invitation a expiré."
 
-#: projects/views/project.py:181
+#: projects/views/project.py:184
 #, python-format
 msgid "Sorry, but this invitation is for the user \"%s\"."
 msgstr "Désolé, mais cette invitation est pour l'utilisateur \"%s\"."
 
-#: projects/views/project.py:195
+#: projects/views/project.py:198
 msgid "Sorry, the invitation link is not valid."
 msgstr "Désolé, le lien d'invitation n'est pas valide."
 
@@ -3704,7 +3724,7 @@ msgstr "La position de ce catalogue dans les listes."
 #: questions/templates/questions/catalogs_modal_form_catalogs.html:196
 #: tasks/models.py:51 tasks/templates/tasks/tasks.html:144
 #: tasks/templates/tasks/tasks_modal_form_tasks.html:144
-#: tasks/templates/tasks/tasks_modal_form_tasks.html:212 views/models.py:51
+#: tasks/templates/tasks/tasks_modal_form_tasks.html:212 views/models.py:53
 #: views/templates/views/views.html:141
 #: views/templates/views/views_modal_form_views.html:143
 #: views/templates/views/views_modal_form_views.html:210
@@ -3717,7 +3737,7 @@ msgstr ""
 "Les sites auxquels ce catalogue appartient (dans une configuration multi-"
 "sites)."
 
-#: questions/models/catalog.py:54 tasks/models.py:56 views/models.py:56
+#: questions/models/catalog.py:54 tasks/models.py:56 views/models.py:58
 msgid "Group"
 msgstr "Groupe"
 
@@ -3726,7 +3746,7 @@ msgid "The groups for which this catalog is active."
 msgstr "Les groupes pour lesquels ce catalogue est actif."
 
 #: questions/models/catalog.py:59 questions/models/questionset.py:77
-#: questions/models/section.py:55 tasks/models.py:61 views/models.py:66
+#: questions/models/section.py:55 tasks/models.py:61 views/models.py:68
 msgid "Title (primary)"
 msgstr "Titre (primaire)"
 
@@ -3735,7 +3755,7 @@ msgid "The title for this catalog in the primary language."
 msgstr "Le titre de ce catalogue dans la langue principale."
 
 #: questions/models/catalog.py:64 questions/models/questionset.py:82
-#: questions/models/section.py:60 tasks/models.py:66 views/models.py:71
+#: questions/models/section.py:60 tasks/models.py:66 views/models.py:73
 msgid "Title (secondary)"
 msgstr "Titre (secondaire)"
 
@@ -3744,7 +3764,7 @@ msgid "The title for this catalog in the secondary language."
 msgstr "Le titre de ce catalogue dans la langue secondaire."
 
 #: questions/models/catalog.py:69 questions/models/questionset.py:87
-#: questions/models/section.py:65 tasks/models.py:71 views/models.py:76
+#: questions/models/section.py:65 tasks/models.py:71 views/models.py:78
 msgid "Title (tertiary)"
 msgstr "Titre (tertiaire)"
 
@@ -3753,7 +3773,7 @@ msgid "The title for this catalog in the tertiary language."
 msgstr "Le titre de ce catalogue en langue tertiaire."
 
 #: questions/models/catalog.py:74 questions/models/questionset.py:92
-#: questions/models/section.py:70 tasks/models.py:76 views/models.py:81
+#: questions/models/section.py:70 tasks/models.py:76 views/models.py:83
 msgid "Title (quaternary)"
 msgstr "Titre (quaternaire)"
 
@@ -3762,7 +3782,7 @@ msgid "The title for this catalog in the quaternary language."
 msgstr "Le titre de ce catalogue en langue quaternaire."
 
 #: questions/models/catalog.py:79 questions/models/questionset.py:97
-#: questions/models/section.py:75 tasks/models.py:81 views/models.py:86
+#: questions/models/section.py:75 tasks/models.py:81 views/models.py:88
 msgid "Title (quinary)"
 msgstr "Titre (quinaire)"
 
@@ -3790,7 +3810,7 @@ msgstr "Le texte d'aide pour ce catalogue dans la langue du quaternaire."
 msgid "The help text for this catalog in the quinary language."
 msgstr "Le texte d'aide de ce catalogue dans la langue quinaire."
 
-#: questions/models/catalog.py:109 tasks/models.py:136 views/models.py:116
+#: questions/models/catalog.py:109 tasks/models.py:136 views/models.py:118
 msgid "Available"
 msgstr "Disponible sur"
 
@@ -3800,7 +3820,7 @@ msgstr "Désigne si ce catalogue est généralement disponible pour les projets.
 
 #: questions/models/catalog.py:116 tasks/models.py:46
 #: tasks/templates/tasks/tasks_modal_form_tasks.html:123
-#: tasks/templates/tasks/tasks_modal_form_tasks.html:187 views/models.py:46
+#: tasks/templates/tasks/tasks_modal_form_tasks.html:187 views/models.py:48
 #: views/templates/views/views_modal_form_views.html:122
 #: views/templates/views/views_modal_form_views.html:185
 msgid "Catalogs"
@@ -4982,29 +5002,29 @@ msgid "An empty list implies that the task can be used with every catalog."
 msgstr ""
 "Une liste vide implique que la vue peut être utilisée avec chaque catalogue."
 
-#: views/models.py:22
+#: views/models.py:24
 msgid "The Uniform Resource Identifier of this view (auto-generated)."
 msgstr ""
 "Identificateur de Ressource Uniforme  (URI) de cette vue (généré "
 "automatiquement)."
 
-#: views/models.py:27
+#: views/models.py:29
 msgid "The prefix for the URI of this view."
 msgstr "Préfixe de l'URI de cette vue."
 
-#: views/models.py:32
+#: views/models.py:34
 msgid "The internal identifier of this view."
 msgstr "Identificateur interne de cette vue."
 
-#: views/models.py:37
+#: views/models.py:39
 msgid "Additional internal information about this view."
 msgstr "Informations internes supplémentaires sur cette vue."
 
-#: views/models.py:42
+#: views/models.py:44
 msgid "Designates whether this view can be changed."
 msgstr "Désigne si cette vue peut être modifiée."
 
-#: views/models.py:47
+#: views/models.py:49
 msgid ""
 "The catalogs this view can be used with. An empty list implies that this "
 "view can be used with every catalog."
@@ -5012,65 +5032,65 @@ msgstr ""
 "Les catalogues avec lesquels cette vue peut être utilisée. Une liste vide "
 "implique que cette vue peut être utilisée avec chaque catalogue."
 
-#: views/models.py:52
+#: views/models.py:54
 msgid "The sites this view belongs to (in a multi site setup)."
 msgstr ""
 "Les sites auxquels cette vue appartient (dans une configuration multi-sites)."
 
-#: views/models.py:57
+#: views/models.py:59
 msgid "The groups for which this view is active."
 msgstr "Les groupes pour lesquels cette vue est active."
 
-#: views/models.py:61 views/templates/views/views_export.html:35
+#: views/models.py:63 views/templates/views/views_export.html:35
 #: views/templates/views/views_modal_form_templates.html:11
 msgid "Template"
 msgstr "Modèle"
 
-#: views/models.py:62
+#: views/models.py:64
 msgid "The template for this view, written in Django template language."
 msgstr "Le modèle de cette vue, écrit en langage de modèle Django."
 
-#: views/models.py:67
+#: views/models.py:69
 msgid "The title for this view in the primary language."
 msgstr "Titre de cette vue dans la langue principale."
 
-#: views/models.py:72
+#: views/models.py:74
 msgid "The title for this view in the secondary language."
 msgstr "Le titre de cette vue dans la langue secondaire."
 
-#: views/models.py:77
+#: views/models.py:79
 msgid "The title for this view in the tertiary language."
 msgstr "Le titre de cette vue dans la langue tertiaire."
 
-#: views/models.py:82
+#: views/models.py:84
 msgid "The title for this view in the quaternary language."
 msgstr "Le titre de cette vue dans la langue quaternaire."
 
-#: views/models.py:87
+#: views/models.py:89
 msgid "The title for this view in the quinary language."
 msgstr "Titre de cette vue dans le langage quinaire."
 
-#: views/models.py:92
+#: views/models.py:94
 msgid "The help text for this view in the primary language."
 msgstr "Le texte d'aide pour cette vue dans la langue principale."
 
-#: views/models.py:97
+#: views/models.py:99
 msgid "The help text for this view in the secondary language."
 msgstr "Le texte d'aide pour cette vue dans la langue secondaire."
 
-#: views/models.py:102
+#: views/models.py:104
 msgid "The help text for this view in the tertiary language."
 msgstr "Le texte d'aide pour cette vue dans la langue tertiaire."
 
-#: views/models.py:107
+#: views/models.py:109
 msgid "The help text for this view in the quaternary language."
 msgstr "Le texte d'aide pour cette vue en langue quaternaire."
 
-#: views/models.py:112
+#: views/models.py:114
 msgid "The help text for this view in the quinary language."
 msgstr "Le texte d'aide pour cette vue dans la langage quinaire."
 
-#: views/models.py:117
+#: views/models.py:119
 msgid "Designates whether this view is generally available for projects."
 msgstr "Désigne si cette vue est généralement disponible pour les projets."
 
diff --git a/rdmo/locale/it/LC_MESSAGES/django.mo b/rdmo/locale/it/LC_MESSAGES/django.mo
index 2ec2076d28..3dee510c0a 100644
Binary files a/rdmo/locale/it/LC_MESSAGES/django.mo and b/rdmo/locale/it/LC_MESSAGES/django.mo differ
diff --git a/rdmo/locale/it/LC_MESSAGES/django.po b/rdmo/locale/it/LC_MESSAGES/django.po
index b338b2296d..e7e692e3b9 100644
--- a/rdmo/locale/it/LC_MESSAGES/django.po
+++ b/rdmo/locale/it/LC_MESSAGES/django.po
@@ -2,7 +2,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: \n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-04-27 16:01+0200\n"
+"POT-Creation-Date: 2023-07-31 16:18+0200\n"
 "PO-Revision-Date: \n"
 "Last-Translator: Dario Pilori <d.pilori@inrim.it>\n"
 "Language-Team: \n"
@@ -25,7 +25,7 @@ msgstr "Nome"
 msgid "Last name"
 msgstr "Cognome"
 
-#: accounts/forms.py:100 core/constants.py:20 projects/models/invite.py:27
+#: accounts/forms.py:100 core/constants.py:20 projects/models/invite.py:30
 msgid "E-mail"
 msgstr "E-mail"
 
@@ -88,7 +88,7 @@ msgstr "Il testo per questo campo aggiuntivo (quinta lingua)"
 
 #: accounts/models.py:47 questions/models/catalog.py:84
 #: questions/models/question.py:77 questions/models/questionset.py:102
-#: views/models.py:91
+#: views/models.py:93
 msgid "Help (primary)"
 msgstr "Aiuto (lingua principale)"
 
@@ -102,7 +102,7 @@ msgstr ""
 
 #: accounts/models.py:52 questions/models/catalog.py:89
 #: questions/models/question.py:82 questions/models/questionset.py:107
-#: views/models.py:96
+#: views/models.py:98
 msgid "Help (secondary)"
 msgstr "Aiuto (seconda lingua)"
 
@@ -115,7 +115,7 @@ msgstr ""
 
 #: accounts/models.py:57 questions/models/catalog.py:94
 #: questions/models/question.py:87 questions/models/questionset.py:112
-#: views/models.py:101
+#: views/models.py:103
 msgid "Help (tertiary)"
 msgstr "Aiuto (terza lingua)"
 
@@ -128,7 +128,7 @@ msgstr ""
 
 #: accounts/models.py:62 questions/models/catalog.py:99
 #: questions/models/question.py:92 questions/models/questionset.py:117
-#: views/models.py:106
+#: views/models.py:108
 msgid "Help (quaternary)"
 msgstr "Aiuto (quarta lingua)"
 
@@ -141,7 +141,7 @@ msgstr ""
 
 #: accounts/models.py:67 questions/models/catalog.py:104
 #: questions/models/question.py:97 questions/models/questionset.py:122
-#: views/models.py:111
+#: views/models.py:113
 msgid "Help (quinary)"
 msgstr "Aiuto (quinta lingua)"
 
@@ -201,7 +201,7 @@ msgstr "Gestore"
 msgid "The sites for which this user is manager."
 msgstr "I siti dei quali l'utente è gestore."
 
-#: accounts/models.py:143 projects/models/invite.py:32
+#: accounts/models.py:143 projects/models/invite.py:35
 #: projects/models/membership.py:32
 #: projects/templates/projects/project_detail_invites.html:16
 #: projects/templates/projects/project_detail_memberships.html:21
@@ -349,14 +349,15 @@ msgstr ""
 "href=\"%(email_url)s\">chiedere una nuova e-mail di conferma</a>."
 
 #: accounts/templates/account/login.html:6
-#: accounts/templates/account/login_form.html:22
+#: accounts/templates/account/login_form.html:33
 #: accounts/templates/account/password_reset_from_key_done.html:14
 #: core/templates/core/base_navigation.html:162
 #: core/templates/core/home.html:55
 msgid "Login"
 msgstr "Accesso"
 
-#: accounts/templates/account/login_form.html:6
+#: accounts/templates/account/login_form.html:8
+#: accounts/templates/account/login_form.html:14
 msgid "Login with Shibboleth"
 msgstr "Accesso con Shibboleth"
 
@@ -747,7 +748,7 @@ msgstr ""
 
 #: accounts/templatetags/accounts_tags.py:19
 #: projects/templates/projects/projects.html:125
-#: projects/templates/projects/site_projects.html:49
+#: projects/templates/projects/site_projects.html:64
 msgid "Name"
 msgstr "Nome"
 
@@ -792,7 +793,7 @@ msgstr "Condizioni"
 #: questions/templates/questions/catalogs_element.html:56
 #: questions/templates/questions/catalogs_sidebar_showuri_checkboxes.html:8
 #: tasks/models.py:21 tasks/templates/tasks/tasks_export.html:15
-#: views/models.py:21 views/templates/views/views_export.html:15
+#: views/models.py:23 views/templates/views/views_export.html:15
 msgid "URI"
 msgstr "URI"
 
@@ -805,7 +806,7 @@ msgstr ""
 #: conditions/models.py:39 domain/models.py:18 options/models.py:20
 #: options/models.py:120 questions/models/catalog.py:24
 #: questions/models/question.py:27 questions/models/questionset.py:26
-#: questions/models/section.py:20 tasks/models.py:26 views/models.py:26
+#: questions/models/section.py:20 tasks/models.py:26 views/models.py:28
 msgid "URI Prefix"
 msgstr "Prefisso dell'URI"
 
@@ -817,7 +818,7 @@ msgstr "Il prefisso dell'URI per questa condizione."
 #: options/models.py:125 projects/models/integration.py:60
 #: questions/models/catalog.py:29 questions/models/question.py:32
 #: questions/models/questionset.py:31 questions/models/section.py:25
-#: tasks/models.py:31 views/models.py:31
+#: tasks/models.py:31 views/models.py:33
 msgid "Key"
 msgstr "Chiave"
 
@@ -836,7 +837,7 @@ msgstr "L'identificativo interno di questa condizione."
 #: questions/templates/questions/catalog_tree.html:65
 #: questions/templates/questions/catalog_tree.html:84
 #: questions/templates/questions/catalog_tree.html:149 tasks/models.py:36
-#: tasks/templates/tasks/tasks_export.html:21 views/models.py:36
+#: tasks/templates/tasks/tasks_export.html:21 views/models.py:38
 #: views/templates/views/views_export.html:21
 msgid "Comment"
 msgstr "Commento"
@@ -848,7 +849,7 @@ msgstr "Informazioni interne addizionali su questa condizione."
 #: conditions/models.py:54 domain/models.py:38 options/models.py:35
 #: options/models.py:140 questions/models/catalog.py:39
 #: questions/models/question.py:47 questions/models/questionset.py:46
-#: questions/models/section.py:40 tasks/models.py:41 views/models.py:41
+#: questions/models/section.py:40 tasks/models.py:41 views/models.py:43
 msgid "Locked"
 msgstr "Bloccato"
 
@@ -939,7 +940,7 @@ msgstr "Tutti i prefissi dell'URI"
 #: projects/templates/projects/project_detail_sidebar.html:11
 #: projects/templates/projects/project_view.html:42
 #: projects/templates/projects/projects.html:25
-#: projects/templates/projects/site_projects.html:33
+#: projects/templates/projects/site_projects.html:48
 #: questions/templates/questions/catalog_tree.html:104
 #: questions/templates/questions/catalogs_sidebar.html:78
 #: questions/templates/questions/catalogs_sidebar_showuri_checkboxes.html:26
@@ -1108,7 +1109,7 @@ msgstr "Inserire il prefisso URI di default"
 #: options/templates/options/options_modal_form_conditions.html:34
 #: options/templates/options/options_modal_form_options.html:182
 #: options/templates/options/options_modal_form_optionsets.html:118
-#: projects/templates/projects/project_questions_buttons.html:27
+#: projects/templates/projects/project_questions_buttons.html:36
 #: projects/templates/projects/project_questions_modal_form_valuesets.html:32
 #: questions/templates/questions/catalogs_modal_form_catalogs.html:216
 #: questions/templates/questions/catalogs_modal_form_questions.html:464
@@ -1173,14 +1174,14 @@ msgid "Tasks"
 msgstr "Attività"
 
 #: conditions/validators.py:14 domain/validators.py:15 options/validators.py:14
-#: options/validators.py:26 projects/forms.py:333 projects/forms.py:354
+#: options/validators.py:26 projects/forms.py:338 projects/forms.py:359
 #: questions/validators.py:16 questions/validators.py:28
 #: questions/validators.py:46 questions/validators.py:64 tasks/validators.py:14
 #: views/validators.py:14
 msgid "This field is required."
 msgstr "Questo campo è richiesto."
 
-#: core/constants.py:14 core/settings.py:296
+#: core/constants.py:14 core/settings.py:302
 #: options/templates/options/options_export.html:39
 #: options/templates/options/options_modal_form_options.html:163
 #: projects/models/value.py:64 questions/constants.py:4
@@ -1226,99 +1227,99 @@ msgstr "Opzione"
 msgid "File"
 msgstr "File"
 
-#: core/models.py:15
+#: core/models.py:16
 msgid "created"
 msgstr "creato"
 
-#: core/models.py:16
+#: core/models.py:17
 msgid "updated"
 msgstr "aggiornato"
 
-#: core/settings.py:124
+#: core/settings.py:128
 msgid "English"
 msgstr "Inglese"
 
-#: core/settings.py:125
+#: core/settings.py:129
 msgid "German"
 msgstr "Tedesco"
 
-#: core/settings.py:239
+#: core/settings.py:245
 msgid "PDF"
 msgstr "PDF"
 
-#: core/settings.py:240
+#: core/settings.py:246
 msgid "Rich Text Format"
 msgstr "Rich Text Format"
 
-#: core/settings.py:241
+#: core/settings.py:247
 msgid "Open Office"
 msgstr "Open Office"
 
-#: core/settings.py:242
+#: core/settings.py:248
 msgid "Microsoft Office"
 msgstr "Microsoft Office"
 
-#: core/settings.py:243
+#: core/settings.py:249
 msgid "HTML"
 msgstr "HTML"
 
-#: core/settings.py:244
+#: core/settings.py:250
 msgid "Markdown"
 msgstr "Markdown"
 
-#: core/settings.py:245
+#: core/settings.py:251
 msgid "mediawiki"
 msgstr "mediawiki"
 
-#: core/settings.py:246
+#: core/settings.py:252
 msgid "LaTeX"
 msgstr "LaTeX"
 
-#: core/settings.py:266 core/settings.py:272
+#: core/settings.py:272 core/settings.py:278
 msgid "RDMO XML"
 msgstr "RDMO XML"
 
-#: core/settings.py:267 domain/templates/domain/domain.html:74
+#: core/settings.py:273 domain/templates/domain/domain.html:74
 msgid "CSV comma separated"
 msgstr "CSV separato da virgole"
 
-#: core/settings.py:268 domain/templates/domain/domain.html:79
+#: core/settings.py:274 domain/templates/domain/domain.html:79
 msgid "CSV semicolon separated"
 msgstr "CSV separato da punto e virgola"
 
-#: core/settings.py:297 questions/constants.py:5
+#: core/settings.py:303 questions/constants.py:5
 msgid "Textarea"
 msgstr "Textarea"
 
-#: core/settings.py:298 questions/constants.py:6
+#: core/settings.py:304 questions/constants.py:6
 msgid "Yes/No"
 msgstr "Sì/No"
 
-#: core/settings.py:299 questions/constants.py:7
+#: core/settings.py:305 questions/constants.py:7
 msgid "Checkboxes"
 msgstr "Caselle di controllo"
 
-#: core/settings.py:300 questions/constants.py:8
+#: core/settings.py:306 questions/constants.py:8
 msgid "Radio buttons"
 msgstr "Pulsanti radio"
 
-#: core/settings.py:301 questions/constants.py:9
+#: core/settings.py:307 questions/constants.py:9
 msgid "Select drop-down"
 msgstr "Seleziona il menu a tendina"
 
-#: core/settings.py:302 questions/constants.py:10
+#: core/settings.py:308 questions/constants.py:10
 msgid "Autocomplete"
 msgstr "Autocompletamento"
 
-#: core/settings.py:303 questions/constants.py:11
+#: core/settings.py:309 questions/constants.py:11
 msgid "Range slider"
 msgstr "Cursore di gamma"
 
-#: core/settings.py:304 questions/constants.py:12
+#: core/settings.py:310 questions/constants.py:12
 msgid "Date picker"
 msgstr "Raccoglitore di date"
 
-#: core/settings.py:305 questions/constants.py:13
+#: core/settings.py:311 questions/constants.py:13
 msgid "File upload"
 msgstr "Caricamento dei file"
 
@@ -1377,10 +1378,10 @@ msgstr "Gestione"
 msgid "Domain"
 msgstr "Dominio"
 
-#: core/templates/core/base_navigation.html:61 projects/forms.py:309
+#: core/templates/core/base_navigation.html:61 projects/forms.py:314
 #: projects/models/project.py:64
 #: projects/templates/projects/project_detail_views.html:11 views/apps.py:7
-#: views/models.py:123 views/templates/views/views.html:91
+#: views/models.py:125 views/templates/views/views.html:91
 #: views/templates/views/views_export.html:6 views/views.py:43
 msgid "Views"
 msgstr "Viste"
@@ -1670,8 +1671,9 @@ msgid "%(foreign_model)s %(foreign_uri)s does not exist."
 msgstr "%(foreign_model)s %(foreign_uri)s non esiste."
 
 #: management/templates/management/import.html:70
-#: projects/templates/projects/project_questions_buttons.html:15
-#: projects/templates/projects/project_questions_buttons.html:48
+#: projects/templates/projects/project_questions_buttons.html:19
+#: projects/templates/projects/project_questions_buttons.html:32
+#: projects/templates/projects/project_questions_buttons.html:58
 msgid "Back"
 msgstr "Indietro"
 
@@ -2017,8 +2019,8 @@ msgstr "Questo campo non può essere nullo."
 msgid "Overlays"
 msgstr "Sovrapposizioni"
 
-#: overlays/models.py:11 projects/forms.py:176
-#: projects/models/continuation.py:18 projects/models/invite.py:22
+#: overlays/models.py:11 projects/forms.py:181
+#: projects/models/continuation.py:18 projects/models/invite.py:25
 #: projects/models/membership.py:27 projects/models/project.py:34
 #: projects/templates/projects/project_detail_invites.html:14
 #: projects/templates/projects/project_detail_memberships.html:19
@@ -2045,7 +2047,7 @@ msgstr "Nome Url"
 msgid "The url_name for this overlay."
 msgstr "Il nome dell'url per questo sovrapposto."
 
-#: overlays/models.py:26 projects/forms.py:321
+#: overlays/models.py:26 projects/forms.py:326
 #: projects/templates/projects/project_answers.html:13
 #: projects/templates/projects/project_view.html:28
 msgid "Current"
@@ -2097,19 +2099,19 @@ msgstr "Autore"
 msgid "Guest"
 msgstr "Ospite"
 
-#: projects/forms.py:175
+#: projects/forms.py:180
 msgid "Username or e-mail"
 msgstr "Nome utente o e-mail"
 
-#: projects/forms.py:177
+#: projects/forms.py:182
 msgid "The username or e-mail of the new user."
 msgstr "Il nome utente o l'e-mail del nuovo utente."
 
-#: projects/forms.py:189
+#: projects/forms.py:194
 msgid "Add member silently"
 msgstr "Aggiungi il membro in silenzio"
 
-#: projects/forms.py:190
+#: projects/forms.py:195
 msgid ""
 "As site manager or admin, you can directly add users without notifying them "
 "via e-mail, when you check the following checkbox."
@@ -2118,11 +2120,11 @@ msgstr ""
 "utenti senza notificarli via e-mail, quando selezioni la seguente casella di "
 "controllo."
 
-#: projects/forms.py:203
+#: projects/forms.py:208 projects/serializers/v1/__init__.py:147
 msgid "The user is already a member of the project."
 msgstr "L'utente è già membro del progetto."
 
-#: projects/forms.py:215
+#: projects/forms.py:220
 msgid ""
 "A user with this username or e-mail was not found. Only registered users can "
 "be invited."
@@ -2130,51 +2132,51 @@ msgstr ""
 "Un utente con questo nome utente o e-mail non è stato trovato. Solo gli "
 "utenti registrati possono essere invitati."
 
-#: projects/forms.py:219
+#: projects/forms.py:224
 msgid "Only existing users can be added silently."
 msgstr "Solo gli utenti esistenti possono essere aggiunti silenziosamente."
 
-#: projects/forms.py:287 projects/forms.py:291
+#: projects/forms.py:292 projects/forms.py:296
 #, python-format
 msgid "Attach %s"
 msgstr "Allega %s"
 
-#: projects/forms.py:297
+#: projects/forms.py:302
 msgid "Subject"
 msgstr "Oggetto"
 
-#: projects/forms.py:298
+#: projects/forms.py:303
 msgid "Message"
 msgstr "Messaggio"
 
-#: projects/forms.py:305
+#: projects/forms.py:310
 msgid "Answers"
 msgstr "Risposte"
 
-#: projects/forms.py:306
+#: projects/forms.py:311
 msgid "Attach the output of \"View answers\"."
 msgstr "Allega il risultato di \"Visualizza risposte\"."
 
-#: projects/forms.py:313
+#: projects/forms.py:318
 msgid "Files"
 msgstr "Files"
 
-#: projects/forms.py:320 projects/models/snapshot.py:33
+#: projects/forms.py:325 projects/models/snapshot.py:33
 #: projects/models/value.py:38
 #: projects/templates/projects/project_detail_snapshots.html:20
 #: projects/templates/projects/project_import.html:93
 msgid "Snapshot"
 msgstr "Istantanea"
 
-#: projects/forms.py:324
+#: projects/forms.py:329
 msgid "Format"
 msgstr "Formato"
 
-#: projects/forms.py:339 projects/forms.py:344
+#: projects/forms.py:344 projects/forms.py:349
 msgid "Recipients"
 msgstr "Destinatari"
 
-#: projects/forms.py:345
+#: projects/forms.py:350
 msgid "Enter recipients line by line"
 msgstr "Inserire i destinatari linea per linea"
 
@@ -2221,7 +2223,7 @@ msgid "Files of this type cannot be imported."
 msgstr "Questa tipologia di file non può essere importata."
 
 #: projects/models/continuation.py:13 projects/models/integration.py:16
-#: projects/models/invite.py:17 projects/models/issue.py:28
+#: projects/models/invite.py:20 projects/models/issue.py:28
 #: projects/models/membership.py:22 projects/models/project.py:70
 #: projects/models/snapshot.py:17 projects/models/value.py:32
 #: projects/templates/projects/project_questions_overview.html:5
@@ -2314,43 +2316,43 @@ msgstr "Opzione di integrazione"
 msgid "Integration options"
 msgstr "Opzioni di integrazione"
 
-#: projects/models/invite.py:18
+#: projects/models/invite.py:21
 msgid "The project for this invite."
 msgstr "Il progetto per questo invito."
 
-#: projects/models/invite.py:23 projects/models/membership.py:28
+#: projects/models/invite.py:26 projects/models/membership.py:28
 msgid "The user for this membership."
 msgstr "L'utente di questa iscrizione."
 
-#: projects/models/invite.py:28
+#: projects/models/invite.py:31
 msgid "The e-mail for this membership."
 msgstr "L'e-mail per questa iscrizione."
 
-#: projects/models/invite.py:33
+#: projects/models/invite.py:36
 msgid "The role for this invite."
 msgstr "Il ruolo per questo invito."
 
-#: projects/models/invite.py:37
+#: projects/models/invite.py:40
 msgid "Token"
 msgstr "Token"
 
-#: projects/models/invite.py:38
+#: projects/models/invite.py:41
 msgid "The token for this invite."
 msgstr "Il token per questo invito."
 
-#: projects/models/invite.py:41
+#: projects/models/invite.py:44
 msgid "Timestamp"
 msgstr "Timestamp"
 
-#: projects/models/invite.py:42
+#: projects/models/invite.py:45
 msgid "The timestamp for this invite."
 msgstr "Il timestamp di questo invito."
 
-#: projects/models/invite.py:47
+#: projects/models/invite.py:50
 msgid "Invite"
 msgstr "Invita"
 
-#: projects/models/invite.py:48
+#: projects/models/invite.py:51
 #: projects/templates/projects/project_detail_invites.html:10
 msgid "Invites"
 msgstr "Invita"
@@ -2689,6 +2691,18 @@ msgstr "Il repository GitLab al quale inviare issues."
 msgid "The secret for a GitLab webhook to close a task."
 msgstr "Il segreto per un webhook di GitLab per chiudere un compito."
 
+#: projects/serializers/v1/__init__.py:152
+msgid "A user with that e-mail is already a member of the project."
+msgstr "Un utente con quell'e-mail è già membro del progetto."
+
+#: projects/serializers/v1/__init__.py:160
+msgid "Either user or e-mail needs to be provided."
+msgstr "È necessario indicare l'utente o l'e-mail."
+
+#: projects/serializers/v1/__init__.py:162
+msgid "User and e-mail are mutually exclusive."
+msgstr "Utente e e-mail si escludono a vicenda."
+
 #: projects/serializers/v1/questionset.py:94
 msgid "item"
 msgstr "elemento"
@@ -3145,7 +3159,7 @@ msgstr "Importare direttamente"
 #: projects/templates/projects/project_detail_snapshots.html:22
 #: projects/templates/projects/project_import.html:34
 #: projects/templates/projects/project_import.html:104
-#: projects/templates/projects/site_projects.html:50
+#: projects/templates/projects/site_projects.html:65
 msgid "Created"
 msgstr "Creato"
 
@@ -3176,7 +3190,7 @@ msgstr ""
 "usata per creare delle viste, e il progetto può anche essere riportato a "
 "un'istantanea precedente se necessario."
 
-#: projects/templates/projects/project_detail_views.html:19 views/models.py:122
+#: projects/templates/projects/project_detail_views.html:19 views/models.py:124
 #: views/templates/views/views.html:127
 msgid "View"
 msgstr "Visualizza"
@@ -3320,17 +3334,18 @@ msgstr ""
 "%(name_plural)s usando il pulsante verde e rimuoverli usando la croce blu "
 "(&times;)."
 
-#: projects/templates/projects/project_questions_buttons.html:9
-#: projects/templates/projects/project_questions_buttons.html:52
-msgid "Skip"
-msgstr "Salta"
-
-#: projects/templates/projects/project_questions_buttons.html:32
-#: projects/templates/projects/project_questions_buttons.html:40
+#: projects/templates/projects/project_questions_buttons.html:10
+#: projects/templates/projects/project_questions_buttons.html:41
+#: projects/templates/projects/project_questions_buttons.html:49
 msgid "Proceed"
 msgstr "Continua"
 
-#: projects/templates/projects/project_questions_buttons.html:34
+#: projects/templates/projects/project_questions_buttons.html:12
+#: projects/templates/projects/project_questions_buttons.html:62
+msgid "Skip"
+msgstr "Salta"
+
+#: projects/templates/projects/project_questions_buttons.html:43
 msgid "Save and proceed"
 msgstr "Salva e continua"
 
@@ -3430,7 +3445,7 @@ msgstr ""
 "i> potrebbero essere saltate in base al tuo input."
 
 #: projects/templates/projects/project_questions_overview.html:14
-#: projects/templates/projects/site_projects.html:37
+#: projects/templates/projects/site_projects.html:52
 msgid "Back to my projects"
 msgstr "Torna ai miei progetti"
 
@@ -3501,7 +3516,7 @@ msgid "Navigation"
 msgstr "Navigazione"
 
 #: projects/templates/projects/project_view.html:90
-#: projects/views/project.py:198
+#: projects/views/project.py:201
 msgid "Error"
 msgstr "Errore"
 
@@ -3546,7 +3561,7 @@ msgid "Click on one of the links to join the projects."
 msgstr "Clicca su uno dei link per unirti ai progetti."
 
 #: projects/templates/projects/projects.html:127
-#: projects/templates/projects/site_projects.html:51
+#: projects/templates/projects/site_projects.html:66
 msgid "Last changed"
 msgstr "Ultima modifica"
 
@@ -3565,7 +3580,11 @@ msgstr "Pagina %(number)s di %(num_pages)s."
 msgid "Next"
 msgstr "Successivo"
 
-#: projects/templates/projects/site_projects.html:44
+#: projects/templates/projects/site_projects.html:32
+msgid "All catalogs"
+msgstr "Tutti i cataloghi"
+
+#: projects/templates/projects/site_projects.html:59
 #, python-format
 msgid "All projects on %(site)s"
 msgstr "Tutti i progetti su %(site)s"
@@ -3591,20 +3610,20 @@ msgstr "Torna indietro"
 msgid "Set"
 msgstr "Raccolta"
 
-#: projects/validators.py:22 projects/viewsets.py:318
+#: projects/validators.py:22 projects/viewsets.py:350
 msgid "You reached the file quota for this project."
 msgstr "Hai raggiunto la quota di file per questo progetto."
 
-#: projects/views/project.py:178
+#: projects/views/project.py:181
 msgid "Sorry, your invitation has been expired."
 msgstr "Spiacente, il tuo invito è scaduto."
 
-#: projects/views/project.py:181
+#: projects/views/project.py:184
 #, python-format
 msgid "Sorry, but this invitation is for the user \"%s\"."
 msgstr "Spiacente, ma questo invito è per l'utente \"%s\"."
 
-#: projects/views/project.py:195
+#: projects/views/project.py:198
 msgid "Sorry, the invitation link is not valid."
 msgstr "Spiacente, il link di invito non è valido."
 
@@ -3642,7 +3661,7 @@ msgstr "La posizione di questo catalogo nelle liste."
 #: questions/templates/questions/catalogs_modal_form_catalogs.html:196
 #: tasks/models.py:51 tasks/templates/tasks/tasks.html:144
 #: tasks/templates/tasks/tasks_modal_form_tasks.html:144
-#: tasks/templates/tasks/tasks_modal_form_tasks.html:212 views/models.py:51
+#: tasks/templates/tasks/tasks_modal_form_tasks.html:212 views/models.py:53
 #: views/templates/views/views.html:141
 #: views/templates/views/views_modal_form_views.html:143
 #: views/templates/views/views_modal_form_views.html:210
@@ -3653,7 +3672,7 @@ msgstr "Siti"
 msgid "The sites this catalog belongs to (in a multi site setup)."
 msgstr "Il sito a cui appartiene questo catalogo (per setup multi sito)."
 
-#: questions/models/catalog.py:54 tasks/models.py:56 views/models.py:56
+#: questions/models/catalog.py:54 tasks/models.py:56 views/models.py:58
 msgid "Group"
 msgstr "Gruppo"
 
@@ -3662,7 +3681,7 @@ msgid "The groups for which this catalog is active."
 msgstr "I gruppi per i quali questo catalogo è attivo."
 
 #: questions/models/catalog.py:59 questions/models/questionset.py:77
-#: questions/models/section.py:55 tasks/models.py:61 views/models.py:66
+#: questions/models/section.py:55 tasks/models.py:61 views/models.py:68
 msgid "Title (primary)"
 msgstr "Titolo (lingua principale)"
 
@@ -3671,7 +3690,7 @@ msgid "The title for this catalog in the primary language."
 msgstr "Il testo per questo catalogo nella lingua principale."
 
 #: questions/models/catalog.py:64 questions/models/questionset.py:82
-#: questions/models/section.py:60 tasks/models.py:66 views/models.py:71
+#: questions/models/section.py:60 tasks/models.py:66 views/models.py:73
 msgid "Title (secondary)"
 msgstr "Titolo (seconda lingua)"
 
@@ -3680,7 +3699,7 @@ msgid "The title for this catalog in the secondary language."
 msgstr "Il testo per questo catalogo nella seconda lingua."
 
 #: questions/models/catalog.py:69 questions/models/questionset.py:87
-#: questions/models/section.py:65 tasks/models.py:71 views/models.py:76
+#: questions/models/section.py:65 tasks/models.py:71 views/models.py:78
 msgid "Title (tertiary)"
 msgstr "Titolo (terza lingua)"
 
@@ -3689,7 +3708,7 @@ msgid "The title for this catalog in the tertiary language."
 msgstr "Il testo per questo catalogo nella terza lingua."
 
 #: questions/models/catalog.py:74 questions/models/questionset.py:92
-#: questions/models/section.py:70 tasks/models.py:76 views/models.py:81
+#: questions/models/section.py:70 tasks/models.py:76 views/models.py:83
 msgid "Title (quaternary)"
 msgstr "Titolo (quarta lingua)"
 
@@ -3698,7 +3717,7 @@ msgid "The title for this catalog in the quaternary language."
 msgstr "Il testo per questo catalogo nella quarta lingua."
 
 #: questions/models/catalog.py:79 questions/models/questionset.py:97
-#: questions/models/section.py:75 tasks/models.py:81 views/models.py:86
+#: questions/models/section.py:75 tasks/models.py:81 views/models.py:88
 msgid "Title (quinary)"
 msgstr "Titolo (quinta lingua)"
 
@@ -3726,7 +3745,7 @@ msgstr "Il testo di aiuto per questo catalogo nella quarta lingua."
 msgid "The help text for this catalog in the quinary language."
 msgstr "Il testo di aiuto per questo catalogo nella quinta lingua."
 
-#: questions/models/catalog.py:109 tasks/models.py:136 views/models.py:116
+#: questions/models/catalog.py:109 tasks/models.py:136 views/models.py:118
 msgid "Available"
 msgstr "Disponibile"
 
@@ -3736,7 +3755,7 @@ msgstr "Indica se questo catalogo è disponibile generalmente per progetti."
 
 #: questions/models/catalog.py:116 tasks/models.py:46
 #: tasks/templates/tasks/tasks_modal_form_tasks.html:123
-#: tasks/templates/tasks/tasks_modal_form_tasks.html:187 views/models.py:46
+#: tasks/templates/tasks/tasks_modal_form_tasks.html:187 views/models.py:48
 #: views/templates/views/views_modal_form_views.html:122
 #: views/templates/views/views_modal_form_views.html:185
 msgid "Catalogs"
@@ -4911,28 +4930,28 @@ msgstr ""
 "Una lista vuota significa che questa attività può essere usata con tutti i "
 "cataloghi."
 
-#: views/models.py:22
+#: views/models.py:24
 msgid "The Uniform Resource Identifier of this view (auto-generated)."
 msgstr ""
 "L'Uniform Resource Identifier di questa vista (generato automaticamente)."
 
-#: views/models.py:27
+#: views/models.py:29
 msgid "The prefix for the URI of this view."
 msgstr "Il prefisso dell'URI per questa vista."
 
-#: views/models.py:32
+#: views/models.py:34
 msgid "The internal identifier of this view."
 msgstr "L'identificativo interno di questa vista."
 
-#: views/models.py:37
+#: views/models.py:39
 msgid "Additional internal information about this view."
 msgstr "Informazioni interne addizionali su questa vista."
 
-#: views/models.py:42
+#: views/models.py:44
 msgid "Designates whether this view can be changed."
 msgstr "Indica se questa vista può essere cambiata."
 
-#: views/models.py:47
+#: views/models.py:49
 msgid ""
 "The catalogs this view can be used with. An empty list implies that this "
 "view can be used with every catalog."
@@ -4940,65 +4959,65 @@ msgstr ""
 "I cataloghi per i quali questa vista può essere utilizzata. Una lista vuota "
 "significa che questa vista può essere usata con tutti i cataloghi."
 
-#: views/models.py:52
+#: views/models.py:54
 msgid "The sites this view belongs to (in a multi site setup)."
 msgstr "Il sito a cui appartiene questa vista (per setup multi sito)."
 
-#: views/models.py:57
+#: views/models.py:59
 msgid "The groups for which this view is active."
 msgstr "I gruppi per i quali questa vista è attiva."
 
-#: views/models.py:61 views/templates/views/views_export.html:35
+#: views/models.py:63 views/templates/views/views_export.html:35
 #: views/templates/views/views_modal_form_templates.html:11
 msgid "Template"
 msgstr "Modello"
 
-#: views/models.py:62
+#: views/models.py:64
 msgid "The template for this view, written in Django template language."
 msgstr ""
 "Il modello per questa vista, scritto nel linguaggio di template di Django."
 
-#: views/models.py:67
+#: views/models.py:69
 msgid "The title for this view in the primary language."
 msgstr "Il titolo per questa vista nella lingua principale."
 
-#: views/models.py:72
+#: views/models.py:74
 msgid "The title for this view in the secondary language."
 msgstr "Il titolo per questa vista nella seconda lingua."
 
-#: views/models.py:77
+#: views/models.py:79
 msgid "The title for this view in the tertiary language."
 msgstr "Il titolo per questa vista nella terza lingua."
 
-#: views/models.py:82
+#: views/models.py:84
 msgid "The title for this view in the quaternary language."
 msgstr "Il titolo per questa vista nella quarta lingua."
 
-#: views/models.py:87
+#: views/models.py:89
 msgid "The title for this view in the quinary language."
 msgstr "Il titolo per questa vista nella quinta lingua."
 
-#: views/models.py:92
+#: views/models.py:94
 msgid "The help text for this view in the primary language."
 msgstr "Il testo di aiuto per questa vista nella lingua principale."
 
-#: views/models.py:97
+#: views/models.py:99
 msgid "The help text for this view in the secondary language."
 msgstr "Il testo di aiuto per questa vista nella seconda lingua."
 
-#: views/models.py:102
+#: views/models.py:104
 msgid "The help text for this view in the tertiary language."
 msgstr "Il testo di aiuto per questa vista nella terza lingua."
 
-#: views/models.py:107
+#: views/models.py:109
 msgid "The help text for this view in the quaternary language."
 msgstr "Il testo di aiuto per questa vista nella quarta lingua."
 
-#: views/models.py:112
+#: views/models.py:114
 msgid "The help text for this view in the quinary language."
 msgstr "Il testo di aiuto per questa vista nella quinta lingua."
 
-#: views/models.py:117
+#: views/models.py:119
 msgid "Designates whether this view is generally available for projects."
 msgstr "Indica se questa vista è generalmente disponibile per progetti."
 
diff --git a/rdmo/locale/nl/LC_MESSAGES/django.po b/rdmo/locale/nl/LC_MESSAGES/django.po
index 22aacb2798..8b6ac43ad7 100644
--- a/rdmo/locale/nl/LC_MESSAGES/django.po
+++ b/rdmo/locale/nl/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-04-27 16:01+0200\n"
+"POT-Creation-Date: 2023-07-31 16:18+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -30,7 +30,7 @@ msgstr ""
 msgid "Last name"
 msgstr ""
 
-#: accounts/forms.py:100 core/constants.py:20 projects/models/invite.py:27
+#: accounts/forms.py:100 core/constants.py:20 projects/models/invite.py:30
 msgid "E-mail"
 msgstr ""
 
@@ -91,7 +91,7 @@ msgstr ""
 
 #: accounts/models.py:47 questions/models/catalog.py:84
 #: questions/models/question.py:77 questions/models/questionset.py:102
-#: views/models.py:91
+#: views/models.py:93
 msgid "Help (primary)"
 msgstr ""
 
@@ -103,7 +103,7 @@ msgstr ""
 
 #: accounts/models.py:52 questions/models/catalog.py:89
 #: questions/models/question.py:82 questions/models/questionset.py:107
-#: views/models.py:96
+#: views/models.py:98
 msgid "Help (secondary)"
 msgstr ""
 
@@ -115,7 +115,7 @@ msgstr ""
 
 #: accounts/models.py:57 questions/models/catalog.py:94
 #: questions/models/question.py:87 questions/models/questionset.py:112
-#: views/models.py:101
+#: views/models.py:103
 msgid "Help (tertiary)"
 msgstr ""
 
@@ -127,7 +127,7 @@ msgstr ""
 
 #: accounts/models.py:62 questions/models/catalog.py:99
 #: questions/models/question.py:92 questions/models/questionset.py:117
-#: views/models.py:106
+#: views/models.py:108
 msgid "Help (quaternary)"
 msgstr ""
 
@@ -139,7 +139,7 @@ msgstr ""
 
 #: accounts/models.py:67 questions/models/catalog.py:104
 #: questions/models/question.py:97 questions/models/questionset.py:122
-#: views/models.py:111
+#: views/models.py:113
 msgid "Help (quinary)"
 msgstr ""
 
@@ -198,7 +198,7 @@ msgstr ""
 msgid "The sites for which this user is manager."
 msgstr ""
 
-#: accounts/models.py:143 projects/models/invite.py:32
+#: accounts/models.py:143 projects/models/invite.py:35
 #: projects/models/membership.py:32
 #: projects/templates/projects/project_detail_invites.html:16
 #: projects/templates/projects/project_detail_memberships.html:21
@@ -335,14 +335,15 @@ msgid ""
 msgstr ""
 
 #: accounts/templates/account/login.html:6
-#: accounts/templates/account/login_form.html:22
+#: accounts/templates/account/login_form.html:33
 #: accounts/templates/account/password_reset_from_key_done.html:14
 #: core/templates/core/base_navigation.html:162
 #: core/templates/core/home.html:55
 msgid "Login"
 msgstr ""
 
-#: accounts/templates/account/login_form.html:6
+#: accounts/templates/account/login_form.html:8
+#: accounts/templates/account/login_form.html:14
 msgid "Login with Shibboleth"
 msgstr ""
 
@@ -696,7 +697,7 @@ msgstr ""
 
 #: accounts/templatetags/accounts_tags.py:19
 #: projects/templates/projects/projects.html:125
-#: projects/templates/projects/site_projects.html:49
+#: projects/templates/projects/site_projects.html:64
 msgid "Name"
 msgstr ""
 
@@ -741,7 +742,7 @@ msgstr ""
 #: questions/templates/questions/catalogs_element.html:56
 #: questions/templates/questions/catalogs_sidebar_showuri_checkboxes.html:8
 #: tasks/models.py:21 tasks/templates/tasks/tasks_export.html:15
-#: views/models.py:21 views/templates/views/views_export.html:15
+#: views/models.py:23 views/templates/views/views_export.html:15
 msgid "URI"
 msgstr ""
 
@@ -752,7 +753,7 @@ msgstr ""
 #: conditions/models.py:39 domain/models.py:18 options/models.py:20
 #: options/models.py:120 questions/models/catalog.py:24
 #: questions/models/question.py:27 questions/models/questionset.py:26
-#: questions/models/section.py:20 tasks/models.py:26 views/models.py:26
+#: questions/models/section.py:20 tasks/models.py:26 views/models.py:28
 msgid "URI Prefix"
 msgstr ""
 
@@ -764,7 +765,7 @@ msgstr ""
 #: options/models.py:125 projects/models/integration.py:60
 #: questions/models/catalog.py:29 questions/models/question.py:32
 #: questions/models/questionset.py:31 questions/models/section.py:25
-#: tasks/models.py:31 views/models.py:31
+#: tasks/models.py:31 views/models.py:33
 msgid "Key"
 msgstr ""
 
@@ -783,7 +784,7 @@ msgstr ""
 #: questions/templates/questions/catalog_tree.html:65
 #: questions/templates/questions/catalog_tree.html:84
 #: questions/templates/questions/catalog_tree.html:149 tasks/models.py:36
-#: tasks/templates/tasks/tasks_export.html:21 views/models.py:36
+#: tasks/templates/tasks/tasks_export.html:21 views/models.py:38
 #: views/templates/views/views_export.html:21
 msgid "Comment"
 msgstr ""
@@ -795,7 +796,7 @@ msgstr ""
 #: conditions/models.py:54 domain/models.py:38 options/models.py:35
 #: options/models.py:140 questions/models/catalog.py:39
 #: questions/models/question.py:47 questions/models/questionset.py:46
-#: questions/models/section.py:40 tasks/models.py:41 views/models.py:41
+#: questions/models/section.py:40 tasks/models.py:41 views/models.py:43
 msgid "Locked"
 msgstr ""
 
@@ -882,7 +883,7 @@ msgstr ""
 #: projects/templates/projects/project_detail_sidebar.html:11
 #: projects/templates/projects/project_view.html:42
 #: projects/templates/projects/projects.html:25
-#: projects/templates/projects/site_projects.html:33
+#: projects/templates/projects/site_projects.html:48
 #: questions/templates/questions/catalog_tree.html:104
 #: questions/templates/questions/catalogs_sidebar.html:78
 #: questions/templates/questions/catalogs_sidebar_showuri_checkboxes.html:26
@@ -1046,7 +1047,7 @@ msgstr ""
 #: options/templates/options/options_modal_form_conditions.html:34
 #: options/templates/options/options_modal_form_options.html:182
 #: options/templates/options/options_modal_form_optionsets.html:118
-#: projects/templates/projects/project_questions_buttons.html:27
+#: projects/templates/projects/project_questions_buttons.html:36
 #: projects/templates/projects/project_questions_modal_form_valuesets.html:32
 #: questions/templates/questions/catalogs_modal_form_catalogs.html:216
 #: questions/templates/questions/catalogs_modal_form_questions.html:464
@@ -1111,14 +1112,14 @@ msgid "Tasks"
 msgstr ""
 
 #: conditions/validators.py:14 domain/validators.py:15 options/validators.py:14
-#: options/validators.py:26 projects/forms.py:333 projects/forms.py:354
+#: options/validators.py:26 projects/forms.py:338 projects/forms.py:359
 #: questions/validators.py:16 questions/validators.py:28
 #: questions/validators.py:46 questions/validators.py:64 tasks/validators.py:14
 #: views/validators.py:14
 msgid "This field is required."
 msgstr ""
 
-#: core/constants.py:14 core/settings.py:296
+#: core/constants.py:14 core/settings.py:302
 #: options/templates/options/options_export.html:39
 #: options/templates/options/options_modal_form_options.html:163
 #: projects/models/value.py:64 questions/constants.py:4
@@ -1164,99 +1165,99 @@ msgstr ""
 msgid "File"
 msgstr ""
 
-#: core/models.py:15
+#: core/models.py:16
 msgid "created"
 msgstr ""
 
-#: core/models.py:16
+#: core/models.py:17
 msgid "updated"
 msgstr ""
 
-#: core/settings.py:124
+#: core/settings.py:128
 msgid "English"
 msgstr ""
 
-#: core/settings.py:125
+#: core/settings.py:129
 msgid "German"
 msgstr ""
 
-#: core/settings.py:239
+#: core/settings.py:245
 msgid "PDF"
 msgstr ""
 
-#: core/settings.py:240
+#: core/settings.py:246
 msgid "Rich Text Format"
 msgstr ""
 
-#: core/settings.py:241
+#: core/settings.py:247
 msgid "Open Office"
 msgstr ""
 
-#: core/settings.py:242
+#: core/settings.py:248
 msgid "Microsoft Office"
 msgstr ""
 
-#: core/settings.py:243
+#: core/settings.py:249
 msgid "HTML"
 msgstr ""
 
-#: core/settings.py:244
+#: core/settings.py:250
 msgid "Markdown"
 msgstr ""
 
-#: core/settings.py:245
+#: core/settings.py:251
 msgid "mediawiki"
 msgstr ""
 
-#: core/settings.py:246
+#: core/settings.py:252
 msgid "LaTeX"
 msgstr ""
 
-#: core/settings.py:266 core/settings.py:272
+#: core/settings.py:272 core/settings.py:278
 msgid "RDMO XML"
 msgstr ""
 
-#: core/settings.py:267 domain/templates/domain/domain.html:74
+#: core/settings.py:273 domain/templates/domain/domain.html:74
 msgid "CSV comma separated"
 msgstr ""
 
-#: core/settings.py:268 domain/templates/domain/domain.html:79
+#: core/settings.py:274 domain/templates/domain/domain.html:79
 msgid "CSV semicolon separated"
 msgstr ""
 
-#: core/settings.py:297 questions/constants.py:5
+#: core/settings.py:303 questions/constants.py:5
 msgid "Textarea"
 msgstr ""
 
-#: core/settings.py:298 questions/constants.py:6
+#: core/settings.py:304 questions/constants.py:6
 msgid "Yes/No"
 msgstr ""
 
-#: core/settings.py:299 questions/constants.py:7
+#: core/settings.py:305 questions/constants.py:7
 msgid "Checkboxes"
 msgstr ""
 
-#: core/settings.py:300 questions/constants.py:8
+#: core/settings.py:306 questions/constants.py:8
 msgid "Radio buttons"
 msgstr ""
 
-#: core/settings.py:301 questions/constants.py:9
+#: core/settings.py:307 questions/constants.py:9
 msgid "Select drop-down"
 msgstr ""
 
-#: core/settings.py:302 questions/constants.py:10
+#: core/settings.py:308 questions/constants.py:10
 msgid "Autocomplete"
 msgstr ""
 
-#: core/settings.py:303 questions/constants.py:11
+#: core/settings.py:309 questions/constants.py:11
 msgid "Range slider"
 msgstr ""
 
-#: core/settings.py:304 questions/constants.py:12
+#: core/settings.py:310 questions/constants.py:12
 msgid "Date picker"
 msgstr ""
 
-#: core/settings.py:305 questions/constants.py:13
+#: core/settings.py:311 questions/constants.py:13
 msgid "File upload"
 msgstr ""
 
@@ -1315,10 +1316,10 @@ msgstr ""
 msgid "Domain"
 msgstr ""
 
-#: core/templates/core/base_navigation.html:61 projects/forms.py:309
+#: core/templates/core/base_navigation.html:61 projects/forms.py:314
 #: projects/models/project.py:64
 #: projects/templates/projects/project_detail_views.html:11 views/apps.py:7
-#: views/models.py:123 views/templates/views/views.html:91
+#: views/models.py:125 views/templates/views/views.html:91
 #: views/templates/views/views_export.html:6 views/views.py:43
 msgid "Views"
 msgstr ""
@@ -1590,8 +1591,9 @@ msgid "%(foreign_model)s %(foreign_uri)s does not exist."
 msgstr ""
 
 #: management/templates/management/import.html:70
-#: projects/templates/projects/project_questions_buttons.html:15
-#: projects/templates/projects/project_questions_buttons.html:48
+#: projects/templates/projects/project_questions_buttons.html:19
+#: projects/templates/projects/project_questions_buttons.html:32
+#: projects/templates/projects/project_questions_buttons.html:58
 msgid "Back"
 msgstr ""
 
@@ -1913,8 +1915,8 @@ msgstr ""
 msgid "Overlays"
 msgstr ""
 
-#: overlays/models.py:11 projects/forms.py:176
-#: projects/models/continuation.py:18 projects/models/invite.py:22
+#: overlays/models.py:11 projects/forms.py:181
+#: projects/models/continuation.py:18 projects/models/invite.py:25
 #: projects/models/membership.py:27 projects/models/project.py:34
 #: projects/templates/projects/project_detail_invites.html:14
 #: projects/templates/projects/project_detail_memberships.html:19
@@ -1941,7 +1943,7 @@ msgstr ""
 msgid "The url_name for this overlay."
 msgstr ""
 
-#: overlays/models.py:26 projects/forms.py:321
+#: overlays/models.py:26 projects/forms.py:326
 #: projects/templates/projects/project_answers.html:13
 #: projects/templates/projects/project_view.html:28
 msgid "Current"
@@ -1990,79 +1992,79 @@ msgstr ""
 msgid "Guest"
 msgstr ""
 
-#: projects/forms.py:175
+#: projects/forms.py:180
 msgid "Username or e-mail"
 msgstr ""
 
-#: projects/forms.py:177
+#: projects/forms.py:182
 msgid "The username or e-mail of the new user."
 msgstr ""
 
-#: projects/forms.py:189
+#: projects/forms.py:194
 msgid "Add member silently"
 msgstr ""
 
-#: projects/forms.py:190
+#: projects/forms.py:195
 msgid ""
 "As site manager or admin, you can directly add users without notifying them "
 "via e-mail, when you check the following checkbox."
 msgstr ""
 
-#: projects/forms.py:203
+#: projects/forms.py:208 projects/serializers/v1/__init__.py:147
 msgid "The user is already a member of the project."
 msgstr ""
 
-#: projects/forms.py:215
+#: projects/forms.py:220
 msgid ""
 "A user with this username or e-mail was not found. Only registered users can "
 "be invited."
 msgstr ""
 
-#: projects/forms.py:219
+#: projects/forms.py:224
 msgid "Only existing users can be added silently."
 msgstr ""
 
-#: projects/forms.py:287 projects/forms.py:291
+#: projects/forms.py:292 projects/forms.py:296
 #, python-format
 msgid "Attach %s"
 msgstr ""
 
-#: projects/forms.py:297
+#: projects/forms.py:302
 msgid "Subject"
 msgstr ""
 
-#: projects/forms.py:298
+#: projects/forms.py:303
 msgid "Message"
 msgstr ""
 
-#: projects/forms.py:305
+#: projects/forms.py:310
 msgid "Answers"
 msgstr ""
 
-#: projects/forms.py:306
+#: projects/forms.py:311
 msgid "Attach the output of \"View answers\"."
 msgstr ""
 
-#: projects/forms.py:313
+#: projects/forms.py:318
 msgid "Files"
 msgstr ""
 
-#: projects/forms.py:320 projects/models/snapshot.py:33
+#: projects/forms.py:325 projects/models/snapshot.py:33
 #: projects/models/value.py:38
 #: projects/templates/projects/project_detail_snapshots.html:20
 #: projects/templates/projects/project_import.html:93
 msgid "Snapshot"
 msgstr ""
 
-#: projects/forms.py:324
+#: projects/forms.py:329
 msgid "Format"
 msgstr ""
 
-#: projects/forms.py:339 projects/forms.py:344
+#: projects/forms.py:344 projects/forms.py:349
 msgid "Recipients"
 msgstr ""
 
-#: projects/forms.py:345
+#: projects/forms.py:350
 msgid "Enter recipients line by line"
 msgstr ""
 
@@ -2105,7 +2107,7 @@ msgid "Files of this type cannot be imported."
 msgstr ""
 
 #: projects/models/continuation.py:13 projects/models/integration.py:16
-#: projects/models/invite.py:17 projects/models/issue.py:28
+#: projects/models/invite.py:20 projects/models/issue.py:28
 #: projects/models/membership.py:22 projects/models/project.py:70
 #: projects/models/snapshot.py:17 projects/models/value.py:32
 #: projects/templates/projects/project_questions_overview.html:5
@@ -2197,43 +2199,43 @@ msgstr ""
 msgid "Integration options"
 msgstr ""
 
-#: projects/models/invite.py:18
+#: projects/models/invite.py:21
 msgid "The project for this invite."
 msgstr ""
 
-#: projects/models/invite.py:23 projects/models/membership.py:28
+#: projects/models/invite.py:26 projects/models/membership.py:28
 msgid "The user for this membership."
 msgstr ""
 
-#: projects/models/invite.py:28
+#: projects/models/invite.py:31
 msgid "The e-mail for this membership."
 msgstr ""
 
-#: projects/models/invite.py:33
+#: projects/models/invite.py:36
 msgid "The role for this invite."
 msgstr ""
 
-#: projects/models/invite.py:37
+#: projects/models/invite.py:40
 msgid "Token"
 msgstr ""
 
-#: projects/models/invite.py:38
+#: projects/models/invite.py:41
 msgid "The token for this invite."
 msgstr ""
 
-#: projects/models/invite.py:41
+#: projects/models/invite.py:44
 msgid "Timestamp"
 msgstr ""
 
-#: projects/models/invite.py:42
+#: projects/models/invite.py:45
 msgid "The timestamp for this invite."
 msgstr ""
 
-#: projects/models/invite.py:47
+#: projects/models/invite.py:50
 msgid "Invite"
 msgstr ""
 
-#: projects/models/invite.py:48
+#: projects/models/invite.py:51
 #: projects/templates/projects/project_detail_invites.html:10
 msgid "Invites"
 msgstr ""
@@ -2560,6 +2562,18 @@ msgstr ""
 msgid "The secret for a GitLab webhook to close a task."
 msgstr ""
 
+#: projects/serializers/v1/__init__.py:152
+msgid "A user with that e-mail is already a member of the project."
+msgstr ""
+
+#: projects/serializers/v1/__init__.py:160
+msgid "Either user or e-mail needs to be provided."
+msgstr ""
+
+#: projects/serializers/v1/__init__.py:162
+msgid "User and e-mail are mutually exclusive."
+msgstr ""
+
 #: projects/serializers/v1/questionset.py:94
 msgid "item"
 msgstr ""
@@ -2983,7 +2997,7 @@ msgstr ""
 #: projects/templates/projects/project_detail_snapshots.html:22
 #: projects/templates/projects/project_import.html:34
 #: projects/templates/projects/project_import.html:104
-#: projects/templates/projects/site_projects.html:50
+#: projects/templates/projects/site_projects.html:65
 msgid "Created"
 msgstr ""
 
@@ -3010,7 +3024,7 @@ msgid ""
 "needed."
 msgstr ""
 
-#: projects/templates/projects/project_detail_views.html:19 views/models.py:122
+#: projects/templates/projects/project_detail_views.html:19 views/models.py:124
 #: views/templates/views/views.html:127
 msgid "View"
 msgstr ""
@@ -3142,17 +3156,18 @@ msgid ""
 "using the green button and remove them using the blue cross (&times;)."
 msgstr ""
 
-#: projects/templates/projects/project_questions_buttons.html:9
-#: projects/templates/projects/project_questions_buttons.html:52
-msgid "Skip"
+#: projects/templates/projects/project_questions_buttons.html:10
+#: projects/templates/projects/project_questions_buttons.html:41
+#: projects/templates/projects/project_questions_buttons.html:49
+msgid "Proceed"
 msgstr ""
 
-#: projects/templates/projects/project_questions_buttons.html:32
-#: projects/templates/projects/project_questions_buttons.html:40
-msgid "Proceed"
+#: projects/templates/projects/project_questions_buttons.html:12
+#: projects/templates/projects/project_questions_buttons.html:62
+msgid "Skip"
 msgstr ""
 
-#: projects/templates/projects/project_questions_buttons.html:34
+#: projects/templates/projects/project_questions_buttons.html:43
 msgid "Save and proceed"
 msgstr ""
 
@@ -3242,7 +3257,7 @@ msgid ""
 msgstr ""
 
 #: projects/templates/projects/project_questions_overview.html:14
-#: projects/templates/projects/site_projects.html:37
+#: projects/templates/projects/site_projects.html:52
 msgid "Back to my projects"
 msgstr ""
 
@@ -3303,7 +3318,7 @@ msgid "Navigation"
 msgstr ""
 
 #: projects/templates/projects/project_view.html:90
-#: projects/views/project.py:198
+#: projects/views/project.py:201
 msgid "Error"
 msgstr ""
 
@@ -3346,7 +3361,7 @@ msgid "Click on one of the links to join the projects."
 msgstr ""
 
 #: projects/templates/projects/projects.html:127
-#: projects/templates/projects/site_projects.html:51
+#: projects/templates/projects/site_projects.html:66
 msgid "Last changed"
 msgstr ""
 
@@ -3365,7 +3380,11 @@ msgstr ""
 msgid "Next"
 msgstr ""
 
-#: projects/templates/projects/site_projects.html:44
+#: projects/templates/projects/site_projects.html:32
+msgid "All catalogs"
+msgstr ""
+
+#: projects/templates/projects/site_projects.html:59
 #, python-format
 msgid "All projects on %(site)s"
 msgstr ""
@@ -3389,20 +3408,20 @@ msgstr ""
 msgid "Set"
 msgstr ""
 
-#: projects/validators.py:22 projects/viewsets.py:318
+#: projects/validators.py:22 projects/viewsets.py:350
 msgid "You reached the file quota for this project."
 msgstr ""
 
-#: projects/views/project.py:178
+#: projects/views/project.py:181
 msgid "Sorry, your invitation has been expired."
 msgstr ""
 
-#: projects/views/project.py:181
+#: projects/views/project.py:184
 #, python-format
 msgid "Sorry, but this invitation is for the user \"%s\"."
 msgstr ""
 
-#: projects/views/project.py:195
+#: projects/views/project.py:198
 msgid "Sorry, the invitation link is not valid."
 msgstr ""
 
@@ -3437,7 +3456,7 @@ msgstr ""
 #: questions/templates/questions/catalogs_modal_form_catalogs.html:196
 #: tasks/models.py:51 tasks/templates/tasks/tasks.html:144
 #: tasks/templates/tasks/tasks_modal_form_tasks.html:144
-#: tasks/templates/tasks/tasks_modal_form_tasks.html:212 views/models.py:51
+#: tasks/templates/tasks/tasks_modal_form_tasks.html:212 views/models.py:53
 #: views/templates/views/views.html:141
 #: views/templates/views/views_modal_form_views.html:143
 #: views/templates/views/views_modal_form_views.html:210
@@ -3448,7 +3467,7 @@ msgstr ""
 msgid "The sites this catalog belongs to (in a multi site setup)."
 msgstr ""
 
-#: questions/models/catalog.py:54 tasks/models.py:56 views/models.py:56
+#: questions/models/catalog.py:54 tasks/models.py:56 views/models.py:58
 msgid "Group"
 msgstr ""
 
@@ -3457,7 +3476,7 @@ msgid "The groups for which this catalog is active."
 msgstr ""
 
 #: questions/models/catalog.py:59 questions/models/questionset.py:77
-#: questions/models/section.py:55 tasks/models.py:61 views/models.py:66
+#: questions/models/section.py:55 tasks/models.py:61 views/models.py:68
 msgid "Title (primary)"
 msgstr ""
 
@@ -3466,7 +3485,7 @@ msgid "The title for this catalog in the primary language."
 msgstr ""
 
 #: questions/models/catalog.py:64 questions/models/questionset.py:82
-#: questions/models/section.py:60 tasks/models.py:66 views/models.py:71
+#: questions/models/section.py:60 tasks/models.py:66 views/models.py:73
 msgid "Title (secondary)"
 msgstr ""
 
@@ -3475,7 +3494,7 @@ msgid "The title for this catalog in the secondary language."
 msgstr ""
 
 #: questions/models/catalog.py:69 questions/models/questionset.py:87
-#: questions/models/section.py:65 tasks/models.py:71 views/models.py:76
+#: questions/models/section.py:65 tasks/models.py:71 views/models.py:78
 msgid "Title (tertiary)"
 msgstr ""
 
@@ -3484,7 +3503,7 @@ msgid "The title for this catalog in the tertiary language."
 msgstr ""
 
 #: questions/models/catalog.py:74 questions/models/questionset.py:92
-#: questions/models/section.py:70 tasks/models.py:76 views/models.py:81
+#: questions/models/section.py:70 tasks/models.py:76 views/models.py:83
 msgid "Title (quaternary)"
 msgstr ""
 
@@ -3493,7 +3512,7 @@ msgid "The title for this catalog in the quaternary language."
 msgstr ""
 
 #: questions/models/catalog.py:79 questions/models/questionset.py:97
-#: questions/models/section.py:75 tasks/models.py:81 views/models.py:86
+#: questions/models/section.py:75 tasks/models.py:81 views/models.py:88
 msgid "Title (quinary)"
 msgstr ""
 
@@ -3521,7 +3540,7 @@ msgstr ""
 msgid "The help text for this catalog in the quinary language."
 msgstr ""
 
-#: questions/models/catalog.py:109 tasks/models.py:136 views/models.py:116
+#: questions/models/catalog.py:109 tasks/models.py:136 views/models.py:118
 msgid "Available"
 msgstr ""
 
@@ -3531,7 +3550,7 @@ msgstr ""
 
 #: questions/models/catalog.py:116 tasks/models.py:46
 #: tasks/templates/tasks/tasks_modal_form_tasks.html:123
-#: tasks/templates/tasks/tasks_modal_form_tasks.html:187 views/models.py:46
+#: tasks/templates/tasks/tasks_modal_form_tasks.html:187 views/models.py:48
 #: views/templates/views/views_modal_form_views.html:122
 #: views/templates/views/views_modal_form_views.html:185
 msgid "Catalogs"
@@ -4630,90 +4649,90 @@ msgstr ""
 msgid "An empty list implies that the task can be used with every catalog."
 msgstr ""
 
-#: views/models.py:22
+#: views/models.py:24
 msgid "The Uniform Resource Identifier of this view (auto-generated)."
 msgstr ""
 
-#: views/models.py:27
+#: views/models.py:29
 msgid "The prefix for the URI of this view."
 msgstr ""
 
-#: views/models.py:32
+#: views/models.py:34
 msgid "The internal identifier of this view."
 msgstr ""
 
-#: views/models.py:37
+#: views/models.py:39
 msgid "Additional internal information about this view."
 msgstr ""
 
-#: views/models.py:42
+#: views/models.py:44
 msgid "Designates whether this view can be changed."
 msgstr ""
 
-#: views/models.py:47
+#: views/models.py:49
 msgid ""
 "The catalogs this view can be used with. An empty list implies that this "
 "view can be used with every catalog."
 msgstr ""
 
-#: views/models.py:52
+#: views/models.py:54
 msgid "The sites this view belongs to (in a multi site setup)."
 msgstr ""
 
-#: views/models.py:57
+#: views/models.py:59
 msgid "The groups for which this view is active."
 msgstr ""
 
-#: views/models.py:61 views/templates/views/views_export.html:35
+#: views/models.py:63 views/templates/views/views_export.html:35
 #: views/templates/views/views_modal_form_templates.html:11
 msgid "Template"
 msgstr ""
 
-#: views/models.py:62
+#: views/models.py:64
 msgid "The template for this view, written in Django template language."
 msgstr ""
 
-#: views/models.py:67
+#: views/models.py:69
 msgid "The title for this view in the primary language."
 msgstr ""
 
-#: views/models.py:72
+#: views/models.py:74
 msgid "The title for this view in the secondary language."
 msgstr ""
 
-#: views/models.py:77
+#: views/models.py:79
 msgid "The title for this view in the tertiary language."
 msgstr ""
 
-#: views/models.py:82
+#: views/models.py:84
 msgid "The title for this view in the quaternary language."
 msgstr ""
 
-#: views/models.py:87
+#: views/models.py:89
 msgid "The title for this view in the quinary language."
 msgstr ""
 
-#: views/models.py:92
+#: views/models.py:94
 msgid "The help text for this view in the primary language."
 msgstr ""
 
-#: views/models.py:97
+#: views/models.py:99
 msgid "The help text for this view in the secondary language."
 msgstr ""
 
-#: views/models.py:102
+#: views/models.py:104
 msgid "The help text for this view in the tertiary language."
 msgstr ""
 
-#: views/models.py:107
+#: views/models.py:109
 msgid "The help text for this view in the quaternary language."
 msgstr ""
 
-#: views/models.py:112
+#: views/models.py:114
 msgid "The help text for this view in the quinary language."
 msgstr ""
 
-#: views/models.py:117
+#: views/models.py:119
 msgid "Designates whether this view is generally available for projects."
 msgstr ""
 
diff --git a/rdmo/projects/filters.py b/rdmo/projects/filters.py
index 24a6cbfc98..843bc576fb 100644
--- a/rdmo/projects/filters.py
+++ b/rdmo/projects/filters.py
@@ -9,7 +9,7 @@ class ProjectFilter(FilterSet):
 
     class Meta:
         model = Project
-        fields = ('title', )
+        fields = ('title', 'catalog')
 
 
 class SnapshotFilterBackend(BaseFilterBackend):
diff --git a/rdmo/projects/forms.py b/rdmo/projects/forms.py
index 9bf14d2421..7ef1761f3a 100644
--- a/rdmo/projects/forms.py
+++ b/rdmo/projects/forms.py
@@ -18,7 +18,12 @@
 
 class CatalogChoiceField(forms.ModelChoiceField):
 
+    _unavailable_icon = ' (<span class="fa fa-eye-slash" aria-hidden="true"></span>)'
+
     def label_from_instance(self, obj):
+        if obj.available is False:
+            return mark_safe('<div class="text-muted">%s%s</br>%s</div>' % (obj.title, self._unavailable_icon, markdown2html(obj.help)))
+
         return mark_safe('<b>%s</b></br>%s' % (obj.title, markdown2html(obj.help)))
 
 
diff --git a/rdmo/projects/managers.py b/rdmo/projects/managers.py
index 92d0763773..95bd5c763d 100644
--- a/rdmo/projects/managers.py
+++ b/rdmo/projects/managers.py
@@ -84,6 +84,25 @@ def filter_user(self, user):
             return self.none()
 
 
+class InviteQuerySet(models.QuerySet):
+
+    def filter_current_site(self):
+        return self.filter(project__site=settings.SITE_ID)
+
+    def filter_user(self, user):
+        if user.is_authenticated:
+            if user.has_perm('projects.view_invite'):
+                return self.all()
+            elif is_site_manager(user):
+                return self.filter_current_site()
+            else:
+                from .models import Project
+                projects = Project.objects.filter(memberships__user=user, memberships__role='owner')
+                return self.filter(project__in=projects)
+        else:
+            return self.none()
+
+
 class SnapshotQuerySet(models.QuerySet):
 
     def filter_current_site(self):
@@ -161,6 +180,15 @@ def filter_user(self, user):
         return self.get_queryset().filter_user(user)
 
 
+class InviteManager(CurrentSiteManagerMixin, models.Manager):
+
+    def get_queryset(self):
+        return InviteQuerySet(self.model, using=self._db)
+
+    def filter_user(self, user):
+        return self.get_queryset().filter_user(user)
+
+
 class SnapshotManager(CurrentSiteManagerMixin, models.Manager):
 
     def get_queryset(self):
diff --git a/rdmo/projects/models/invite.py b/rdmo/projects/models/invite.py
index c689af2200..174868e580 100644
--- a/rdmo/projects/models/invite.py
+++ b/rdmo/projects/models/invite.py
@@ -6,12 +6,15 @@
 from django.utils.translation import gettext_lazy as _
 
 from ..constants import ROLE_CHOICES
+from ..managers import InviteManager
 
 
 class Invite(models.Model):
 
     key_salt = 'rdmo.projects.models.invite.Invite'
 
+    objects = InviteManager()
+
     project = models.ForeignKey(
         'Project', on_delete=models.CASCADE, related_name='invites',
         verbose_name=_('Project'),
diff --git a/rdmo/projects/serializers/v1/__init__.py b/rdmo/projects/serializers/v1/__init__.py
index 01445a6118..09a104cf17 100644
--- a/rdmo/projects/serializers/v1/__init__.py
+++ b/rdmo/projects/serializers/v1/__init__.py
@@ -1,11 +1,12 @@
 from django.conf import settings
 from django.contrib.auth import get_user_model
+from django.utils.translation import gettext_lazy as _
 from rest_framework import serializers
 
 from rdmo.services.validators import ProviderValidator
 
-from ...models import (Integration, IntegrationOption, Issue, IssueResource,
-                       Membership, Project, Snapshot, Value)
+from ...models import (Integration, IntegrationOption, Invite, Issue,
+                       IssueResource, Membership, Project, Snapshot, Value)
 from ...validators import ValueValidator
 
 
@@ -127,6 +128,65 @@ def update(self, integration, validated_data):
         return integration
 
 
+class ProjectInviteSerializer(serializers.ModelSerializer):
+
+    timestamp = serializers.DateTimeField(read_only=True)
+
+    class Meta:
+        model = Invite
+        fields = (
+            'id',
+            'user',
+            'email',
+            'role',
+            'timestamp'
+        )
+
+    def validate_user(self, value):
+        if self.context['view'].project.memberships.filter(user=value).exists():
+            raise serializers.ValidationError(_('The user is already a member of the project.'))
+        return value
+
+    def validate_email(self, value):
+        if self.context['view'].project.memberships.filter(user__email=value).exists():
+            raise serializers.ValidationError(_('A user with that e-mail is already a member of the project.'))
+        return value
+
+    def validate(self, data):
+        user = data.get('user')
+        email = data.get('email')
+
+        if not user and not email:
+            raise serializers.ValidationError(_('Either user or e-mail needs to be provided.'))
+        elif user and email:
+            raise serializers.ValidationError(_('User and e-mail are mutually exclusive.'))
+        elif user:
+            data['email'] = user.email
+        elif email:
+            usermodel = get_user_model()
+            try:
+                data['user'] = usermodel.objects.get(email=email)
+            except usermodel.DoesNotExist:
+                data['user'] = None
+
+        return data
+
+    def create(self, validated_data):
+        invite = super().create(validated_data)
+        invite.make_token()
+        invite.save()
+        return invite
+
+
+class ProjectInviteUpdateSerializer(serializers.ModelSerializer):
+
+    class Meta:
+        model = Invite
+        fields = (
+            'role',
+        )
+
+
 class ProjectIssueResourceSerializer(serializers.ModelSerializer):
 
     integration = serializers.PrimaryKeyRelatedField(read_only=True)
@@ -217,6 +277,20 @@ class Meta:
         )
 
 
+class InviteSerializer(serializers.ModelSerializer):
+
+    class Meta:
+        model = Invite
+        fields = (
+            'id',
+            'project',
+            'user',
+            'email',
+            'role',
+            'timestamp'
+        )
+
+
 class IssueResourceSerializer(serializers.ModelSerializer):
 
     integration = serializers.PrimaryKeyRelatedField(read_only=True)
diff --git a/rdmo/projects/static/projects/js/project_questions/services.js b/rdmo/projects/static/projects/js/project_questions/services.js
index a22a5a6ba3..a412382fd6 100644
--- a/rdmo/projects/static/projects/js/project_questions/services.js
+++ b/rdmo/projects/static/projects/js/project_questions/services.js
@@ -803,15 +803,28 @@ angular.module('project_questions')
     service.prev = function() {
         service.error = null; // reset error when moving to previous questionset
         if (service.questionset.prev !== null) {
-            back = true;
-            service.initView(service.questionset.prev);
+            if (service.settings.project_questions_autosave) {
+                service.save(false).then(function() {
+                    back = true;
+                    service.initView(service.questionset.prev);
+                })
+            } else {
+                back = true;
+                service.initView(service.questionset.prev);
+            }
         }
     };
 
     service.next = function() {
         service.error = null; // reset error when moving to next questionset
         if (service.questionset.id !== null) {
-            service.initView(service.questionset.next);
+            if (service.settings.project_questions_autosave) {
+                service.save(false).then(function() {
+                    service.initView(service.questionset.next);
+                })
+            } else {
+                service.initView(service.questionset.next);
+            }
         }
     };
 
diff --git a/rdmo/projects/templates/projects/project_questions_buttons.html b/rdmo/projects/templates/projects/project_questions_buttons.html
index 0616c07a4a..b8b680c4b8 100644
--- a/rdmo/projects/templates/projects/project_questions_buttons.html
+++ b/rdmo/projects/templates/projects/project_questions_buttons.html
@@ -6,7 +6,11 @@
         <div class="pull-right">
             <button type="button" ng-click="service.next()" ng-disabled="!service.questionset.next"
                     class="btn btn-default btn-xs">
+                {% if settings.PROJECT_QUESTIONS_AUTOSAVE %}
+                {% trans 'Proceed' %}
+                {% else %}
                 {% trans 'Skip' %}
+                {% endif %}
             </button>
         </div>
         <div>
@@ -22,7 +26,12 @@
     <div class="form-group questions-buttons">
         <div class="pull-right">
             <div ng-hide="service.project.read_only || service.questionset.elements.length < 1">
-                {% if not settings.PROJECT_QUESTIONS_AUTOSAVE %}
+                {% if settings.PROJECT_QUESTIONS_AUTOSAVE %}
+                <button type="button" ng-click="service.prev()" ng-disabled="!service.questionset.prev"
+                        class="btn btn-default">
+                    {% trans 'Back' %}
+                </button>
+                {% else %}
                 <button type="button" ng-click="service.save(false)" class="btn btn-default">
                     {% trans 'Save' %}
                 </button>
@@ -42,6 +51,7 @@
             </div>
         </div>
 
+        {% if not settings.PROJECT_QUESTIONS_AUTOSAVE %}
         <div>
             <button type="button" ng-click="service.prev()" ng-disabled="!service.questionset.prev"
                     class="btn btn-default">
@@ -52,6 +62,7 @@
                 {% trans 'Skip' %}
             </button>
         </div>
+        {% endif %}
     </div>
 
 {% endif %}
diff --git a/rdmo/projects/templates/projects/site_projects.html b/rdmo/projects/templates/projects/site_projects.html
index a8d65e83af..f2a8f49c23 100644
--- a/rdmo/projects/templates/projects/site_projects.html
+++ b/rdmo/projects/templates/projects/site_projects.html
@@ -15,7 +15,7 @@
 {% block sidebar %}
     <h2>{% trans 'Filter projects' %}</h2>
 
-    <form method="GET" class="projects-search">
+    <form method="GET" id="projects-search" class="projects-search">
         <input type="text" class="form-control" id="search" name="title" placeholder="{% trans 'Search project title' %}"
                value="{{request.GET.title }}" />
         <a href="{% url 'site_projects' %}" class="projects-search-reset">
@@ -28,7 +28,22 @@ <h2>{% trans 'Filter projects' %}</h2>
                 {% endblocktrans %}
             </small>
         </p>
+        <select name="catalog" id="catalog" class="form-control">
+            <option value="">{% trans 'All catalogs' %}</option>
+            {% for catalog in catalogs %}
+                <option value="{{ catalog.id }}" {% if catalog.id|stringformat:'s' == request.GET.catalog %}selected{% endif %}>
+                    {{ catalog.title }}
+                </option>
+            {% endfor %}
+        </select>
     </form>
+    <script>
+        $(document).ready(function() {
+            $('#catalog').change(function() {
+                $('#projects-search').trigger('submit');
+            });
+        });
+    </script>
 
     <h2>{% trans 'Options' %}</h2>
 
diff --git a/rdmo/projects/urls/v1.py b/rdmo/projects/urls/v1.py
index 198e1191d7..262e3930c6 100644
--- a/rdmo/projects/urls/v1.py
+++ b/rdmo/projects/urls/v1.py
@@ -1,8 +1,9 @@
 from django.urls import include, path
 from rest_framework_extensions.routers import ExtendedDefaultRouter
 
-from ..viewsets import (IntegrationViewSet, IssueViewSet, MembershipViewSet,
-                        ProjectIntegrationViewSet, ProjectIssueViewSet,
+from ..viewsets import (IntegrationViewSet, InviteViewSet, IssueViewSet,
+                        MembershipViewSet, ProjectIntegrationViewSet,
+                        ProjectInviteViewSet, ProjectIssueViewSet,
                         ProjectMembershipViewSet, ProjectQuestionSetViewSet,
                         ProjectSnapshotViewSet, ProjectValueViewSet,
                         ProjectViewSet, SnapshotViewSet, ValueViewSet)
@@ -15,6 +16,8 @@
                        parents_query_lookups=['project'])
 project_route.register(r'integrations', ProjectIntegrationViewSet, basename='project-integration',
                        parents_query_lookups=['project'])
+project_route.register(r'invites', ProjectInviteViewSet, basename='project-invite',
+                       parents_query_lookups=['project'])
 project_route.register(r'issues', ProjectIssueViewSet, basename='project-issue',
                        parents_query_lookups=['project'])
 project_route.register(r'snapshots', ProjectSnapshotViewSet, basename='project-snapshot',
@@ -25,6 +28,7 @@
                        parents_query_lookups=['project'])
 router.register(r'memberships', MembershipViewSet, basename='membership')
 router.register(r'integrations', IntegrationViewSet, basename='integration')
+router.register(r'invites', InviteViewSet, basename='invite')
 router.register(r'issues', IssueViewSet, basename='issue')
 router.register(r'snapshots', SnapshotViewSet, basename='snapshot')
 router.register(r'values', ValueViewSet, basename='value')
diff --git a/rdmo/projects/utils.py b/rdmo/projects/utils.py
index 9461e21d34..931052d0db 100644
--- a/rdmo/projects/utils.py
+++ b/rdmo/projects/utils.py
@@ -3,8 +3,11 @@
 
 from django.conf import settings
 from django.contrib.sites.models import Site
+from django.template.loader import render_to_string
 from django.urls import reverse
 
+from rdmo.core.mail import send_mail
+
 logger = logging.getLogger(__name__)
 
 
@@ -147,6 +150,23 @@ def get_invite_email_project_path(invite) -> str:
     return project_invite_path
 
 
+def send_invite_email(request, invite):
+    project_invite_path = get_invite_email_project_path(invite)
+    context = {
+        'invite_url': request.build_absolute_uri(project_invite_path),
+        'invite_user': invite.user,
+        'project': invite.project,
+        'user': request.user,
+        'site': Site.objects.get_current()
+    }
+
+    subject = render_to_string('projects/email/project_invite_subject.txt', context)
+    message = render_to_string('projects/email/project_invite_message.txt', context)
+
+    # send the email
+    send_mail(subject, message, to=[invite.email])
+
+
 def set_context_querystring_with_filter_and_page(context: dict) -> dict:
     '''prepares the filter part of the querystring for the next and previous hyperlinks in the pagination'''
     if context["filter"].data:
diff --git a/rdmo/projects/views/membership.py b/rdmo/projects/views/membership.py
index 4fd965ea80..19c76fff4a 100644
--- a/rdmo/projects/views/membership.py
+++ b/rdmo/projects/views/membership.py
@@ -1,21 +1,19 @@
 import logging
 
-from django.contrib.sites.models import Site
+from django.conf import settings
 from django.http import (HttpResponseBadRequest, HttpResponseForbidden,
                          HttpResponseRedirect)
 from django.shortcuts import get_object_or_404
-from django.template.loader import render_to_string
 from django.urls import reverse
 from django.views.generic import DeleteView, UpdateView
 from django.views.generic.edit import FormView
 
 from rdmo.accounts.utils import is_site_manager
-from rdmo.core.mail import send_mail
 from rdmo.core.views import ObjectPermissionMixin, RedirectViewMixin
 
 from ..forms import MembershipCreateForm
 from ..models import Membership, Project
-from ..utils import is_last_owner, get_invite_email_project_path
+from ..utils import is_last_owner, send_invite_email
 
 logger = logging.getLogger(__name__)
 
@@ -47,23 +45,8 @@ def get_success_url(self):
 
     def form_valid(self, form):
         invite = form.save()
-        if invite is not None:
-            
-            project_invite_path = get_invite_email_project_path(invite)
-            context = {
-                'invite_url': self.request.build_absolute_uri(project_invite_path),
-                'invite_user': invite.user,
-                'project': invite.project,
-                'user': self.request.user,
-                'site': Site.objects.get_current()
-            }
-
-            subject = render_to_string('projects/email/project_invite_subject.txt', context)
-            message = render_to_string('projects/email/project_invite_message.txt', context)
-
-            # send the email
-            send_mail(subject, message, to=[invite.email])
-
+        if invite is not None and settings.PROJECT_SEND_INVITE:
+            send_invite_email(self.request, invite)
         return super().form_valid(form)
 
 
diff --git a/rdmo/projects/views/project.py b/rdmo/projects/views/project.py
index 2b9f059850..16050259d0 100644
--- a/rdmo/projects/views/project.py
+++ b/rdmo/projects/views/project.py
@@ -105,6 +105,9 @@ def get_context_data(self, **kwargs):
         context['number_of_projects'] = self.get_queryset().count()
         context['number_of_filtered_projects'] = context["filter"].qs.count()
         context = set_context_querystring_with_filter_and_page(context)
+        context['catalogs'] = Catalog.objects.filter_current_site() \
+                                .filter_group(self.request.user) \
+                                .filter_availability(self.request.user)
         return context
 
 
diff --git a/rdmo/projects/views/project_create.py b/rdmo/projects/views/project_create.py
index aadc44cdb7..9e2f6e8137 100644
--- a/rdmo/projects/views/project_create.py
+++ b/rdmo/projects/views/project_create.py
@@ -24,7 +24,8 @@ class ProjectCreateView(LoginRequiredMixin, RedirectViewMixin, CreateView):
     def get_form_kwargs(self):
         catalogs = Catalog.objects.filter_current_site() \
                                   .filter_group(self.request.user) \
-                                  .filter_availability(self.request.user)
+                                  .filter_availability(self.request.user) \
+                                  .order_by('-available', 'order')
         projects = Project.objects.filter_user(self.request.user)
 
         form_kwargs = super().get_form_kwargs()
diff --git a/rdmo/projects/viewsets.py b/rdmo/projects/viewsets.py
index f0b3abc80f..0353c96408 100644
--- a/rdmo/projects/viewsets.py
+++ b/rdmo/projects/viewsets.py
@@ -4,7 +4,6 @@
 from django.http import Http404, HttpResponseRedirect
 from django.utils.translation import gettext_lazy as _
 from django_filters.rest_framework import DjangoFilterBackend
-
 from rest_framework import serializers
 from rest_framework.decorators import action
 from rest_framework.exceptions import NotFound
@@ -26,11 +25,13 @@
 from rdmo.views.models import View
 
 from .filters import SnapshotFilterBackend, ValueFilterBackend
-from .models import (Continuation, Integration, Issue, Membership, Project,
-                     Snapshot, Value)
-from .serializers.v1 import (IntegrationSerializer, IssueSerializer,
-                             MembershipSerializer,
+from .models import (Continuation, Integration, Invite, Issue, Membership,
+                     Project, Snapshot, Value)
+from .serializers.v1 import (IntegrationSerializer, InviteSerializer,
+                             IssueSerializer, MembershipSerializer,
                              ProjectIntegrationSerializer,
+                             ProjectInviteSerializer,
+                             ProjectInviteUpdateSerializer,
                              ProjectIssueSerializer,
                              ProjectMembershipSerializer,
                              ProjectMembershipUpdateSerializer,
@@ -39,7 +40,7 @@
                              ValueSerializer)
 from .serializers.v1.overview import ProjectOverviewSerializer
 from .serializers.v1.questionset import QuestionSetSerializer
-from .utils import check_conditions
+from .utils import check_conditions, send_invite_email
 
 
 class ProjectViewSet(ModelViewSet):
@@ -193,6 +194,7 @@ def get_detail_permission_object(self, obj):
         return self.project
 
     def perform_create(self, serializer):
+        # this call provides the nested serializers with the project
         serializer.save(project=self.project)
 
 
@@ -237,6 +239,36 @@ def get_queryset(self):
             return Integration.objects.none()
 
 
+class ProjectInviteViewSet(ProjectNestedViewSetMixin, ModelViewSet):
+    permission_classes = (HasModelPermission | HasObjectPermission, )
+
+    filter_backends = (DjangoFilterBackend, )
+    filterset_fields = (
+        'user',
+        'user__username',
+        'email',
+        'role'
+    )
+
+    def get_queryset(self):
+        try:
+            return Invite.objects.filter(project=self.project)
+        except AttributeError:
+            # this is needed for the swagger ui
+            return Invite.objects.none()
+
+    def get_serializer_class(self):
+        if self.action == 'update':
+            return ProjectInviteUpdateSerializer
+        else:
+            return ProjectInviteSerializer
+
+    def perform_create(self, serializer):
+        super().perform_create(serializer)
+        if settings.PROJECT_SEND_INVITE:
+            send_invite_email(self.request, serializer.instance)
+
+
 class ProjectIssueViewSet(ProjectNestedViewSetMixin, ListModelMixin, RetrieveModelMixin,
                           UpdateModelMixin, GenericViewSet):
     permission_classes = (HasModelPermission | HasObjectPermission, )
@@ -427,6 +459,25 @@ def get_detail_permission_object(self, obj):
         return obj.project
 
 
+class InviteViewSet(ReadOnlyModelViewSet):
+    permission_classes = (HasModelPermission | HasObjectPermission, )
+    serializer_class = InviteSerializer
+
+    filter_backends = (DjangoFilterBackend, )
+    filterset_fields = (
+        'user',
+        'user__username',
+        'email',
+        'role'
+    )
+
+    def get_queryset(self):
+        return Invite.objects.filter_user(self.request.user)
+
+    def get_detail_permission_object(self, obj):
+        return obj.project
+
+
 class IssueViewSet(ReadOnlyModelViewSet):
     permission_classes = (HasModelPermission | HasObjectPermission, )
     serializer_class = IssueSerializer
diff --git a/rdmo/tasks/templates/tasks/tasks_modal_form_tasks.html b/rdmo/tasks/templates/tasks/tasks_modal_form_tasks.html
index f7a3895858..893857b143 100644
--- a/rdmo/tasks/templates/tasks/tasks_modal_form_tasks.html
+++ b/rdmo/tasks/templates/tasks/tasks_modal_form_tasks.html
@@ -200,7 +200,7 @@ <h2 class="modal-title">
                                         data-model="service.values.groups"
                                         data-errors="service.errors.groups"
                                         data-options="service.groups"
-                                        data-options-label="uri"
+                                        data-options-label="name"
                                         data-help="{% trans 'Hold down Control, or Command on a Mac, to select more than one.' %}">
                                     </formgroup>
                                 </div>
@@ -213,7 +213,7 @@ <h2 class="modal-title">
                                         data-model="service.values.sites"
                                         data-errors="service.errors.sites"
                                         data-options="service.sites"
-                                        data-options-label="uri"
+                                        data-options-label="name"
                                         data-help="{% trans 'Hold down Control, or Command on a Mac, to select more than one.' %}">
                                     </formgroup>
                                 </div>
diff --git a/rdmo/views/models.py b/rdmo/views/models.py
index b78ddb2179..964cc19410 100644
--- a/rdmo/views/models.py
+++ b/rdmo/views/models.py
@@ -4,6 +4,8 @@
 from django.db import models
 from django.template import Context, Template
 from django.utils.translation import gettext_lazy as _
+
+from rdmo import __version__
 from rdmo.core.models import TranslationMixin
 from rdmo.core.utils import copy_model, get_pandoc_main_version, join_url
 from rdmo.questions.models import Catalog
@@ -154,11 +156,17 @@ def is_locked(self):
     def render(self, project, snapshot=None, export_format=None):
         # render the template to a html string
         # it is important not to use models here
+        site = Site.objects.get_current()
         project_wrapper = ProjectWrapper(project, snapshot)
         return Template(self.template).render(Context({
             'project': project_wrapper,
             'conditions': project_wrapper.conditions,
             'format': export_format,
+            'rdmo_version': __version__,
+            'site': {
+                'name': site.name,
+                'domain': site.domain
+            },
             'pandoc_version': get_pandoc_main_version()
         }))
 
diff --git a/rdmo/views/utils.py b/rdmo/views/utils.py
index 005065da2e..e670ec0825 100644
--- a/rdmo/views/utils.py
+++ b/rdmo/views/utils.py
@@ -29,6 +29,15 @@ def title(self):
     def description(self):
         return self._project.description
 
+    @property
+    def catalog(self):
+        return {
+            'id': self._catalog.id,
+            'uri': self._catalog.uri,
+            'title': self._catalog.title,
+            'help': self._catalog.help
+        }
+
     @property
     def created(self):
         return self._project.created
diff --git a/requirements.txt b/requirements.txt
index dbe6fbf275..aba711a7ea 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,7 +2,7 @@ coverage==5.5
 defusedcsv~=2.0.0
 defusedxml~=0.7.1
 Django~=3.2.14
-django-allauth~=0.51.0
+django-allauth~=0.53.1
 django-cleanup~=6.0.0
 django-compressor==4.0
 django-extensions~=3.2.0


Test patch:
diff --git a/rdmo/accounts/tests/test_views.py b/rdmo/accounts/tests/test_views.py
index ef06426c68..108a227359 100644
--- a/rdmo/accounts/tests/test_views.py
+++ b/rdmo/accounts/tests/test_views.py
@@ -8,7 +8,9 @@
 from django.urls import reverse
 from django.urls.exceptions import NoReverseMatch
 
-from pytest_django.asserts import assertTemplateUsed
+from pytest_django.asserts import assertTemplateUsed, assertRedirects, \
+                                    assertInHTML, assertContains, \
+                                    assertNotContains, assertURLEqual
 
 from rdmo.accounts.tests.utils import reload_app_urlconf_in_testcase
 
@@ -595,3 +597,119 @@ def test_token_post_for_anonymous(db, client, settings, test_setting):
     else:
         with pytest.raises(NoReverseMatch):
             reverse('account_token')
+
+
+@pytest.mark.parametrize('LOGIN_FORM', boolean_toggle)
+@pytest.mark.parametrize('username,password', users)
+def test_home_login_form(db, client, settings, LOGIN_FORM, username, password):
+    settings.LOGIN_FORM = LOGIN_FORM
+    reload_app_urlconf_in_testcase('accounts')
+    # Anonymous user lands on home
+    client.login(username='anonymous', password=None)
+    url = reverse('home')
+    response = client.get(url)
+    response.status_code == 200
+    if LOGIN_FORM:
+        assertContains(response, f'<form method="post" action="{reverse("account_login")}"')
+    else:
+        assertNotContains(response, f'<form method="post" action="{reverse("account_login")}"')
+
+
+@pytest.mark.parametrize('SHIBBOLETH', boolean_toggle)
+@pytest.mark.parametrize('SHIBBOLETH_LOGIN_URL', (None, '/shibboleth/login'))
+@pytest.mark.parametrize('username,password', users)
+def test_shibboleth_for_home_url(db, client, settings, SHIBBOLETH, SHIBBOLETH_LOGIN_URL, username, password):
+    settings.SHIBBOLETH = SHIBBOLETH
+    settings.SHIBBOLETH_LOGIN_URL  = SHIBBOLETH_LOGIN_URL
+    settings.ACCOUNT = False
+    reload_app_urlconf_in_testcase('accounts')
+    # Anonymous user lands on home
+    client.login(username='anonymous', password=None)
+    url = reverse('home')
+    response = client.get(url)
+
+    if SHIBBOLETH:
+        # Anyonymous user is redirected to login
+        response.status_code == 200
+
+        if SHIBBOLETH_LOGIN_URL:
+            assertContains(response, 'href="' + reverse('shibboleth_login'))
+        else:
+            assertContains(response, 'href="' + reverse('account_login'))
+
+
+@pytest.mark.parametrize('username,password', users)
+def test_shibboleth_login_view(db, client, settings, username, password):
+    settings.SHIBBOLETH = True
+    settings.SHIBBOLETH_LOGIN_URL  = '/shibboleth/login'
+    reload_app_urlconf_in_testcase('accounts')
+    # Anonymous user lands on home
+    client.login(username='anonymous', password=None)
+
+    if settings.SHIBBOLETH and settings.SHIBBOLETH_LOGIN_URL:
+        url = reverse('shibboleth_login')
+        response = client.get(url)
+
+        # Anyonymous user is redirected to login
+        assertRedirects(response,
+                        settings.SHIBBOLETH_LOGIN_URL + f'?target={response.request["PATH_INFO"]}',
+                        target_status_code=404)
+
+        # Redirected user logs in
+        if password:
+            client.login(username=username, password=password)
+            response = client.get(url)
+            assertRedirects(response, reverse('projects'))
+
+
+@pytest.mark.parametrize('SHIBBOLETH', boolean_toggle)
+@pytest.mark.parametrize('SHIBBOLETH_LOGIN_URL', (None, '/shibboleth/login'))
+@pytest.mark.parametrize('username,password', users)
+def test_shibboleth_for_projects_url(db, client, settings, SHIBBOLETH, SHIBBOLETH_LOGIN_URL, username, password):
+    settings.SHIBBOLETH = SHIBBOLETH
+    settings.SHIBBOLETH_LOGIN_URL  = SHIBBOLETH_LOGIN_URL
+    settings.ACCOUNT = False
+    reload_app_urlconf_in_testcase('accounts')
+    client.login(username='anonymous', password=None)
+    # Try to access projects
+    url = reverse('projects')
+    response = client.get(url)
+
+    if SHIBBOLETH and SHIBBOLETH_LOGIN_URL:
+        # Anyonymous user is redirected to login
+        response.status_code == 302
+
+        assertRedirects(response, reverse('account_login') + '?next=' + reverse('projects'))
+
+        response = client.get(response.url)
+        # Shibboleth login is shown
+        response.status_code == 200
+        assertContains(response, 'href="/account/shibboleth/login/">')
+
+        # Redirected user logs in 
+        client.login(username=username, password=password)
+        response = client.get(response)
+
+        if password:
+            # Logged in user lands on projects
+            response.status_code == 200
+        else:
+            # Anonymous user is redirected to shibboleth login
+            response.status_code == 404
+
+
+@pytest.mark.parametrize('SHIBBOLETH', boolean_toggle)
+@pytest.mark.parametrize('username,password', users)
+def test_shibboleth_logout_username_pattern(db, client, settings, SHIBBOLETH, username, password):
+    settings.SHIBBOLETH = SHIBBOLETH
+    settings.SHIBBOLETH_USERNAME_PATTERN  = username
+    reload_app_urlconf_in_testcase('accounts')
+
+    client.login(username=username, password=password)
+    if SHIBBOLETH:
+        url = reverse('shibboleth_logout')
+        response = client.get(url)
+        if password is not None:
+            assertURLEqual(response.url, reverse('account_logout') + f'?next={settings.SHIBBOLETH_LOGOUT_URL}')
+        else:
+            assertURLEqual(response.url, reverse('account_logout'))
diff --git a/rdmo/core/tests/test_models.py b/rdmo/core/tests/test_models.py
new file mode 100644
index 0000000000..50b27adcf8
--- /dev/null
+++ b/rdmo/core/tests/test_models.py
@@ -0,0 +1,63 @@
+import pytest
+
+from django.utils.translation import get_language
+
+from rdmo.core.models import TranslationMixin
+from rdmo.core.utils import get_languages
+
+boolean_toggle = (True, False)
+test_languages = ('en', 'de')
+
+test_lang_mapper = {
+    'en': {
+        'title': 'title_lang1',
+        'text': 'text_lang1'
+    },
+    'de': {
+        'title': 'title_lang2',
+        'text': 'text_lang2'
+    }
+}
+
+class TestTranslationModel(TranslationMixin):
+    
+    title_lang1 = 'title-1'
+    title_lang2 = 'title-2'
+
+    text_lang1 = 'text-1'
+    text_lang2 = 'text-2'
+
+
+
+@pytest.mark.parametrize('test_setting', boolean_toggle)
+@pytest.mark.parametrize('test_lang', test_languages)
+def test_translationmixin_trans(settings, test_setting, test_lang):
+    settings.REPLACE_MISSING_TRANSLATION = test_setting
+
+    settings.LANGUAGE_CODE = test_lang
+    instance = TestTranslationModel()
+    # trans should return the correct value
+    assert instance.trans('title') == getattr(instance, test_lang_mapper[test_lang]['title'])
+    assert instance.trans('text') == getattr(instance, test_lang_mapper[test_lang]['text'])
+    del instance
+
+@pytest.mark.parametrize('test_setting', boolean_toggle)
+@pytest.mark.parametrize('test_lang', test_languages)
+def test_translationmixin_trans_empty_field(settings, test_setting, test_lang):
+    settings.REPLACE_MISSING_TRANSLATION = test_setting
+    empty_lang = 'en'
+
+    settings.LANGUAGE_CODE = test_lang
+    instance = TestTranslationModel()
+    instance.title_lang1 = ''
+    instance.text_lang1 = ''
+
+    other_lang = 'de' if test_lang == 'en' else 'en'
+
+    if test_lang == empty_lang and settings.REPLACE_MISSING_TRANSLATION:
+        assert instance.trans('title') == getattr(instance, test_lang_mapper[other_lang]['title'])
+        assert instance.trans('text') == getattr(instance, test_lang_mapper[other_lang]['text'])
+    else:
+        assert instance.trans('title') == getattr(instance, test_lang_mapper[test_lang]['title'])
+        assert instance.trans('text') == getattr(instance, test_lang_mapper[test_lang]['text'])
+    del instance
diff --git a/rdmo/projects/tests/test_commands.py b/rdmo/projects/tests/test_commands.py
index f79bc47992..f698364f91 100644
--- a/rdmo/projects/tests/test_commands.py
+++ b/rdmo/projects/tests/test_commands.py
@@ -4,17 +4,21 @@
 from django.core.management import call_command
 from django.core.management.base import CommandError
 
+from rdmo.projects.models import Project
+
+projects_without_owner = [6, 7, 8, 9, 11]
+
 prune_assignments = [
-    ('guest', "['owner', 'guest', 'author', 'manager']", [6]),
-    ('author', "['owner', 'author', 'manager']", [6, 9]),
-    ('manager', "['owner', 'manager']", [6, 8, 9]),
-    ('owner', "['owner']", [6, 7, 8, 9])
+    ('guest', "['owner', 'guest', 'author', 'manager']", [6, 11]),
+    ('author', "['owner', 'author', 'manager']", [6, 9, 11]),
+    ('manager', "['owner', 'manager']", [6, 8, 9, 11]),
+    ('owner', "['owner']", projects_without_owner)
 ]
 
 def get_prune_output(projects, remove=False):
     project_output = ""
     for proj in projects:
-        project_output += "Prune Test (id=%i)\n" % proj
+        project_output += '%s (id=%s)\n' % (proj.title, proj.id)
         if remove:
             project_output += "...removing...OK\n"
     return project_output
@@ -32,31 +36,37 @@ def test_prune_projects_error(db, settings):
 @pytest.mark.parametrize('min_role,role_list,projects', prune_assignments)
 def test_prune_projects_output(db, settings, min_role, role_list, projects):
     stdout, stderr = io.StringIO(), io.StringIO()
-
+    
+    instances = Project.objects.filter(id__in=projects).all()
     call_command('prune_projects', '--min_role', min_role, stdout=stdout, stderr=stderr)
 
     assert stdout.getvalue() == \
-        "Found projects without %s:\n%s" % (role_list, get_prune_output(projects))
+        "Found projects without %s:\n%s" % (role_list, get_prune_output(instances))
     assert not stderr.getvalue()
 
 
 def test_prune_projects_output2(db, settings):
     stdout, stderr = io.StringIO(), io.StringIO()
 
+    instances = Project.objects.filter(id__in=projects_without_owner)
     call_command('prune_projects', stdout=stdout, stderr=stderr)
 
     assert stdout.getvalue() == \
-        "Found projects without ['owner']:\n%s" % (get_prune_output([6, 7, 8, 9]))
+        "Found projects without ['owner']:\n%s" % (get_prune_output(instances))
     assert not stderr.getvalue()
 
 
 def test_prune_projects_remove(db, settings):
     stdout, stderr = io.StringIO(), io.StringIO()
 
+    instances = list(Project.objects.filter(id__in=projects_without_owner).all()).copy()
+
     call_command('prune_projects', '--remove', stdout=stdout, stderr=stderr)
 
-    assert stdout.getvalue() == \
-        "Found projects without ['owner']:\n%s" % (get_prune_output([6, 7, 8, 9], True))
+    std_output = stdout.getvalue()
+    prune_output = "Found projects without ['owner']:\n%s" % (get_prune_output(instances, True))
+
+    assert std_output == prune_output
     assert not stderr.getvalue()
 
     stdout, stderr = io.StringIO(), io.StringIO()
diff --git a/rdmo/projects/tests/test_view_membership.py b/rdmo/projects/tests/test_view_membership.py
index c731a0f896..e9e38a0175 100644
--- a/rdmo/projects/tests/test_view_membership.py
+++ b/rdmo/projects/tests/test_view_membership.py
@@ -1,8 +1,9 @@
 import pytest
+
 from django.core import mail
 from django.urls import reverse
 
-from ..models import Invite, Membership
+from rdmo.projects.models import Invite, Membership
 
 users = (
     ('owner', 'owner'),
@@ -25,6 +26,8 @@
 
 membership_roles = ('owner', 'manager', 'author', 'guest')
 
+excluded_invite_ids = [1, 2]
+
 
 @pytest.mark.parametrize('username,password', users)
 @pytest.mark.parametrize('project_id', projects)
@@ -42,10 +45,13 @@ def test_membership_create_get(db, client, username, password, project_id):
         assert response.status_code == 302
 
 
+@pytest.mark.parametrize('PROJECT_SEND_INVITE', [False, True])
 @pytest.mark.parametrize('username,password', users)
 @pytest.mark.parametrize('project_id', projects)
 @pytest.mark.parametrize('membership_role', membership_roles)
-def test_membership_create_post(db, client, username, password, project_id, membership_role):
+def test_membership_create_post(db, client, settings,
+                                username, password, project_id, membership_role, PROJECT_SEND_INVITE):
+    settings.PROJECT_SEND_INVITE = PROJECT_SEND_INVITE
     client.login(username=username, password=password)
 
     url = reverse('membership_create', args=[project_id])
@@ -57,23 +63,28 @@ def test_membership_create_post(db, client, username, password, project_id, memb
 
     if project_id in add_membership_permission_map.get(username, []):
         assert response.status_code == 302
-        assert Invite.objects.get(project_id=project_id, user__username='user', email='user@example.com', role=membership_role)
-        assert not Membership.objects.filter(project_id=project_id, user__username='user', role=membership_role).exists()
-        assert len(mail.outbox) == 1
+        assert Invite.objects.get(project_id=project_id, user__username='user',
+                                  email='user@example.com', role=membership_role)
+        assert not Membership.objects.filter(project_id=project_id,
+                                             user__username='user', role=membership_role).exists()
+        assert len(mail.outbox) == int(PROJECT_SEND_INVITE)
     else:
         if password:
             assert response.status_code == 403
         else:
             assert response.status_code == 302
 
-        assert not Invite.objects.exists()
+        assert not Invite.objects.exclude(id__in=excluded_invite_ids).exists()
         assert len(mail.outbox) == 0
 
 
+@pytest.mark.parametrize('PROJECT_SEND_INVITE', [False, True])
 @pytest.mark.parametrize('username,password', users)
 @pytest.mark.parametrize('project_id', projects)
 @pytest.mark.parametrize('membership_role', membership_roles)
-def test_membership_create_post_mail(db, client, username, password, project_id, membership_role):
+def test_membership_create_post_mail(db, client, settings,
+                                     username, password, project_id, membership_role, PROJECT_SEND_INVITE):
+    settings.PROJECT_SEND_INVITE = PROJECT_SEND_INVITE
     client.login(username=username, password=password)
 
     url = reverse('membership_create', args=[project_id])
@@ -84,16 +95,20 @@ def test_membership_create_post_mail(db, client, username, password, project_id,
     response = client.post(url, data)
 
     if project_id in add_membership_permission_map.get(username, []):
-        assert response.status_code == 302
-        assert Invite.objects.get(project_id=project_id, user=None, email='someuser@example.com', role=membership_role)
-        assert len(mail.outbox) == 1
+        if not PROJECT_SEND_INVITE:
+            assert response.status_code == 200
+            assert not Invite.objects.filter(project_id=project_id, user=None, email='someuser@example.com', role=membership_role)
+        else:
+            assert response.status_code == 302
+            assert Invite.objects.get(project_id=project_id, user=None, email='someuser@example.com', role=membership_role)
+        assert len(mail.outbox) == int(PROJECT_SEND_INVITE)
     else:
         if password:
             assert response.status_code == 403
         else:
             assert response.status_code == 302
 
-        assert not Invite.objects.exists()
+        assert not Invite.objects.exclude(id__in=excluded_invite_ids).exists()
         assert len(mail.outbox) == 0
 
 
@@ -110,7 +125,7 @@ def test_membership_create_post_error(db, client, username, password, membership
     }
     response = client.post(url, data)
 
-    assert not Invite.objects.exists(), Invite.objects.all()
+    assert not Invite.objects.exclude(id__in=excluded_invite_ids).exists()
     assert len(mail.outbox) == 0
 
     if project_id in add_membership_permission_map.get(username, []):
@@ -135,7 +150,7 @@ def test_membership_create_post_mail_error(db, client, username, password, membe
     }
     response = client.post(url, data)
 
-    assert not Invite.objects.exists(), Invite.objects.all()
+    assert not Invite.objects.exclude(id__in=excluded_invite_ids).exists()
     assert len(mail.outbox) == 0
 
     if project_id in add_membership_permission_map.get(username, []):
@@ -165,7 +180,7 @@ def test_membership_create_post_silent(db, client, username, password, membershi
         assert response.status_code == 302
 
         if username == 'site':
-            assert not Invite.objects.exists()
+            assert not Invite.objects.exclude(id__in=excluded_invite_ids).exists()
             assert Membership.objects.get(project_id=project_id, user__username='user', role=membership_role)
             assert len(mail.outbox) == 0
         else:
diff --git a/rdmo/projects/tests/test_view_project.py b/rdmo/projects/tests/test_view_project.py
index 1c48b5d985..acf0cd896a 100644
--- a/rdmo/projects/tests/test_view_project.py
+++ b/rdmo/projects/tests/test_view_project.py
@@ -4,8 +4,11 @@
 from django.urls import reverse
 from pytest_django.asserts import assertTemplateUsed
 
+from rdmo.questions.models import Catalog
+from rdmo.questions.tests.test_models import unavailable_catalogs_kwargs
 from rdmo.views.models import View
 
+from ..forms import CatalogChoiceField
 from ..models import Project
 
 users = (
@@ -16,6 +19,9 @@
     ('user', 'user'),
     ('site', 'site'),
     ('anonymous', None),
+    ('editor', 'editor'),
+    ('reviewer', 'reviewer'),
+    ('api', 'api'),
 )
 
 view_project_permission_map = {
@@ -23,28 +29,25 @@
     'manager': [1, 3, 5, 7],
     'author': [1, 3, 5, 8],
     'guest': [1, 3, 5, 9],
-    'api': [1, 2, 3, 4, 5],
-    'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+    'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+    'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
 }
 
 change_project_permission_map = {
     'owner': [1, 2, 3, 4, 5, 10],
     'manager': [1, 3, 5, 7],
-    'api': [1, 2, 3, 4, 5],
     'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 }
 
 delete_project_permission_map = {
     'owner': [1, 2, 3, 4, 5, 10],
-    'api': [1, 2, 3, 4, 5],
-    'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+    'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
 }
 
 export_project_permission_map = {
     'owner': [1, 2, 3, 4, 5, 10],
     'manager': [1, 3, 5, 7],
-    'api': [1, 2, 3, 4, 5],
-    'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+    'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
 }
 
 projects = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
@@ -69,11 +72,10 @@ def test_list(db, client, username, password):
         assert response.status_code == 200
         assertTemplateUsed(response, 'projects/projects.html')
 
-        if username == 'site':
+        if username in ('site', 'api'):
             assert projects == []
             assert response.context['number_of_projects'] == len([])
         else:
-            # breakpoint()
             user_projects_map = view_project_permission_map.get(username, [])
             assert sorted(list(set(map(int, projects)))) == user_projects_map
             assert response.context['number_of_projects'] == len(user_projects_map)
@@ -113,7 +115,10 @@ def test_detail(db, client, username, password, project_id):
     response = client.get(url)
 
     if project_id in view_project_permission_map.get(username, []):
-        assert response.status_code == 200
+        if username == 'api':
+            assert response.status_code == 403
+        else:
+            assert response.status_code == 200
     else:
         if password:
             assert response.status_code == 403
@@ -138,6 +143,38 @@ def test_project_create_get(db, client, username, password):
         assert response.status_code == 302
 
 
+@pytest.mark.parametrize('username,password', users)
+def test_project_create_get_for_extra_users_and_unavailable_catalogs(db, client, username, password):
+
+    for catalog_kwargs in unavailable_catalogs_kwargs:
+        Catalog.objects.create(**catalog_kwargs)
+
+    client.login(username=username, password=password)
+
+    url = reverse('project_create')
+    response = client.get(url)
+
+    if password:
+        assert response.status_code == 200
+        # check the catalogs that are displayed in the form
+        find_catalog_ids = re.findall(r'id="id_catalog_(\d+)', response.content.decode())
+        catalogs_in_form = response.context_data['form'].fields['catalog'].queryset
+        # Catalog.objects.filter_availability(response.context['user'])
+        assert find_catalog_ids == list(map(str, range(catalogs_in_form.count())))
+
+        # check the unavailable catalogs that are displayed in the form
+        # should happen only for users that can see unavailable catalogs (editors, api,..)
+        for unavailable_catalog in catalogs_in_form.filter(available=False):
+            _label = unavailable_catalog.title + CatalogChoiceField._unavailable_icon
+            assert _label in response.content.decode()
+
+        # check the parent select dropdown
+        for project_id in re.findall(r'<option value="(\d+)"', response.content.decode()):
+            assert int(project_id) in view_project_permission_map.get(username, [])
+    else:
+        assert response.status_code == 302
+
+
 @pytest.mark.parametrize('username,password', users)
 def test_project_create_post(db, client, username, password):
     client.login(username=username, password=password)
@@ -173,7 +210,7 @@ def test_project_create_parent_post(db, client, username, password):
     }
     response = client.post(url, data)
 
-    if username == 'user':
+    if username in ('user', 'editor', 'reviewer'):
         assert response.status_code == 200
         assert Project.objects.count() == project_count
     elif password:
@@ -565,7 +602,10 @@ def test_project_answers(db, client, username, password, project_id):
     response = client.get(url)
 
     if project_id in view_project_permission_map.get(username, []):
-        assert response.status_code == 200
+        if username == 'api':
+            assert response.status_code == 403
+        else:
+            assert response.status_code == 200
     else:
         if password:
             assert response.status_code == 403
@@ -583,7 +623,10 @@ def test_project_answers_export(db, client, username, password, project_id, expo
     response = client.get(url)
 
     if project_id in view_project_permission_map.get(username, []):
-        assert response.status_code == 200
+        if username == 'api':
+            assert response.status_code == 403
+        else:
+            assert response.status_code == 200
     else:
         if password:
             assert response.status_code == 403
@@ -602,6 +645,9 @@ def test_project_view(db, client, username, password, project_id):
         response = client.get(url)
 
         if project_id in view_project_permission_map.get(username, []):
+            if username == 'api':
+                assert response.status_code == 403
+                return
             if view in project_views:
                 assert response.status_code == 200
             else:
@@ -625,6 +671,9 @@ def test_project_view_export(db, client, username, password, project_id, export_
         response = client.get(url)
 
         if project_id in view_project_permission_map.get(username, []):
+            if username == 'api':
+                assert response.status_code == 403
+                return
             if view in project_views:
                 assert response.status_code == 200
             else:
@@ -645,6 +694,9 @@ def test_project_questions(db, client, username, password, project_id):
     response = client.get(url)
 
     if project_id in view_project_permission_map.get(username, []):
+        if username == 'api':
+            assert response.status_code == 403
+            return
         assert response.status_code == 200
     elif password:
         assert response.status_code == 403
@@ -661,7 +713,10 @@ def test_project_error(db, client, username, password, project_id):
     response = client.get(url)
 
     if project_id in view_project_permission_map.get(username, []):
-        assert response.status_code == 200
+        if username == 'api':
+            assert response.status_code == 403
+        else:
+            assert response.status_code == 200
     elif password:
         assert response.status_code == 403
     else:
diff --git a/rdmo/projects/tests/test_viewset_invite.py b/rdmo/projects/tests/test_viewset_invite.py
new file mode 100644
index 0000000000..ffa24601f8
--- /dev/null
+++ b/rdmo/projects/tests/test_viewset_invite.py
@@ -0,0 +1,113 @@
+import pytest
+from django.urls import reverse
+
+from ..models import Invite
+
+users = (
+    ('owner', 'owner'),
+    ('manager', 'manager'),
+    ('author', 'author'),
+    ('guest', 'guest'),
+    ('api', 'api'),
+    ('user', 'user'),
+    ('site', 'site'),
+    ('anonymous', None),
+)
+
+view_issue_permission_map = {
+    'owner': [1],
+    'manager': [],
+    'author': [],
+    'guest': [],
+    'api': [1, 11],
+    'site': [1, 11]
+}
+
+urlnames = {
+    'list': 'v1-projects:invite-list',
+    'detail': 'v1-projects:invite-detail'
+}
+
+invites = [1, 2]
+
+
+@pytest.mark.parametrize('username,password', users)
+def test_list(db, client, username, password):
+    client.login(username=username, password=password)
+
+    url = reverse(urlnames['list'])
+    response = client.get(url)
+
+    if password:
+        assert response.status_code == 200
+        assert isinstance(response.json(), list)
+
+        if username == 'user':
+            assert sorted([item['id'] for item in response.json()]) == []
+        else:
+            values_list = Invite.objects.filter(project__in=view_issue_permission_map.get(username, [])) \
+                                        .order_by('id').values_list('id', flat=True)
+            assert sorted([item['id'] for item in response.json()]) == list(values_list)
+    else:
+        assert response.status_code == 401
+
+
+@pytest.mark.parametrize('username,password', users)
+@pytest.mark.parametrize('invite_id', invites)
+def test_detail(db, client, username, password, invite_id):
+    client.login(username=username, password=password)
+    invite = Invite.objects.get(id=invite_id)
+
+    url = reverse(urlnames['detail'], args=[invite_id])
+    response = client.get(url)
+
+    if invite.project.id in view_issue_permission_map.get(username, []):
+        assert response.status_code == 200
+        assert isinstance(response.json(), dict)
+        assert response.json().get('id') == invite_id
+    elif password:
+        assert response.status_code == 404
+    else:
+        assert response.status_code == 401
+
+
+@pytest.mark.parametrize('username,password', users)
+def test_create(db, client, username, password):
+    client.login(username=username, password=password)
+
+    url = reverse(urlnames['list'])
+    response = client.post(url)
+
+    if password:
+        assert response.status_code == 405
+    else:
+        assert response.status_code == 401
+
+
+@pytest.mark.parametrize('username,password', users)
+@pytest.mark.parametrize('invite_id', invites)
+def test_update(db, client, username, password, invite_id):
+    client.login(username=username, password=password)
+
+    url = reverse(urlnames['detail'], args=[invite_id])
+    data = {}
+    response = client.put(url, data, content_type='application/json')
+
+    if password:
+        assert response.status_code == 405
+    else:
+        assert response.status_code == 401
+
+
+@pytest.mark.parametrize('username,password', users)
+@pytest.mark.parametrize('invite_id', invites)
+def test_delete(db, client, username, password, invite_id):
+    client.login(username=username, password=password)
+
+    url = reverse(urlnames['detail'], args=[invite_id])
+    response = client.delete(url)
+
+    if password:
+        assert response.status_code == 405
+    else:
+        assert response.status_code == 401
diff --git a/rdmo/projects/tests/test_viewset_project.py b/rdmo/projects/tests/test_viewset_project.py
index 104b157fcb..5fd369f449 100644
--- a/rdmo/projects/tests/test_viewset_project.py
+++ b/rdmo/projects/tests/test_viewset_project.py
@@ -19,8 +19,8 @@
     'manager': [1, 3, 5, 7],
     'author': [1, 3, 5, 8],
     'guest': [1, 3, 5, 9],
-    'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
-    'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+    'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+    'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
 }
 
 change_project_permission_map = {
diff --git a/rdmo/projects/tests/test_viewset_project_invite.py b/rdmo/projects/tests/test_viewset_project_invite.py
new file mode 100644
index 0000000000..f0d11b958f
--- /dev/null
+++ b/rdmo/projects/tests/test_viewset_project_invite.py
@@ -0,0 +1,198 @@
+import pytest
+from django.contrib.auth import get_user_model
+from django.urls import reverse
+
+from ..models import Invite
+
+users = (
+    ('owner', 'owner'),
+    ('manager', 'manager'),
+    ('author', 'author'),
+    ('guest', 'guest'),
+    ('api', 'api'),
+    ('user', 'user'),
+    ('site', 'site'),
+    ('anonymous', None),
+)
+
+view_invite_permission_map = {
+    'owner': [1],
+    'manager': [],
+    'author': [],
+    'guest': [],
+    'api': [1, 11],
+    'site': [1, 11]
+}
+
+add_invite_permission_map = change_invite_permission_map = delete_invite_permission_map = {
+    'owner': [1],
+    'api': [1, 11],
+    'site': [1, 11]
+}
+
+urlnames = {
+    'list': 'v1-projects:project-invite-list',
+    'detail': 'v1-projects:project-invite-detail'
+}
+
+projects = [1, 11]
+invites = [1, 2]
+
+
+@pytest.mark.parametrize('username,password', users)
+@pytest.mark.parametrize('project_id', projects)
+def test_list(db, client, username, password, project_id):
+    client.login(username=username, password=password)
+
+    url = reverse(urlnames['list'], args=[project_id])
+    response = client.get(url)
+
+    if project_id in view_invite_permission_map.get(username, []):
+        assert response.status_code == 200
+
+        if username == 'user':
+            assert sorted([item['id'] for item in response.json()]) == []
+        else:
+            values_list = Invite.objects.filter(project_id=project_id) \
+                                        .order_by('id').values_list('id', flat=True)
+            assert sorted([item['id'] for item in response.json()]) == list(values_list)
+    else:
+        assert response.status_code == 404
+
+
+@pytest.mark.parametrize('username,password', users)
+@pytest.mark.parametrize('project_id', projects)
+@pytest.mark.parametrize('invite_id', invites)
+def test_detail(db, client, username, password, project_id, invite_id):
+    client.login(username=username, password=password)
+    invite = Invite.objects.filter(project_id=project_id, id=invite_id).first()
+
+    url = reverse(urlnames['detail'], args=[project_id, invite_id])
+    response = client.get(url)
+
+    if invite and project_id in view_invite_permission_map.get(username, []):
+        assert response.status_code == 200
+        assert isinstance(response.json(), dict)
+        assert response.json().get('id') == invite_id
+    else:
+        assert response.status_code == 404
+
+
+@pytest.mark.parametrize('username,password', users)
+@pytest.mark.parametrize('project_id', projects)
+def test_create_user(db, client, username, password, project_id):
+    client.login(username=username, password=password)
+
+    user = get_user_model().objects.get(username='user')
+
+    url = reverse(urlnames['list'], args=[project_id])
+    data = {
+        'user': user.pk,
+        'role': 'guest'
+    }
+    response = client.post(url, data)
+
+    if project_id in add_invite_permission_map.get(username, []):
+        assert response.status_code == 201
+    elif project_id in view_invite_permission_map.get(username, []):
+        assert response.status_code == 403
+    else:
+        assert response.status_code == 404
+
+
+@pytest.mark.parametrize('username,password', users)
+@pytest.mark.parametrize('project_id', projects)
+def test_create_email(db, client, username, password, project_id):
+    client.login(username=username, password=password)
+
+    url = reverse(urlnames['list'], args=[project_id])
+    data = {
+        'email': 'user@example.com',
+        'role': 'guest'
+    }
+    response = client.post(url, data)
+
+    if project_id in add_invite_permission_map.get(username, []):
+        assert response.status_code == 201
+    elif project_id in view_invite_permission_map.get(username, []):
+        assert response.status_code == 403
+    else:
+        assert response.status_code == 404
+
+
+def test_create_error(db, client):
+    client.login(username='owner', password='owner')
+
+    url = reverse(urlnames['list'], args=[1])
+    data = {
+        'role': 'guest'
+    }
+    response = client.post(url, data)
+    assert response.status_code == 400
+    assert response.json()['non_field_errors'][0] == 'Either user or e-mail needs to be provided.'
+
+
+def test_create_error_user(db, client):
+    client.login(username='owner', password='owner')
+
+    url = reverse(urlnames['list'], args=[1])
+    data = {
+        'user': 800,
+        'role': 'guest'
+    }
+    response = client.post(url, data)
+    assert response.status_code == 400
+    assert 'Invalid pk' in response.json()['user'][0]
+
+
+def test_create_error_user_and_email(db, client):
+    client.login(username='owner', password='owner')
+
+    url = reverse(urlnames['list'], args=[1])
+    data = {
+        'user': 1,
+        'email': 'user@example.com',
+        'role': 'guest'
+    }
+    response = client.post(url, data)
+    assert response.status_code == 400
+    assert response.json()['non_field_errors'][0] == 'User and e-mail are mutually exclusive.'
+
+
+@pytest.mark.parametrize('username,password', users)
+@pytest.mark.parametrize('project_id', projects)
+@pytest.mark.parametrize('invite_id', invites)
+def test_update(db, client, username, password, project_id, invite_id):
+    client.login(username=username, password=password)
+    invite = Invite.objects.filter(project_id=project_id, id=invite_id).first()
+
+    url = reverse(urlnames['detail'], args=[project_id, invite_id])
+    data = {
+        'role': 'guest'
+    }
+    response = client.put(url, data, content_type='application/json')
+
+    if invite and project_id in change_invite_permission_map.get(username, []):
+        assert response.status_code == 200
+    elif invite and project_id in view_invite_permission_map.get(username, []):
+        assert response.status_code == 403
+    else:
+        assert response.status_code == 404
+
+
+@pytest.mark.parametrize('username,password', users)
+@pytest.mark.parametrize('project_id', projects)
+@pytest.mark.parametrize('invite_id', invites)
+def test_delete(db, client, username, password, project_id, invite_id):
+    client.login(username=username, password=password)
+    invite = Invite.objects.filter(project_id=project_id, id=invite_id).first()
+
+    url = reverse(urlnames['detail'], args=[project_id, invite_id])
+    response = client.delete(url)
+
+    if invite and project_id in delete_invite_permission_map.get(username, []):
+        assert response.status_code == 204
+    elif invite and project_id in view_invite_permission_map.get(username, []):
+        assert response.status_code == 403
+    else:
+        assert response.status_code == 404
diff --git a/rdmo/questions/tests/test_models.py b/rdmo/questions/tests/test_models.py
index 0a15480ea1..2259541623 100644
--- a/rdmo/questions/tests/test_models.py
+++ b/rdmo/questions/tests/test_models.py
@@ -1,6 +1,32 @@
 from ..models import Catalog, Question, QuestionSet, Section
 
 
+unavailable_catalogs_kwargs = [
+    {
+        'uri': 'https://experimental.com/terms/questions/unavailable-1',
+        'uri_prefix': 'https://experimental.com/terms',
+        'key': 'unavailable-1',
+        'order': 1,
+        'title_lang1': 'New Catalog 1',
+        'title_lang2': 'Neuer Katalog 1',
+        'help_lang1': 'Help text',
+        'help_lang2': 'Hilfe Text',
+        'available': False
+    },
+    {
+        'uri': 'https://experimental.com/terms/questions/unavailable-2',
+        'uri_prefix': 'https://experimental.com/terms',
+        'key': 'unavailable-2',
+        'order': 2,
+        'title_lang1': 'New Catalog 2',
+        'title_lang2': 'Neuer Katalog 2',
+        'help_lang1': 'Help text',
+        'help_lang2': 'Hilfe Text',
+        'available': False
+    },
+]
+
+
 def test_catalog_str(db):
     instances = Catalog.objects.all()
     for instance in instances:
diff --git a/testing/fixtures/projects.json b/testing/fixtures/projects.json
index 5198e8c6d1..4085eee012 100644
--- a/testing/fixtures/projects.json
+++ b/testing/fixtures/projects.json
@@ -475,6 +475,28 @@
       ]
     }
   },
+  {
+    "model": "projects.project",
+    "pk": 11,
+    "fields": {
+      "created": "2023-07-10T14:38:32.052Z",
+      "updated": "2023-07-10T14:38:32.052Z",
+      "parent": null,
+      "site": 1,
+      "title": "Other",
+      "description": "",
+      "catalog": 1,
+      "lft": 1,
+      "rght": 2,
+      "tree_id": 2,
+      "level": 0,
+      "views": [
+        1,
+        2,
+        3
+      ]
+    }
+  },
   {
     "model": "projects.snapshot",
     "pk": 1,
@@ -5720,5 +5742,29 @@
       "unit": "",
       "external_id": ""
     }
+  },
+  {
+    "model": "projects.invite",
+    "pk": 1,
+    "fields": {
+      "project": 1,
+      "user": 11,
+      "email": "other@example.com",
+      "role": "author",
+      "token": "10159fadb66e97365a36",
+      "timestamp": "2023-07-10T14:06:55.112Z"
+    }
+  },
+  {
+    "model": "projects.invite",
+    "pk": 2,
+    "fields": {
+      "project": 11,
+      "user": 4,
+      "email": "user@example.com",
+      "role": "author",
+      "token": "10159fadb66e97365a36",
+      "timestamp": "2023-07-10T14:06:55.112Z"
+    }
   }
 ]
\ No newline at end of file