forked from potsda.mn/mobilizon
Merge branch 'improve-docs' into 'master'
ℹ Improve docs and documentation (again) See merge request framasoft/mobilizon!99
This commit is contained in:
commit
7e42716e9a
308
CONTRIBUTING.md
308
CONTRIBUTING.md
|
@ -1,307 +1 @@
|
||||||
# Contributing to Mobilizon
|
Please read our full contributing document at [https://framasoft.frama.io/mobilizon/contributing.html](https://framasoft.frama.io/mobilizon/contributing.html)
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How we collaborate
|
|
||||||
|
|
||||||
First off, thank you for considering contributing to Mobilizon!
|
|
||||||
|
|
||||||
Our aim is for this project to make you feel welcome as a contributor.
|
|
||||||
We hugely value the comments and contributions of community members in the various
|
|
||||||
publicly-accessible areas in use, which currently are:
|
|
||||||
|
|
||||||
* Home: [Home](https://joinmobilizon.org)
|
|
||||||
* Wiki: [Project Wiki](https://framagit.org/framasoft/mobilizon/wikis) (Gitlab)
|
|
||||||
* Code: [Framagit](https://framagit.org/framasoft/mobilizon) (Gitlab)
|
|
||||||
* Issue tracking: [Framagit](https://framagit.org/framasoft/mobilizon) (Gitlab)
|
|
||||||
* Discussion: [Mobilizon forum](https://framacolibri.org/c/mobilizon) (Discourse)
|
|
||||||
* Chat: [#Mobilizon:matrix.org](https://matrix.org/#/room/#Mobilizon:matrix.org) (Matrix) or `#mobilizon` on Freenode (IRC)
|
|
||||||
|
|
||||||
### How to communicate with others ?
|
|
||||||
|
|
||||||
As you may know, English is not the only language in the community and communicating in various languages can be quite tricky. Some might not understand what you're saying, while other have a hard time writing in a language that is not their primary language.
|
|
||||||
|
|
||||||
If you don't have noticed yet, French is the de-facto language for communication in various channels. This is because a huge part of our community speaks French, as Framasoft is a French organization.
|
|
||||||
|
|
||||||
We don't want to impose French as the only language in the project, though, because we would lose a ton of potential users users and contributors that can help us make Mobilizon better.
|
|
||||||
|
|
||||||
You can write what you would like to tell us in the language you're most comfortable with and then use automagic translation to translate your message in English. It's far from perfect, but if we can understand the main idea you wanted to expose, it's sufficient.
|
|
||||||
|
|
||||||
Of course, you can and should express yourself directly in English if you're confident doing so!
|
|
||||||
|
|
||||||
### Other useful information
|
|
||||||
|
|
||||||
* It's early days for Mobilizon, so we're focused on laying down the foundations for the project. This means that you won't find much usable code yet.
|
|
||||||
* We're working as openly and transparently as possible with this project. As a contributor or member of the Mobilizon community, almost everything you come across will link to some other things that you should have access to. If you don't have access, and you think you should, just ask!
|
|
||||||
|
|
||||||
### Contributopia
|
|
||||||
|
|
||||||
This project is part of [Framasoft](https://framasoft.org)'s [Contributopia Campaign](https://contributopia.org), creating and offering tools designed with a different set of values.
|
|
||||||
|
|
||||||
![Framameet illustration on Contributopia Website](https://contributopia.org/img/services-framameet.jpg)
|
|
||||||
|
|
||||||
### We practice [Ethical Design](https://2017.ind.ie/ethical-design/)
|
|
||||||
|
|
||||||
We endeavour to build technology that respects human rights, human effort, and human experience, and hope you will join in this effort.
|
|
||||||
|
|
||||||
[![Ethical Design diagram](https://i.imgur.com/O7RJo60.png)](https://2017.ind.ie/ethical-design/)
|
|
||||||
|
|
||||||
---
|
|
||||||
## Code of Conduct
|
|
||||||
|
|
||||||
This section explains Framasoft's commitment towards contributors to the [Mobilizon Project](https://joinmobilizon.org), as well as expectations that community members should have of one another. It is based on the [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/version/1/4/code-of-conduct) v1.4.
|
|
||||||
|
|
||||||
### Pledge
|
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers - including Framasoft - pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
|
||||||
|
|
||||||
### Our Standards
|
|
||||||
|
|
||||||
Examples of behaviour that contributes to creating a positive environment include:
|
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
|
||||||
* Being respectful of differing viewpoints and experiences
|
|
||||||
* Gracefully accepting constructive criticism
|
|
||||||
* Focusing on what is best for the community
|
|
||||||
* Showing empathy towards other community members
|
|
||||||
|
|
||||||
Examples of unacceptable behaviour by participants include:
|
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
||||||
* Public or private harassment
|
|
||||||
* Publishing others’ private information, such as a physical or electronic address, without explicit permission
|
|
||||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
|
||||||
|
|
||||||
### Our Responsibilities
|
|
||||||
|
|
||||||
The Project maintainers are responsible for clarifying the standards of acceptable behaviour and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behaviour.
|
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviours that they deem inappropriate, threatening, offensive, or harmful.
|
|
||||||
|
|
||||||
### Scope
|
|
||||||
|
|
||||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
|
||||||
|
|
||||||
### Enforcement
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behaviour may be reported by contacting the project team at tcit plus mobilizon at framasoft dot org. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
|
||||||
|
|
||||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project’s leadership.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How Can I Contribute
|
|
||||||
|
|
||||||
### Submitting Feature Requests, Enhancement Suggestions or Bug Reports
|
|
||||||
|
|
||||||
This section guides you through submitting a ✨ feature request, 💄 enhancement suggestions, and 🐛 bug reports for Mobilizon - anything from errors and crashes, to minor improvements, to completely new features.
|
|
||||||
|
|
||||||
When you post an issue, please include as many details as possible. **Fill in the issue template** (available below) to help us resolve issues faster.
|
|
||||||
|
|
||||||
### Before making a submission
|
|
||||||
|
|
||||||
Please go through the checklist below before posting any ✨ 💄 🐛
|
|
||||||
|
|
||||||
* **Check if you're using the latest version** of Mobilizon and all its relevant components and if you can get the desired behaviour by changing some config settings.
|
|
||||||
* **Perform a cursory search** in the issue tracker to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
|
|
||||||
* Never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to tcit plus mobilizon at framasoft dot org.
|
|
||||||
|
|
||||||
> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one.
|
|
||||||
|
|
||||||
### Suggesting Features & Enhancements
|
|
||||||
|
|
||||||
If you find yourself wishing for a feature that doesn't exist in Mobilizon, you are probably not alone. There are probably others out there with similar needs.
|
|
||||||
|
|
||||||
Open an issue providing the following information:
|
|
||||||
|
|
||||||
* **Use a clear and descriptive title** for the issue to identify the suggestion.
|
|
||||||
* **Provide a description of the suggested enhancement** in as many details as possible, including the steps that you imagine you (as a user) would take if the feature you're requesting existed.
|
|
||||||
* **Describe the current behaviour** and **explain which behaviour you would like to see instead** and why.
|
|
||||||
* **Include screenshots and animated GIFs** which help you demonstrate the steps or point out the parts of Mobilizon which the suggestion is related to. You can use a tool called [LICEcap](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [Silentcast](https://github.com/colinkeenan/silentcast) or [Byzanz](https://github.com/GNOME/byzanz) on Linux.
|
|
||||||
* **Provide specific examples to demonstrate the enhancements**. If possible, include drawings, screenshots, or gifs of similar features in another app.
|
|
||||||
* **Explain why this enhancement would be useful** to most participants of Mobilizon and isn't something that can or should be implemented as a community extension.
|
|
||||||
* **List some other communities, platforms or apps where this enhancement exists.**
|
|
||||||
|
|
||||||
### Reporting Bugs 🐛
|
|
||||||
|
|
||||||
Open an issue providing the following information by filling in issue template below, explaining the problem and including additional details to help maintainers reproduce the problem:
|
|
||||||
|
|
||||||
* **Use a clear and descriptive title** for the issue to identify the problem.
|
|
||||||
* **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining where you started in Mobilizon, and then which actions you took. When listing steps, **don't just say what you did, but explain how you did it**. For example, if you moved the cursor to the end of a line, explain if you used the mouse or a keyboard shortcut, and if so which one?
|
|
||||||
* **Provide specific examples to demonstrate the steps**. Include links to pages, screenshots, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use Markdown code blocks.
|
|
||||||
* **Describe the behaviour you observed after following the steps** and point out what exactly is the problem with that behaviour.
|
|
||||||
* **Explain which behaviour you expected to see instead and why.**
|
|
||||||
* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux.
|
|
||||||
* **If you're reporting a crash**, include a crash report with error logs.
|
|
||||||
* **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below.
|
|
||||||
|
|
||||||
Provide more context by answering these questions:
|
|
||||||
|
|
||||||
* **Did the problem start happening recently** (e.g. after updating to a new version) or was this always a problem?
|
|
||||||
* If the problem started happening recently, **can you reproduce the problem in an older version?** What's the most recent version in which the problem doesn't happen?
|
|
||||||
* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens.
|
|
||||||
* If the problem is related to an event, comment or actor (eg. user or group), **does the problem happen for all of them or only some?** Does the problem happen only when working with local (originating from your Mobilizon instance) or remote ones, with anything specific about the object? Is there anything else special about the actors or events in question?
|
|
||||||
|
|
||||||
Include details about your configuration and environment:
|
|
||||||
|
|
||||||
* **Which version of each component are you using?**
|
|
||||||
* **What's the name and version of the OS you're using**?
|
|
||||||
* **Are you running in a virtual machine?** If so, which VM software are you using and which operating systems and versions are used for the host and the guest?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Contributing
|
|
||||||
|
|
||||||
A common misconception about contributing to free and open source projects is that you need to contribute code. In fact, it’s often the other parts of a project that are most overlooked. You’ll do the project a huge favour by offering to pitch in with these types of contributions!
|
|
||||||
|
|
||||||
Even if you like to write code, other types of contributions are a great way to get involved with a project and meet other community members. Building those relationships may open up unexpected opportunities.
|
|
||||||
|
|
||||||
#### Do you like to design? 🎨
|
|
||||||
|
|
||||||
* Restructure layouts to improve the project's usability
|
|
||||||
* Conduct user research to reorganise and refine the project's navigation or menus
|
|
||||||
* Create art for icons and app screens
|
|
||||||
|
|
||||||
#### Do you like to write? ✏
|
|
||||||
|
|
||||||
* Write and improve the project's documentation
|
|
||||||
* Write tutorials for the project
|
|
||||||
* Curate a wiki page of examples showing how the project can be used
|
|
||||||
|
|
||||||
#### Do you like organising? 📥
|
|
||||||
|
|
||||||
* Link to duplicate issues, and suggest new issue labels, to keep things organised
|
|
||||||
* Go through open issues and suggest revisiting or closing old ones
|
|
||||||
* Ask clarifying questions on recently opened issues to move the discussion forward
|
|
||||||
|
|
||||||
#### Do you like helping people? 🙋
|
|
||||||
|
|
||||||
* Answer questions about the project on forums and other sites
|
|
||||||
* Answer questions for people on open issues
|
|
||||||
|
|
||||||
#### Do you like helping others code? 👐
|
|
||||||
|
|
||||||
* Review code on other people’s submissions
|
|
||||||
* Write tutorials for how a project can be used
|
|
||||||
* Offer to mentor another contributor
|
|
||||||
|
|
||||||
#### Do you like to code? 🔩
|
|
||||||
|
|
||||||
* Find an open issue to tackle
|
|
||||||
* Offer to help write a new feature
|
|
||||||
* Improve tooling, testing & deployment options
|
|
||||||
* Read the **next section for guidelines**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Contributing Code
|
|
||||||
|
|
||||||
Unsure where to begin contributing? You can start by looking through issues tagged with:
|
|
||||||
|
|
||||||
* [`first-timers-only`](https://framagit.org/framasoft/mobilizon/issues?scope=all&utf8=✓&state=opened&label_name[]=🌱 first-timers-only) [https://www.firsttimersonly.com](https://www.firsttimersonly.com)- issues which should only require a few lines of code, and a test or two.
|
|
||||||
* [`help-wanted`](https://framagit.org/framasoft/mobilizon/issues?label_name[]=🙏 help-wanted) - issues which should be a bit more involved than `first-timers-only` issues.
|
|
||||||
|
|
||||||
#### Local development
|
|
||||||
|
|
||||||
Mobilizon can be developed locally. For instructions on how to do this, please see [the documentation](development.html).
|
|
||||||
|
|
||||||
#### Coding & git practices
|
|
||||||
|
|
||||||
* We use GitLab's merge requests as our code review tool
|
|
||||||
* We encourage any dev to comment on merge requests and we think of the merge request not as a "please approve my code" but as a space for co-developing.
|
|
||||||
* We develop features on separate branches identified by issue numbers.
|
|
||||||
* We don't currently use release branches or tags because we don't have release management at this phase of development.
|
|
||||||
|
|
||||||
#### How to make changes
|
|
||||||
|
|
||||||
* Make your changes on a separate branch which includes an issue number e.g. `1234-some-new-feature` where 1234 is the issue number where the feature is documented. Make sure the branch is based on `develop`.
|
|
||||||
* Do not commit changes to files that are irrelevant to your feature or bugfix.
|
|
||||||
* Use commit messages descriptive of your changes.
|
|
||||||
* Push to the upstream of your new branch.
|
|
||||||
* Create a merge request at GitLab.
|
|
||||||
|
|
||||||
#### Git commit messages
|
|
||||||
|
|
||||||
* Limit the first line to 72 characters or less, referencing relevant issue numbers
|
|
||||||
* Be as descriptive as you want after the first line
|
|
||||||
* Consider starting the commit message with an applicable emoji (see Issue & Commit Categories below)
|
|
||||||
|
|
||||||
#### Merge requests
|
|
||||||
|
|
||||||
* Follow [the code styleguides](styleguide.html).
|
|
||||||
* Document new code based on [the documentation styleguide](https://hexdocs.pm/elixir/writing-documentation.html)
|
|
||||||
* Each merge request should implement ONE feature or bugfix. If you want to add or fix more than one thing, submit more than one merge request.
|
|
||||||
* Fill in the merge request template below
|
|
||||||
* Include relevant issue number(s) in the merge request title
|
|
||||||
* Include screenshots or animated GIFs in your merge request whenever possible.
|
|
||||||
* End all files with a newline
|
|
||||||
|
|
||||||
#### Template for merge requests
|
|
||||||
|
|
||||||
* Description of the change
|
|
||||||
* Applicable issue numbers
|
|
||||||
* Alternate designs/implementations
|
|
||||||
* Benefits of this implementation
|
|
||||||
* Possible drawbacks
|
|
||||||
* Why should this be part of a core component?
|
|
||||||
* Testing process
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### Issue & Commit Categories
|
|
||||||
|
|
||||||
* 🚑 `critical` : Critical hotfix!
|
|
||||||
* 💄 `enhancement` : General improvements.
|
|
||||||
* ✨ `feature` : New features.
|
|
||||||
* 🐛 `bug` : Confirmed bugs, or reports that are likely to be bugs.
|
|
||||||
* 🙋 `question` : Questions (e.g. how can I do X?)
|
|
||||||
* 📮 `feedback` : General feedback.
|
|
||||||
* 🎨 `ui` : Visual design.
|
|
||||||
* 📜 `copy` : Text in the apps, or translations.
|
|
||||||
* ℹ `documentation` : Documentation.
|
|
||||||
* 🐎 `performance` : Performance.
|
|
||||||
* 🔒 `security` : Security.
|
|
||||||
* 🔌 `api` : Mobilizon's APIs.
|
|
||||||
* 👽 `external` : External libraries or API integrations.
|
|
||||||
* ⚠ `exception` : Uncaught exceptions.
|
|
||||||
* 🔥 `crash` : Crash.
|
|
||||||
* 🔣 `encoding` : Character encoding or data serialization issue.
|
|
||||||
* 🚚 `cleanup` : Removing, moving or refactoring code or files.
|
|
||||||
* ✅ `tests` : Testing
|
|
||||||
|
|
||||||
#### Issue Status
|
|
||||||
|
|
||||||
* 💬 `discussion` : Discussion to clarify this issue is ongoing.
|
|
||||||
* 🔜 `todo` : This has been discussed and now needs work.
|
|
||||||
* 🔁 `needs-more-info` : More information needs to be collected about these problems or feature requests (e.g. steps to reproduce).
|
|
||||||
* 💡 `idea` : Needs to be discussed further. Could be a feature request which might be good to first implement as a community extension.
|
|
||||||
* 🚧 `in-progress` : Someone is working on this...
|
|
||||||
* 🙏 `help-wanted` : The Mobilizon core team would appreciate help from the community in resolving these issues.
|
|
||||||
* 🌱 `first-timers-only` : Less complex issues which would be good first issues to work on for users who want to contribute.
|
|
||||||
* 🔢 `needs-reproduction` : Likely bugs, but haven't been reliably reproduced.
|
|
||||||
* 🔴 `blocked` : Blocked on other issues.
|
|
||||||
* 2️⃣ `duplicate` : Duplicate of another issue, i.e. has been reported before.
|
|
||||||
* 🙅 `wontfix` : The Mobilizon core team has decided not to fix these issues (or add these features) for now, because they're working as intended, or some other reason.
|
|
||||||
* 🚮 `invalid` : Issues which are not valid (e.g. spam or submitted by error).
|
|
||||||
|
|
||||||
|
|
||||||
#### Merge Request Status
|
|
||||||
|
|
||||||
* 🚧 `in-progress` : Still being worked on, more changes will follow.
|
|
||||||
* 🚏 `needs-review` : Needs code review and approval from maintainers.
|
|
||||||
* 🔍 `under-review` : Being reviewed by maintainers.
|
|
||||||
* 🔧 `changes-required` : Needs to be updated based on review comments and then reviewed again.
|
|
||||||
* 👀 `needs-testing` : Needs manual testing.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Credits
|
|
||||||
The following documents have greatly helped us put this together. A big thank you to their authors and contributors!
|
|
||||||
|
|
||||||
* [MoodleNet's own Contributing document](https://gitlab.com/moodlenet/project/meta/issues/16)
|
|
||||||
* [Contributor Covenant](https://www.contributor-covenant.org/version/1/4/code-of-conduct)
|
|
||||||
* [Open Source Guides](https://opensource.guide)
|
|
||||||
* [Holochain's Development Protocols](https://github.com/holochain/holochain-proto/wiki/Development-Protocols)
|
|
||||||
* [Atom's contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md)
|
|
||||||
* [Funkwhale's pad on how to Communicate with others](https://hackmd.io/qESHvdHZSWuhLNjeanaVQw?both)
|
|
||||||
|
|
|
@ -92,6 +92,10 @@ defmodule Mobilizon.Actors.Actor do
|
||||||
|> unique_constraint(:url, name: :actors_url_index)
|
|> unique_constraint(:url, name: :actors_url_index)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Changeset for person registration
|
||||||
|
"""
|
||||||
|
@spec registration_changeset(struct(), map()) :: Ecto.Changeset.t()
|
||||||
def registration_changeset(%Actor{} = actor, attrs) do
|
def registration_changeset(%Actor{} = actor, attrs) do
|
||||||
actor
|
actor
|
||||||
|> Ecto.Changeset.cast(attrs, [
|
|> Ecto.Changeset.cast(attrs, [
|
||||||
|
@ -116,6 +120,10 @@ defmodule Mobilizon.Actors.Actor do
|
||||||
|
|
||||||
# TODO : Use me !
|
# TODO : Use me !
|
||||||
# @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
|
# @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
|
||||||
|
@doc """
|
||||||
|
Changeset for remote actor creation
|
||||||
|
"""
|
||||||
|
@spec remote_actor_creation(map()) :: Ecto.Changeset.t()
|
||||||
def remote_actor_creation(params) do
|
def remote_actor_creation(params) do
|
||||||
changes =
|
changes =
|
||||||
%Actor{}
|
%Actor{}
|
||||||
|
@ -158,6 +166,10 @@ defmodule Mobilizon.Actors.Actor do
|
||||||
changes
|
changes
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Changeset for group creation
|
||||||
|
"""
|
||||||
|
@spec group_creation(struct(), map()) :: Ecto.Changeset.t()
|
||||||
def group_creation(%Actor{} = actor, params) do
|
def group_creation(%Actor{} = actor, params) do
|
||||||
actor
|
actor
|
||||||
|> Ecto.Changeset.cast(params, [
|
|> Ecto.Changeset.cast(params, [
|
||||||
|
@ -260,6 +272,7 @@ defmodule Mobilizon.Actors.Actor do
|
||||||
|
|
||||||
If actor A and C both follow actor B, actor B's followers are A and C
|
If actor A and C both follow actor B, actor B's followers are A and C
|
||||||
"""
|
"""
|
||||||
|
@spec get_followers(struct(), number(), number()) :: list()
|
||||||
def get_followers(%Actor{id: actor_id} = _actor, page \\ nil, limit \\ nil) do
|
def get_followers(%Actor{id: actor_id} = _actor, page \\ nil, limit \\ nil) do
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from(
|
from(
|
||||||
|
@ -277,6 +290,7 @@ defmodule Mobilizon.Actors.Actor do
|
||||||
|
|
||||||
If actor A follows actor B and C, actor A's followings are B and B
|
If actor A follows actor B and C, actor A's followings are B and B
|
||||||
"""
|
"""
|
||||||
|
@spec get_followings(struct(), number(), number()) :: list()
|
||||||
def get_followings(%Actor{id: actor_id} = _actor, page \\ nil, limit \\ nil) do
|
def get_followings(%Actor{id: actor_id} = _actor, page \\ nil, limit \\ nil) do
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from(
|
from(
|
||||||
|
@ -289,6 +303,10 @@ defmodule Mobilizon.Actors.Actor do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the groups an actor is member of
|
||||||
|
"""
|
||||||
|
@spec get_groups_member_of(struct()) :: list()
|
||||||
def get_groups_member_of(%Actor{id: actor_id}) do
|
def get_groups_member_of(%Actor{id: actor_id}) do
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from(
|
from(
|
||||||
|
@ -300,6 +318,10 @@ defmodule Mobilizon.Actors.Actor do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the members for a group actor
|
||||||
|
"""
|
||||||
|
@spec get_members_for_group(struct()) :: list()
|
||||||
def get_members_for_group(%Actor{id: actor_id}) do
|
def get_members_for_group(%Actor{id: actor_id}) do
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from(
|
from(
|
||||||
|
@ -311,6 +333,9 @@ defmodule Mobilizon.Actors.Actor do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Make an actor follow another
|
||||||
|
"""
|
||||||
@spec follow(struct(), struct(), boolean()) :: Follower.t() | {:error, String.t()}
|
@spec follow(struct(), struct(), boolean()) :: Follower.t() | {:error, String.t()}
|
||||||
def follow(%Actor{} = followed, %Actor{} = follower, approved \\ true) do
|
def follow(%Actor{} = followed, %Actor{} = follower, approved \\ true) do
|
||||||
with {:suspended, false} <- {:suspended, followed.suspended},
|
with {:suspended, false} <- {:suspended, followed.suspended},
|
||||||
|
@ -327,6 +352,9 @@ defmodule Mobilizon.Actors.Actor do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Unfollow an actor (remove a `Mobilizon.Actors.Follower`)
|
||||||
|
"""
|
||||||
@spec unfollow(struct(), struct()) :: {:ok, Follower.t()} | {:error, Ecto.Changeset.t()}
|
@spec unfollow(struct(), struct()) :: {:ok, Follower.t()} | {:error, Ecto.Changeset.t()}
|
||||||
def unfollow(%Actor{} = followed, %Actor{} = follower) do
|
def unfollow(%Actor{} = followed, %Actor{} = follower) do
|
||||||
with {:already_following, %Follower{} = follow} <-
|
with {:already_following, %Follower{} = follow} <-
|
||||||
|
@ -338,6 +366,8 @@ defmodule Mobilizon.Actors.Actor do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec do_follow(struct(), struct(), boolean) ::
|
||||||
|
{:ok, Follower.t()} | {:error, Ecto.Changeset.t()}
|
||||||
defp do_follow(%Actor{} = follower, %Actor{} = followed, approved) do
|
defp do_follow(%Actor{} = follower, %Actor{} = followed, approved) do
|
||||||
Actors.create_follower(%{
|
Actors.create_follower(%{
|
||||||
"actor_id" => follower.id,
|
"actor_id" => follower.id,
|
||||||
|
@ -346,7 +376,10 @@ defmodule Mobilizon.Actors.Actor do
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec following?(struct(), struct()) :: boolean()
|
@doc """
|
||||||
|
Returns whether an actor is following another
|
||||||
|
"""
|
||||||
|
@spec following?(struct(), struct()) :: Follower.t() | false
|
||||||
def following?(
|
def following?(
|
||||||
%Actor{} = follower_actor,
|
%Actor{} = follower_actor,
|
||||||
%Actor{} = followed_actor
|
%Actor{} = followed_actor
|
||||||
|
@ -381,13 +414,34 @@ defmodule Mobilizon.Actors.Actor do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Return display name and username
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
iex> display_name_and_username(%Actor{name: "Thomas C", preferred_username: "tcit", domain: nil})
|
||||||
|
"Thomas (tcit)"
|
||||||
|
|
||||||
|
iex> display_name_and_username(%Actor{name: "Thomas C", preferred_username: "tcit", domain: "framapiaf.org"})
|
||||||
|
"Thomas (tcit@framapiaf.org)"
|
||||||
|
|
||||||
|
iex> display_name_and_username(%Actor{name: nil, preferred_username: "tcit", domain: "framapiaf.org"})
|
||||||
|
"tcit@framapiaf.org"
|
||||||
|
|
||||||
|
"""
|
||||||
|
@spec display_name_and_username(struct()) :: String.t()
|
||||||
def display_name_and_username(%Actor{name: nil} = actor), do: actor_acct_from_actor(actor)
|
def display_name_and_username(%Actor{name: nil} = actor), do: actor_acct_from_actor(actor)
|
||||||
def display_name_and_username(%Actor{name: ""} = actor), do: actor_acct_from_actor(actor)
|
def display_name_and_username(%Actor{name: ""} = actor), do: actor_acct_from_actor(actor)
|
||||||
|
|
||||||
def display_name_and_username(%Actor{name: name} = actor),
|
def display_name_and_username(%Actor{name: name} = actor),
|
||||||
do: name <> " (" <> actor_acct_from_actor(actor) <> ")"
|
do: name <> " (" <> actor_acct_from_actor(actor) <> ")"
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Clear multiple caches for an actor
|
||||||
|
"""
|
||||||
|
@spec clear_cache(struct()) :: {:ok, true}
|
||||||
def clear_cache(%Actor{preferred_username: preferred_username, domain: nil}) do
|
def clear_cache(%Actor{preferred_username: preferred_username, domain: nil}) do
|
||||||
Cachex.del(:activity_pub, "actor_" <> preferred_username)
|
Cachex.del(:activity_pub, "actor_" <> preferred_username)
|
||||||
|
Cachex.del(:feed, "actor_" <> preferred_username)
|
||||||
|
Cachex.del(:ics, "actor_" <> preferred_username)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -56,6 +56,7 @@ defmodule Mobilizon.Actors do
|
||||||
Repo.get!(Actor, id)
|
Repo.get!(Actor, id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Get actor by ID and preload organized events, followers and followings
|
||||||
@spec get_actor_with_everything(integer()) :: Ecto.Query
|
@spec get_actor_with_everything(integer()) :: Ecto.Query
|
||||||
defp do_get_actor_with_everything(id) do
|
defp do_get_actor_with_everything(id) do
|
||||||
from(a in Actor, where: a.id == ^id, preload: [:organized_events, :followers, :followings])
|
from(a in Actor, where: a.id == ^id, preload: [:organized_events, :followers, :followings])
|
||||||
|
@ -144,6 +145,7 @@ defmodule Mobilizon.Actors do
|
||||||
%Ecto.Changeset{data: %Mobilizon.Actors.Actor{}}
|
%Ecto.Changeset{data: %Mobilizon.Actors.Actor{}}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@spec change_actor(Actor.t()) :: Ecto.Changeset.t()
|
||||||
def change_actor(%Actor{} = actor) do
|
def change_actor(%Actor{} = actor) do
|
||||||
Actor.changeset(actor, %{})
|
Actor.changeset(actor, %{})
|
||||||
end
|
end
|
||||||
|
@ -151,6 +153,7 @@ defmodule Mobilizon.Actors do
|
||||||
@doc """
|
@doc """
|
||||||
List the groups
|
List the groups
|
||||||
"""
|
"""
|
||||||
|
@spec list_groups(number(), number()) :: list(Actor.t())
|
||||||
def list_groups(page \\ nil, limit \\ nil) do
|
def list_groups(page \\ nil, limit \\ nil) do
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from(
|
from(
|
||||||
|
@ -164,6 +167,7 @@ defmodule Mobilizon.Actors do
|
||||||
@doc """
|
@doc """
|
||||||
Get the default member role depending on the actor openness
|
Get the default member role depending on the actor openness
|
||||||
"""
|
"""
|
||||||
|
@spec get_default_member_role(Actor.t()) :: atom()
|
||||||
def get_default_member_role(%Actor{openness: :open}), do: :member
|
def get_default_member_role(%Actor{openness: :open}), do: :member
|
||||||
def get_default_member_role(%Actor{}), do: :not_approved
|
def get_default_member_role(%Actor{}), do: :not_approved
|
||||||
|
|
||||||
|
@ -227,6 +231,12 @@ defmodule Mobilizon.Actors do
|
||||||
Repo.delete!(group)
|
Repo.delete!(group)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Upsert an actor.
|
||||||
|
|
||||||
|
Conflicts on actor's URL/AP ID. Replaces keys, avatar and banner, name and summary.
|
||||||
|
"""
|
||||||
|
@spec insert_or_update_actor(map(), boolean()) :: {:ok, Actor.t()}
|
||||||
def insert_or_update_actor(data, preload \\ false) do
|
def insert_or_update_actor(data, preload \\ false) do
|
||||||
cs = Actor.remote_actor_creation(data)
|
cs = Actor.remote_actor_creation(data)
|
||||||
|
|
||||||
|
@ -344,24 +354,50 @@ defmodule Mobilizon.Actors do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Get actor by username and domain is nil
|
# Get actor by username and domain is nil
|
||||||
|
@spec do_get_actor_by_name(Ecto.Queryable.t(), String.t()) :: Ecto.Queryable.t()
|
||||||
defp do_get_actor_by_name(query, name) do
|
defp do_get_actor_by_name(query, name) do
|
||||||
from(a in query, where: a.preferred_username == ^name and is_nil(a.domain))
|
from(a in query, where: a.preferred_username == ^name and is_nil(a.domain))
|
||||||
end
|
end
|
||||||
|
|
||||||
# Get actor by username and domain
|
# Get actor by username and domain
|
||||||
|
@spec do_get_actor_by_name(Ecto.Queryable.t(), String.t(), String.t()) :: Ecto.Queryable.t()
|
||||||
defp do_get_actor_by_name(query, name, domain) do
|
defp do_get_actor_by_name(query, name, domain) do
|
||||||
from(a in query, where: a.preferred_username == ^name and a.domain == ^domain)
|
from(a in query, where: a.preferred_username == ^name and a.domain == ^domain)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Return a local actor by it's preferred username
|
||||||
|
"""
|
||||||
|
@spec get_local_actor_by_name(String.t()) :: Actor.t() | nil
|
||||||
def get_local_actor_by_name(name) do
|
def get_local_actor_by_name(name) do
|
||||||
Repo.one(from(a in Actor, where: a.preferred_username == ^name and is_nil(a.domain)))
|
Repo.one(from(a in Actor, where: a.preferred_username == ^name and is_nil(a.domain)))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Return a local actor by it's preferred username and preload associations
|
||||||
|
|
||||||
|
Preloads organized_events, followers and followings
|
||||||
|
"""
|
||||||
|
@spec get_local_actor_by_name_with_everything(String.t()) :: Actor.t() | nil
|
||||||
def get_local_actor_by_name_with_everything(name) do
|
def get_local_actor_by_name_with_everything(name) do
|
||||||
actor = Repo.one(from(a in Actor, where: a.preferred_username == ^name and is_nil(a.domain)))
|
actor = Repo.one(from(a in Actor, where: a.preferred_username == ^name and is_nil(a.domain)))
|
||||||
Repo.preload(actor, [:organized_events, :followers, :followings])
|
Repo.preload(actor, [:organized_events, :followers, :followings])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns actor by name and preloads the organized events
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
iex> get_actor_by_name_with_everything("tcit")
|
||||||
|
%Mobilizon.Actors.Actor{preferred_username: "tcit", domain: nil, organized_events: []}
|
||||||
|
|
||||||
|
iex> get_actor_by_name_with_everything("tcit@social.tcit.fr")
|
||||||
|
%Mobilizon.Actors.Actor{preferred_username: "tcit", domain: "social.tcit.fr", organized_events: []}
|
||||||
|
|
||||||
|
iex> get_actor_by_name_with_everything("tcit", :Group)
|
||||||
|
nil
|
||||||
|
|
||||||
|
"""
|
||||||
@spec get_actor_by_name_with_everything(String.t(), atom() | nil) :: Actor.t()
|
@spec get_actor_by_name_with_everything(String.t(), atom() | nil) :: Actor.t()
|
||||||
def get_actor_by_name_with_everything(name, type \\ nil) do
|
def get_actor_by_name_with_everything(name, type \\ nil) do
|
||||||
name
|
name
|
||||||
|
@ -403,6 +439,13 @@ defmodule Mobilizon.Actors do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Getting an actor from url, eventually creating it
|
||||||
|
|
||||||
|
Returns an error if fetch failed
|
||||||
|
"""
|
||||||
|
# TODO: Move this to Mobilizon.Service.ActivityPub
|
||||||
|
@spec get_or_fetch_by_url!(String.t(), bool()) :: Actor.t()
|
||||||
def get_or_fetch_by_url!(url, preload \\ false) do
|
def get_or_fetch_by_url!(url, preload \\ false) do
|
||||||
with {:ok, actor} <- get_actor_by_url(url, preload) do
|
with {:ok, actor} <- get_actor_by_url(url, preload) do
|
||||||
actor
|
actor
|
||||||
|
@ -422,6 +465,7 @@ defmodule Mobilizon.Actors do
|
||||||
Find local users by their username
|
Find local users by their username
|
||||||
"""
|
"""
|
||||||
# TODO: This doesn't seem to be used anyway
|
# TODO: This doesn't seem to be used anyway
|
||||||
|
@spec find_local_by_username(String.t()) :: list(Actor.t())
|
||||||
def find_local_by_username(username) do
|
def find_local_by_username(username) do
|
||||||
actors =
|
actors =
|
||||||
Repo.all(
|
Repo.all(
|
||||||
|
@ -507,6 +551,7 @@ defmodule Mobilizon.Actors do
|
||||||
@doc """
|
@doc """
|
||||||
Create a new person actor
|
Create a new person actor
|
||||||
"""
|
"""
|
||||||
|
@spec new_person(map()) :: {:ok, Actor.t()} | any
|
||||||
def new_person(args) do
|
def new_person(args) do
|
||||||
key = :public_key.generate_key({:rsa, 2048, 65_537})
|
key = :public_key.generate_key({:rsa, 2048, 65_537})
|
||||||
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
|
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
|
||||||
|
@ -522,6 +567,10 @@ defmodule Mobilizon.Actors do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Register a new bot actor.
|
||||||
|
"""
|
||||||
|
@spec register_bot_account(map()) :: Actor.t()
|
||||||
def register_bot_account(%{name: name, summary: summary}) do
|
def register_bot_account(%{name: name, summary: summary}) do
|
||||||
key = :public_key.generate_key({:rsa, 2048, 65_537})
|
key = :public_key.generate_key({:rsa, 2048, 65_537})
|
||||||
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
|
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
|
||||||
|
@ -630,7 +679,11 @@ defmodule Mobilizon.Actors do
|
||||||
Member.changeset(member, %{})
|
Member.changeset(member, %{})
|
||||||
end
|
end
|
||||||
|
|
||||||
def groups_for_actor(%Actor{id: id} = _actor) do
|
@doc """
|
||||||
|
Returns the memberships for an actor
|
||||||
|
"""
|
||||||
|
@spec groups_memberships_for_actor(Actor.t()) :: list(Member.t())
|
||||||
|
def groups_memberships_for_actor(%Actor{id: id} = _actor) do
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from(
|
from(
|
||||||
m in Member,
|
m in Member,
|
||||||
|
@ -640,7 +693,11 @@ defmodule Mobilizon.Actors do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def members_for_group(%Actor{type: :Group, id: id} = _group) do
|
@doc """
|
||||||
|
Returns the memberships for a group
|
||||||
|
"""
|
||||||
|
@spec memberships_for_group(Actor.t()) :: list(Member.t())
|
||||||
|
def memberships_for_group(%Actor{type: :Group, id: id} = _group) do
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from(
|
from(
|
||||||
m in Member,
|
m in Member,
|
||||||
|
@ -681,6 +738,9 @@ defmodule Mobilizon.Actors do
|
||||||
"""
|
"""
|
||||||
def get_bot!(id), do: Repo.get!(Bot, id)
|
def get_bot!(id), do: Repo.get!(Bot, id)
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Get the bot associated to an actor
|
||||||
|
"""
|
||||||
@spec get_bot_by_actor(Actor.t()) :: Bot.t()
|
@spec get_bot_by_actor(Actor.t()) :: Bot.t()
|
||||||
def get_bot_by_actor(%Actor{} = actor) do
|
def get_bot_by_actor(%Actor{} = actor) do
|
||||||
Repo.get_by!(Bot, actor_id: actor.id)
|
Repo.get_by!(Bot, actor_id: actor.id)
|
||||||
|
@ -772,6 +832,9 @@ defmodule Mobilizon.Actors do
|
||||||
|> Repo.preload([:actor, :target_actor])
|
|> Repo.preload([:actor, :target_actor])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Get a follower by the followed actor and following actor
|
||||||
|
"""
|
||||||
@spec get_follower(Actor.t(), Actor.t()) :: Follower.t()
|
@spec get_follower(Actor.t(), Actor.t()) :: Follower.t()
|
||||||
def get_follower(%Actor{id: followed_id}, %Actor{id: follower_id}) do
|
def get_follower(%Actor{id: followed_id}, %Actor{id: follower_id}) do
|
||||||
Repo.one(
|
Repo.one(
|
||||||
|
|
|
@ -58,7 +58,7 @@ defmodule MobilizonWeb.API.Comments do
|
||||||
@spec get_in_reply_to_comment(nil) :: nil
|
@spec get_in_reply_to_comment(nil) :: nil
|
||||||
defp get_in_reply_to_comment(nil), do: nil
|
defp get_in_reply_to_comment(nil), do: nil
|
||||||
@spec get_in_reply_to_comment(String.t()) :: Comment.t()
|
@spec get_in_reply_to_comment(String.t()) :: Comment.t()
|
||||||
defp get_in_reply_to_comment(inReplyToCommentURL) do
|
defp get_in_reply_to_comment(in_reply_to_comment_url) do
|
||||||
ActivityPub.fetch_object_from_url(inReplyToCommentURL)
|
ActivityPub.fetch_object_from_url(in_reply_to_comment_url)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,6 +9,9 @@ defmodule MobilizonWeb.API.Events do
|
||||||
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
|
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
|
||||||
import MobilizonWeb.API.Utils
|
import MobilizonWeb.API.Utils
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Create an event
|
||||||
|
"""
|
||||||
@spec create_event(map()) :: {:ok, Activity.t()} | any()
|
@spec create_event(map()) :: {:ok, Activity.t()} | any()
|
||||||
def create_event(
|
def create_event(
|
||||||
%{
|
%{
|
||||||
|
|
|
@ -9,6 +9,9 @@ defmodule MobilizonWeb.API.Groups do
|
||||||
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
|
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
|
||||||
import MobilizonWeb.API.Utils
|
import MobilizonWeb.API.Utils
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Create a group
|
||||||
|
"""
|
||||||
@spec create_group(map()) :: {:ok, Activity.t()} | any()
|
@spec create_group(map()) :: {:ok, Activity.t()} | any()
|
||||||
def create_group(
|
def create_group(
|
||||||
%{
|
%{
|
||||||
|
|
|
@ -4,8 +4,13 @@ defmodule MobilizonWeb.Resolvers.Address do
|
||||||
"""
|
"""
|
||||||
require Logger
|
require Logger
|
||||||
alias Mobilizon.Addresses
|
alias Mobilizon.Addresses
|
||||||
|
alias Mobilizon.Addresses.Address
|
||||||
alias Mobilizon.Service.Geospatial
|
alias Mobilizon.Service.Geospatial
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Search an address
|
||||||
|
"""
|
||||||
|
@spec search(map(), map(), map()) :: {:ok, list(Address.t())}
|
||||||
def search(_parent, %{query: query}, %{context: %{ip: ip}}) do
|
def search(_parent, %{query: query}, %{context: %{ip: ip}}) do
|
||||||
country = Geolix.lookup(ip) |> Map.get(:country, nil)
|
country = Geolix.lookup(ip) |> Map.get(:country, nil)
|
||||||
|
|
||||||
|
@ -18,6 +23,10 @@ defmodule MobilizonWeb.Resolvers.Address do
|
||||||
{:ok, addresses}
|
{:ok, addresses}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Reverse geocode some coordinates
|
||||||
|
"""
|
||||||
|
@spec reverse_geocode(map(), map(), map()) :: {:ok, list(Address.t())}
|
||||||
def reverse_geocode(_parent, %{longitude: longitude, latitude: latitude}, %{context: %{ip: ip}}) do
|
def reverse_geocode(_parent, %{longitude: longitude, latitude: latitude}, %{context: %{ip: ip}}) do
|
||||||
country = Geolix.lookup(ip) |> Map.get(:country, nil)
|
country = Geolix.lookup(ip) |> Map.get(:country, nil)
|
||||||
|
|
||||||
|
|
6
mix.exs
6
mix.exs
|
@ -149,7 +149,8 @@ defmodule Mobilizon.Mixfile do
|
||||||
"support/guides/install/dependencies.md",
|
"support/guides/install/dependencies.md",
|
||||||
"support/guides/install/docker.md",
|
"support/guides/install/docker.md",
|
||||||
"support/guides/introduction.md",
|
"support/guides/introduction.md",
|
||||||
"CONTRIBUTING.md"
|
"support/guides/contributing.md",
|
||||||
|
"support/guides/code_of_conduct.md"
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -297,7 +298,8 @@ defmodule Mobilizon.Mixfile do
|
||||||
[
|
[
|
||||||
Introduction: [
|
Introduction: [
|
||||||
"support/guides/introduction.md",
|
"support/guides/introduction.md",
|
||||||
"CONTRIBUTING.md"
|
"support/guides/contributing.md",
|
||||||
|
"support/guides/code_of_conduct.md"
|
||||||
],
|
],
|
||||||
Development: [
|
Development: [
|
||||||
"support/guides/development/development.md",
|
"support/guides/development/development.md",
|
||||||
|
|
47
support/guides/code_of_conduct.md
Normal file
47
support/guides/code_of_conduct.md
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# Code of Conduct
|
||||||
|
|
||||||
|
This section explains Framasoft's commitment towards contributors to the [Mobilizon Project](https://joinmobilizon.org), as well as expectations that community members should have of one another. It is based on the [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/version/1/4/code-of-conduct) v1.4.
|
||||||
|
|
||||||
|
## Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as contributors and maintainers - including Framasoft - pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
We also follow the [Post-Meritocracy Manifesto](https://postmeritocracy.org).
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behaviour that contributes to creating a positive environment include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behaviour by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others’ private information, such as a physical or electronic address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
The Project maintainers are responsible for clarifying the standards of acceptable behaviour and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behaviour.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviours that they deem inappropriate, threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
|
||||||
|
Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
||||||
|
Representation of a project may be further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behaviour may be reported by contacting the project team at tcit plus mobilizon at framasoft dot org. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project’s leadership.
|
||||||
|
|
||||||
|
---
|
270
support/guides/contributing.md
Normal file
270
support/guides/contributing.md
Normal file
|
@ -0,0 +1,270 @@
|
||||||
|
# Contributing to Mobilizon
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How we collaborate
|
||||||
|
|
||||||
|
First off, thank you for considering contributing to Mobilizon!
|
||||||
|
|
||||||
|
Our aim is for this project to make you feel welcome as a contributor.
|
||||||
|
We hugely value the comments and contributions of community members in the various
|
||||||
|
publicly-accessible areas in use, which currently are:
|
||||||
|
|
||||||
|
* Home: [Home](https://joinmobilizon.org)
|
||||||
|
* Wiki: [Project Wiki](https://framagit.org/framasoft/mobilizon/wikis) (Gitlab)
|
||||||
|
* Code: [Framagit](https://framagit.org/framasoft/mobilizon) (Gitlab)
|
||||||
|
* Issue tracking: [Framagit](https://framagit.org/framasoft/mobilizon) (Gitlab)
|
||||||
|
* Discussion: [Mobilizon forum](https://framacolibri.org/c/mobilizon) (Discourse)
|
||||||
|
* Chat: [#Mobilizon:matrix.org](https://matrix.org/#/room/#Mobilizon:matrix.org) (Matrix) or `#mobilizon` on Freenode (IRC)
|
||||||
|
|
||||||
|
### How to communicate with others ?
|
||||||
|
|
||||||
|
As you may know, English is not the only language in the community and communicating in various languages can be quite tricky. Some might not understand what you're saying, while other have a hard time writing in a language that is not their primary language.
|
||||||
|
|
||||||
|
If you don't have noticed yet, French is the de-facto language for communication in various channels. This is because a huge part of our community speaks French, as Framasoft is a French organization.
|
||||||
|
|
||||||
|
We don't want to impose French as the only language in the project, though, because we would lose a ton of potential users users and contributors that can help us make Mobilizon better.
|
||||||
|
|
||||||
|
You can write what you would like to tell us in the language you're most comfortable with and then use automagic translation to translate your message in English. It's far from perfect, but if we can understand the main idea you wanted to expose, it's sufficient.
|
||||||
|
|
||||||
|
Of course, you can and should express yourself directly in English if you're confident doing so!
|
||||||
|
|
||||||
|
### Other useful information
|
||||||
|
|
||||||
|
* It's early days for Mobilizon, so we're focused on laying down the foundations for the project. This means that you won't find much usable code yet.
|
||||||
|
* We're working as openly and transparently as possible with this project. As a contributor or member of the Mobilizon community, almost everything you come across will link to some other things that you should have access to. If you don't have access, and you think you should, just ask!
|
||||||
|
|
||||||
|
### Contributopia
|
||||||
|
|
||||||
|
This project is part of [Framasoft](https://framasoft.org)'s [Contributopia Campaign](https://contributopia.org), creating and offering tools designed with a different set of values.
|
||||||
|
|
||||||
|
![Framameet illustration on Contributopia Website](https://contributopia.org/img/services-framameet.jpg)
|
||||||
|
|
||||||
|
### We practice [Ethical Design](https://2017.ind.ie/ethical-design/)
|
||||||
|
|
||||||
|
We endeavour to build technology that respects human rights, human effort, and human experience, and hope you will join in this effort.
|
||||||
|
|
||||||
|
[![Ethical Design diagram](https://i.imgur.com/O7RJo60.png)](https://2017.ind.ie/ethical-design/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [Code Of Conduct](code_of_conduct.html)
|
||||||
|
|
||||||
|
We require all participants to read and accept our [Code of Conduct](code_of_conduct.html).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How Can I Contribute
|
||||||
|
|
||||||
|
### Submitting Feature Requests, Enhancement Suggestions or Bug Reports
|
||||||
|
|
||||||
|
This section guides you through submitting a ✨ feature request, 💄 enhancement suggestions, and 🐛 bug reports for Mobilizon - anything from errors and crashes, to minor improvements, to completely new features.
|
||||||
|
|
||||||
|
When you post an issue, please include as many details as possible. **Fill in the issue template** (available below) to help us resolve issues faster.
|
||||||
|
|
||||||
|
### Before making a submission
|
||||||
|
|
||||||
|
Please go through the checklist below before posting any ✨ 💄 🐛
|
||||||
|
|
||||||
|
* **Check if you're using the latest version** of Mobilizon and all its relevant components and if you can get the desired behaviour by changing some config settings.
|
||||||
|
* **Perform a cursory search** in the issue tracker to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
|
||||||
|
* Never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to tcit plus mobilizon at framasoft dot org.
|
||||||
|
|
||||||
|
> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one.
|
||||||
|
|
||||||
|
### Suggesting Features & Enhancements
|
||||||
|
|
||||||
|
If you find yourself wishing for a feature that doesn't exist in Mobilizon, you are probably not alone. There are probably others out there with similar needs.
|
||||||
|
|
||||||
|
Open an issue providing the following information:
|
||||||
|
|
||||||
|
* **Use a clear and descriptive title** for the issue to identify the suggestion.
|
||||||
|
* **Provide a description of the suggested enhancement** in as many details as possible, including the steps that you imagine you (as a user) would take if the feature you're requesting existed.
|
||||||
|
* **Describe the current behaviour** and **explain which behaviour you would like to see instead** and why.
|
||||||
|
* **Include screenshots and animated GIFs** which help you demonstrate the steps or point out the parts of Mobilizon which the suggestion is related to. You can use a tool called [LICEcap](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [Silentcast](https://github.com/colinkeenan/silentcast) or [Byzanz](https://github.com/GNOME/byzanz) on Linux.
|
||||||
|
* **Provide specific examples to demonstrate the enhancements**. If possible, include drawings, screenshots, or gifs of similar features in another app.
|
||||||
|
* **Explain why this enhancement would be useful** to most participants of Mobilizon and isn't something that can or should be implemented as a community extension.
|
||||||
|
* **List some other communities, platforms or apps where this enhancement exists.**
|
||||||
|
|
||||||
|
### Reporting Bugs 🐛
|
||||||
|
|
||||||
|
Open an issue providing the following information by filling in issue template below, explaining the problem and including additional details to help maintainers reproduce the problem:
|
||||||
|
|
||||||
|
* **Use a clear and descriptive title** for the issue to identify the problem.
|
||||||
|
* **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining where you started in Mobilizon, and then which actions you took. When listing steps, **don't just say what you did, but explain how you did it**. For example, if you moved the cursor to the end of a line, explain if you used the mouse or a keyboard shortcut, and if so which one?
|
||||||
|
* **Provide specific examples to demonstrate the steps**. Include links to pages, screenshots, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use Markdown code blocks.
|
||||||
|
* **Describe the behaviour you observed after following the steps** and point out what exactly is the problem with that behaviour.
|
||||||
|
* **Explain which behaviour you expected to see instead and why.**
|
||||||
|
* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux.
|
||||||
|
* **If you're reporting a crash**, include a crash report with error logs.
|
||||||
|
* **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below.
|
||||||
|
|
||||||
|
Provide more context by answering these questions:
|
||||||
|
|
||||||
|
* **Did the problem start happening recently** (e.g. after updating to a new version) or was this always a problem?
|
||||||
|
* If the problem started happening recently, **can you reproduce the problem in an older version?** What's the most recent version in which the problem doesn't happen?
|
||||||
|
* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens.
|
||||||
|
* If the problem is related to an event, comment or actor (eg. user or group), **does the problem happen for all of them or only some?** Does the problem happen only when working with local (originating from your Mobilizon instance) or remote ones, with anything specific about the object? Is there anything else special about the actors or events in question?
|
||||||
|
|
||||||
|
Include details about your configuration and environment:
|
||||||
|
|
||||||
|
* **Which version of each component are you using?**
|
||||||
|
* **What's the name and version of the OS you're using**?
|
||||||
|
* **Are you running in a virtual machine?** If so, which VM software are you using and which operating systems and versions are used for the host and the guest?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
A common misconception about contributing to free and open source projects is that you need to contribute code. In fact, it’s often the other parts of a project that are most overlooked. You’ll do the project a huge favour by offering to pitch in with these types of contributions!
|
||||||
|
|
||||||
|
Even if you like to write code, other types of contributions are a great way to get involved with a project and meet other community members. Building those relationships may open up unexpected opportunities.
|
||||||
|
|
||||||
|
#### Do you like to design? 🎨
|
||||||
|
|
||||||
|
* Restructure layouts to improve the project's usability
|
||||||
|
* Conduct user research to reorganise and refine the project's navigation or menus
|
||||||
|
* Create art for icons and app screens
|
||||||
|
|
||||||
|
#### Do you like to write? ✏
|
||||||
|
|
||||||
|
* Write and improve the project's documentation
|
||||||
|
* Write tutorials for the project
|
||||||
|
* Curate a wiki page of examples showing how the project can be used
|
||||||
|
|
||||||
|
#### Do you like organising? 📥
|
||||||
|
|
||||||
|
* Link to duplicate issues, and suggest new issue labels, to keep things organised
|
||||||
|
* Go through open issues and suggest revisiting or closing old ones
|
||||||
|
* Ask clarifying questions on recently opened issues to move the discussion forward
|
||||||
|
|
||||||
|
#### Do you like helping people? 🙋
|
||||||
|
|
||||||
|
* Answer questions about the project on forums and other sites
|
||||||
|
* Answer questions for people on open issues
|
||||||
|
|
||||||
|
#### Do you like helping others code? 👐
|
||||||
|
|
||||||
|
* Review code on other people’s submissions
|
||||||
|
* Write tutorials for how a project can be used
|
||||||
|
* Offer to mentor another contributor
|
||||||
|
|
||||||
|
#### Do you like to code? 🔩
|
||||||
|
|
||||||
|
* Find an open issue to tackle
|
||||||
|
* Offer to help write a new feature
|
||||||
|
* Improve tooling, testing & deployment options
|
||||||
|
* Read the **next section for guidelines**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Contributing Code
|
||||||
|
|
||||||
|
Unsure where to begin contributing? You can start by looking through issues tagged with:
|
||||||
|
|
||||||
|
* [`first-timers-only`](https://framagit.org/framasoft/mobilizon/issues?scope=all&utf8=✓&state=opened&label_name[]=🌱 first-timers-only) [https://www.firsttimersonly.com](https://www.firsttimersonly.com)- issues which should only require a few lines of code, and a test or two.
|
||||||
|
* [`help-wanted`](https://framagit.org/framasoft/mobilizon/issues?label_name[]=🙏 help-wanted) - issues which should be a bit more involved than `first-timers-only` issues.
|
||||||
|
|
||||||
|
#### Local development
|
||||||
|
|
||||||
|
Mobilizon can be developed locally. For instructions on how to do this, please see [the documentation](development.html).
|
||||||
|
|
||||||
|
#### Coding & git practices
|
||||||
|
|
||||||
|
* We use GitLab's merge requests as our code review tool
|
||||||
|
* We encourage any dev to comment on merge requests and we think of the merge request not as a "please approve my code" but as a space for co-developing.
|
||||||
|
* We develop features on separate branches identified by issue numbers.
|
||||||
|
* We don't currently use release branches or tags because we don't have release management at this phase of development.
|
||||||
|
|
||||||
|
#### How to make changes
|
||||||
|
|
||||||
|
* Make your changes on a separate branch which includes an issue number e.g. `1234-some-new-feature` where 1234 is the issue number where the feature is documented. Make sure the branch is based on `develop`.
|
||||||
|
* Do not commit changes to files that are irrelevant to your feature or bugfix.
|
||||||
|
* Use commit messages descriptive of your changes.
|
||||||
|
* Push to the upstream of your new branch.
|
||||||
|
* Create a merge request at GitLab.
|
||||||
|
|
||||||
|
#### Git commit messages
|
||||||
|
|
||||||
|
* Limit the first line to 72 characters or less, referencing relevant issue numbers
|
||||||
|
* Be as descriptive as you want after the first line
|
||||||
|
* Consider starting the commit message with an applicable emoji (see Issue & Commit Categories below)
|
||||||
|
|
||||||
|
#### Merge requests
|
||||||
|
|
||||||
|
* Follow [the code styleguides](styleguide.html).
|
||||||
|
* Document new code based on [the documentation styleguide](https://hexdocs.pm/elixir/writing-documentation.html)
|
||||||
|
* Each merge request should implement ONE feature or bugfix. If you want to add or fix more than one thing, submit more than one merge request.
|
||||||
|
* Fill in the merge request template below
|
||||||
|
* Include relevant issue number(s) in the merge request title
|
||||||
|
* Include screenshots or animated GIFs in your merge request whenever possible.
|
||||||
|
* End all files with a newline
|
||||||
|
|
||||||
|
#### Template for merge requests
|
||||||
|
|
||||||
|
* Description of the change
|
||||||
|
* Applicable issue numbers
|
||||||
|
* Alternate designs/implementations
|
||||||
|
* Benefits of this implementation
|
||||||
|
* Possible drawbacks
|
||||||
|
* Why should this be part of a core component?
|
||||||
|
* Testing process
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Issue & Commit Categories
|
||||||
|
|
||||||
|
* 🚑 `critical` : Critical hotfix!
|
||||||
|
* 💄 `enhancement` : General improvements.
|
||||||
|
* ✨ `feature` : New features.
|
||||||
|
* 🐛 `bug` : Confirmed bugs, or reports that are likely to be bugs.
|
||||||
|
* 🙋 `question` : Questions (e.g. how can I do X?)
|
||||||
|
* 📮 `feedback` : General feedback.
|
||||||
|
* 🎨 `ui` : Visual design.
|
||||||
|
* 📜 `copy` : Text in the apps, or translations.
|
||||||
|
* ℹ `documentation` : Documentation.
|
||||||
|
* 🐎 `performance` : Performance.
|
||||||
|
* 🔒 `security` : Security.
|
||||||
|
* 🔌 `api` : Mobilizon's APIs.
|
||||||
|
* 👽 `external` : External libraries or API integrations.
|
||||||
|
* ⚠ `exception` : Uncaught exceptions.
|
||||||
|
* 🔥 `crash` : Crash.
|
||||||
|
* 🔣 `encoding` : Character encoding or data serialization issue.
|
||||||
|
* 🚚 `cleanup` : Removing, moving or refactoring code or files.
|
||||||
|
* ✅ `tests` : Testing
|
||||||
|
|
||||||
|
#### Issue Status
|
||||||
|
|
||||||
|
* 💬 `discussion` : Discussion to clarify this issue is ongoing.
|
||||||
|
* 🔜 `todo` : This has been discussed and now needs work.
|
||||||
|
* 🔁 `needs-more-info` : More information needs to be collected about these problems or feature requests (e.g. steps to reproduce).
|
||||||
|
* 💡 `idea` : Needs to be discussed further. Could be a feature request which might be good to first implement as a community extension.
|
||||||
|
* 🚧 `in-progress` : Someone is working on this...
|
||||||
|
* 🙏 `help-wanted` : The Mobilizon core team would appreciate help from the community in resolving these issues.
|
||||||
|
* 🌱 `first-timers-only` : Less complex issues which would be good first issues to work on for users who want to contribute.
|
||||||
|
* 🔢 `needs-reproduction` : Likely bugs, but haven't been reliably reproduced.
|
||||||
|
* 🔴 `blocked` : Blocked on other issues.
|
||||||
|
* 2️⃣ `duplicate` : Duplicate of another issue, i.e. has been reported before.
|
||||||
|
* 🙅 `wontfix` : The Mobilizon core team has decided not to fix these issues (or add these features) for now, because they're working as intended, or some other reason.
|
||||||
|
* 🚮 `invalid` : Issues which are not valid (e.g. spam or submitted by error).
|
||||||
|
|
||||||
|
|
||||||
|
#### Merge Request Status
|
||||||
|
|
||||||
|
* 🚧 `in-progress` : Still being worked on, more changes will follow.
|
||||||
|
* 🚏 `needs-review` : Needs code review and approval from maintainers.
|
||||||
|
* 🔍 `under-review` : Being reviewed by maintainers.
|
||||||
|
* 🔧 `changes-required` : Needs to be updated based on review comments and then reviewed again.
|
||||||
|
* 👀 `needs-testing` : Needs manual testing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
The following documents have greatly helped us put this together. A big thank you to their authors and contributors!
|
||||||
|
|
||||||
|
* [MoodleNet's own Contributing document](https://gitlab.com/moodlenet/project/meta/issues/16)
|
||||||
|
* [Contributor Covenant](https://www.contributor-covenant.org/version/1/4/code-of-conduct)
|
||||||
|
* [Open Source Guides](https://opensource.guide)
|
||||||
|
* [Holochain's Development Protocols](https://github.com/holochain/holochain-proto/wiki/Development-Protocols)
|
||||||
|
* [Atom's contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md)
|
||||||
|
* [Funkwhale's pad on how to Communicate with others](https://hackmd.io/qESHvdHZSWuhLNjeanaVQw?both)
|
|
@ -14,3 +14,5 @@ These two commands must not return an error code, since they are required to pas
|
||||||
|
|
||||||
We use `tslint` with the `tslint-config-airbnb` preset.
|
We use `tslint` with the `tslint-config-airbnb` preset.
|
||||||
Errors should be reported when running in dev mode `npm run dev` or when building a production bundle `npm run build`.
|
Errors should be reported when running in dev mode `npm run dev` or when building a production bundle `npm run build`.
|
||||||
|
|
||||||
|
We also try to follow the [official Vue.js style guide](https://vuejs.org/v2/style-guide/).
|
||||||
|
|
|
@ -43,7 +43,7 @@ There's no lock-in, you can interact with the event without registration.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
We appreciate any contribution to MobiliZon. Check our [CONTRIBUTING](contributing.html) file for more information.
|
We appreciate any contribution to MobiliZon. Check our [Contributing](contributing.html) page for more information.
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue