This commit is contained in:
Tasos Katsoulas 2023-11-23 15:17:57 +02:00
Родитель e50e001ccb
Коммит f9be3e6639
22 изменённых файлов: 2390 добавлений и 8 удалений

199
docs/api.md Normal file
Просмотреть файл

@ -0,0 +1,199 @@
---
title: API
---
SUMO has a series of API endpoints to access data.
::: contents
:::
# Search suggest API
Endpoint
: `/api/2/search/suggest/`
Method
: `GET`
Content type
: `application/json`
Response
: `application/json`
The search suggest API allows you to get back kb documents and aaq
questions that match specified arguments.
Arguments can be specified in the url querystring or in the HTTP request
body.
## Required arguments
------------------------------------------------------------------------
argument type notes
---------------- -------- ----------------------------------------------
q string This is the text you\'re querying for.
------------------------------------------------------------------------
## Optional arguments
+---------------+------+----------------------------------------------+
| argument | type | notes |
+===============+======+==============================================+
| locale | st | default: `settings.WIKI_DEFAULT_LANGUAGE` |
| | ring | |
| | | The locale code to restrict results to. |
| | | |
| | | Examples: |
| | | |
| | | - `en-US` |
| | | - `fr` |
+---------------+------+----------------------------------------------+
| product | st | default: None |
| | ring | |
| | | The product to restrict results to. |
| | | |
| | | Example: |
| | | |
| | | - `firefox` |
+---------------+------+----------------------------------------------+
| max_documents | int | default: 10 |
| | eger | |
| | | The maximum number of documents you want |
| | | back. |
+---------------+------+----------------------------------------------+
| max_questions | int | default: 10 |
| | eger | |
| | | The maximum number of questions you want |
| | | back. |
+---------------+------+----------------------------------------------+
## Responses
All response bodies are in JSON.
### HTTP 200: Success
With an HTTP 200, you\'ll get back a set of results in JSON.
{
"documents": [
{
"id": ... # id of kb article
"title": ... # title of kb article
"url": ... # url of kb article
"slug": ... # slug of kb article
"locale": ... # locale of the article
"products": ... # list of products for the article
"topics": ... # list of topics for the article
"summary": ... # paragraph summary of kb article (plaintext)
"html": ... # html of the article
}
...
],
"questions": [
{
"id": ... # integer id of the question
"answers": ... # list of answer ids
"content": ... # content of question (in html)
"created": ... # datetime stamp in iso-8601 format
"creator": ... # JSON object describing the creator
"involved": ... # list of JSON objects describing everyone who
participated in the question
"is_archived": ... # boolean for whether this question is archived
"is_locked": ... # boolean for whether this question is locked
"is_solved": ... # boolean for whether this question is solved
"is_spam": ... # boolean for whether this question is spam
"is_taken": ... # FIXME:
"last_answer": ... # id for the last answer
"num_answers": ... # total number of answers
"locale": ... # the locale for the question
"metadata": ... # metadata collected for the question
"tags": ... # tags for the question
"num_votes_past_week": ... # the number of votes in the last week
"num_votes": ... # the total number of votes
"product": ... # the product
"solution": ... # id of answer marked as a solution if any
"taken_until": ... # FIXME:
"taken_by": ... # FIXME:
"title": ... # title of the question
"topic": ... # FIXME:
"updated_by": ... # FIXME:
"updated": ... # FIXME:
},
...
]
}
## Examples
Using curl:
curl -X GET "http://localhost:8000/api/2/search/suggest/?q=videos"
curl -X GET "http://localhost:8000/api/2/search/suggest/?q=videos&max_documents=3&max_questions=3"
curl -X GET "http://localhost:8000/api/2/search/suggest/" \
-H "Content-Type: application/json" \
-d '
{
"q": "videos",
"max_documents": 3,
"max_questions": 0
}'
# Locales API
> All locales supported by SUMO.
>
> **Example request**:
>
> ``` http
> GET /api/2/locales/ HTTP/1.1
> Accept: application/json
> ```
>
> **Example response**:
>
> ``` http
> HTTP/1.0 200 OK
> Vary: Accept, X-Mobile, User-Agent
> Allow: OPTIONS, GET
> X-Frame-Options: DENY
> Content-Type: application/json
>
> {
> "vi": {
> "name": "Vietnamese",
> "localized_name": "Ti\u1ebfng Vi\u1ec7t",
> "aaq_enabled": false
> },
> "el": {
> "name": "Greek",
> "localized_name": "\u0395\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac",
> "aaq_enabled": false
> },
> "en-US": {
> "name": "English",
> "localized_name": "English",
> "aaq_enabled": true
> }
> }
> ```
>
> reqheader Accept
>
> : application/json
>
> resheader Content-Type
>
> : application/json
>
> statuscode 200
>
> : no error

Просмотреть файл

@ -0,0 +1,12 @@
---
title: Architectural Decision Records
---
We record major architectural decisions for Kitsune/SUMO in Architecture
Decision Records (ADR), as [described by Michael
Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).
Below is the list of our current ADRs.
::: {.toctree maxdepth="1" glob=""}
architecture/decisions/\*
:::

68
docs/badges.md Normal file
Просмотреть файл

@ -0,0 +1,68 @@
---
title: Badges
---
::: warning
::: title
Warning
:::
This section of documentation may be outdated.
:::
Badges in kitsune are based off of [Django
Badger](https://github.com/mozilla/django-badger),
As of Q3 2018, kitsune issues four badges per calendar year:
1. KB Badge
2. L10n Badge
3. Support Forum Badge
4. Army of Awesome Badge
A list of active badges can be seen at
[https://support.mozilla.org/badges/](https://support.mozilla.org/en-US/badges/).
# KB Badge & L10n Badge
The KB Badge is awarded after a user has reached 10 approved English
edits on knowledge base articles.
The L10n Badge is awarded after a user has reached 10 approved
translation edits on knowledge base articles.
Logic for both badges can be found in `kitsune.wiki.badges`.
The number of edits needed is configurable in
`settings.BADGE_LIMIT_L10N_KB`.
# Support Forum Badge
The Support Forum Badge is awarded after a user has reached 30 support
forum replies.
Logic for awarding this badge can be found in
`kitsune.questions.badges`.
The number of replies needed is configurable in
`settings.BADGE_LIMIT_SUPPORT_FORUM`.
# Army of Awesome Badge
::: warning
::: title
Warning
:::
This badge is no longer available.
:::
The Army of Awesome Badge is awarded when a user has tweeted 50 Army of
Awesome replies.
## Badge Creation
Badges are either created manually through the Django Admin *or* created
automatically via `get_or_create_badge` in `kitsune.kbadge.utils`.
Creation through the Django Admin is the usual and preferred method.

45
docs/celery.md Normal file
Просмотреть файл

@ -0,0 +1,45 @@
---
title: Celery
---
Kitsune uses [Celery](http://celeryproject.org/) to enable offline task
processing for long-running jobs like sending email notifications and
re-rendering the Knowledge Base.
Though Celery supports multiple message backends, we use
[Redis](https://redis.io/).
# When is Celery Appropriate
You can use Celery to do any processing that doesn\'t need to happen in
the current request-response cycle. Examples are generating thumbnails,
sending out notification emails, updating content that isn\'t about to
be displayed to the user, and others.
Ask yourself the question: \"Is the user going to need this data on the
page I\'m about to send them?\" If not, using a Celery task may be a
good choice.
# Configuring and Running
Celery will automatically start when you run:
make run
We set some reasonable defaults for Celery in `settings.py`. These can
be overriden in `.env`.
If you don\'t want to use Celery, you can set this in `.env`:
CELERY_TASK_ALWAYS_EAGER = True
Setting this to `True` causes all task processing to be done online.
This is useful when debugging tasks, for instance.
You can also configure the concurrency. Here is the default:
CELERY_WORKER_CONCURRENCY = 4
Then to restart the Celery workers, you just need to run:
docker-compose restart celery

25
docs/contactus.md Normal file
Просмотреть файл

@ -0,0 +1,25 @@
---
title: Contact us
---
# SUMO contributor forums
If you\'re a SUMO contributor, then consider using the [contributor
forums](https://support.mozilla.org/en-US/forums). This is the place for
SUMO community discussions.
# Kitsune hackers
If you\'re hacking on the Kitsune code and have questions, ping us on
[Matrix](https://wiki.mozilla.org/Matrix).
We hang out in
[#support-platform:mozilla.org](https://chat.mozilla.org/#/room/#support-platform:mozilla.org).
If you ask something and all you get is silence, then it\'s probably the
case that we\'re not around. Please try pinging us again.
Current developers:
- Tasos Katsoulas (tasos)
- Ryan Johnson (ryan)

49
docs/contributors.md Normal file
Просмотреть файл

@ -0,0 +1,49 @@
---
title: Join this project!
---
Kitsune is the software that runs [SUMO
(support.mozilla.org)](https://support.mozilla.org/) which provides
support for Firefox and other Mozilla software.
Interested in helping out? Here\'s a bunch of things we need your help
with.
# Help with support!
First off, you can help people get the most out of Firefox by joining
the awesome support community. This community not only helps people with
their Firefox issues, but is also the front line in helping drive
Firefox development.
For more information on this, see the [quickstart
guide](https://support.mozilla.org/en-US/get-involved) on the SUMO site.
# Help reporting bugs
Please report any bugs you find with Kitsune on Bugzilla:
<https://bugzilla.mozilla.org/enter_bug.cgi?product=support.mozilla.org>
# Help with hacking!
First step is to set up Kitsune so you can run it and hack on it. For
that, see `hacking_howto`{.interpreted-text role="any"}.
If you have problems, please let us know! See
`contact-us-chapter`{.interpreted-text role="ref"}.
# Help with making Kitsune easier for hacking on!
We\'re working on making Kitsune easier to hack on. This entails:
- reducing the steps it takes to get Kitsune running down to a smaller
minimal set
- making this documentation better
- providing better resources for people who are interested in helping
out
- providing better scripts to automate installing and maintaining
Kitsune
Any thoughts you have on making this easier are much appreciated.
Further, if you could help us, that\'d be valuable to us and all those
who follow in your footsteps.

68
docs/conventions.md Normal file
Просмотреть файл

@ -0,0 +1,68 @@
---
title: Conventions
---
This document contains coding conventions, and things to watch out for,
etc.
# Coding conventions
We follow most of the practices as detailed in the [Mozilla webdev
bootcamp
guide](https://mozweb.readthedocs.io/en/latest/guide/development_process.html).
It is recommended that you
`install pre-commit<hacking_howto:Install linting tools>`{.interpreted-text
role="ref"}.
## Type hints
When creating and/or modifying Python functions/methods, we add [type
hints](https://docs.python.org/3/library/typing.html) to their arguments
and result, but only when it makes sense. See
`our Architectural Decision Record<architecture/decisions/0004-type-checking>`{.interpreted-text
role="doc"} for more details.
# Git conventions
## Git workflow
See `patching`{.interpreted-text role="ref"} for how we use Git,
branches and merging.
## Git commit messages
Git commit messages should have the following form:
[bug xxxxxxx] Short summary
Longer explanation with paragraphs and lists and all that where
each line is under 72 characters.
* bullet 1
* bullet 2
Etc. etc.
Summary line should be capitalized, short and shouldn\'t exceed 50
characters. Why? Because this is a convention many git tools take
advantage of.
If the commit relates to a bug, the bug should show up in the summary
line in brackets.
There should be a blank line between the summary and the rest of the
commit message. Lines shouldn\'t exceed 72 characters.
See [these
guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
for some more explanation.
## Git resources and tools
See [Webdev bootcamp
guide](https://mozweb.readthedocs.io/en/latest/reference/git_github.html)
for:
- helpful resources for learning git
- helpful tools

57
docs/deployments.md Normal file
Просмотреть файл

@ -0,0 +1,57 @@
---
title: Kitsune Deployments
---
This documents the current development (dev), staging and production
(prod) servers for the `support.mozilla.com` instance of
[Kitsune](https://github.com/mozilla/kitsune).
# The Source
All of the source code for Kitsune lives in [a single Github
repo](https://github.com/mozilla/kitsune).
# Branches
## main
The `main` branch is our main integration points. All new patches should
be based on the latest `main` (or rebased to it).
Pull requests are created from those branches. Pull requests may be
opened at any time, including before any code has been written.
Pull requests get reviewed.
Once reviewed, the branch is merged into `main`, except in special cases
such as changes that require re-indexing. See
`Changes that involve reindexing <changes_reindexing>`{.interpreted-text
role="ref"}.
We deploy to production from `main`.
# Deploying
We currently use Kubernetes for our infrastructure, see the instructions
here for how to do deployments:
<https://github.com/mozilla/kitsune/blob/main/k8s/README.md>
# Servers
## Development
<https://support-dev.allizom.org/>
We use dev primarily to develop infrastructure changes.
## Staging
<https://support.allizom.org/>
We deploy to stage anything we want to test including deployments
themselves.
## Production
<https://support.mozilla.org/>

231
docs/development.md Normal file
Просмотреть файл

@ -0,0 +1,231 @@
---
title: Development
---
This covers loosely how we do big feature changes.
# Changes that involve new Python dependencies
All python dependencies have an associated hash (or several) that are
checked at download time. This ensures malicious code doesn\'t sneak in
through dependencies being hacked, and also makes sure we always get the
exact code we developed against. Changes in dependencies, malicious or
not, will set off red flags and require human intervention.
A pip requirement stanza with hashes looks something like this:
Django==1.8.15 \
--hash=sha256:e2e41aeb4fb757575021621dc28fceb9ad137879ae0b854067f1726d9a772807 \
--hash=sha256:863e543ac985d5cfbce09213fa30bc7c802cbdf60d6db8b5f9dab41e1341eacd
hash lines can be repeated, and other comments can be added. The stanza
is delimited by non-comment lines (such as blank lines or other
requirements).
Fortunately we do not need to add or edit those manaully. Using
[pip-compile-multi](https://github.com/peterdemin/pip-compile-multi) we
list only our top-level dependencies in
[requirements/\*.in]{.title-ref}. To add a dependency, put it in the
appropriate [requirements/\*.in]{.title-ref} file, then compile:
pip-compile-multi -g default
# Changes that involve database migrations
Any changes to the database (model fields, model field data, adding
permissions, \...) require a migration.
## Running migrations
To run migrations, you do:
$ ./manage.py migrate
It\'ll perform any migrations that haven\'t been performed for all apps.
## Creating a schema migration
To create a new migration the automatic way:
1. make your model changes
2. run:
./manage.py makemigrations <app>
where `<app>` is the app name (sumo, wiki, questions, \...).
3. run the migration on your machine:
./manage.py migrate
4. run the tests to make sure everything works
5. add the new migration files to git
6. commit
::: seealso
<https://docs.djangoproject.com/en/stable/topics/migrations/#adding-migrations-to-apps>
: Django documentation: Adding migrations to apps
:::
# Creating a data migration
Creating data migrations is pretty straight-forward in most cases.
To create a data migration the automatic way:
1. run:
./manage.py makemigrations --empty <app>
where `<app>` is the app name (sumo, wiki, questions, \...).
2. edit the data migration you just created to do what you need it to
do
3. make sure to add [reverse_code]{.title-ref} arguments to all
[RunPython]{.title-ref} operations which undoes the changes
4. add a module-level docstring explaining what this migration is doing
5. run the migration forwards and backwards to make sure it works
correctly
6. add the new migration file to git
7. commit
::: seealso
<https://docs.djangoproject.com/en/stable/topics/migrations/#data-migrations>
: Django documentation: Data Migrations
:::
::: seealso
<https://docs.djangoproject.com/en/stable/ref/migration-operations/#runpython>
:::
## Data migrations for data in non-kitsune apps
If you\'re doing a data migration that adds data to an app that\'s not
part of kitsune, but is instead a library (e.g. django-waffle), then
create the data migration in the sumo app and add a dependency to the
latest migration in the library app.
For example, this adds a dependency to django-waffle\'s initial
migration:
class Migration(migrations.Migration):
dependencies = [
...
('waffle', '0001_initial'),
...
]
# Changes that involve reindexing {#changes_reindexing}
With Elastic Search, it takes a while to reindex. We need to be able to
reindex without taking down search.
This walks through the workflow for making changes to our Elastic Search
code that require reindexing.
## Things about non-trivial changes
1. We should roll multiple reindex-requiring changes into megapacks
when it makes sense and doesn\'t add complexity.
2. Developers should test changes with recent sumo dumps.
## Workflow for making the changes
1. work on the changes in a separate branch (just like everything else
we do)
2. make a pull request
3. get the pull request reviewed
4. rebase the changes so they\'re in two commits:
1. a stage 1 commit that changes `ES_WRITE_INDEXES`, updates the
mappings and updates the indexing code
2. a stage 2 commit that changes `ES_INDEXES`, changes
`ES_WRITE_INDEXES`, and changes the search view code
**Avoid cosmetic changes that don\'t need to be made (e.g. pep-8
fixes, etc.)**
5. push those changes to the same pull request
6. get those two changes reviewed
Once that\'s ok, then that branch should get updated from main, then
pushed to stage to get tested.
That branch should **not** land in main, yet.
## Workflow for reviewing changes
Go through and do a normal review.
After everything looks good, the developer should rebase the changes so
they\'re in a stage 1 commit and a stage 2 commit.
At that point:
1. Verify each commit individually. Make sure the code is correct. Make
sure the tests pass. Make sure the site is functional.
2. Verify that the `ES_INDEXES` and `ES_WRITE_INDEXES` settings have
the correct values in each commit.
## Workflow for pushing changes to stage
Don\'t land the changes in main, yet!
If you hit problems, deploy the main branch back to the stage server and
go back to coding/fixing.
1. Push the branch you have your changes in to the official
mozilla/kitsune remote.
2. Deploy the stage 1 commit to stage.
3. Verify that search still works.
4. Verify that the index settings are correct\-\--look at the
`ES_INDEXES` and `ES_WRITE_INDEXES` values.
5. Destructively reindex.
6. Deploy the stage 2 commit to stage.
7. Verify that search still works.
8. Verify that the index settings are correct\-\--look at the
`ES_INDEXES` and `ES_WRITE_INDEXES` values.
9. Verify bugs that were fixed with the new search code.
## Workflow for pushing those changes to production
If we\'re also doing a production push, first push next to production
and verify that everything is fine. Then continue.
1. Tell the other sumo devs to hold off on pushing to main branch and
deploying. Preferably by email and IRC.
2. Once you\'ve told everyone, land the changes in main.
3. Deploy the stage 1 commit to production.
4. Verify that search works.
5. Destructively reindex to the new write index.
6. When reindexing is done, push the stage 2 commit to production.
7. Verify that search works.
8. Verify bugs that were fixed with the new search code.
Pretty sure this process allows us to back out at any time with minimal
downtime.
## On the next day
If everything is still fine, then merge the special branch into main and
delete the old read index.
Announce \"STUCK THE LANDING!\" after a successful mapping change
deployment.

31
docs/email.md Normal file
Просмотреть файл

@ -0,0 +1,31 @@
---
title: Email from Kitsune
---
The default settings for Kitsune *do not send email*. However, outgoing
email is printed to the the command line.
# Viewing email through Mailcatcher
To view the contents of outgoing email in a slightly easier form than
the command line, [Mailcatcher](https://mailcatcher.me/) can be used.
This still won\'t send the email, but show a web-based \"outbox\" with
the contents of all email which would be sent if Kitsune was hooked up
to an email server.
The docker-compose config includes a mailcatcher container, which can be
brought up with:
docker-compose up mailcatcher
Kitsune should then be configured to use it:
EMAIL_LOGGING_REAL_BACKEND = django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST = mailcatcher
EMAIL_HOST_USER =
EMAIL_HOST_PASSWORD =
EMAIL_PORT = 1025
EMAIL_USE_TLS = False
Now all outgoing email will be captured, and can be viewed through
<http://localhost:1080/>.

29
docs/index.md Normal file
Просмотреть файл

@ -0,0 +1,29 @@
---
title: Welcome to Kitsune\'s documentation!
---
# Part 1: Contributor\'s Guide
::: {.toctree maxdepth="2"}
contributors hacking_howto contactus
:::
# Part 2: Developer\'s Guide
::: {.toctree maxdepth="2"}
conventions patching development tests celery email localization
elastic_search frontend svelte browser_permissions zendesk seo notes
switching_devices
:::
# Part 3: SUMO
::: {.toctree maxdepth="2"}
api deployments k8s sla architectural-decisions
:::
# Part 4: User Guide
::: {.toctree maxdepth="2"}
users questions badges advanced-search
:::

520
docs/localization.md Normal file
Просмотреть файл

@ -0,0 +1,520 @@
---
title: Localization
---
::: {.contents local=""}
:::
Kitsune is localized with
[gettext](http://www.gnu.org/software/gettext/). User-facing strings in
the code or templates need to be marked for gettext localization.
We use [Pontoon](https://pontoon.mozilla.org/) to provide an easy
interface to localizing these files. Localizers are also free to
download the PO files and use whatever tool they are comfortable with.
# Making Strings Localizable
Making strings in templates localizable is exceptionally easy. Making
strings in Python localizable is a little more complicated. The short
answer, though, is just wrap the string in `_()`.
## Interpolation
A string is often a combination of a fixed string and something
changing, for example, `Welcome, James` is a combination of the fixed
part `Welcome,`, and the changing part `James`. The naive solution is to
localize the first part and the follow it with the name:
_('Welcome, ') + username
This is **wrong!**
In some locales, the word order may be different. Use Python string
formatting to interpolate the changing part into the string:
_('Welcome, {name}').format(name=username)
Python gives you a lot of ways to interpolate strings. The best way is
to use Py3k formatting and kwargs. That\'s the clearest for localizers.
The worst way is to use `%(label)s`, as localizers seem to have all
manner of trouble with it. Options like `%s` and `{0}` are somewhere in
the middle, and generally OK if it\'s clear from context what they will
be.
## Localization Comments
Sometimes, it can help localizers to describe where a string comes from,
particularly if it can be difficult to find in the interface, or is not
very self-descriptive (e.g. very short strings). If you immediately
precede the string with a comment that starts `L10n:`, the comment will
be added to the PO file, and visible to localizers.
Example:
rev_data.append({
'x': 1000 * int(time.mktime(rdate.timetuple())),
# L10n: 'R' is the first letter of "Revision".
'title': _('R', 'revision_heading'),
'text': str(_('Revision %s')) % rev.created
#'url': 'http://www.google.com/' # Not supported yet
})
## Adding Context with msgctxt
Strings may be the same in English, but different in other languages.
English, for example, has no grammatical gender, and sometimes the noun
and verb forms of a word are identical.
To make it possible to localize these correctly, we can add \"context\"
(known in gettext as \"msgctxt\") to differentiate two otherwise
identical strings.
For example, the string \"Search\" may be a noun or a verb in English.
In a heading, it may be considered a noun, but on a button, it may be a
verb. It\'s appropriate to add a context (like \"button\") to one of
them.
Generally, we should only add context if we are sure the strings aren\'t
used in the same way, or if localizers ask us to.
Example:
from tower import ugettext as _
...
foo = _('Search', context='text for the search button on the form')
## Plurals
\"You have 1 new messages\" grates on discerning ears. Fortunately,
gettext gives us a way to fix that in English *and* other locales, the
`ngettext` function:
ngettext('singular', 'plural', count)
A more realistic example might be:
ngettext('Found {count} result.',
'Found {count} results',
len(results)).format(count=len(results))
This method takes three arguments because English only needs three,
i.e., zero is considered \"plural\" for English. Other locales may have
different plural rules, and require different phrases for, say 0, 1,
2-3, 4-10, \>10. That\'s absolutely fine, and gettext makes it possible.
## Strings in HTML Templates
When putting new text into a template, all you need to do is wrap it in
a `_()` call:
<h1>{{ _('Heading') }}</h1>
Adding context is easy, too:
<h1>{{ _('Heading', 'context') }}</h1>
L10n comments need to be Jinja2 comments:
{# L10n: Describes this heading #}
<h1>{{ _('Heading') }}</h1>
Note that Jinja2 escapes all content output through `{{ }}` by default.
To put HTML in a string, you\'ll need to add the `|safe` filter:
<h1>{{ _('Firefox <span>Help</span>')|safe }}</h1>
To interpolate, you should use one of two Jinja2 filters: `|f()` or, in
some cases, `|fe()`. `|f()` has exactly the same arguments as
`u''.format()`:
{{ _('Welcome, {name}!')|f(name=request.user.username) }}
The `|fe()` is exactly like the `|f()` filter, but escapes its arguments
before interpolating, then returns a \"safe\" object. Use it when the
localized string contains HTML:
{{ _('Found <strong>{0}</strong> results.')|fe(num_results) }}
Note that you *do not need* to use `|safe` with `|fe()`. Also note that
while it may look similar, the following is *not* safe:
{{ _('Found <strong>{0}</strong> results.')|f(num_results)|safe }}
The `ngettext` function is also available:
{{ ngettext('Found {0} result.',
'Found {0} results.',
num_results)|f(num_results) }}
### Using `{% trans %}` Blocks for Long Strings
When a string is very long, i.e. long enough to make Github scroll
sideways, it should be line-broken and put in a `{% trans %}` block.
`{% trans %}` blocks work like other block-level tags in Jinja2, except
they cannot have other tags, except strings, inside them.
The only thing that should be inside a `{% trans %}` block is printing a
string with `{{ string }}`. These are defined in the opening
`{% trans %}` tag:
{% trans user=request.user.username %}
Thanks for registering, {{ user }}! We're so...
hope that you'll...
{% endtrans %}
You can also provide comments:
{# L10n: User is a username #}
{% trans user=request.user.username %}
Thanks for registering, {{ user }}! We're so...
hope that you'll...
{% endtrans %}
When a block contains HTML with attributes, those which don\'t need to
be localized should be passed as arguments. This ensures strings won\'t
need to be re-localized if those attributes change:
{% trans url="http://example.com" %}
Please visit <a href="{{ url }}" title="External Site">our FAQ</a> for more information.
{% endtrans %}
## Strings in Python
::: note
::: title
Note
:::
Whenever you are adding a string in Python, ask yourself if it really
needs to be there, or if it should be in the template. Keep logic and
presentation separate!
:::
Strings in Python are more complex for two reasons:
1. We need to make sure we\'re always using Unicode strings and the
Unicode-friendly versions of the functions.
2. If you use the `ugettext` function in the wrong place, the string
may end up in the wrong locale!
Here\'s how you might localize a string in a view:
from tower import ugettext as _
def my_view(request):
if request.user.is_superuser:
msg = _(u'Oh hi, staff!')
else:
msg = _(u'You are not staff!')
Interpolation is done through normal Python string formatting:
msg = _(u'Oh, hi, {user}').format(user=request.user.username)
`ugettext` supports context, too:
msg = _('Search', 'context')
L10n comments are normal one-line Python comments:
# L10n: A message to users.
msg = _(u'Oh, hi there!')
If you need to use plurals, import the function `ungettext` from Tower:
from tower import ungettext, ugettext as _
n = len(results)
msg = ungettext('Found {0} result', 'Found {0} results', n).format(n)
### Lazily Translated Strings
You can use `ugettext` or `ungettext` only in views or functions called
from views. If the function will be evaluated when the module is loaded,
then the string may end up in English or the locale of the last request!
(We\'re tracking down that issue.)
Examples include strings in module-level code, arguments to functions in
class definitions, strings in functions called from outside the context
of a view. To localize these strings, you need to use the `_lazy`
versions of the above methods, `ugettext_lazy` and `ungettext_lazy`. The
result doesn\'t get translated until it is evaluated as a string, for
example by being output or passed to `str()`:
from tower import ugettext_lazy as _lazy
PAGE_TITLE = _lazy(u'Page Title')
`ugettext_lazy` also supports context.
It is very important to pass Unicode objects to the `_lazy` versions of
these functions. Failure to do so results in significant issues when
they are evaluated as strings.
If you need to work with a lazily-translated string, you\'ll first need
to convert it to a `str` object:
from tower import ugettext_lazy as _lazy
WELCOME = _lazy(u'Welcome, %s')
def my_view(request):
# Fails:
WELCOME % request.user.username
# Works:
str(WELCOME) % request.user.username
## Strings in the Database
There is some user generated content that needs to be localizable. For
example, karma titles can be created in the admin site and need to be
localized when displayed to users. A django management command is used
for this. The first step to making a model\'s field localizable is
adding it to `DB_LOCALIZE` in `settings.py`:
``` python
DB_LOCALIZE = {
'karma': {
'Title': {
'attrs': ['name'],
'comments': ['This is a karma title.'],
}
},
'appname': {
'ModelName': {
'attrs': ['field_name'],
'comments': ['Optional comments for localizers.'],
}
}
}
```
Then, all you need to do is run the `extract_db` management command:
$ python manage.py extract_db
*Be sure to have a recent database from production when running the
command.*
By default, this will write all the strings to
[kitsune/sumo/db_strings.py]{.title-ref} and they will get picked up
during the normal string extraction (see below).
## Strings in Email Templates
Currently, email templates are text-based and not in HTML. Because of
that you should use this style guide:
1. The entire email should be wrapped in autoescape. e.g.
``` {.html+jinja linenos=""}
{% autoescape false %}
{% trans %}
The entire email should be wrapped in autoescape.
{% endtrans %}
...
{% endautoescape %}
```
2. After an `{% endtrans %}`, you need two blank lines (three carriage
returns). The first is eaten by the tag. The other two show up in
the email. e.g.
``` {.jinja linenos=""}
{% trans %}
To confirm your subscription, stand up, put your hands on
your hips and do the hokey pokey.
{% endtrans %}
{{ _('Thanks!') }}
```
Produces this:
``` {.text linenos=""}
To confirm your subscription, stand up, put your hands on
your hips and do the hokey pokey.
Thanks!
```
3. Putting in line breaks in a `trans` block doesn\'t have an effect
since `trans` blocks get gettexted and whitespace is collapsed.
# Testing localized strings
When we add strings that need to be localized, it can take a couple of
weeks for us to get translations of those localized strings. This makes
it difficult to find localization issues.
Enter [Dennis](https://github.com/willkg/dennis/).
Run:
$ ./scripts/test_locales.sh
It\'ll extract all the strings, create a `.pot` file, then create a
Pirate translation of all strings. The Pirate strings are available in
the xx locale. After running the `test_locales.sh` script, you can
access the xx locale with:
> <http://localhost:8000/xx/>
Strings in the Pirate translation have the following properties:
1. they are longer than the English string: helps us find layout and
wrapping issues
2. they have at least one unicode character: helps us find unicode
issues
3. they are easily discernable from the English versions: helps us find
strings that aren\'t translated
::: note
::: title
Note
:::
The xx locale is only available on your local machine. It is not
available on -dev, -stage, or -prod.
:::
# Linting localized strings
You can lint localized strings for warnings and errors:
$ dennis-cmd lint locale/
Or just errors:
$ dennis-cmd lint --errorsonly locale/
You can see help text:
$ dennis-cmd
# Getting the Localizations {#getting-localizations}
Localizations are not stored in this repository, but are in a separate
Git repo:
> <https://github.com/mozilla-l10n/sumo-l10n>
You don\'t need the localization files for general development. However,
if you need them for something, they\'re pretty easy to get:
$ cd kitsune
$ git clone https://github.com/mozilla-l10n/sumo-l10n locale
# Updating the Localizations
When strings are added or updated, we need to update the templates and
PO files for localizers. Updating strings is pretty easy. Check out the
localizations as above, then:
$ python manage.py extract
$ python manage.py merge
Congratulations! You\'ve now updated the POT and PO files.
Sometimes this can leave a bunch of garbage files with `.po~`
extensions. You should delete these, never commit them:
$ find . -name "*.po~" -delete
## Adding a New Locale
Say you wanted to add `fa-IR`:
$ mkdir -p locale/fa-IR/LC_MESSAGES
$ python manage.py merge
Then add \'fa-IR\' to SUMO_LANGUAGES in settings.py and make sure there
is an entry in lib/languages.json (if not, add it).
And finally, add a migration with:
INSERT INTO `wiki_locale` (`locale`) VALUES ('fa-IR');
Done!
# Compiling MO Files
gettext is so fast for localization because it doesn\'t parse text
files, it reads a binary format. You can easily compile that binary file
from the PO files in the repository.
We don\'t store MO files in the repository because they need to change
every time the corresponding PO file changes, so it\'s silly and not
worth it. They are ignored by `.gitignore`, but please make sure you
don\'t forcibly add them to the repository.
There is a shell script to compile the MO files for you:
$ ./locale/compile-mo.sh locale
Done!
# Why aren\'t localized strings getting updated on prod?
We use Dennis to
`lint .po files for errors<localization:Linting localized strings>`{.interpreted-text
role="ref"} that cause HTTP 500 errors in production. Things like
malformed variables, variables in the translated string that aren\'t in
the original and that sort of thing.
For example, this would cause the site to break:
#: kitsune/questions/templates/questions/includes/answer.html:19
msgid "{num} answers"
msgstr "{0} antwoorden"
In this example, the `{0}` is wrong.
## Reporting errors in .po files
When we do a deployment to production, we dump all the Dennis output
into:
<https://support.mozilla.org/static/postatus.txt>
We need to check that periodically and report the errors.
If there are errors in those files, we need to open up a bug in
**Mozilla Localizations** -\> *locale code* with the specifics.
Product:
> Mozilla Localizations
Component:
> The locale code for the language in question
Bug summary:
> Use the error line
Bug description template:
> We found errors in the translated strings for Mozilla Support
> <https://support.mozilla.org/>. The errors are as follows:
>
>
> <paste errors here>
>
>
> Until these errors are fixed, we can't deploy updates to the
> strings for this locale to production.
>
> Mozilla Support strings can be fixed in the Support Mozilla project
> in Pontoon <https://pontoon.mozilla.org/projects/sumo/>.
>
> If you have any questions, let us know.

28
docs/notes.md Normal file
Просмотреть файл

@ -0,0 +1,28 @@
---
title: Other Notes
---
::: warning
::: title
Warning
:::
This section of documentation may be outdated.
:::
# Questions
## memcached
::: note
::: title
Note
:::
This should probably be somewhere else, but the easy way to flush your
cache is something like this:
echo "flush_all" | nc localhost 11211
Assuming you have memcache configured to listen to 11211.
:::

294
docs/patching.md Normal file
Просмотреть файл

@ -0,0 +1,294 @@
---
title: Patching Kitsune
---
::: warning
::: title
Warning
:::
This section of documentation may be outdated.
:::
Submitting a patch to [Kitsune](https://support.mozilla.com) is easy!
(Fair warning: writing the patch may not be ;)
We use [pull requests](https://github.com/mozilla/kitsune/pulls) to
manage patches and code reviews, and
[Bugzilla](https://bugzilla.mozilla.org) to handle actual bug tracking.
Because of our infrastructure and how we do deployments, we\'ve
developed a fairly straight-forward workflow in git for submitting
patches. This is outlined below.
You should run the tests before submitting a pull request. You can find
help for getting set up in the
`installation docs <hacking_howto>`{.interpreted-text role="any"} and
help for running tests in the
`testing docs <tests-chapter>`{.interpreted-text role="ref"}.
If you ever find yourself stuck,
`contact us <contactus>`{.interpreted-text role="any"}. We\'re happy to
help!
You\'ll need a Github account and a Bugzilla account.
# The Quick and Dirty
Very quick, very little explanation. Those with strong git fu may
already see some shortcuts. Use them!
First, clone your fork, and then point the main branch to Mozilla\'s
fork. Assuming your Github account is `foobar` and you\'ve already
forked Kitsune:
git clone https://github.com/foobar/kitsune
cd kitsune
git remote add mozilla https://github.com/mozilla/kitsune.git
git fetch mozilla
git checkout -t mozilla/main -B main
If you haven\'t set up your local git user, please do before committing
any code for Kitsune. This way you can take credit for your work:
git config user.email your@github.email
git config user.name "Your Name"
You should only need to do that once. Here\'s the bit to do every time:
git checkout main
git reset --hard mozilla/main
git checkout -b my-feature-123456
# Make a change and commit it.
$EDITOR path/to/file.py
git add path/to/file.py
git commit -m "[Bug 123456] Fooing and the Barring."
git push --set-upstream origin my-feature
# Open a pull request, get review.
# Respond to feedback:
$EDITOR path/to/file.py
git add path/to/file.py
git commit -m "Feedback from Barfoo"
git push
Eventually you\'ll get an r+. If you have commit access, now you can go
ahead and merge your branch. You may, if you want, rebase your branch to
clean up any embarrassing mistakes, but it isn\'t required. If you
don\'t have commit access the next part will be done by someone who
does.
There are two options. The first is to press the Big Green Button in
GitHub PRs that says \"Merge pull Request\". If you would prefer to do
it manually (or if there are merge conflicts, you can do this:
# r+! Merge
git checkout main
git fetch mozilla
git reset --hard mozilla/main
git merge --no-ff my-feature-123456
git push mozilla main # Bots will alert everyone!
git push origin main # Optional but nice.
After the pull request is closed:
git push origin :my-feature # Delete the remote branch. Nice to others.
git branch -D my-feature # Delete the local branch, if you're done.
# The Details
This is the process in more detail, for a relatively small change that
will only need one commit, and doesn\'t need any special treatment, like
landing on special branches.
## Fork and Clone Kitsune
On Github, hit the **Fork** button. You\'ll want to clone **your** fork
of the project, at least initially:
git clone git@github.com:<yourname>/kitsune.git
To help keep up to date, you should add `mozilla/kitsune` as a remote:
cd kitsune
git remote add mozilla https://github.com/mozilla/kitsune.git
You should avoid changing your `main` branch, it should track
`mozilla/main`. This can help:
git fetch mozilla
# Update your main branch to track Mozilla's main branch instead.
git checkout -B main -t mozilla/main # Update your main branch to
If you haven\'t set up your local git user, please do before committing
any code for Kitsune. This way you can take credit for your work:
git config user.email your@github.email
git config user.name "Your Name"
The correct way to keep your local main up to date is:
git checkout main
git fetch mozilla
git reset --hard mozilla/main
This will forcibly move your local main branch to whatever is on the
Mozilla main branch, destroying anything you have committed that wasn\'t
pushed. Remember to always work on a branch that is not main!
## Find a Bug
Step one is to make sure there\'s a bug in Bugzilla. Obvious \"bugs\"
just need a Bugzilla bug to track the work for all the involved teams.
There are [a number of open bugs](http://bit.ly/LUTjcY) if you want to
try your hand at fixing something!
New features or changes to features need bugs to build a consensus of
developers, support team members, and community members, before we
decide to make the change. If you want to change something like this, be
sure to file the bug and get a consensus first. We\'d hate to have you
spend time on a patch we can\'t take.
## Take the Bug
To make sure no one else is working on the bug at the same time, assign
it to yourself in Bugzilla. If you have the proper permissions there\'s
an easy \"take\" link next to the Assignee field. Ask in the IRC for
details.
You can assign bugs to yourself even if you aren\'t going to immediately
work on them (though make sure you will get to them sooner rather than
later). Once you are actively working on a bug, set the bug to the
`ASSIGNED` state.
## Fix the Bug on a Branch
::: note
::: title
Note
:::
This describes the process for fixing a relatively small bug in a
single-commit. Large features may differ.
:::
All bug fixes, changes, new features, etc, should be done on a \"feature
branch\", which just means \"any branch besides `main`.\" You should
make sure your local `main` branch is up to date (see above) before
starting a new feature branch. Your feature branch should include the
bug number in the branch name, if applicable.
git checkout main
git fetch mozilla
git reset --hard upstream/main # Update local main.
git checkout -b my-feature-branch-123456 # Some logical name.
Now you\'re on a feature branch, go ahead and make your changes.
Assuming you haven\'t added any new files, you can do:
git commit -a -m "[Bug 123456] Fix the foo and the bar."
If you did add new files, you will have to `git add` them before
committing.
Note that the commit message contains the bug number after the word
\"Bug\". This helps us and our IRC bots!
## Open a Pull Request
Once you have the bug fixed locally, you\'ll need to push the changes up
to Github so you can open a pull request.
git push --set-upstream origin my-feature-branch
Then, in your browser, navigate to
`https://github.com/<yourname>/kitsune/compare/my-feature-branch` and
hit the **Pull Request** button. If the commit message is clear, the
form should be filled out enough for you to submit it right away.
We add an `r?` in the pull request message indicating that this pull
request is ready to go and is looking for someone to review it.
Othertimes you may want to open a pull request early that isn\'t quite
ready to merge. This is a great way to share the work that you are
doing, and get early feedback. Make it clear that your PR isn\'t ready
by putting `[WIP]` in the title. Also make sure to say when it is ready!
The best way to do this is to remove `[WIP]` from the title and make a
comment asking for `r?`.
## Respond to Review
It\'s very rare that pull requests will be checked in immediately. Most
of the time they will go through one or more rounds of code review and
clean-up.
Code review is usually comments made on the pull request or commits in
Github, asking for specific changes to be made. If the requested change
isn\'t clear, or you disagree with it, feel free to ask questions
inline. Isn\'t Github\'s line-by-line commenting great?
Assuming a few small changes need to be made, make the changes locally
on the feature branch, then put them in a *new commit*. This makes it
easier from reviewers. For example, if Erik reviewed the pull request
and asked for some fixes, you might do this:
git checkout my-feature-branch
# Make the changes.
git commit -a -m "Feedback from Erik."
git push origin my-feature-branch
Github will automatically add the new commit to the pull request, so
we\'ll see it. Leaving it in a separate commit at this stage helps the
reviewer see what changes you\'ve made.
There may be more than one round of feedback, especially for complex
bugs. The process is exactly the same after each round: make the
changes, add them in yet another new commit, push the changes.
There are also a few bots that might interact with your PR. In
particular, our continuous integration service will run tests and style
checks on your new code. All PRs must be approved by the CI system
before they will be merged, so watch out. They show up as either a red X
or a green check mark in the PR.
## Ready to Merge!
Once a pull request has gotten an `r+` (\"R-plus\", it\'s from Bugzilla)
it\'s ready to merge in. At this point you can rebase and squash any
feedback/fixup commits you want, but this isn\'t required.
If you don\'t have commit access, someone who does may do this for you,
if they have time. Alternatively, if you have commit access, you can
press GitHub\'s \"Merge pull request\" button, which does a similar
process to below. This is the preferred way to merge PRs when there are
no complications.
git checkout main
git reset --hard mozilla/main
git merge --no-ff my-feature-branch-123456
# Make sure tests pass.
python manage.py test
git push
You\'re done! Congratulations, soon you\'ll have code running on one of
the biggest sites in the world!
Before pushing to `mozilla/main`, I like to verify that the merge went
fine in the logs. For the vast majority of merges, *there should not be
a merge commit*.
git log --graph --decorate
git push mozilla main # !!! Pushing code to the primary repo/branch!
# Optionally, you can keep your Github main in sync.
git push origin main # Not strictly necessary but kinda nice.
git push origin :my-feature-branch # Nice to clean up.
This should automatically close the PR, as GitHub will notice the merge
commit.
Once the commit is on `mozilla/main`, copy the commit url to the bug.
Once the commit has been deployed to stage and prod, set the bug to
`RESOLVED FIXED`. This tells everyone that the fix is in production.

70
docs/questions.md Normal file
Просмотреть файл

@ -0,0 +1,70 @@
---
title: Ask A Question
---
This document explains what kinds of question states exist in Kitsune,
how they are set and their implications.
# Configuring new products
To configure a new product for AAQ you must edit `config.py` within the
questions app.
First, ensure the `Product` object exists for this product in the
products app. If not create a new `Product`.
Next, Add a new item to the `products` dictionary using something like:
('product-slug', {
'name': _lazy(u'Product Name'),
'subtitle': _lazy('A brief description'),
'extra_fields': [],
'tags': ['tag-slug'],
'product': 'product-slug',
'categories': SortedDict([
('topic-slug', {
'name': _lazy(u'Topic name'),
'topic': 'topic-slug',
'tags': []
}),
])
}),
`'product-slug'` should be the slug of the `Product` object for this
product.
# Question States
## Default
This is the unmarked state of the thread.
Implications:
- Users can reply
- Visible in regular SUMO searches (with at least one helpful reply)
- Visible to external searches
- Visible in the regular questions list
- Visible in the [related threads]{.title-ref} section
## Locked
This is the locked state of a thread. A thread can be locked in two
ways:
- By manually locking it via the question UI
- Automatically after 180 days.
Implications:
- Users can\'t reply
- Moderators can unlock to reply
- If there is an answer, the locked thread is still shown by search
engines and our internal search.
- If there is no answer, the locked thread will not be shown by search
engines and our internal search.
## Not indexed
Questions with no answers that are older than 30 days have a meta tag
telling search engines not to show them.

54
docs/seo.md Normal file
Просмотреть файл

@ -0,0 +1,54 @@
---
title: SEO
---
This document covers notes and policies related to SEO.
# Prefer `meta` tag if possible
If an entire page should not be indexed, and/or none of its links
followed, prefer to use the `<meta name="robots" ...>` tag by specifying
something like:
{% set meta = (('robots', 'noindex'),) %}
or:
{% set meta = (('robots', 'noindex, nofollow'),) %}
within the lowest-level Jinja2 templates of the inheritance chain that
apply to only the desired pages.
However, if you only want to discourage the crawling of specific links
within a page, you\'ll have to add `rel="nofollow"` to each of those
links within its template. For example:
<a rel="nofollow" href="...">...</a>
# Breadcrumbs
If one or more of the breadcrumb links for a page should not be crawled,
you can add an extra string to those breadcrumb tuples to specify the
proper attribute to use, for example:
{% set crumbs = [((profile_url(user), 'rel="nofollow"'), user.username), (None, title)] %}
or:
{% set crumbs = [(document.get_absolute_url(), document.title), ((url('wiki.discuss.threads', document.slug), 'rel="ugc nofollow"'), _('Discuss'))] %}
# KB Forums
KB forums are user-generated content about KB articles. They are not
official content, and therefore not meant to be searchable. All links to
KB forums should be marked with `rel="ugc nofollow"`.
# User Links
User-related pages are also not meant to be indexed (searchable), and
links to them should not be crawled, so the base user template
(`kitsune/users/jinja2/users/base.html`) contains:
{% set meta = (('robots', 'noindex'),) %}
and all user links on all pages should be marked with `rel="nofollow"`.

38
docs/sla.md Normal file
Просмотреть файл

@ -0,0 +1,38 @@
---
title: Service Level Agreement
---
::: warning
::: title
Warning
:::
This section of documentation may be outdated.
:::
This isn\'t a zero-tolerance policy, but a series of policy points we
work towards when making changes to the site.
Measurements are based on what we can see in graphite which means it\'s
all server-side. Also, we use the upper_90 line since that tracks the
more extreme side of performance.
This SLA will probably change over time. Here it is now.
1. View performance
upper_90 for server-side rendering of views for GET requests should
be under 800ms.
2. Search availability
Search should work and return useful results.
The implication here is that it\'s not ok to be reindexing into an
index that searches are against.
3. Browser support
See this page in the wiki:
<https://wiki.mozilla.org/Support/Browser_Support>

153
docs/tests.md Normal file
Просмотреть файл

@ -0,0 +1,153 @@
---
title: All about testing
---
::: warning
::: title
Warning
:::
This section of documentation may be outdated.
:::
Kitsune has a fairly comprehensive Python test suite. Changes should not
break tests\-\--only change a test if there is a good reason to change
the expected behavior\-\--and new code should come with tests.
# Running the Test Suite
If you followed the steps in `the installation docs
<hacking_howto>`{.interpreted-text role="any"}, then you should be all
set setup-wise.
To run the tests, you need to do:
./manage.py test
That doesn\'t provide the most sensible defaults for running the tests.
Here is a good command to alias to something short:
./manage.py test -s --noinput --logging-clear-handlers
The `-s` flag is important if you want to be able to drop into PDB from
within tests.
Some other helpful flags are:
`-x`:
: Fast fail. Exit immediately on failure. No need to run the whole
test suite if you already know something is broken.
`--pdb`:
: Drop into PDB on an uncaught exception. (These show up as `E` or
errors in the test results, not `F` or failures.)
`--pdb-fail`:
: Drop into PDB on a test failure. This usually drops you right at the
assertion.
`--no-skip`:
: All SkipTests show up as errors. This is handy when things
shouldn\'t be skipping silently with reckless abandon.
## Running a Subset of Tests
You can run part of the test suite by specifying the apps you want to
run, like:
./manage.py test kitsune/wiki kitsune/search kitsune/kbforums
You can also specify modules:
./manage.py test kitsune.wiki.tests.test_views
You can specify specific tests:
./manage.py test kitsune.wiki.tests.test_views:VersionGroupTests.test_version_groups
See the output of `./manage.py test --help` for more arguments.
## Running tests without collecting static files
By default the test runner will run `collectstatic` to ensure that all
the required assets have been collected to the static folder. If you do
not want this default behavior you can run:
REUSE_STATIC=1 ./manage.py test
## The Test Database
The test suite will create a new database named `test_%s` where `%s` is
whatever value you have for `settings.DATABASES['default']['NAME']`.
Make sure the user has `ALL` on the test database as well. This is
covered in the installation chapter.
When the schema changes, you may need to drop the test database. You can
also run the test suite with `FORCE_DB` once to cause Django to drop and
recreate it:
FORCE_DB=1 ./manage.py test -s --noinput --logging-clear-handlers
# Writing New Tests
Code should be written so it can be tested, and then there should be
tests for it.
When adding code to an app, tests should be added in that app that cover
the new functionality. All apps have a `tests` module where tests should
go. They will be discovered automatically by the test runner as long as
the look like a test.
- We use \"modelmakers\" instead of fixtures. Models should have
modelmakers defined in the tests module of the Django app. For
example, `forums.tests.document` is the modelmaker for
`forums.Models.Document` class.
# Changing Tests
Unless the current behavior, and thus the test that verifies that
behavior is correct, is demonstrably wrong, don\'t change tests. Tests
may be refactored as long as its clear that the result is the same.
# Removing Tests
On those rare, wonderful occasions when we get to remove code, we should
remove the tests for it, as well.
If we liberate some functionality into a new package, the tests for that
functionality should move to that package, too.
# JavaScript Tests
Frontend JavaScript is currently tested with
[Mocha](https://mochajs.org/).
## Running JavaScript Tests
To run tests, make sure you have have the NPM dependencies installed,
and then run:
$ npm run webpack:test
## Writing JavaScript Tests
Mocha tests are discovered using the pattern
`kitsune/*/static/*/js/tests/**/*.js`. That means that any app can have
a [tests]{.title-ref} directory in its JavaScript directory, and the
files in there will all be considered test files. Files that don\'t
define tests won\'t cause issues, so it is safe to put testing utilities
in these directories as well.
Here are a few tips for writing tests:
- Any HTML required for your test should be added by the tests or a
`beforeEach` function in that test suite. React is useful for this.
- You can use [sinon]{.title-ref} to mock out parts of libraries or
functions under test. This is useful for testing AJAX.
- The tests run in a Node.js environment. A browser environment can be
simulated using `jsdom`.

7
docs/users.md Normal file
Просмотреть файл

@ -0,0 +1,7 @@
---
subtitle: Mozilla Accounts
title: Users
---
Kitsune uses Mozilla accounts for authentication:
<https://support.mozilla.org/kb/firefox-accounts-mozilla-support-faq>

98
mkdocs.yml Normal file
Просмотреть файл

@ -0,0 +1,98 @@
site_name: Kitsune Documentation
site_url: https://support.mozilla.org
site_description: 'Kitsune is the platform that powers SuMo'
repo_name: 'mozilla/kitsune'
repo_url: 'https://github.com/mozilla/kitsune'
markdown_extensions:
- admonition
- abbr
- def_list
- tables
- toc:
baselevel: 2
permalink: true
- pymdownx.betterem:
smart_enable: all
- pymdownx.arithmatex:
generic: true
- pymdownx.caret
- pymdownx.critic
- pymdownx.details
- pymdownx.inlinehilite
- pymdownx.magiclink
- pymdownx.highlight
- pymdownx.inlinehilite
- pymdownx.keys
- pymdownx.mark
- pymdownx.smartsymbols
- pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true
- pymdownx.tasklist:
custom_checkbox: true
- pymdownx.tilde
- pymdownx.superfences
theme:
name: material
features:
- navigation.instant
- navigation.instant.progress
- navigation.tracking
- navigation.top
- toc.follow
palette:
# Palette toggle for light mode
- scheme: default
media: "(prefers-color-scheme: light)"
primary: deep purple
accent: deep orange
toggle:
icon: material/brightness-7
name: Switch to dark mode
# Palette toggle for dark mode
- scheme: slate
media: "(prefers-color-scheme: dark)"
primary: deep purple
accent: deep orange
toggle:
icon: material/brightness-4
name: Switch to light mode
nav:
- Home: index.md
- Introduction:
- contributors.md
- hacking_howto.md
- contactus.md
- Developer's guide:
- conventions.md
- patching.md
- development.md
- tests.md
- celery.md
- email.md
- localization.md
- elastic_search.md
- frontend.md
- svelte.md
- browser_permissions.md
- zendesk.md
- seo.md
- notes.md
- switching_devices.md
- SuMo:
- api.md
- deployments.md
- k8s.md
- sla.md
- User Guide:
- users.md
- questions.md
- badges.md
- advanced-search.md
- Architectural Decisions:
- architectural-decisions.md
- Record: architecture/decisions/0001-record-architecture-decisions.md
- Search - L10N content: architecture/decisions/0002-es-l10n-content.md
- Search - AAQ content: architecture/decisions/0003-es-aaq-documents.md
- Type Checking: architecture/decisions/0004-type-checking.md

320
poetry.lock сгенерированный
Просмотреть файл

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand.
[[package]]
name = "alabaster"
@ -1487,6 +1487,23 @@ monitor = ["psutil (>=5.7.0)"]
recommended = ["cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "psutil (>=5.7.0)"]
test = ["cffi (>=1.12.2)", "coverage (>=5.0)", "dnspython (>=1.16.0,<2.0)", "idna", "objgraph", "psutil (>=5.7.0)", "requests", "setuptools"]
[[package]]
name = "ghp-import"
version = "2.1.0"
description = "Copy your docs directly to the gh-pages branch."
optional = false
python-versions = "*"
files = [
{file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
{file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
]
[package.dependencies]
python-dateutil = ">=2.8.1"
[package.extras]
dev = ["flake8", "markdown", "twine", "wheel"]
[[package]]
name = "google-api-core"
version = "2.11.0"
@ -2299,6 +2316,21 @@ html5 = ["html5lib"]
htmlsoup = ["BeautifulSoup4"]
source = ["Cython (>=0.29.7)"]
[[package]]
name = "markdown"
version = "3.5.1"
description = "Python implementation of John Gruber's Markdown."
optional = false
python-versions = ">=3.8"
files = [
{file = "Markdown-3.5.1-py3-none-any.whl", hash = "sha256:5874b47d4ee3f0b14d764324d2c94c03ea66bee56f2d929da9f2508d65e722dc"},
{file = "Markdown-3.5.1.tar.gz", hash = "sha256:b65d7beb248dc22f2e8a31fb706d93798093c308dc1aba295aedeb9d41a813bd"},
]
[package.extras]
docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
testing = ["coverage", "pyyaml"]
[[package]]
name = "markupsafe"
version = "2.1.2"
@ -2383,6 +2415,87 @@ files = [
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
[[package]]
name = "mergedeep"
version = "1.3.4"
description = "A deep merge function for 🐍."
optional = false
python-versions = ">=3.6"
files = [
{file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"},
{file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
]
[[package]]
name = "mkdocs"
version = "1.5.3"
description = "Project documentation with Markdown."
optional = false
python-versions = ">=3.7"
files = [
{file = "mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1"},
{file = "mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2"},
]
[package.dependencies]
click = ">=7.0"
colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""}
ghp-import = ">=1.0"
jinja2 = ">=2.11.1"
markdown = ">=3.2.1"
markupsafe = ">=2.0.1"
mergedeep = ">=1.3.4"
packaging = ">=20.5"
pathspec = ">=0.11.1"
platformdirs = ">=2.2.0"
pyyaml = ">=5.1"
pyyaml-env-tag = ">=0.1"
watchdog = ">=2.0"
[package.extras]
i18n = ["babel (>=2.9.0)"]
min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pathspec (==0.11.1)", "platformdirs (==2.2.0)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"]
[[package]]
name = "mkdocs-material"
version = "9.4.10"
description = "Documentation that simply works"
optional = false
python-versions = ">=3.8"
files = [
{file = "mkdocs_material-9.4.10-py3-none-any.whl", hash = "sha256:207c4ebc07faebb220437d2c626edb0c9760c82ccfc484500bd3eb30dfce988c"},
{file = "mkdocs_material-9.4.10.tar.gz", hash = "sha256:421adedaeaa461dcaf55b8d406673934ade3d4f05ed9819e4cc7b4ee1d646a62"},
]
[package.dependencies]
babel = ">=2.10,<3.0"
colorama = ">=0.4,<1.0"
jinja2 = ">=3.0,<4.0"
markdown = ">=3.2,<4.0"
mkdocs = ">=1.5.3,<2.0"
mkdocs-material-extensions = ">=1.3,<2.0"
paginate = ">=0.5,<1.0"
pygments = ">=2.16,<3.0"
pymdown-extensions = ">=10.2,<11.0"
regex = ">=2022.4"
requests = ">=2.26,<3.0"
[package.extras]
git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2,<2.0)"]
imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=9.4,<10.0)"]
recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"]
[[package]]
name = "mkdocs-material-extensions"
version = "1.3.1"
description = "Extension pack for Python Markdown and MkDocs Material."
optional = false
python-versions = ">=3.8"
files = [
{file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"},
{file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"},
]
[[package]]
name = "mozilla-django-oidc"
version = "3.0.0"
@ -2493,6 +2606,16 @@ files = [
{file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"},
]
[[package]]
name = "paginate"
version = "0.5.6"
description = "Divides large result sets into pages for easier browsing"
optional = false
python-versions = "*"
files = [
{file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"},
]
[[package]]
name = "parameterized"
version = "0.8.1"
@ -2524,13 +2647,13 @@ testing = ["docopt", "pytest (<6.0.0)"]
[[package]]
name = "pathspec"
version = "0.11.0"
version = "0.11.2"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.7"
files = [
{file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"},
{file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"},
{file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"},
{file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"},
]
[[package]]
@ -2803,6 +2926,8 @@ files = [
{file = "psycopg2-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:426f9f29bde126913a20a96ff8ce7d73fd8a216cfb323b1f04da402d452853c3"},
{file = "psycopg2-2.9.9-cp311-cp311-win32.whl", hash = "sha256:ade01303ccf7ae12c356a5e10911c9e1c51136003a9a1d92f7aa9d010fb98372"},
{file = "psycopg2-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:121081ea2e76729acfb0673ff33755e8703d45e926e416cb59bae3a86c6a4981"},
{file = "psycopg2-2.9.9-cp312-cp312-win32.whl", hash = "sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024"},
{file = "psycopg2-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:a7653d00b732afb6fc597e29c50ad28087dcb4fbfb28e86092277a559ae4e693"},
{file = "psycopg2-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:5e0d98cade4f0e0304d7d6f25bbfbc5bd186e07b38eac65379309c4ca3193efa"},
{file = "psycopg2-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:7e2dacf8b009a1c1e843b5213a87f7c544b2b042476ed7755be813eaf4e8347a"},
{file = "psycopg2-2.9.9-cp38-cp38-win32.whl", hash = "sha256:ff432630e510709564c01dafdbe996cb552e0b9f3f065eb89bdce5bd31fabf4c"},
@ -2923,17 +3048,18 @@ files = [
[[package]]
name = "pygments"
version = "2.15.0"
version = "2.17.2"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.7"
files = [
{file = "Pygments-2.15.0-py3-none-any.whl", hash = "sha256:77a3299119af881904cd5ecd1ac6a66214b6e9bed1f2db16993b54adede64094"},
{file = "Pygments-2.15.0.tar.gz", hash = "sha256:f7e36cffc4c517fbc252861b9a6e4644ca0e5abadf9a113c72d1358ad09b9500"},
{file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"},
{file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"},
]
[package.extras]
plugins = ["importlib-metadata"]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pylint"
@ -2992,6 +3118,24 @@ files = [
[package.dependencies]
pylint = ">=1.7"
[[package]]
name = "pymdown-extensions"
version = "10.4"
description = "Extension pack for Python Markdown."
optional = false
python-versions = ">=3.8"
files = [
{file = "pymdown_extensions-10.4-py3-none-any.whl", hash = "sha256:cfc28d6a09d19448bcbf8eee3ce098c7d17ff99f7bd3069db4819af181212037"},
{file = "pymdown_extensions-10.4.tar.gz", hash = "sha256:bc46f11749ecd4d6b71cf62396104b4a200bad3498cb0f5dad1b8502fe461a35"},
]
[package.dependencies]
markdown = ">=3.2"
pyyaml = "*"
[package.extras]
extra = ["pygments (>=2.12)"]
[[package]]
name = "pyopenssl"
version = "23.2.0"
@ -3360,6 +3504,20 @@ files = [
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
]
[[package]]
name = "pyyaml-env-tag"
version = "0.1"
description = "A custom YAML tag for referencing environment variables in YAML files. "
optional = false
python-versions = ">=3.6"
files = [
{file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"},
{file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
]
[package.dependencies]
pyyaml = "*"
[[package]]
name = "q"
version = "2.7"
@ -3405,6 +3563,103 @@ async-timeout = {version = ">=4.0.2", markers = "python_version <= \"3.11.2\""}
hiredis = ["hiredis (>=1.0.0)"]
ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"]
[[package]]
name = "regex"
version = "2023.10.3"
description = "Alternative regular expression module, to replace re."
optional = false
python-versions = ">=3.7"
files = [
{file = "regex-2023.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c34d4f73ea738223a094d8e0ffd6d2c1a1b4c175da34d6b0de3d8d69bee6bcc"},
{file = "regex-2023.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8f4e49fc3ce020f65411432183e6775f24e02dff617281094ba6ab079ef0915"},
{file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cd1bccf99d3ef1ab6ba835308ad85be040e6a11b0977ef7ea8c8005f01a3c29"},
{file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:81dce2ddc9f6e8f543d94b05d56e70d03a0774d32f6cca53e978dc01e4fc75b8"},
{file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c6b4d23c04831e3ab61717a707a5d763b300213db49ca680edf8bf13ab5d91b"},
{file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c15ad0aee158a15e17e0495e1e18741573d04eb6da06d8b84af726cfc1ed02ee"},
{file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6239d4e2e0b52c8bd38c51b760cd870069f0bdf99700a62cd509d7a031749a55"},
{file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4a8bf76e3182797c6b1afa5b822d1d5802ff30284abe4599e1247be4fd6b03be"},
{file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9c727bbcf0065cbb20f39d2b4f932f8fa1631c3e01fcedc979bd4f51fe051c5"},
{file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3ccf2716add72f80714b9a63899b67fa711b654be3fcdd34fa391d2d274ce767"},
{file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:107ac60d1bfdc3edb53be75e2a52aff7481b92817cfdddd9b4519ccf0e54a6ff"},
{file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:00ba3c9818e33f1fa974693fb55d24cdc8ebafcb2e4207680669d8f8d7cca79a"},
{file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f0a47efb1dbef13af9c9a54a94a0b814902e547b7f21acb29434504d18f36e3a"},
{file = "regex-2023.10.3-cp310-cp310-win32.whl", hash = "sha256:36362386b813fa6c9146da6149a001b7bd063dabc4d49522a1f7aa65b725c7ec"},
{file = "regex-2023.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:c65a3b5330b54103e7d21cac3f6bf3900d46f6d50138d73343d9e5b2900b2353"},
{file = "regex-2023.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90a79bce019c442604662d17bf69df99090e24cdc6ad95b18b6725c2988a490e"},
{file = "regex-2023.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c7964c2183c3e6cce3f497e3a9f49d182e969f2dc3aeeadfa18945ff7bdd7051"},
{file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ef80829117a8061f974b2fda8ec799717242353bff55f8a29411794d635d964"},
{file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5addc9d0209a9afca5fc070f93b726bf7003bd63a427f65ef797a931782e7edc"},
{file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c148bec483cc4b421562b4bcedb8e28a3b84fcc8f0aa4418e10898f3c2c0eb9b"},
{file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d1f21af4c1539051049796a0f50aa342f9a27cde57318f2fc41ed50b0dbc4ac"},
{file = "regex-2023.10.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b9ac09853b2a3e0d0082104036579809679e7715671cfbf89d83c1cb2a30f58"},
{file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ebedc192abbc7fd13c5ee800e83a6df252bec691eb2c4bedc9f8b2e2903f5e2a"},
{file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d8a993c0a0ffd5f2d3bda23d0cd75e7086736f8f8268de8a82fbc4bd0ac6791e"},
{file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:be6b7b8d42d3090b6c80793524fa66c57ad7ee3fe9722b258aec6d0672543fd0"},
{file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4023e2efc35a30e66e938de5aef42b520c20e7eda7bb5fb12c35e5d09a4c43f6"},
{file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d47840dc05e0ba04fe2e26f15126de7c755496d5a8aae4a08bda4dd8d646c54"},
{file = "regex-2023.10.3-cp311-cp311-win32.whl", hash = "sha256:9145f092b5d1977ec8c0ab46e7b3381b2fd069957b9862a43bd383e5c01d18c2"},
{file = "regex-2023.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:b6104f9a46bd8743e4f738afef69b153c4b8b592d35ae46db07fc28ae3d5fb7c"},
{file = "regex-2023.10.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bff507ae210371d4b1fe316d03433ac099f184d570a1a611e541923f78f05037"},
{file = "regex-2023.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be5e22bbb67924dea15039c3282fa4cc6cdfbe0cbbd1c0515f9223186fc2ec5f"},
{file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a992f702c9be9c72fa46f01ca6e18d131906a7180950958f766c2aa294d4b41"},
{file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7434a61b158be563c1362d9071358f8ab91b8d928728cd2882af060481244c9e"},
{file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2169b2dcabf4e608416f7f9468737583ce5f0a6e8677c4efbf795ce81109d7c"},
{file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9e908ef5889cda4de038892b9accc36d33d72fb3e12c747e2799a0e806ec841"},
{file = "regex-2023.10.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12bd4bc2c632742c7ce20db48e0d99afdc05e03f0b4c1af90542e05b809a03d9"},
{file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bc72c231f5449d86d6c7d9cc7cd819b6eb30134bb770b8cfdc0765e48ef9c420"},
{file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bce8814b076f0ce5766dc87d5a056b0e9437b8e0cd351b9a6c4e1134a7dfbda9"},
{file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:ba7cd6dc4d585ea544c1412019921570ebd8a597fabf475acc4528210d7c4a6f"},
{file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b0c7d2f698e83f15228ba41c135501cfe7d5740181d5903e250e47f617eb4292"},
{file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5a8f91c64f390ecee09ff793319f30a0f32492e99f5dc1c72bc361f23ccd0a9a"},
{file = "regex-2023.10.3-cp312-cp312-win32.whl", hash = "sha256:ad08a69728ff3c79866d729b095872afe1e0557251da4abb2c5faff15a91d19a"},
{file = "regex-2023.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:39cdf8d141d6d44e8d5a12a8569d5a227f645c87df4f92179bd06e2e2705e76b"},
{file = "regex-2023.10.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4a3ee019a9befe84fa3e917a2dd378807e423d013377a884c1970a3c2792d293"},
{file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76066d7ff61ba6bf3cb5efe2428fc82aac91802844c022d849a1f0f53820502d"},
{file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe50b61bab1b1ec260fa7cd91106fa9fece57e6beba05630afe27c71259c59b"},
{file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fd88f373cb71e6b59b7fa597e47e518282455c2734fd4306a05ca219a1991b0"},
{file = "regex-2023.10.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ab05a182c7937fb374f7e946f04fb23a0c0699c0450e9fb02ef567412d2fa3"},
{file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dac37cf08fcf2094159922edc7a2784cfcc5c70f8354469f79ed085f0328ebdf"},
{file = "regex-2023.10.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e54ddd0bb8fb626aa1f9ba7b36629564544954fff9669b15da3610c22b9a0991"},
{file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3367007ad1951fde612bf65b0dffc8fd681a4ab98ac86957d16491400d661302"},
{file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:16f8740eb6dbacc7113e3097b0a36065a02e37b47c936b551805d40340fb9971"},
{file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:f4f2ca6df64cbdd27f27b34f35adb640b5d2d77264228554e68deda54456eb11"},
{file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:39807cbcbe406efca2a233884e169d056c35aa7e9f343d4e78665246a332f597"},
{file = "regex-2023.10.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7eece6fbd3eae4a92d7c748ae825cbc1ee41a89bb1c3db05b5578ed3cfcfd7cb"},
{file = "regex-2023.10.3-cp37-cp37m-win32.whl", hash = "sha256:ce615c92d90df8373d9e13acddd154152645c0dc060871abf6bd43809673d20a"},
{file = "regex-2023.10.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f649fa32fe734c4abdfd4edbb8381c74abf5f34bc0b3271ce687b23729299ed"},
{file = "regex-2023.10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b98b7681a9437262947f41c7fac567c7e1f6eddd94b0483596d320092004533"},
{file = "regex-2023.10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:91dc1d531f80c862441d7b66c4505cd6ea9d312f01fb2f4654f40c6fdf5cc37a"},
{file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82fcc1f1cc3ff1ab8a57ba619b149b907072e750815c5ba63e7aa2e1163384a4"},
{file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7979b834ec7a33aafae34a90aad9f914c41fd6eaa8474e66953f3f6f7cbd4368"},
{file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef71561f82a89af6cfcbee47f0fabfdb6e63788a9258e913955d89fdd96902ab"},
{file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd829712de97753367153ed84f2de752b86cd1f7a88b55a3a775eb52eafe8a94"},
{file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00e871d83a45eee2f8688d7e6849609c2ca2a04a6d48fba3dff4deef35d14f07"},
{file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:706e7b739fdd17cb89e1fbf712d9dc21311fc2333f6d435eac2d4ee81985098c"},
{file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cc3f1c053b73f20c7ad88b0d1d23be7e7b3901229ce89f5000a8399746a6e039"},
{file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6f85739e80d13644b981a88f529d79c5bdf646b460ba190bffcaf6d57b2a9863"},
{file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:741ba2f511cc9626b7561a440f87d658aabb3d6b744a86a3c025f866b4d19e7f"},
{file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e77c90ab5997e85901da85131fd36acd0ed2221368199b65f0d11bca44549711"},
{file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:979c24cbefaf2420c4e377ecd1f165ea08cc3d1fbb44bdc51bccbbf7c66a2cb4"},
{file = "regex-2023.10.3-cp38-cp38-win32.whl", hash = "sha256:58837f9d221744d4c92d2cf7201c6acd19623b50c643b56992cbd2b745485d3d"},
{file = "regex-2023.10.3-cp38-cp38-win_amd64.whl", hash = "sha256:c55853684fe08d4897c37dfc5faeff70607a5f1806c8be148f1695be4a63414b"},
{file = "regex-2023.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2c54e23836650bdf2c18222c87f6f840d4943944146ca479858404fedeb9f9af"},
{file = "regex-2023.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:69c0771ca5653c7d4b65203cbfc5e66db9375f1078689459fe196fe08b7b4930"},
{file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ac965a998e1388e6ff2e9781f499ad1eaa41e962a40d11c7823c9952c77123e"},
{file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c0e8fae5b27caa34177bdfa5a960c46ff2f78ee2d45c6db15ae3f64ecadde14"},
{file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c56c3d47da04f921b73ff9415fbaa939f684d47293f071aa9cbb13c94afc17d"},
{file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ef1e014eed78ab650bef9a6a9cbe50b052c0aebe553fb2881e0453717573f52"},
{file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d29338556a59423d9ff7b6eb0cb89ead2b0875e08fe522f3e068b955c3e7b59b"},
{file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9c6d0ced3c06d0f183b73d3c5920727268d2201aa0fe6d55c60d68c792ff3588"},
{file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:994645a46c6a740ee8ce8df7911d4aee458d9b1bc5639bc968226763d07f00fa"},
{file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:66e2fe786ef28da2b28e222c89502b2af984858091675044d93cb50e6f46d7af"},
{file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:11175910f62b2b8c055f2b089e0fedd694fe2be3941b3e2633653bc51064c528"},
{file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:06e9abc0e4c9ab4779c74ad99c3fc10d3967d03114449acc2c2762ad4472b8ca"},
{file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fb02e4257376ae25c6dd95a5aec377f9b18c09be6ebdefa7ad209b9137b73d48"},
{file = "regex-2023.10.3-cp39-cp39-win32.whl", hash = "sha256:3b2c3502603fab52d7619b882c25a6850b766ebd1b18de3df23b2f939360e1bd"},
{file = "regex-2023.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:adbccd17dcaff65704c856bd29951c58a1bd4b2b0f8ad6b826dbd543fe740988"},
{file = "regex-2023.10.3.tar.gz", hash = "sha256:3fef4f844d2290ee0ba57addcec17eec9e3df73f10a2748485dfd6a3a188cc0f"},
]
[[package]]
name = "requests"
version = "2.28.2"
@ -4237,6 +4492,45 @@ platformdirs = ">=2.4,<4"
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"]
[[package]]
name = "watchdog"
version = "3.0.0"
description = "Filesystem events monitoring"
optional = false
python-versions = ">=3.7"
files = [
{file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41"},
{file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397"},
{file = "watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96"},
{file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae"},
{file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9"},
{file = "watchdog-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7"},
{file = "watchdog-3.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674"},
{file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f"},
{file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc"},
{file = "watchdog-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3"},
{file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3"},
{file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0"},
{file = "watchdog-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8"},
{file = "watchdog-3.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100"},
{file = "watchdog-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346"},
{file = "watchdog-3.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64"},
{file = "watchdog-3.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a"},
{file = "watchdog-3.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44"},
{file = "watchdog-3.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a"},
{file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709"},
{file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83"},
{file = "watchdog-3.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d"},
{file = "watchdog-3.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33"},
{file = "watchdog-3.0.0-py3-none-win32.whl", hash = "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f"},
{file = "watchdog-3.0.0-py3-none-win_amd64.whl", hash = "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c"},
{file = "watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759"},
{file = "watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9"},
]
[package.extras]
watchmedo = ["PyYAML (>=3.10)"]
[[package]]
name = "wcwidth"
version = "0.2.6"
@ -4349,6 +4643,16 @@ files = [
{file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"},
{file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"},
{file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"},
{file = "wrapt-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ecee4132c6cd2ce5308e21672015ddfed1ff975ad0ac8d27168ea82e71413f55"},
{file = "wrapt-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2020f391008ef874c6d9e208b24f28e31bcb85ccff4f335f15a3251d222b92d9"},
{file = "wrapt-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2feecf86e1f7a86517cab34ae6c2f081fd2d0dac860cb0c0ded96d799d20b335"},
{file = "wrapt-1.14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:240b1686f38ae665d1b15475966fe0472f78e71b1b4903c143a842659c8e4cb9"},
{file = "wrapt-1.14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9008dad07d71f68487c91e96579c8567c98ca4c3881b9b113bc7b33e9fd78b8"},
{file = "wrapt-1.14.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6447e9f3ba72f8e2b985a1da758767698efa72723d5b59accefd716e9e8272bf"},
{file = "wrapt-1.14.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:acae32e13a4153809db37405f5eba5bac5fbe2e2ba61ab227926a22901051c0a"},
{file = "wrapt-1.14.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49ef582b7a1152ae2766557f0550a9fcbf7bbd76f43fbdc94dd3bf07cc7168be"},
{file = "wrapt-1.14.1-cp311-cp311-win32.whl", hash = "sha256:358fe87cc899c6bb0ddc185bf3dbfa4ba646f05b1b0b9b5a27c2cb92c2cea204"},
{file = "wrapt-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:26046cd03936ae745a502abf44dac702a5e6880b2b01c29aea8ddf3353b68224"},
{file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"},
{file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"},
{file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"},
@ -4501,4 +4805,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "cb2a6169c73b017992b840f313e61d62903f5d688032562228c5b965cc2f35c0"
content-hash = "e12b50ceae4fcb5c7f726674e75cfa26aeb278a525ba4e332914cb903d1911db"

Просмотреть файл

@ -85,6 +85,8 @@ graphene-django = "~3.0.0"
django-guardian = "^2.4.0"
django-email-bandit = "^2.0"
psycopg2 = "^2.9.9"
mkdocs = "^1.5.3"
mkdocs-material = "^9.4.10"
[tool.poetry.group.dev.dependencies]
ipdb = "^0.13.11"