summaryrefslogtreecommitdiffstats
path: root/CONTRIBUTING.rst
blob: b3ae3508212cd519808c68fe3657b0cadd72536d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
Contribute to reel2bits development
===================================

First of all, thank you for your interest in the project! We really
appreciate the fact that you're about to take some time to read this
and hack on the project.

This document will guide you through common operations such as:

- Setup your development environment
- Working on your first issue
- Writing unit tests to validate your work
- Submit your work

Quick summary of the various stacks / libraries we use
------------------------------------------------------

Backend:

- `Flask <https://flask.palletsprojects.com/en/1.1.x/>`_
- `SQLAlchemy <https://www.sqlalchemy.org/>`_
- `Gettext <https://www.gnu.org/software/gettext/>`_
- `Celery <http://www.celeryproject.org/>`_
- ActivityPub (through `little-boxes <https://little-boxes.readthedocs.io/en/latest/>`_ library)
- `PostgreSQL <https://www.postgresql.org/>`_
- `MJML <https://mjml.io/>`_ (npm install mjml -g) for Email layout templates

Frontend

- `VueJS <https://vuejs.org/>`_
- `Bootstrap-vue <https://bootstrap-vue.js.org/>`_
- `Gettext <https://www.gnu.org/software/gettext/>`_

A quick path to contribute on the front-end
-------------------------------------------

The next sections of this document include a full installation guide to help
you setup a local, development version of reel2bits.

As the front-end can work with any reel2bits server, you can work with the front-end only,
and make it talk with an existing instance (like the demo one, or you own instance, if you have one).

Setup front-end only development environment
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

1. Clone the repository::

    git clone https://github.com/rhaamo/reel2bits.git
    cd reel2bits
    cd front

2. Install `nodejs <https://nodejs.org/en/download/package-manager/>`_ and `yarn <https://yarnpkg.com/lang/en/docs/install/#debian-stable>`_

3. Install the dependencies::

    yarn install

4. Compile the translations::

    yarn i18n-compile

5. Configure your frontend for instance location::

    cp config/local.example.json config/local.json
    # Then edit config/local.json to point to any instance wanted
    # Don't forget to set the "host" to the right hostname the instance expect

6. Launch the development server::

    # this will serve the front-end on http://localhost:8081/
    npm run dev

7. Start hacking!

Setup your development environment
----------------------------------

If you want to fix a bug or implement a feature, you'll need
to run a local, development copy of reel2bits.

We provide a docker based development environment, which should
be both easy to setup and work similarly regardless of your
development machine setup.

Installing docker and docker-compose
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

This is already cover in the relevant documentations:

- https://docs.docker.com/install/
- https://docs.docker.com/compose/install/

A note about branches
^^^^^^^^^^^^^^^^^^^^^

Everything happens in ``master`` branch. Therefore, when submitting Merge Requests, ensure you are merging on the master branch.

Working with docker
^^^^^^^^^^^^^^^^^^^

In development, we use the docker-compose file named ``dev.yml``, residing in the reel2bits directory, and this is why all our docker-compose commands will look like this::

    docker-compose -f dev.yml logs

If you do not want to add the ``-f dev.yml`` snippet every time, you can run this command before starting your work::

    export COMPOSE_FILE=dev.yml


Creating your env file
^^^^^^^^^^^^^^^^^^^^^^

We provide a working .env.dev configuration file that is suitable for
development. However, to enable customization on your machine, you should
also create a .env file that will hold your personal environment
variables (those will not be commited to the project).

Create it like this::

    touch .env

Create docker network
^^^^^^^^^^^^^^^^^^^^^

Create the federation network::

    docker network create federation


Building the containers
^^^^^^^^^^^^^^^^^^^^^^^

On your initial clone, or if there have been some changes in the
app dependencies, you will have to rebuild your containers. This is done
via the following command::

    docker-compose -f dev.yml build


Database management
^^^^^^^^^^^^^^^^^^^

You first have to add an extension in the postgresql database, run this command one time::

    docker-compose -f dev.yml run --rm api psql -U postgres -h postgres -w -c 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp";' postgres

Then you can run the database migrations::

    docker-compose -f dev.yml run --rm api flask db upgrade

This will create all the tables needed for the API to run properly.
You will also need to run this whenever changes are made on the database
schema.

It is safe to run this command multiple times, so you can run it whenever
you fetch develop.

