diff --git a/CHANGELOG b/CHANGELOG index 1dffe8e4a..af14221db 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2203,7 +2203,7 @@ On both docker and non-docker setup, you'll also have to update your nginx configuration for websocket support. Ensure you have the following blocks included in your virtualhost file: -.. code-block:: txt +.. code-block:: text map $http_upgrade $connection_upgrade { default upgrade; diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 19f034b9f..53e99fa55 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -353,9 +353,339 @@ Internationalization -------------------- We're using https://github.com/Polyconseil/vue-gettext to manage i18n in the project. +<<<<<<< HEAD When working on the front-end, any end-user string should be translated using either ``yourstring`` or ``$gettext('yourstring')`` function. +||||||| parent of 21fb39dd... Update docs/developers/index.rst, docs/developers/subsonic.rst files +When working on the front-end, any end-user string should be marked as a translatable string, +with the proper context, as described below. + +Translations in HTML +^^^^^^^^^^^^^^^^^^^^ + +Translations in HTML use the ```` tag:: + + + +Anything between the `` and `` delimiters will be considered as a translatable string. +You can use variables in the translated string via the ``:translate-params="{var: 'value'}"`` directive, and reference them like this: +``val value is %{ value }``. + +For pluralization, you need to use ``translate-params`` in conjunction with ``translate-plural`` and ``translate-n``: + +- ``translate-params`` should contain the variable you're using for pluralization (which is usually shown to the user) +- ``translate-n`` should match the same variable +- The ```` delimiters contain the non-pluralized version of your string +- The ``translate-plural`` directive contains the pluralized version of your string + + +Translations in javascript +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Translations in javascript work by calling the ``this.$*gettext`` functions:: + + export default { + computed: { + strings () { + let tracksCount = 42 + let playButton = this.$pgettext('Sidebar/Player/Button/Verb, Short', 'Play') + let loginMessage = this.$pgettext('*/Login/Message', 'Welcome back %{ username }') + let addedMessage = this.$npgettext('*/Player/Message', 'One track was queued', '%{ count } tracks were queued', tracksCount) + console.log(this.$gettextInterpolate(addedMessage, {count: tracksCount})) + console.log(this.$gettextInterpolate(loginMessage, {username: 'alice'})) + } + } + } + +The first argument of the ``$pgettext`` and ``$npgettext`` functions is the string context. + +Contextualization +^^^^^^^^^^^^^^^^^ + +Translation contexts provided via the ``translate-context`` directive and the ``$pgettext`` and ``$npgettext`` are never shown to end users +but visible by Funkwhale translators. They help translators where and how the strings are used, +especially with short or ambiguous strings, like ``May``, which can refer a month or a verb. + +While we could in theory use free form context, like ``This string is inside a button, in the main page, and is a call to action``, +Funkwhale use a hierarchical structure to write contexts and keep them short and consistents accross the app. The previous context, +rewritten correctly would be: ``Content/Home/Button/Call to action``. + +This hierarchical structure is made of several parts: + +- The location part, which is required and refers to the big blocks found in Funkwhale UI where the translated string is displayed: + - ``Content`` + - ``Footer`` + - ``Head`` + - ``Menu`` + - ``Popup`` + - ``Sidebar`` + - ``*`` for strings that are not tied to a specific location + +- The feature part, which is required, and refers to the feature associated with the translated string: + - ``About`` + - ``Admin`` + - ``Album`` + - ``Artist`` + - ``Embed`` + - ``Home`` + - ``Login`` + - ``Library`` + - ``Moderation`` + - ``Player`` + - ``Playlist`` + - ``Profile`` + - ``Favorites`` + - ``Notifications`` + - ``Radio`` + - ``Search`` + - ``Settings`` + - ``Signup`` + - ``Track`` + - ``Queue`` + - ``*`` for strings that are not tied to a specific feature + +- The component part, which is required and refers to the type of element that contain the string: + - ``Button`` + - ``Card`` + - ``Checkbox`` + - ``Dropdown`` + - ``Error message`` + - ``Form`` + - ``Header`` + - ``Help text`` + - ``Hidden text`` + - ``Icon`` + - ``Input`` + - ``Image`` + - ``Label`` + - ``Link`` + - ``List item`` + - ``Menu`` + - ``Message`` + - ``Paragraph`` + - ``Placeholder`` + - ``Tab`` + - ``Table`` + - ``Title`` + - ``Tooltip`` + - ``*`` for strings that are not tied to a specific component + +The detail part, which is optional and refers to the contents of the string itself, such as: + - ``Adjective`` + - ``Call to action`` + - ``Noun`` + - ``Short`` + - ``Unit`` + - ``Verb`` + +Here are a few examples of valid context hierarchies: + +- ``Sidebar/Player/Button`` +- ``Content/Home/Button/Call to action`` +- ``Footer/*/Help text`` +- ``*/*/*/Verb, Short`` +- ``Popup/Playlist/Button`` +- ``Content/Admin/Table.Label/Short, Noun (Value is a date)`` + +It's possible to nest multiple component parts to reach a higher level of detail. The component parts are then separated by a dot: + +- ``Sidebar/Queue/Tab.Title`` +- ``Content/*/Button.Title`` +- ``Content/*/Table.Header`` +- ``Footer/*/List item.Link`` +- ``Content/*/Form.Help text`` + +Collecting translatable strings +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you want to ensure your translatable strings are correctly marked for translation, +you can try to extract them. + +When working on the front-end, any end-user string should be marked as a translatable string, +with the proper context, as described below. + +Translations in HTML +^^^^^^^^^^^^^^^^^^^^ + +Translations in HTML use the ```` tag:: + + + +Anything between the `` and `` delimiters will be considered as a translatable string. +You can use variables in the translated string via the ``:translate-params="{var: 'value'}"`` directive, and reference them like this: +``val value is %{ value }``. + +For pluralization, you need to use ``translate-params`` in conjunction with ``translate-plural`` and ``translate-n``: + +- ``translate-params`` should contain the variable you're using for pluralization (which is usually shown to the user) +- ``translate-n`` should match the same variable +- The ```` delimiters contain the non-pluralized version of your string +- The ``translate-plural`` directive contains the pluralized version of your string + + +Translations in javascript +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Translations in javascript work by calling the ``this.$*gettext`` functions:: + + export default { + computed: { + strings () { + let tracksCount = 42 + let playButton = this.$pgettext('Sidebar/Player/Button/Verb, Short', 'Play') + let loginMessage = this.$pgettext('*/Login/Message', 'Welcome back %{ username }') + let addedMessage = this.$npgettext('*/Player/Message', 'One track was queued', '%{ count } tracks were queued', tracksCount) + console.log(this.$gettextInterpolate(addedMessage, {count: tracksCount})) + console.log(this.$gettextInterpolate(loginMessage, {username: 'alice'})) + } + } + } + +The first argument of the ``$pgettext`` and ``$npgettext`` functions is the string context. + +Contextualization +^^^^^^^^^^^^^^^^^ + +Translation contexts provided via the ``translate-context`` directive and the ``$pgettext`` and ``$npgettext`` are never shown to end users +but visible by Funkwhale translators. They help translators where and how the strings are used, +especially with short or ambiguous strings, like ``May``, which can refer a month or a verb. + +While we could in theory use free form context, like ``This string is inside a button, in the main page, and is a call to action``, +Funkwhale use a hierarchical structure to write contexts and keep them short and consistents accross the app. The previous context, +rewritten correctly would be: ``Content/Home/Button/Call to action``. + +This hierarchical structure is made of several parts: + +- The location part, which is required and refers to the big blocks found in Funkwhale UI where the translated string is displayed: + - ``Content`` + - ``Footer`` + - ``Head`` + - ``Menu`` + - ``Popup`` + - ``Sidebar`` + - ``*`` for strings that are not tied to a specific location + +- The feature part, which is required, and refers to the feature associated with the translated string: + - ``About`` + - ``Admin`` + - ``Album`` + - ``Artist`` + - ``Embed`` + - ``Home`` + - ``Login`` + - ``Library`` + - ``Moderation`` + - ``Player`` + - ``Playlist`` + - ``Profile`` + - ``Favorites`` + - ``Notifications`` + - ``Radio`` + - ``Search`` + - ``Settings`` + - ``Signup`` + - ``Track`` + - ``Queue`` + - ``*`` for strings that are not tied to a specific feature + +- The component part, which is required and refers to the type of element that contain the string: + - ``Button`` + - ``Card`` + - ``Checkbox`` + - ``Dropdown`` + - ``Error message`` + - ``Form`` + - ``Header`` + - ``Help text`` + - ``Hidden text`` + - ``Icon`` + - ``Input`` + - ``Image`` + - ``Label`` + - ``Link`` + - ``List item`` + - ``Menu`` + - ``Message`` + - ``Paragraph`` + - ``Placeholder`` + - ``Tab`` + - ``Table`` + - ``Title`` + - ``Tooltip`` + - ``*`` for strings that are not tied to a specific component + +The detail part, which is optional and refers to the contents of the string itself, such as: + - ``Adjective`` + - ``Call to action`` + - ``Noun`` + - ``Short`` + - ``Unit`` + - ``Verb`` + +Here are a few examples of valid context hierarchies: + +- ``Sidebar/Player/Button`` +- ``Content/Home/Button/Call to action`` +- ``Footer/*/Help text`` +- ``*/*/*/Verb, Short`` +- ``Popup/Playlist/Button`` +- ``Content/Admin/Table.Label/Short, Noun (Value is a date)`` + +It's possible to nest multiple component parts to reach a higher level of detail. The component parts are then separated by a dot: + +- ``Sidebar/Queue/Tab.Title`` +- ``Content/*/Button.Title`` +- ``Content/*/Table.Header`` +- ``Footer/*/List item.Link`` +- ``Content/*/Form.Help text`` + +Collecting translatable strings +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you want to ensure your translatable strings are correctly marked for translation, +you can try to extract them. +>>>>>>> 21fb39dd... Update docs/developers/index.rst, docs/developers/subsonic.rst files Extraction is done by calling ``yarn run i18n-extract``, which will pull all the strings from source files and put them in a PO file. diff --git a/api/funkwhale_api/federation/activity.py b/api/funkwhale_api/federation/activity.py index b9f8ffd69..2436044d7 100644 --- a/api/funkwhale_api/federation/activity.py +++ b/api/funkwhale_api/federation/activity.py @@ -173,70 +173,77 @@ class Router: class InboxRouter(Router): + def get_matching_handlers(self, payload): + return [ + handler for route, handler in self.routes if match_route(route, payload) + ] + @transaction.atomic - def dispatch(self, payload, context): + def dispatch(self, payload, context, call_handlers=True): """ Receives an Activity payload and some context and trigger our - business logic + business logic. + + call_handlers should be False when are delivering a local activity, because + we want only want to bind activities to their recipients, not reapply the changes. """ from . import api_serializers from . import models - for route, handler in self.routes: - if match_route(route, payload): + handlers = self.get_matching_handlers(payload) + for handler in handlers: + if call_handlers: r = handler(payload, context=context) - activity_obj = context.get("activity") - if activity_obj and r: - # handler returned additional data we can use - # to update the activity target - for key, value in r.items(): - setattr(activity_obj, key, value) + else: + r = None + activity_obj = context.get("activity") + if activity_obj and r: + # handler returned additional data we can use + # to update the activity target + for key, value in r.items(): + setattr(activity_obj, key, value) - update_fields = [] - for k in r.keys(): - if k in ["object", "target", "related_object"]: - update_fields += [ - "{}_id".format(k), - "{}_content_type".format(k), - ] - else: - update_fields.append(k) - activity_obj.save(update_fields=update_fields) + update_fields = [] + for k in r.keys(): + if k in ["object", "target", "related_object"]: + update_fields += [ + "{}_id".format(k), + "{}_content_type".format(k), + ] + else: + update_fields.append(k) + activity_obj.save(update_fields=update_fields) - if payload["type"] not in BROADCAST_TO_USER_ACTIVITIES: - return - - inbox_items = context.get( - "inbox_items", models.InboxItem.objects.none() - ) - inbox_items = ( - inbox_items.select_related() - .select_related("actor__user") - .prefetch_related( - "activity__object", - "activity__target", - "activity__related_object", - ) - ) - - for ii in inbox_items: - user = ii.actor.get_user() - if not user: - continue - group = "user.{}.inbox".format(user.pk) - channels.group_send( - group, - { - "type": "event.send", - "text": "", - "data": { - "type": "inbox.item_added", - "item": api_serializers.InboxItemSerializer(ii).data, - }, - }, - ) + if payload["type"] not in BROADCAST_TO_USER_ACTIVITIES: return + inbox_items = context.get("inbox_items", models.InboxItem.objects.none()) + inbox_items = ( + inbox_items.select_related() + .select_related("actor__user") + .prefetch_related( + "activity__object", "activity__target", "activity__related_object" + ) + ) + + for ii in inbox_items: + user = ii.actor.get_user() + if not user: + continue + group = "user.{}.inbox".format(user.pk) + channels.group_send( + group, + { + "type": "event.send", + "text": "", + "data": { + "type": "inbox.item_added", + "item": api_serializers.InboxItemSerializer(ii).data, + }, + }, + ) + return + ACTOR_KEY_ROTATION_LOCK_CACHE_KEY = "federation:actor-key-rotation-lock:{}" diff --git a/api/funkwhale_api/federation/tasks.py b/api/funkwhale_api/federation/tasks.py index f7d8913b7..e7aafc2e8 100644 --- a/api/funkwhale_api/federation/tasks.py +++ b/api/funkwhale_api/federation/tasks.py @@ -71,7 +71,7 @@ def get_files(storage, *parts): @celery.app.task(name="federation.dispatch_inbox") @celery.require_instance(models.Activity.objects.select_related(), "activity") -def dispatch_inbox(activity): +def dispatch_inbox(activity, call_handlers=True): """ Given an activity instance, triggers our internal delivery logic (follow creation, etc.) @@ -84,6 +84,7 @@ def dispatch_inbox(activity): "actor": activity.actor, "inbox_items": activity.inbox_items.filter(is_read=False), }, + call_handlers=call_handlers, ) @@ -96,7 +97,7 @@ def dispatch_outbox(activity): inbox_items = activity.inbox_items.filter(is_read=False).select_related() if inbox_items.exists(): - dispatch_inbox.delay(activity_id=activity.pk) + dispatch_inbox.delay(activity_id=activity.pk, call_handlers=False) if not preferences.get("federation__enabled"): # federation is disabled, we only deliver to local recipients diff --git a/api/tests/federation/test_activity.py b/api/tests/federation/test_activity.py index fa83ed1f4..a1eedeb49 100644 --- a/api/tests/federation/test_activity.py +++ b/api/tests/federation/test_activity.py @@ -190,6 +190,16 @@ def test_inbox_routing(factories, mocker): assert a.target == target +def test_inbox_routing_no_handler(factories, mocker): + router = activity.InboxRouter() + a = factories["federation.Activity"](type="Follow") + handler = mocker.Mock() + router.connect({"type": "Follow"}, handler) + + router.dispatch({"type": "Follow"}, context={"activity": a}, call_handlers=False) + handler.assert_not_called() + + def test_inbox_routing_send_to_channel(factories, mocker): group_send = mocker.patch("funkwhale_api.common.channels.group_send") a = factories["federation.Activity"](type="Follow") diff --git a/api/tests/federation/test_tasks.py b/api/tests/federation/test_tasks.py index f3216eed7..21aa181f8 100644 --- a/api/tests/federation/test_tasks.py +++ b/api/tests/federation/test_tasks.py @@ -74,10 +74,12 @@ def test_handle_in(factories, mocker, now, queryset_equal_list): a = factories["federation.Activity"](payload={"hello": "world"}) ii1 = factories["federation.InboxItem"](activity=a, actor=r1) ii2 = factories["federation.InboxItem"](activity=a, actor=r2) - tasks.dispatch_inbox(activity_id=a.pk) + tasks.dispatch_inbox(activity_id=a.pk, call_handlers=False) mocked_dispatch.assert_called_once_with( - a.payload, context={"actor": a.actor, "activity": a, "inbox_items": [ii1, ii2]} + a.payload, + context={"actor": a.actor, "activity": a, "inbox_items": [ii1, ii2]}, + call_handlers=False, ) @@ -90,7 +92,7 @@ def test_dispatch_outbox(factories, mocker): factories["federation.InboxItem"](activity=activity) delivery = factories["federation.Delivery"](activity=activity) tasks.dispatch_outbox(activity_id=activity.pk) - mocked_inbox.assert_called_once_with(activity_id=activity.pk) + mocked_inbox.assert_called_once_with(activity_id=activity.pk, call_handlers=False) mocked_deliver_to_remote.assert_called_once_with(delivery_id=delivery.pk) @@ -104,7 +106,7 @@ def test_dispatch_outbox_disabled_federation(factories, mocker, preferences): factories["federation.InboxItem"](activity=activity) factories["federation.Delivery"](activity=activity) tasks.dispatch_outbox(activity_id=activity.pk) - mocked_inbox.assert_called_once_with(activity_id=activity.pk) + mocked_inbox.assert_called_once_with(activity_id=activity.pk, call_handlers=False) mocked_deliver_to_remote.assert_not_called() diff --git a/changes/changelog.d/511.bugfix b/changes/changelog.d/511.bugfix new file mode 100644 index 000000000..0c4b61375 --- /dev/null +++ b/changes/changelog.d/511.bugfix @@ -0,0 +1 @@ +i18n: Update page title when changing the App's language. (#511) diff --git a/changes/changelog.d/630.enhancement b/changes/changelog.d/630.enhancement new file mode 100644 index 000000000..019911337 --- /dev/null +++ b/changes/changelog.d/630.enhancement @@ -0,0 +1 @@ +Ask for confirmation before leaving upload page if there is a an upload in process (#630) \ No newline at end of file diff --git a/changes/changelog.d/735.enhancement b/changes/changelog.d/735.enhancement new file mode 100644 index 000000000..d6fb9ad70 --- /dev/null +++ b/changes/changelog.d/735.enhancement @@ -0,0 +1 @@ +Truncate filename in library file table to ensure correct display of the table. (#735) diff --git a/changes/changelog.d/737.bugfix b/changes/changelog.d/737.bugfix new file mode 100644 index 000000000..c65050a6e --- /dev/null +++ b/changes/changelog.d/737.bugfix @@ -0,0 +1 @@ +Fixed delivering of local activities causing unintended side effects, such as rollbacking changes (#737) diff --git a/docs/upgrading/0.17.rst b/docs/admin/0.17.rst similarity index 99% rename from docs/upgrading/0.17.rst rename to docs/admin/0.17.rst index 5111a99fd..0b5637b2c 100644 --- a/docs/upgrading/0.17.rst +++ b/docs/admin/0.17.rst @@ -38,7 +38,7 @@ to help you understand the scale of the changes: +----------------------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ From a shared, instance-wide library to users libraries ------------------------------------------------------- +------------------------------------------------------- As you can see, there is a big switch: in earlier versions, each instance had one big library, that was available to all its users. This model don't scale well (especially if you put diff --git a/docs/configuration.rst b/docs/admin/configuration.rst similarity index 96% rename from docs/configuration.rst rename to docs/admin/configuration.rst index ffb702d6b..bc6ab99b4 100644 --- a/docs/configuration.rst +++ b/docs/admin/configuration.rst @@ -14,7 +14,7 @@ and technical aspects of your instance, such as database credentials. .. note:: - You should restart all funkwhale processes when you change the values + You should restart all Funkwhale processes when you change the values on environment variables. @@ -38,7 +38,7 @@ settings in this interface. .. note:: If you have any issue with the web application, a management interface is also - available for those settings from Django's administration interface. It's + available for those settings from :doc:`Django's administration interface `. It's less user friendly, though, and we recommend you use the web app interface whenever possible. @@ -110,7 +110,7 @@ for this value. For non-docker installation, you can use any absolute path. Default: :ref:`setting-MUSIC_DIRECTORY_PATH` -When using Docker, the value of :ref:`MUSIC_DIRECTORY_PATH` in your containers +When using Docker, the value of :ref:`setting-MUSIC_DIRECTORY_PATH` in your containers may differ from the real path on your host. Assuming you have the following directive in your :file:`docker-compose.yml` file:: @@ -156,7 +156,7 @@ permissions are: other instances, and accept/deny federation requests from other instances There is no dedicated interface to manage users permissions, but superusers -can login on the Django's admin at ``/api/admin/`` and grant permissions +can login on the :doc:`Django's admin ` at ``/api/admin/`` and grant permissions to users at ``/api/admin/users/user/``. Front-end settings diff --git a/docs/admin/django.rst b/docs/admin/django.rst new file mode 100644 index 000000000..1fadd62f4 --- /dev/null +++ b/docs/admin/django.rst @@ -0,0 +1,79 @@ +Using the Django Administration Backend +======================================= + +Funkwhale is being actively developed, and new features are being added to the frontend all the time. However, there are some administrative tasks that can only be undertaken in the Django Administration backend. + +.. Warning:: + Deleting items on the backend is **not** recommended. Deletions performed on the backend are permanent. If you remove something in the backend, you will need to re-add it from scratch. + +Accessing the Django Backend +---------------------------- + +To access your instance's backend, navigate to ``https://yourdomain/api/admin``. You will be prompted to log in. By default, the login details will be those of the priviliged user created during the setup process. + +Deleting Items +------------------- + +By default, deleting items in the front end removes the file from the server but **does not** delete associated entities such as artists, albums, and track data, meaning that they will still be viewable but no longer playable. Items deleted in this way will also still count on the instance statistics. To remove them completely, it is necessary to remove them from the database entirely using the Django Administration backend. + +.. Warning:: + Deleting tracks, albums, or artists will also remove them completely from any associated playlists, radios, or favorites lists. Before continuing, make sure other users on the instance are aware of the deletion(s). + +Deleting a Track +^^^^^^^^^^^^^^^^ + +* Navigate to ``https://yourdomain/api/admin/music/track`` +* Select the track(s) you wish to delete +* In the ``Action`` dropdown menu, select "Delete Selected Items" +* Click on "Go". You will be prompted to confirm the track's deletion + +Deleting an Album +^^^^^^^^^^^^^^^^^ + +* Navigate to ``https://yourdomain/api/admin/music/album`` +* Select the album(s) you wish to delete +* In the ``Action`` dropdown menu, select "Delete Selected Items" +* Click on "Go". You will be prompted to confirm the album's deletion + +.. note:: + + Deleting an album will remove all tracks associated with the album + +Deleting an Artist +^^^^^^^^^^^^^^^^^^ + +* Navigate to ``https://yourdomain/api/admin/music/artist`` +* Select the artist(s) you wish to delete +* In the ``Action`` dropdown menu, select "Delete Selected Items" +* Click on "Go". You will be prompted to confirm the artist's deletion + +.. note:: + + Deleting an artist will remove all tracks and albums associated with the artist + +Removing a Followed Library +--------------------------- + +In Funkwhale, unfollowing a library will leave the items in place but inaccessible. To completely remove them: + +* Navigate to ``https://yourdomain/api/admin/music/library/`` +* Tick the box next to the library you wish to remove +* In the ``Action`` dropdown menu, select "Delete Selected Items" +* Click on "Go". You will be prompted to confirm the library's deletion + +Adding Missing Album Art +------------------------- + +Sometimes album art can fail to appear despite music being properly tagged. When this happens, it is possible to replace the missing art. + +* Navigate to ``https://yourdomain/api/admin/music/album`` +* Search for and select the album in question +* Find the item marked "Cover" +* Click "Browse" and select the file from your computer +* Click "Save" to confirm the changes + +The album art will now be present on the frontend. + +.. note:: + + You can also clear currently loaded album art by checking the checkbox next to the current item and selecting "Clear" diff --git a/docs/importing-music.rst b/docs/admin/importing-music.rst similarity index 99% rename from docs/importing-music.rst rename to docs/admin/importing-music.rst index bbb21c1ad..1677d919b 100644 --- a/docs/importing-music.rst +++ b/docs/admin/importing-music.rst @@ -151,4 +151,4 @@ From other instances -------------------- Funkwhale also supports importing music from other instances. Please refer -to :doc:`federation` for more details. +to :doc:`../federation/index` for more details. diff --git a/docs/admin/index.rst b/docs/admin/index.rst new file mode 100644 index 000000000..a385a2e5e --- /dev/null +++ b/docs/admin/index.rst @@ -0,0 +1,34 @@ +Administrator Documentation +===================================== + +This documentation is targeted at administrators of instances. This typically refers to +the person(s) responsible for running the server and managing the software on a technical +level. + +Setup Guides +------------ + +.. toctree:: + :maxdepth: 2 + + ../installation/index + configuration + importing-music + +Administration +-------------- + +.. toctree:: + :maxdepth: 2 + + django + url + upgrading + +Troubleshooting Issues +---------------------- + +.. toctree:: + :maxdepth: 2 + + troubleshooting diff --git a/docs/troubleshooting.rst b/docs/admin/troubleshooting.rst similarity index 76% rename from docs/troubleshooting.rst rename to docs/admin/troubleshooting.rst index 57813d5e7..a8c6f2546 100644 --- a/docs/troubleshooting.rst +++ b/docs/admin/troubleshooting.rst @@ -14,7 +14,7 @@ Diagnose problems ^^^^^^^^^^^^^^^^^ Funkwhale is made of several components, each one being a potential cause for failure. Having an even basic overview -of Funkwhale's technical architecture can help you understand what is going on. You can refer to :doc:`the technical architecture ` for that. +of Funkwhale's technical architecture can help you understand what is going on. You can refer to :doc:`the technical architecture <../developers/architecture>` for that. Problems usually fall into one of those categories: @@ -28,54 +28,6 @@ Each category comes with its own set of diagnose tools and/or commands we will d steps for each type of problem. Please try those to see if it fix your issues. If none of those works, please report your issue on our issue tracker. -Frontend issues -^^^^^^^^^^^^^^^ - -Diagnostic tools: - -- Javascript and network logs from your browser console (see instructions on how to open it in `Chrome `_ and `Firefox `_ -- Proxy and API access and error logs (see :ref:`access-logs`) -- The same operation works from a different browser - -Common problems -*************** - -The front-end is completely blank -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You are visiting Funkwhale, but you don't see anything. - -- Try from a different browser -- Check network errors in your browser console. If you see responses with 40X or 50X statuses, there is probably an issue with the webserver configuration -- If you don't see anything wrong in the network console, check the Javascript console -- Disable your browser extensions (like adblockers) - -Music is not playing -~~~~~~~~~~~~~~~~~~~~ - -You have some tracks in your queue that don't play, or the queue is jumping from one track to the next until -there is no more track available: - -- Try with other tracks. If it works with some tracks but not other tracks, this may means that the failing tracks are not probably imported - or that your browser does not support a specific audio format -- Check the network and javascript console for potential errors - -Tracks are not appending to the queue -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When clicking on "Play", "Play all albums" or "Play all" buttons, some tracks are not appended to the queue. This is -actually a feature of Funkwhale: those tracks have no file associated with them, so we cannot play them. - -Specific pages are loading forever or blank -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When viewing a given page, the page load never ends (you continue to see the spinner), or nothing seems to appear at all: - -- Ensure your internet connection is up and running -- Ensure your instance is up and running -- Check the network and javascript console for potential errors - - Backend issues ^^^^^^^^^^^^^^ @@ -207,6 +159,6 @@ similar issues before doing that, and use the issue tracker only to report bugs, Improving this documentation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -If you feel like something should be improved in this document (and in the documentation in general), feel free to `edit -it `_ and open a Merge Request. If you lack time or skills -to do that, you can open an issue to discuss that, and someone else will do it. +If you feel like something should be improved in this document (and in the documentation in general), feel free to :doc:`contribute to the documentation <../documentation/creating>`. +If you're not comfortable contributing or would like to ask somebody else to do it, feel free to :doc:`request a change in documentation <../documentation/identifying>`. + diff --git a/docs/upgrading/index.rst b/docs/admin/upgrading.rst similarity index 97% rename from docs/upgrading/index.rst rename to docs/admin/upgrading.rst index 01ed47a4a..9a33836cc 100644 --- a/docs/upgrading/index.rst +++ b/docs/admin/upgrading.rst @@ -13,7 +13,7 @@ Upgrading your Funkwhale instance to a newer version Reading the release notes ------------------------- -Please take a few minutes to read the :doc:`changelog`: updates should work +Please take a few minutes to read the :doc:`../changelog`: updates should work similarly from version to version, but some of them may require additional steps. Those steps would be described in the version release notes. @@ -33,7 +33,7 @@ when possible. Docker setup ------------ -If you've followed the setup instructions in :doc:`Docker`, upgrade path is +If you've followed the setup instructions in :doc:`../installation/docker`, upgrade path is easy: Mono-container installation diff --git a/docs/admin/url.rst b/docs/admin/url.rst new file mode 100644 index 000000000..daa11a805 --- /dev/null +++ b/docs/admin/url.rst @@ -0,0 +1,93 @@ +Changing Your Instance URL +========================== + +At some point, you may wish to change your instance URL. In order to +do this, you will need to change the following: + +- The instance URL in your .env file +- The instance URL in your ``/etc/nginx/sites-enabled/funkwhale.conf`` or ``/etc/apache2/sites-enabled/funkwhale.conf`` depending on your web server setup +- Any references to the old URL in your database + +The changes to the database can be achieved with the ``fix_federation_ids`` script in the ``manage.py`` +file. + +Example output: + +.. code-block:: shell + + # For Docker setups + docker-compose run --rm api python manage.py fix_federation_ids https://old-url https://new-url --no-dry-run --no-input + + # For non-Docker setups + python manage.py fix_federation_ids https://old-url https://new-url --no-dry-run --no-input + + # Output + Will replace 108 found occurences of 'https://old-url' by 'https://new-url': + + - 20 music.Artist + - 13 music.Album + - 39 music.Track + - 31 music.Upload + - 1 music.Library + - 4 federation.Actor + - 0 federation.Activity + - 0 federation.Follow + - 0 federation.LibraryFollow + + Replacing on 20 music.Artist… + Replacing on 13 music.Album… + Replacing on 39 music.Track… + Replacing on 31 music.Upload… + Replacing on 1 music.Library… + Replacing on 4 federation.Actor… + Replacing on 0 federation.Activity… + Replacing on 0 federation.Follow… + Replacing on 0 federation.LibraryFollow… + +On Docker Installations +----------------------- + +If you have followed the :doc:`Docker installation instructions <../installation/docker>`, you +will need to do the following: + +- Edit your .env file to change the ``FUNKWHALE_HOSTNAME`` and ``DJANGO_ALLOWED_HOSTS`` value to your new URL +- Edit your ``/etc/nginx/sites-enabled/funkwhale.conf`` file to change the ``server_name`` values to your new URL +- Run the following command to change all mentions of your old instance URL in the database: + +.. code-block:: shell + + docker-compose run --rm api python manage.py fix_federation_ids https://old-url https://new-url --no-dry-run --no-input + +- Restart Nginx or Apache to pick up the new changes + +.. code-block:: shell + + # For Nginx + sudo systemctl restart nginx + + # For Apache + sudo systemctl restart apache2 + +On Non-Docker Installations +--------------------------- + +If you have followed the :doc:`non-docker setup <../installation/debian>`, you will need to do the following: + +- Edit your .env file to change the ``FUNKWHALE_HOSTNAME`` and ``DJANGO_ALLOWED_HOSTS`` value to your new URL +- Edit your ``/etc/nginx/sites-enabled/funkwhale.conf`` file to change the ``server_name`` values to your new URL +- Run the following command to change all mentions of your old instance URL in the database: + +.. code-block:: shell + + python manage.py fix_federation_ids https://old-url https://new-url --no-dry-run --no-input + +- Restart Nginx or Apache to pick up the new changes + +.. code-block:: shell + + # For Nginx + sudo systemctl restart nginx + + # For Apache + sudo systemctl restart apache2 + \ No newline at end of file diff --git a/docs/backup.rst b/docs/backup.rst new file mode 100644 index 000000000..d474678ae --- /dev/null +++ b/docs/backup.rst @@ -0,0 +1,79 @@ +Backup your Funkwhale instance +============================== + +.. note:: + + Before upgrading your instance, we strongly advise you to make at least a database backup. Ideally, you should make a full backup, including the database and the media files. + + +Docker setup +------------ + +If you've followed the setup instructions in :doc:`../installation/docker`, here is the backup path: + +Multi-container installation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Backup the db +^^^^^^^^^^^^^ + +On docker setups, you have to ``pg_dumpall`` in container ``funkwhale_postgres_1``: + +.. code-block:: shell + + docker exec -t funkwhale_postgres_1 pg_dumpall -c -U postgres > dump_`date +%d-%m-%Y"_"%H_%M_%S`.sql + +Backup the media files +^^^^^^^^^^^^^^^^^^^^^^ + +To backup docker data volumes, as the volumes are bound mounted to the host, the ``rsync`` way would go like this: + +.. code-block:: shell + + rsync -avzhP /srv/funkwhale/data/media /path/to/your/backup/media + rsync -avzhP /srv/funkwhale/data/music /path/to/your/backup/music + + +Backup the configuration files +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +On docker setups, the configuration file is located at the root level: + +.. code-block:: shell + + rsync -avzhP /srv/funkwhale/.env /path/to/your/backup/.env + + +Non-docker setup +---------------- + +Backup the db +^^^^^^^^^^^^^ + +On non-docker setups, you have to ``pg_dump`` as user ``postgres``: + +.. code-block:: shell + + sudo -u postgres -H pg_dump funkwhale > /path/to/your/backup/dump_`date +%d-%m-%Y"_"%H_%M_%S`.sql + +Backup the media files +^^^^^^^^^^^^^^^^^^^^^^ + +A simple way to backup your media files is to use ``rsync``: + +.. code-block:: shell + + rsync -avzhP /srv/funkwhale/data/media /path/to/your/backup/media + rsync -avzhP /srv/funkwhale/data/music /path/to/your/backup/music + +Backup the configuration files +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: shell + + rsync -avzhP /srv/funkwhale/config/.env /path/to/your/backup/.env + +.. note:: + You may also want to backup your proxy configuration file. + + For frequent backups, you may want to use deduplication and compression to keep the backup size low. In this case, a tool like ``borg`` will be more appropriate. diff --git a/docs/conf.py b/docs/conf.py index f371fa165..eb3ae5cdd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,6 +19,8 @@ # import os import sys +import datetime +from shutil import copyfile sys.path.insert(0, os.path.abspath("../api")) @@ -48,8 +50,9 @@ source_suffix = ".rst" master_doc = "index" # General information about the project. +year = datetime.datetime.now().year project = "funkwhale" -copyright = "2017, Eliot Berriot" +copyright = "{}, Eliot Berriot".format(year) author = "Eliot Berriot" # The version info for the project you're documenting, acts as replacement for @@ -81,7 +84,6 @@ pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False - # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for @@ -100,7 +102,6 @@ html_theme = "alabaster" # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] - # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. @@ -155,3 +156,48 @@ texinfo_documents = [ "Miscellaneous", ) ] + +# -- Build legacy redirect files ------------------------------------------- + +# Define list of redirect files to be build in the Sphinx build process + +redirect_files = [ + + ('importing-music.html', 'admin/importing-music.html'), + ('architecture.html', 'developer/architecture.html'), + ('troubleshooting.html', 'admin/troubleshooting.html'), + ('configuration.html', 'admin/configuration.html'), + ('upgrading/index.html', '../admin/upgrading.html'), + ('upgrading/0.17.html', '../admin/0.17.html'), + ('users/django.html', '../admin/django.html'), + +] + +# Generate redirect template + +redirect_template = """\ + + + + + + +""" + +# Tell Sphinx to copy the files + +def copy_legacy_redirects(app, docname): + if app.builder.name == 'html': + for html_src_path, new in redirect_files: + page = redirect_template.format(new=new) + target_path = app.outdir + '/' + html_src_path + if not os.path.exists(os.path.dirname(target_path)): + os.makedirs(os.path.dirname(target_path)) + with open(target_path, 'w') as f: + f.write(page) + + +def setup(app): + app.connect('build-finished', copy_legacy_redirects) diff --git a/docs/architecture.rst b/docs/developers/architecture.rst similarity index 98% rename from docs/architecture.rst rename to docs/developers/architecture.rst index ef52b3b38..9ea5ab48e 100644 --- a/docs/architecture.rst +++ b/docs/developers/architecture.rst @@ -50,7 +50,7 @@ or persist data. Third-party apps ---------------- -Since Funkwhale implements a subset of the Subsonic API, it's compatible with existing apps such +Since Funkwhale implements a subset of the Subsonic API, it's compatible with existing :doc:`apps <../users/apps>` such as DSub, Ultrasonic or Clementine that support this API. Those apps can be used as a replacement or in conjunction of the web interface, but the underlying data is the same. diff --git a/docs/developers/index.rst b/docs/developers/index.rst index 65512923e..69f22f2c6 100644 --- a/docs/developers/index.rst +++ b/docs/developers/index.rst @@ -10,6 +10,7 @@ Reference .. toctree:: :maxdepth: 2 + architecture ../api ../federation/index subsonic diff --git a/docs/developers/subsonic.rst b/docs/developers/subsonic.rst index d7493f1e7..ccc7eb6f8 100644 --- a/docs/developers/subsonic.rst +++ b/docs/developers/subsonic.rst @@ -57,7 +57,6 @@ in our payload, which you can use to adapt your client behaviour if needed: { "subsonic-response": { - ... "type": "funkwhale", "funkwhaleVersion": "0.17" } diff --git a/docs/documentation/creating.rst b/docs/documentation/creating.rst new file mode 100644 index 000000000..57045896a --- /dev/null +++ b/docs/documentation/creating.rst @@ -0,0 +1,129 @@ +Adding New Documents +==================== + +Writing Documents +----------------- + +Before you start writing documents: + +- Make sure you have all the necessary information and :doc:`tools you need ` to get started +- Check the `current documents `_ carefully to make sure you're not repeating something somebody has already said +- Familiarize yourself with :doc:`reStructuredText ` and :doc:`the recommended document style diff --git a/front/src/components/library/FileUpload.vue b/front/src/components/library/FileUpload.vue index 06fbdbad6..7b7673b5a 100644 --- a/front/src/components/library/FileUpload.vue +++ b/front/src/components/library/FileUpload.vue @@ -166,14 +166,24 @@ export default { id: "fileUpload", handler: this.handleImportEvent }); + window.onbeforeunload = e => this.onBeforeUnload(e); }, destroyed() { this.$store.commit("ui/removeWebsocketEventHandler", { eventName: "import.status_updated", id: "fileUpload" }); + window.onbeforeunload = null; }, methods: { + onBeforeUnload(e = {}) { + const returnValue = ('This page is asking you to confirm that you want to leave - data you have entered may not be saved.'); + if (!this.hasActiveUploads) return null; + Object.assign(e, { + returnValue, + }); + return returnValue; + }, inputFile(newFile, oldFile) { this.$refs.upload.active = true; }, @@ -291,6 +301,9 @@ export default { f.statusIndex = statusIndex return f }), ['statusIndex', 'name']) + }, + hasActiveUploads () { + return this.sortedFiles.filter((f) => { return f.active }).length > 0 } }, watch: { diff --git a/front/src/components/library/Home.vue b/front/src/components/library/Home.vue index 5e81dbdc8..19b667335 100644 --- a/front/src/components/library/Home.vue +++ b/front/src/components/library/Home.vue @@ -19,8 +19,8 @@ -
-
+
+
diff --git a/front/src/filters.js b/front/src/filters.js index 145a98253..1edea76f6 100644 --- a/front/src/filters.js +++ b/front/src/filters.js @@ -17,7 +17,15 @@ export function ago (date, locale) { locale = locale || 'en' const m = moment(date) m.locale(locale) - return m.fromNow() + return m.calendar(null, { + sameDay: 'LT', + nextDay: 'L', + nextWeek: 'L', + lastDay: 'L', + lastWeek: 'L', + sameElse: 'L' +}) + } Vue.filter('ago', ago) diff --git a/front/src/main.js b/front/src/main.js index fbe676106..9f058d8ec 100644 --- a/front/src/main.js +++ b/front/src/main.js @@ -57,28 +57,17 @@ Vue.use(GetTextPlugin, { Vue.use(VueMasonryPlugin) Vue.use(VueLazyload) Vue.config.productionTip = false -Vue.directive('title', { - inserted: (el, binding) => { - let parts = [] - let instanceName = store.state.instance.settings.instance.name.value - if (instanceName.length === 0) { - instanceName = 'Funkwhale' - } - parts.unshift(instanceName) - parts.unshift(binding.value) - document.title = parts.join(' - ') - }, - updated: (el, binding) => { - let parts = [] - let instanceName = store.state.instance.settings.instance.name.value - if (instanceName.length === 0) { - instanceName = 'Funkwhale' - } - parts.unshift(instanceName) - parts.unshift(binding.value) - document.title = parts.join(' - ') +Vue.directive('title', function (el, binding) { + let parts = [] + let instanceName = store.state.instance.settings.instance.name.value + if (instanceName.length === 0) { + instanceName = 'Funkwhale' } -}) + parts.unshift(instanceName) + parts.unshift(binding.value) + document.title = parts.join(' - ') + } +) axios.interceptors.request.use(function (config) { // Do something before request is sent if (store.state.auth.token) { diff --git a/front/src/style/_main.scss b/front/src/style/_main.scss index 311e0e9dc..76d0372f3 100644 --- a/front/src/style/_main.scss +++ b/front/src/style/_main.scss @@ -75,6 +75,9 @@ // see https://github.com/webpack/webpack/issues/215 @import "./vendor/media"; +$desktop-sidebar-width: 300px; +$widedesktop-sidebar-width: 350px; + html, body { @include media("desktop") { + width: $desktop-sidebar-width; + } + + @include media(">widedesktop") { + width: $widedesktop-sidebar-width; + } +} .main.pusher, .footer { @include media(">desktop") { - margin-left: 350px !important; + margin-left: $desktop-sidebar-width !important; margin-top: 50px; } + + @include media(">widedesktop") { + margin-left: $widedesktop-sidebar-width !important;; + } transform: none !important; } @@ -118,11 +134,14 @@ body { } @include media(">desktop") { position: fixed; - left: 350px; + left: $desktop-sidebar-width; right: 0px; top: 0px; z-index: 99; } + @include media(">widedesktop") { + left: $widedesktop-sidebar-width; + } background-color: white; .item { padding-top: 1.5em; @@ -135,7 +154,10 @@ body { bottom: 1em; left: 1em; @include media(">desktop") { - left: 350px; + left: $desktop-sidebar-width; + } + @include media(">widedesktop") { + left: $widedesktop-sidebar-width; } } .main-pusher { @@ -143,9 +165,12 @@ body { } .ui.stripe.segment, #footer { - padding: 2em; + padding: 1em; @include media(">tablet") { - padding: 4em; + padding: 2em; + } + @include media(">widedesktop") { + padding: 3em; } } diff --git a/front/src/style/vendor/_media.scss b/front/src/style/vendor/_media.scss index 2328eff8c..8d24baa71 100644 --- a/front/src/style/vendor/_media.scss +++ b/front/src/style/vendor/_media.scss @@ -34,7 +34,8 @@ $breakpoints: ( 'phone': 320px, 'tablet': 768px, - 'desktop': 1024px + 'desktop': 1024px, + 'widedesktop': 1200px ) !default; diff --git a/front/src/views/content/libraries/FilesTable.vue b/front/src/views/content/libraries/FilesTable.vue index d63ad6cee..dcdd4b87d 100644 --- a/front/src/views/content/libraries/FilesTable.vue +++ b/front/src/views/content/libraries/FilesTable.vue @@ -73,7 +73,7 @@ diff --git a/front/src/views/content/libraries/Upload.vue b/front/src/views/content/libraries/Upload.vue index 1da3ad554..8547aa570 100644 --- a/front/src/views/content/libraries/Upload.vue +++ b/front/src/views/content/libraries/Upload.vue @@ -4,7 +4,7 @@
Loading library data…
- +
@@ -20,6 +20,19 @@ export default { components: { DetailArea, FileUpload + }, + beforeRouteLeave (to, from, next){ + if (this.$refs.fileupload.hasActiveUploads){ + const answer = window.confirm('This page is asking you to confirm that you want to leave - data you have entered may not be saved.') + if (answer) { + next() + } else { + next(false) + } + } + else{ + next() + } } } diff --git a/front/tests/unit/specs/filters/filters.spec.js b/front/tests/unit/specs/filters/filters.spec.js index 1464e5c97..f4f3610dc 100644 --- a/front/tests/unit/specs/filters/filters.spec.js +++ b/front/tests/unit/specs/filters/filters.spec.js @@ -1,5 +1,5 @@ import {expect} from 'chai' - +import moment from 'moment' import {truncate, ago, capitalize, year} from '@/filters' describe('filters', () => { @@ -24,7 +24,15 @@ describe('filters', () => { it('works', () => { const input = new Date() let output = ago(input) - expect(output).to.equal('a few seconds ago') + let expected = moment(input).calendar(input, { + sameDay: 'LT', + nextDay: 'L', + nextWeek: 'L', + lastDay: 'L', + lastWeek: 'L', + sameElse: 'L' + }) + expect(output).to.equal(expected) }) }) describe('year', () => {