Then run the database seeds:

    docker-compose -f dev.yml run --rm api flask db-datas 000-seeds

You should run only one time this command.

Development data
^^^^^^^^^^^^^^^^

You'll need at least an admin user to work
locally.

Create an admin user with the following command::

    docker-compose -f dev.yml run --rm api flask users create


Launch all services
^^^^^^^^^^^^^^^^^^^

Before the first reel2bits launch, it is required to run this::

    docker-compose -f dev.yml run --rm front yarn run i18n-compile

Then you can run everything with::

    docker-compose -f dev.yml up front api nginx celeryworker

This will launch all services, and output the logs in your current terminal window.
If you prefer to launch them in the background instead, use the ``-d`` flag, and access the logs when you need it via ``docker-compose -f dev.yml logs --tail=50 --follow``.

Once everything is up, you can access the various funkwhale's components:

- The Vue webapp, on http://localhost:8081/
- The Backend API, on http://localhost:8000/home
- The documentation, on http://localhost:8001/ if you launch the 'docs' container.

Development note only:
Unfortunately because on how the frontend is made, we can't proxy it through http from the backend.
You have to access the frontend directly, which will automatically proxy the backend API.

Stopping everything
^^^^^^^^^^^^^^^^^^^

Once you're down with your work, you can stop running containers, if any, with::

    docker-compose -f dev.yml stop


Removing everything
^^^^^^^^^^^^^^^^^^^

If you want to wipe your development environment completely (e.g. if you want to start over from scratch), just run::

    docker-compose -f dev.yml down -v

This will wipe your containers and data, so please be careful before running it.

You can keep your data by removing the ``-v`` flag.


Working with federation locally
-------------------------------

This is not needed unless you need to work on federation-related features.

To achieve that, you'll need:

1. to update your dns resolver to resolve all your .dev hostnames locally
2. a reverse proxy (such as traefik or nginx) to catch those .dev requests and
   and with https certificate
3. two instances (or more) running locally, following the regular dev setup

Typical workflow for a contribution
-----------------------------------

0. Fork the project if you did not already or if you do not have access to the main repository
1. Checkout the development branch and pull most recent changes: ``git checkout master && git pull``
2. If working on an issue, assign yourself to the issue. Otherwise, consider open an issue before starting to work on something, especially for new features.
3. Create a dedicated branch for your work ``42-awesome-fix``. It is good practice to prefix your branch name with the ID of the issue you are solving.
4. Work on your stuff
5. Commit small, atomic changes to make it easier to review your contribution
6. Add a changelog fragment to summarize your changes: ``echo "Implemented awesome stuff (#42)" >> CHANGELOG``
7. Push your branch
8. Create your merge request
9. Take a step back and enjoy, we're really grateful you did all of this and took the time to contribute!

Internationalization
--------------------

We're using https://github.com/Polyconseil/vue-gettext to manage i18n in the project.

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 ``<translate>`` tag::

    <template>
      <div>
        <h1><translate translate-context="Content/Profile/Header">User profile</translate></h1>
        <p>
          <translate
            translate-context="Content/Profile/Paragraph"
            :translate-params="{username: 'alice'}">
            You are logged in as %{ username }
          </translate>
        </p>
         <p>
          <translate
            translate-context="Content/Profile/Paragraph"
            translate-plural="You have %{ count } new messages, that's a lot!"
            :translate-n="unreadMessagesCount"
            :translate-params="{count: unreadMessagesCount}">
            You have 1 new message
          </translate>
        </p>
      </div>
    </template>

Anything between the `<translate>` and `</translate>` 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 ``<translate>`` 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 reel2bits 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``,
reel2bits 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 reel2bits UI where the translated string is displayed:
    - ``Content``
    - ``Footer``
    - ``Head``
    - ``Menu``
    - ``*`` for strings that are not tied to a specific location

- The feature part, which is required, and refers to the feature/component associated with the translated string:
    - ``About``
    - ``AlbumEdit``
    - ``AlbumNew``
    - ``Login``
    - ``Logs(user)``
    - ``NotFound``
    - ``PasswordReset``
    - ``PasswordResetToken``
    - ``Register``
    - ``Timeline``
    - ``TimelineTabs``
    - ``TrackEdit``
    - ``TrackShow``
    - ``TrackUpload``
    - ``UserCard``
    - ``UserCardList``
    - ``UserFollowers``
    - ``UserFollowings``
    - ``UserSettings``
    - ``UserProfile``
    - ``*`` 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``
    - ``Feedback``
    - ``*`` 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``
    - ``Or anything useful``

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)``
- ``Header/*/Input/Search ARIA`` (ARIA html key)

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.

Extraction is done by calling ``yarn run i18n-extract``, which
will pull all the strings from source files and put them in a PO files.

You can then inspect the PO files to ensure everything is fine (but don't commit them, it's not needed).

Contributing to the Backend API
-------------------------------

Project structure
^^^^^^^^^^^^^^^^^

.. code-block:: shell

    backend (api/):
    .
    ├── controllers             # backend controllers, some are in this folder, which would be /something endpoints
    ├── └── api                 # anything under the /api/ namespace is here
    ├──     └── v1              # same for /api/v1/
    ├── activitypub             # ActivityPub related things (LittleBoxes backend mostly)
    ├── migrations              # Database migrations, always respect the format "<incr number>_<autogenerated thing>.py" for readability
    ├── templates               # Views rendered by the backend, or email templates
    └── tests                   # unit tests for the backend

    frontend (front/):
    .
    ├── build               # webpack and build related stuff
    ├── config              # configuration for frontend
    ├── locales             # translations locales
    ├── scripts             # helpers scripts
    ├── src
    │   ├── backend         # actually oauth related stuff   ├── boot            # setup of store and front settings   ├── components      # components of vue app   ├── lib             # actually persisted state handling   ├── modules         # modules shared by the whole app   ├── services        # some helpers   ├── translations    # translations files   └── views           # for things bigger than "components", more organised like views/tracks/Show.vue, views/tracks/Upload.vue, ...
    └── test                # testing stuff
        ├── e2e
        ├── fixtures
        └── unit

.. note::

    Unless trivial, API contributions must include unittests to ensure
    your fix or feature is working as expected and won't break in the future

Running tests
^^^^^^^^^^^^^

To run tests for backend::

    APP_SETTINGS="config.testing.Config"
    python setup.py test


Writing tests
^^^^^^^^^^^^^

Although teaching you how to write unit tests is outside of the scope of this
document, you'll find below a collection of tips, snippets and resources
you can use if you want to learn on that subject.

Useful links:

- `A quick introduction to unit test writing with pytest <https://semaphoreci.com/community/tutorials/testing-python-applications-with-pytest>`_
- `A complete guide to Test-Driven Development (although not using Pytest) <https://www.obeythetestinggoat.com/>`_
- `pytest <https://docs.pytest.org/en/latest/>`_: documentation of our testing engine and runner

Recommendations:

- Test files for must target a module and ideally mimic ``controllers`` directory structure: if you're writing tests for ``controllers/api/v1/foobar.py``, you should put thoses tests in ``tests/api/v1/foobar.py``
- Tests should be small and test one thing. If you need to test multiple things, write multiple tests.

We provide some utils and fixtures to make the process of writing tests as
painless as possible.

.. note::

    The back-end test suite coverage is still pretty low

Linters & format
^^^^^^^^^^^^^^^^

We use black and flake8::

    flake8 .
    black .

Various notes
^^^^^^^^^^^^^

- Authlib doesn't handle JSON, do crimes like in controllers/api/v1/auth.py#oauth_token()
- Authlib revoke token wants basic auth, no idea what to give, so it doesn't works
- Authlib does handle optional bearer auth, uses: @require_oauth(optional=True)

Translations notes
^^^^^^^^^^^^^^^^^^

While there is still some in the backend, and that is going to change.

Parse translation strings::

    pybabel extract -F babel.cfg -k gettext -o messages.pot .

create document::

    pybabel init -i messages.pot -d translations -l de

update document::

    pybabel update -i messages.pot -d translations

compile documents::

    pybabel compile -d translations

Contributing to the front-end
-----------------------------

Backend proxy
^^^^^^^^^^^^^

The frontend will automatically proxy the backend configured in ``config/local.json``.

Running tests
^^^^^^^^^^^^^

To run the front-end test suite, use the following command::

    cd front
    npm run unit

.. note::

    The front-end test suite coverage is still pretty low

Linters & format
^^^^^^^^^^^^^^^^

Check::

    npm run lint

Lazy autofix (check if nothing gots wrong)::

    npm run lint-fix

MJML for emails
---------------

Rebuilding the email html template::

    cd api/templates/email
    mmjml layout.mjml > ../layout_email.jinja2
    cd -