diff --git a/.pylintrc b/.pylintrc index e3ae2e3..c884d98 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,13 +1,11 @@ [MASTER] load-plugins=pylint_django, pylint_celery django-settings-module=config.settings.local +ignore=migrations, tests [FORMAT] max-line-length=120 -[MESSAGES CONTROL] -disable=missing-docstring,invalid-name - [DESIGN] max-parents=13 @@ -15,6 +13,5 @@ max-parents=13 generated-members=REQUEST,acl_users,aq_parent,"[a-zA-Z]+_set{1,2}",save,delete [SIMILARITIES] - # Ignore imports when computing similarities. ignore-imports=yes diff --git a/.readthedocs.yml b/.readthedocs.yml index 501b019..5564388 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,12 +1,20 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required version: 2 +# Set the version of Python and other tools you might need build: - image: testing + os: ubuntu-22.04 + tools: + python: '3.12' +# Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py +# Python requirements required to build your docs python: - version: 3.9 install: - requirements: requirements/local.txt diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index ea47a80..39f4727 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,4 +1,175 @@ +.. _contributing: + +*************************** Contributing to Democrasite -=========================== +*************************** + +.. index:: pip, virtualenv, PostgreSQL + + +What to contribute +================== + +Obviously this project doesn't work without people adding contributions, +preferably many people adding lots of little contributions frequently. The +project is oriented towards contributions which add new apps or features, with +the constitution mechanic aimed at making it harder to significantly alter +existing code than to add new things. The constitution is also aimed at +protecting the core purpose and philosophy of the site, *not* its +functionality. There are innumerable ways to just break the deployment or +delivery of the website and I humbly ask that you refrain from intentionally +doing so. If I see a pull request which breaks the site, I will close it. Use +this website as a playground for whatever project you have that you want to +see hosted as long as Django can serve it; I'd love for this to serve +people's random creations. That being said, there are plenty of contributions +and additions you could make to the site itself, such as to this document, +which tells people nothing about how to contribute at the moment. Regardless of +how you choose contribute, as long as it is in good faith, I appreciate it. + + +.. Adapted from https://cookiecutter-django.readthedocs.io/en/latest/developing-locally.html + +Getting Up and Running +====================== + +Setting Up Development Environment +---------------------------------- + +Make sure to have the following on your host: + +* Python 3.12 +* PostgreSQL_ +* Redis_ +* Cookiecutter_ + +First things first. + +#. Create a virtualenv:: + + $ python3.12 -m venv + +#. Activate the virtualenv you have just created:: + + $ source /bin/activate + +#. Install development requirements:: + + $ cd + $ pip install -r requirements/local.txt + $ git init # A git repo is required for pre-commit to install + $ pre-commit install + + .. note:: + + the `pre-commit` hook exists in the generated project as default. + For the details of `pre-commit`, follow the `pre-commit`_ site. + +#. Create a new PostgreSQL database using createdb_:: + + $ createdb --username=postgres + + ``project_slug`` is what you have entered as the project_slug at the setup stage. + + .. note:: + + if this is the first time a database is created on your machine you might need an + `initial PostgreSQL set up`_ to allow local connections & set a password for + the ``postgres`` user. The `postgres documentation`_ explains the syntax of the config file + that you need to change. + + +#. Set the environment variables for your database(s):: + + $ export DATABASE_URL=postgres://postgres:@127.0.0.1:5432/ + # Optional: set broker URL if using Celery + $ export CELERY_BROKER_URL=redis://localhost:6379/0 + + .. seealso:: + + To help setting up your environment variables, you have a few options: + + * create an ``.env`` file in the root of your project and define all the variables you need in it. + There's a .env.sample in the root of the repository which you can rename to serve as a basis. + Then you just need to have ``DJANGO_READ_DOT_ENV_FILE=True`` in your machine and all the variables + will be read. + * Use a local environment manager like `direnv`_ + +#. Apply migrations:: + + $ python manage.py migrate + +#. See the application being served through Django development server:: + + $ python manage.py runserver 0.0.0.0:8000 + +.. _PostgreSQL: https://www.postgresql.org/download/ +.. _Redis: https://redis.io/download +.. _CookieCutter: https://github.com/cookiecutter/cookiecutter +.. _createdb: https://www.postgresql.org/docs/current/static/app-createdb.html +.. _initial PostgreSQL set up: https://web.archive.org/web/20190303010033/http://suite.opengeo.org/docs/latest/dataadmin/pgGettingStarted/firstconnect.html +.. _postgres documentation: https://www.postgresql.org/docs/current/static/auth-pg-hba-conf.html +.. _pre-commit: https://pre-commit.com/ +.. _direnv: https://direnv.net/ + + +Celery +------ + +If the project is configured to use Celery as a task scheduler then, by default, tasks are set to run on the main thread when developing locally instead of getting sent to a broker. However, if you have Redis setup on your local machine, you can set the following in ``config/settings/local.py``:: + + CELERY_TASK_ALWAYS_EAGER = False + +Next, make sure `redis-server` is installed (per the `Getting started with +Redis guide`_) and run the server in one terminal:: + + $ redis-server + +Start the Celery worker by running the following command in another terminal:: + + $ celery -A config.celery_app worker --loglevel=info + +That Celery worker should be running whenever your app is running, typically as +a background process, so that it can pick up any tasks that get queued. Learn +more from the `Celery Workers Guide`_. + +You can also use Django admin to queue up tasks, thanks to the +`django-celerybeat`_ package. + +.. _Getting started with Redis guide: https://redis.io/docs/getting-started/ +.. _Celery Workers Guide: https://docs.celeryq.dev/en/stable/userguide/workers.html +.. _django-celerybeat: https://django-celery-beat.readthedocs.io/en/latest/ + + +Creating a webhook +------------------ + +:obj:`democrasite.webiscite` needs `webhooks`_ to find out about events on +Github. `Create a webhook`_ in your fork of the repository, then generate a +secret key for your hook and store it in your environment (either through your +terminal or ``.env`` file) as ``GITHUB_SECRET_KEY``. + +To test your webhook, follow these `instructions`_. (If you have a preferred +tool for exposing your local server, feel free to replace smee with it.) If you +are using smee, be sure to run:: + + smee --url WEBHOOK_PROXY_URL --path /webhooks/github --port 8000 + +to set the correct port and path. + +.. _webhooks: https://docs.github.com/en/developers/webhooks-and-events/webhooks/about-webhooks +.. _create a webhook: https://docs.github.com/en/webhooks/using-webhooks/creating-webhooks +.. _instructions: https://docs.github.com/en/webhooks/using-webhooks/handling-webhook-deliveries + + +Automating the Repository +------------------------- + +When a :class:`~democrasite.webiscite.models.Bill` passes, the corresponding +pull request is automatically merged into the master branch, and if code blocks +from the Constitution are moved, their locations are automatically updated in +the remote constitution.json. In order to test this functionality in your fork +of the repository, you will need to `create a Github personal access token`_ +and store it in your environment as ``GITHUB_TOKEN``. Make sure it at least has +write access to your fork of the repository. -Obviously this project doesn't work without people adding contributions, preferably many people adding lots of little contributions frequently. The project is oriented towards contributions which add new apps or features, with the constitution mechanic aimed at making it harder to significantly alter existing code than to add new things. The constitution is also aimed at protecting the core purpose and philosophy of the site, *not* its functionality. There are innumerable ways to just break the deployment or delivery of the website and I humbly ask that you refrain from intentionally doing so. If I see a pull request which breaks the site, I will close it. Use this website as a playground for whatever project you have that you want to see hosted as long as Django can serve it; I'd love for this to serve people's random creations. That being said, there are plenty of contributions and additions you could make to the site itself, such as to this document, which tells people nothing about how to contribute at the moment. Regardless of how you choose contribute, as long as it is in good faith, I appreciate it. +.. _create a Github personal access token: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens diff --git a/README.rst b/README.rst index bf12d70..015d1a9 100644 --- a/README.rst +++ b/README.rst @@ -19,14 +19,20 @@ Democrasite :License: MIT -Democrasite is a website which automatically merges changes based on popular approval. For more information on the nature and purpose of the project, visit our `about page`_. This page is meant for people who want to clone the repository and contribute to the project. This project is approximately in beta development (hence the repository being named "cookiestocracy" – a reference to cookiecutter and `kakistocracy`_). The alpha version is `here`_ and the full version doesn't exist yet. +Democrasite is a website which automatically merges changes based on popular +approval. For more information on the nature and purpose of the project, visit +our `about page`_. This page is meant for people who want to clone the +repository and contribute to the project. This project is approximately in beta +development (hence the repository being named "cookiestocracy" - a reference +to cookiecutter and `kakistocracy`_). The alpha version is `here`_ and the +full version doesn't exist yet. * Homepage: https://democrasite.herokuapp.com * Source code: https://github.com/mfosterw/cookiestocracy * Documentation: - https://democrasite.readthedocs.io + https://cookiestocracy.readthedocs.io/en/latest/ .. _`about page`: https://democrasite.herokuapp.com/about/ .. _`kakistocracy`: https://en.wikipedia.org/wiki/Kakistocracy @@ -35,13 +41,13 @@ Democrasite is a website which automatically merges changes based on popular app Contributing ------------ -Please read the `contribution guide`_ and then see the basic commands below. -It is also recommended that you rename ".env.sample" in the root of the -repository to ".env" and set the environment variable +Please read the :ref:`contribution guide ` and then see the basic +commands below. It is also recommended that you rename ".env.sample" in the +root of the repository to ".env" and set the environment variable ``DJANGO_READ_DOT_ENV_FILE=True`` so you can more easily keep track of your environment variables. -.. _`contribution guide`: https://github.com/mfosterw/cookiestocracy/blob/master/CONTRIBUTING.rst +.. _`contribution guide`: https://cookiestocracy.readthedocs.io/en/latest/CONTRIBUTING.html Basic Commands -------------- @@ -49,11 +55,9 @@ Basic Commands Getting Started ^^^^^^^^^^^^^^^ -To start the server, run this command in the root of the repository: +To start the server, run this command in the root of the repository:: -:: - - $ python manage.py runserver + $ python manage.py runserver_plus Setting Up Your Users ^^^^^^^^^^^^^^^^^^^^^ @@ -62,9 +66,19 @@ Setting Up Your Users $ python manage.py createsuperuser -* To create a normal account, it's easiest to use the admin site. For convenience, you can keep your normal user logged in on Chrome and your superuser logged in on Firefox (or similar), so that you can see how the site behaves for both kinds of users. +* To test logging in with a third party provider, you will need oauth keys from + the provider you're using. See the information on `django-allauth`_ for + `GitHub`_ and `Google`_ keys respectively, and once you have the keys create + environment variables named `-CLIENT-ID` and `-SECRET`. + Once you have these set up, log in normally with your provider. For + convenience, you can keep your normal user logged in on Chrome and your + superuser logged in on Firefox (or similar), so that you can see how the site + behaves for both kinds of users. -* To test logging in with a third party provider, you will need oauth keys from the provider you're using. See the information on `django-allauth`_ for `GitHub`_ and `Google`_ keys respectively, and once you have the keys create environment variables named `-CLIENT-ID` and `-SECRET`. Once you have these set up, log in normally with your provider. A folder will be created in the repository root called "app-messages" which contains the confirmation email. Open the link in that file and your account will be activated. + .. note:: + Accounts created through the admin page do not have a normal way to + sign in since there is no login page. To test working with + non-superuser accounts, please login through a social provider. .. _`django-allauth`: https://django-allauth.readthedocs.io/en/latest/overview.html .. _`GitHub`: https://django-allauth.readthedocs.io/en/latest/providers.html#github @@ -73,11 +87,17 @@ Setting Up Your Users Type checks ^^^^^^^^^^^ -Running type checks with mypy: +Running type checks with mypy:: + + $ mypy democrasite + + +Running tests with py.test +~~~~~~~~~~~~~~~~~~~~~~~~~~ :: - $ mypy democrasite + $ pytest Test coverage ^^^^^^^^^^^^^ @@ -88,13 +108,6 @@ To run the tests, check your test coverage, and generate an HTML coverage report $ coverage html $ open htmlcov/index.html -Running tests with py.test -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -:: - - $ pytest - Celery ^^^^^^ diff --git a/config/settings/base.py b/config/settings/base.py index 27e27d2..62864b3 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -307,8 +307,7 @@ ACCOUNT_USERNAME_REQUIRED = False ACCOUNT_ADAPTER = "democrasite.users.adapters.AccountAdapter" # https://docs.allauth.org/en/latest/socialaccount/configuration.html -# TODO: determine if adapters should stay -# SOCIALACCOUNT_ADAPTER = "democrasite.users.adapters.SocialAccountAdapter" +SOCIALACCOUNT_ADAPTER = "democrasite.users.adapters.SocialAccountAdapter" SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = True # Enable social logins INSTALLED_APPS += [ diff --git a/democrasite/__init__.py b/democrasite/__init__.py index 6ecb24e..2ec216c 100644 --- a/democrasite/__init__.py +++ b/democrasite/__init__.py @@ -1,3 +1,4 @@ +# pylint: disable=missing-module-docstring __version__ = "0.1.0" __version_info__ = tuple( # pylint: disable=consider-using-generator [ diff --git a/democrasite/conftest.py b/democrasite/conftest.py index 275f5eb..a5a02eb 100644 --- a/democrasite/conftest.py +++ b/democrasite/conftest.py @@ -1,3 +1,4 @@ +"""Global test fixtures for the project.""" import pytest from democrasite.users.models import User @@ -6,14 +7,16 @@ @pytest.fixture(autouse=True) def media_storage(settings, tmpdir): + """Change the media root to a temporary directory.""" settings.MEDIA_ROOT = tmpdir.strpath @pytest.fixture(autouse=True) def enable_db_access_for_all_tests(db): # pylint: disable=unused-argument - pass + """Give all tests access to the database.""" @pytest.fixture def user() -> User: + """Return a User instance.""" return UserFactory() diff --git a/democrasite/templates/pages/about.html b/democrasite/templates/pages/about.html index c268d4c..6e5b96e 100644 --- a/democrasite/templates/pages/about.html +++ b/democrasite/templates/pages/about.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% load i18n %} -{% block title %}{% trans "About" %} - {{ block.super }}{% endblock %} +{% block title %}{% trans "About" %} - {{ block.super }}{% endblock title %} {% block content %}
@@ -54,7 +54,7 @@
{% blocktrans %}This is an example of a bill{% endblocktr
- ✓ {% trans "Yes" %}: 0 + ✓ {% trans "Yes" %}: 0 X {% trans "No" %}: 0 @@ -67,4 +67,4 @@
{% blocktrans %}This is an example of a bill{% endblocktr

Privacy Policy

-{% endblock %} +{% endblock content %} diff --git a/democrasite/templates/webiscite/bill_detail.html b/democrasite/templates/webiscite/bill_detail.html index 827e30f..5ce10da 100644 --- a/democrasite/templates/webiscite/bill_detail.html +++ b/democrasite/templates/webiscite/bill_detail.html @@ -2,7 +2,7 @@ {% load i18n static %} -{% block title %}{{ bill.name }} - {{ block.super }}{% endblock %} +{% block title %}{{ bill.name }} - {{ block.super }}{% endblock title %} {% block content %}
@@ -57,7 +57,7 @@

{{ bill }}

{% endif %} {% endif %} > - ✓ {% trans 'Yes' %}: {{ bill.yes_votes.count }} + ✓ {% trans 'Yes' %}: {{ bill.yes_votes.count }} {{ bill }}
-{% endblock %} +{% endblock content %} {% block inline_javascript %} {% if user.is_authenticated and bill.state == bill.OPEN %} {% endif %} -{% endblock %} +{% endblock inline_javascript %} diff --git a/democrasite/users/adapters.py b/democrasite/users/adapters.py index c2f3814..f4e73fa 100644 --- a/democrasite/users/adapters.py +++ b/democrasite/users/adapters.py @@ -1,58 +1,33 @@ +"""This module contains adapters for the allauth package. + +The adapters are used to customize the behavior of allauth accounts. These are +used to allow disabling local and social account registration via settings. +""" from allauth.account.adapter import DefaultAccountAdapter from allauth.socialaccount.adapter import DefaultSocialAccountAdapter from allauth.socialaccount.models import SocialLogin from django.conf import settings from django.contrib.auth import get_user_model -from django.core.exceptions import ValidationError from django.http import HttpRequest -from django.utils.translation import gettext_lazy as _ User = get_user_model() class AccountAdapter(DefaultAccountAdapter): + """Adapter for accounts, overwritten to allow disabling local account + registration with a setting""" + def is_open_for_signup(self, request: HttpRequest): return getattr(settings, "ACCOUNT_ALLOW_LOCAL_REGISTRATION", True) class SocialAccountAdapter(DefaultSocialAccountAdapter): - def is_open_for_signup(self, request: HttpRequest, sociallogin: SocialLogin): - return getattr(settings, "ACCOUNT_ALLOW_SOCIAL_REGISTRATION", True) - - def pre_social_login(self, request: HttpRequest, sociallogin: SocialLogin): - """Invoked just after a user successfully authenticates via a social provider, - but before the login is actually processed (and before the pre_social_login - signal is emitted). - - If a social account with an email address associated with an existing user is - provided, they will automatically be linked if not already linked. - """ - # social account already exists, so this is just a login - if sociallogin.is_existing: - return - - # NOTE: Eventually I want to rewrite the login flow to always require email - # verification for each new social account connection. - - assert ( - sociallogin.email_addresses is not None - ), "All logins should include an email address" - - # If user does not exist, their account should be created - try: - user = User.objects.get(email=sociallogin.email_addresses[0].email) - except User.DoesNotExist: - # Require new users to verify email - sociallogin.email_addresses[0].verified = False - return - - # If user exists, connect the account to the existing account and login - sociallogin.connect(request, user) - - def validate_disconnect(self, account, accounts): - """ - Disables users disconnecting social accounts - """ - raise ValidationError( - _("Social accounts may not be disconnected at this time.") - ) + """Adapter for social accounts, overwritten to allow disabling social + account registration with a setting""" + + def is_open_for_signup( + self, request: HttpRequest, sociallogin: SocialLogin + ) -> bool: + return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) + + # TODO: Allow requiring verification for each social account via setting diff --git a/democrasite/users/admin.py b/democrasite/users/admin.py index 4e04b64..0723e68 100644 --- a/democrasite/users/admin.py +++ b/democrasite/users/admin.py @@ -1,3 +1,4 @@ +"""Registration of the User model in the admin site.""" from django.contrib import admin from django.contrib.auth import admin as auth_admin from django.contrib.auth import get_user_model @@ -10,6 +11,8 @@ @admin.register(User) class UserAdmin(auth_admin.UserAdmin): + """Define admin model for custom User model with no email field.""" + form = UserChangeForm add_form = UserCreationForm fieldsets = ( diff --git a/democrasite/users/apps.py b/democrasite/users/apps.py index 54030b3..0e15c2c 100644 --- a/democrasite/users/apps.py +++ b/democrasite/users/apps.py @@ -1,7 +1,10 @@ +"""Users app configuration.""" from django.apps import AppConfig from django.utils.translation import gettext_lazy as _ class UsersConfig(AppConfig): + """Users app definition.""" + name = "democrasite.users" verbose_name = _("Users") diff --git a/democrasite/users/forms.py b/democrasite/users/forms.py index 5236f81..006434d 100644 --- a/democrasite/users/forms.py +++ b/democrasite/users/forms.py @@ -1,3 +1,4 @@ +"""Override forms from allauth.""" from allauth.account.forms import ( ChangePasswordForm, ResetPasswordForm, @@ -13,11 +14,15 @@ class UserChangeForm(auth_forms.UserChangeForm): + """Override UserChangeForm to use custom User model.""" + class Meta(auth_forms.UserChangeForm.Meta): model = User class UserCreationForm(auth_forms.UserCreationForm): + """Override UserCreationForm to use custom User model.""" + class Meta(auth_forms.UserCreationForm.Meta): model = User @@ -27,20 +32,32 @@ class Meta(auth_forms.UserCreationForm.Meta): class DisabledChangePasswordForm(ChangePasswordForm): + """Substitute form to disable password changes.""" + def clean(self): + """Always raise a validation error.""" raise ValidationError(_("You cannot change your password.")) class DisabledSetPasswordForm(SetPasswordForm): + """Substitute form to disable password set.""" + def clean(self): + """Always raise a validation error.""" raise ValidationError(_("You cannot set a password.")) class DisabledResetPasswordForm(ResetPasswordForm): + """Substitute form to disable password reset.""" + def clean(self): + """Always raise a validation error.""" raise ValidationError(_("You cannot reset your password.")) class DisabledResetPasswordKeyForm(ResetPasswordKeyForm): + """Substitute form to disable password reset.""" + def clean(self): + """Always raise a validation error.""" raise ValidationError(_("You cannot reset your password.")) diff --git a/democrasite/users/models.py b/democrasite/users/models.py index 0ec6cda..57bfa6c 100644 --- a/democrasite/users/models.py +++ b/democrasite/users/models.py @@ -1,3 +1,15 @@ +"""Models related to users and accounts. + +Starting a new project, it's highly recommended to set up a custom user model, +even if the default User model is sufficient. + +This model behaves identically to the default user model, but it can be +customized in the future if the need arises. + +Additional fields can be added to the model in other apps by creating a new +model with a OneToOneField to the User model. +""" + from django.contrib.auth.models import AbstractUser from django.db.models import CharField from django.urls import reverse diff --git a/democrasite/users/tests/test_adapters.py b/democrasite/users/tests/test_adapters.py deleted file mode 100644 index c9c7acc..0000000 --- a/democrasite/users/tests/test_adapters.py +++ /dev/null @@ -1,129 +0,0 @@ -# pylint: disable=too-few-public-methods,no-self-use -import pytest -from allauth.account.models import EmailAddress -from allauth.account.views import signup -from allauth.socialaccount.helpers import complete_social_login, complete_social_signup -from allauth.socialaccount.models import SocialAccount, SocialLogin -from django.contrib.auth.models import AnonymousUser -from django.contrib.messages.middleware import MessageMiddleware -from django.contrib.sessions.middleware import SessionMiddleware -from django.core.exceptions import ValidationError -from django.http import HttpRequest -from django.test import RequestFactory -from faker import Faker - -from democrasite.users.adapters import SocialAccountAdapter -from democrasite.users.models import User -from democrasite.users.tests.factories import UserFactory - - -class TestAccountAdapter: - def test_register(self, settings, rf: RequestFactory): - """Ensure users can't sign up with registration disabled""" - settings.ACCOUNT_ALLOW_LOCAL_REGISTRATION = False - request = rf.get("/fake-url/") - request.user = AnonymousUser() - - response = signup(request) - - assert "signup_closed" in response.template_name - assert User.objects.count() == 0 - - -class TestSocialAccountAdapter: - # In order to gain a better undestanding of the django-allauth system and ensure - # everything behaved as expected in a realistic environment, I tested the - # pre_social_login automatic linking using complete_social_login rather than calling - # it directly - def dummy_get_response(self, request: HttpRequest): - return None - - @pytest.mark.xfail() - def test_register(self, rf: RequestFactory): - """Test that users can register automatically with a social account""" - # In order to reduce the manual setup, this test might be better implemented - # by mocking request.get (called to get the github api endpoint) and calling - # GithubOauth2Provider.complete_login (see - # https://github.com/pennersr/django-allauth/blob/master/allauth/socialaccount/providers/github/views.py - # for aruments and more information) - - request = rf.get("/fake-url/") - request.user = AnonymousUser() - SessionMiddleware(self.dummy_get_response).process_request(request) - MessageMiddleware(self.dummy_get_response).process_request(request) - - # Create a user but do not save them to the database yet - user = User(email=Faker().email()) - email = EmailAddress(user=user, email=user.email) - assert user.pk is None # not saved yet - - account = SocialAccount(provider="github", uid="1") - sociallogin = SocialLogin(user=user, account=account, email_addresses=[email]) - complete_social_login(request, sociallogin) - - # Assert that the user and their social account exist - assert user.pk is not None - assert user.socialaccount_set.filter(provider=account.provider).exists() - assert EmailAddress.objects.get(user=user).verified is False - - @pytest.mark.xfail() - def test_connect(self, rf: RequestFactory): - """Test that social accounts are automatically linked by email""" - request = rf.get("/fake-url/") - # Add the session/message middleware to the request - request.user = AnonymousUser() - SessionMiddleware(self.dummy_get_response).process_request(request) - MessageMiddleware(self.dummy_get_response).process_request(request) - - # Use .build() to avoid saving the user to the database which breaks complete_social_login - user = UserFactory.build() - user_email = EmailAddress(user=user, email=user.email) - account = SocialAccount(provider="github", uid="1") - sociallogin = SocialLogin( - user=user, account=account, email_addresses=[user_email] - ) - complete_social_login(request, sociallogin) - - assert user.socialaccount_set.filter(provider=account.provider).exists() # type: ignore[attr-defined] - - @pytest.mark.xfail() - def test_login(self, user: User, rf: RequestFactory): - """Test that users can still login through social accounts""" - # Use .build() to avoid saving the user to the database which breaks complete_social_login - # user = UserFactory.build() - user_email = EmailAddress( - user=user, email=user.email, verified=True, primary=True - ) - user_email.save() - - signup_request = rf.get("/fake-url/") - signup_request.user = AnonymousUser() - SessionMiddleware(self.dummy_get_response).process_request(signup_request) - MessageMiddleware(self.dummy_get_response).process_request(signup_request) - - # Create an unitialized account and then manually call complete_social_login - # This should connect the user to the provider - signup_account = SocialAccount(provider="github", uid="1") - signup_sociallogin = SocialLogin( - user=user, account=signup_account, email_addresses=[user_email] - ) - complete_social_signup(signup_request, signup_sociallogin) - - # Create a new request for the login process - login_request = rf.get("/fake-url/") - login_request.user = AnonymousUser() - SessionMiddleware(self.dummy_get_response).process_request(login_request) - MessageMiddleware(self.dummy_get_response).process_request(login_request) - - # Login again with the same account - login_account = SocialAccount(user=user, provider="github", uid="1") - login_sociallogin = SocialLogin(user=user, account=login_account) - complete_social_login(login_request, login_sociallogin) - - assert login_sociallogin.is_existing - assert login_request.user == user - - def test_disconnect(self): - """Ensure that users cannot disconnect social accounts""" - with pytest.raises(ValidationError): - SocialAccountAdapter().validate_disconnect(None, None) diff --git a/democrasite/users/urls.py b/democrasite/users/urls.py index 465a922..c1d2110 100644 --- a/democrasite/users/urls.py +++ b/democrasite/users/urls.py @@ -1,3 +1,5 @@ +"""URLs relating to users and accounts, if not defined in allauth.""" + from django.urls import path from democrasite.users.views import ( diff --git a/democrasite/users/views.py b/democrasite/users/views.py index f6839a7..18a1720 100644 --- a/democrasite/users/views.py +++ b/democrasite/users/views.py @@ -1,3 +1,5 @@ +"""Views for the users app.""" + from django.contrib.auth import get_user_model from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.messages.views import SuccessMessageMixin @@ -9,6 +11,8 @@ class UserDetailView(LoginRequiredMixin, DetailView): + """Page for viewing a user's profile.""" + model = User slug_field = "username" slug_url_kwarg = "username" @@ -18,6 +22,8 @@ class UserDetailView(LoginRequiredMixin, DetailView): class UserUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): + """Page for updating a user's profile.""" + model = User fields = ["name"] success_message = _("Information successfully updated") @@ -33,6 +39,8 @@ def get_object(self): # pylint: disable=arguments-differ class UserRedirectView(LoginRequiredMixin, RedirectView): + """Redirect to a user's profile.""" + permanent = False def get_redirect_url(self): # pylint: disable=arguments-differ diff --git a/democrasite/utils/context_processors.py b/democrasite/utils/context_processors.py index 8a36961..6e5547e 100644 --- a/democrasite/utils/context_processors.py +++ b/democrasite/utils/context_processors.py @@ -1,3 +1,5 @@ +"""Global context processors for the democrasite app.""" + from django.conf import settings diff --git a/democrasite/webiscite/apps.py b/democrasite/webiscite/apps.py index a46e8e0..2ddb405 100644 --- a/democrasite/webiscite/apps.py +++ b/democrasite/webiscite/apps.py @@ -1,5 +1,9 @@ +"""Webiscite app configuration.""" + from django.apps import AppConfig class WebisciteConfig(AppConfig): + """Webiscite app definition.""" + name = "democrasite.webiscite" diff --git a/democrasite/webiscite/models.py b/democrasite/webiscite/models.py index cbf186f..f71bd90 100644 --- a/democrasite/webiscite/models.py +++ b/democrasite/webiscite/models.py @@ -1,3 +1,5 @@ +"""Models for the webiscite app""" + from typing import Any from django.contrib.auth import get_user_model diff --git a/democrasite/webiscite/tasks.py b/democrasite/webiscite/tasks.py index 4f59a16..94c9ea5 100644 --- a/democrasite/webiscite/tasks.py +++ b/democrasite/webiscite/tasks.py @@ -1,3 +1,10 @@ +"""All celery tasks should be defined in this module + +This module contains all of the celery tasks used by the webiscite app. +Currently, the defined tasks are all related to processing pull requests +from :func:`democrasite.webiscite.webhooks`. +""" + from datetime import timedelta from typing import Any diff --git a/democrasite/webiscite/urls.py b/democrasite/webiscite/urls.py index d054057..ac07225 100644 --- a/democrasite/webiscite/urls.py +++ b/democrasite/webiscite/urls.py @@ -1,4 +1,4 @@ -"""This module contains the URLs for Webiscite pages.""" +"""URLs for Webiscite views.""" from django.urls import path from .views import ( diff --git a/democrasite/webiscite/views.py b/democrasite/webiscite/views.py index 1f021b6..062a9fd 100644 --- a/democrasite/webiscite/views.py +++ b/democrasite/webiscite/views.py @@ -1,3 +1,5 @@ +"""Views for the webiscite app.""" + from typing import Callable from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin diff --git a/democrasite/webiscite/webhooks.py b/democrasite/webiscite/webhooks.py index 9726ea0..4b4f3d2 100644 --- a/democrasite/webiscite/webhooks.py +++ b/democrasite/webiscite/webhooks.py @@ -1,3 +1,7 @@ +"""Views for processing webhooks. + +Each service that sends webhooks should have its own function-based view.""" + import hmac import json from typing import Callable diff --git a/docs/CONTRIBUTING.rst b/docs/CONTRIBUTING.rst new file mode 120000 index 0000000..798f2aa --- /dev/null +++ b/docs/CONTRIBUTING.rst @@ -0,0 +1 @@ +../CONTRIBUTING.rst \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile index d12e025..85abe6b 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -22,7 +22,7 @@ livehtml: # Outputs rst files from django application code apidocs: - sphinx-apidoc -e -M --tocfile democrasite -o $(SOURCEDIR)/api ../democrasite ../democrasite/contrib ../democrasite/*/migrations + sphinx-apidoc -e -M --tocfile democrasite -o $(SOURCEDIR)/api ../democrasite ../democrasite/contrib ../democrasite/*/migrations ../democrasite/*/tests ../democrasite/conftest.py # Remove all autogenerated files, ensuring a new build picks up all changes clean: diff --git a/docs/README.rst b/docs/README.rst new file mode 120000 index 0000000..89a0106 --- /dev/null +++ b/docs/README.rst @@ -0,0 +1 @@ +../README.rst \ No newline at end of file diff --git a/docs/api/democrasite.conftest.rst b/docs/api/democrasite.conftest.rst deleted file mode 100644 index 61db9b6..0000000 --- a/docs/api/democrasite.conftest.rst +++ /dev/null @@ -1,6 +0,0 @@ -democrasite.conftest module -=========================== - -.. automodule:: democrasite.conftest - :members: - :show-inheritance: diff --git a/docs/api/democrasite.rst b/docs/api/democrasite.rst index 3b4ca13..f206fc3 100644 --- a/docs/api/democrasite.rst +++ b/docs/api/democrasite.rst @@ -14,11 +14,3 @@ Subpackages democrasite.users democrasite.utils democrasite.webiscite - -Submodules ----------- - -.. toctree:: - :maxdepth: 4 - - democrasite.conftest diff --git a/docs/api/democrasite.users.rst b/docs/api/democrasite.users.rst index f5512c6..ca6801c 100644 --- a/docs/api/democrasite.users.rst +++ b/docs/api/democrasite.users.rst @@ -5,14 +5,6 @@ democrasite.users package :members: :show-inheritance: -Subpackages ------------ - -.. toctree:: - :maxdepth: 4 - - democrasite.users.tests - Submodules ---------- diff --git a/docs/api/democrasite.users.tests.factories.rst b/docs/api/democrasite.users.tests.factories.rst deleted file mode 100644 index b331542..0000000 --- a/docs/api/democrasite.users.tests.factories.rst +++ /dev/null @@ -1,6 +0,0 @@ -democrasite.users.tests.factories module -======================================== - -.. automodule:: democrasite.users.tests.factories - :members: - :show-inheritance: diff --git a/docs/api/democrasite.users.tests.rst b/docs/api/democrasite.users.tests.rst deleted file mode 100644 index a4b6e8f..0000000 --- a/docs/api/democrasite.users.tests.rst +++ /dev/null @@ -1,21 +0,0 @@ -democrasite.users.tests package -=============================== - -.. automodule:: democrasite.users.tests - :members: - :show-inheritance: - -Submodules ----------- - -.. toctree:: - :maxdepth: 4 - - democrasite.users.tests.factories - democrasite.users.tests.test_adapters - democrasite.users.tests.test_admin - democrasite.users.tests.test_forms - democrasite.users.tests.test_models - democrasite.users.tests.test_templates - democrasite.users.tests.test_urls - democrasite.users.tests.test_views diff --git a/docs/api/democrasite.users.tests.test_adapters.rst b/docs/api/democrasite.users.tests.test_adapters.rst deleted file mode 100644 index b03d1ef..0000000 --- a/docs/api/democrasite.users.tests.test_adapters.rst +++ /dev/null @@ -1,6 +0,0 @@ -democrasite.users.tests.test\_adapters module -============================================= - -.. automodule:: democrasite.users.tests.test_adapters - :members: - :show-inheritance: diff --git a/docs/api/democrasite.users.tests.test_admin.rst b/docs/api/democrasite.users.tests.test_admin.rst deleted file mode 100644 index 890c127..0000000 --- a/docs/api/democrasite.users.tests.test_admin.rst +++ /dev/null @@ -1,6 +0,0 @@ -democrasite.users.tests.test\_admin module -========================================== - -.. automodule:: democrasite.users.tests.test_admin - :members: - :show-inheritance: diff --git a/docs/api/democrasite.users.tests.test_forms.rst b/docs/api/democrasite.users.tests.test_forms.rst deleted file mode 100644 index cfb52c0..0000000 --- a/docs/api/democrasite.users.tests.test_forms.rst +++ /dev/null @@ -1,6 +0,0 @@ -democrasite.users.tests.test\_forms module -========================================== - -.. automodule:: democrasite.users.tests.test_forms - :members: - :show-inheritance: diff --git a/docs/api/democrasite.users.tests.test_models.rst b/docs/api/democrasite.users.tests.test_models.rst deleted file mode 100644 index 2bb4b30..0000000 --- a/docs/api/democrasite.users.tests.test_models.rst +++ /dev/null @@ -1,6 +0,0 @@ -democrasite.users.tests.test\_models module -=========================================== - -.. automodule:: democrasite.users.tests.test_models - :members: - :show-inheritance: diff --git a/docs/api/democrasite.users.tests.test_templates.rst b/docs/api/democrasite.users.tests.test_templates.rst deleted file mode 100644 index 7a8d225..0000000 --- a/docs/api/democrasite.users.tests.test_templates.rst +++ /dev/null @@ -1,6 +0,0 @@ -democrasite.users.tests.test\_templates module -============================================== - -.. automodule:: democrasite.users.tests.test_templates - :members: - :show-inheritance: diff --git a/docs/api/democrasite.users.tests.test_urls.rst b/docs/api/democrasite.users.tests.test_urls.rst deleted file mode 100644 index 7807a2f..0000000 --- a/docs/api/democrasite.users.tests.test_urls.rst +++ /dev/null @@ -1,6 +0,0 @@ -democrasite.users.tests.test\_urls module -========================================= - -.. automodule:: democrasite.users.tests.test_urls - :members: - :show-inheritance: diff --git a/docs/api/democrasite.users.tests.test_views.rst b/docs/api/democrasite.users.tests.test_views.rst deleted file mode 100644 index c4fbfbb..0000000 --- a/docs/api/democrasite.users.tests.test_views.rst +++ /dev/null @@ -1,6 +0,0 @@ -democrasite.users.tests.test\_views module -========================================== - -.. automodule:: democrasite.users.tests.test_views - :members: - :show-inheritance: diff --git a/docs/api/democrasite.webiscite.rst b/docs/api/democrasite.webiscite.rst index 9913d6c..f43b0e4 100644 --- a/docs/api/democrasite.webiscite.rst +++ b/docs/api/democrasite.webiscite.rst @@ -5,14 +5,6 @@ democrasite.webiscite package :members: :show-inheritance: -Subpackages ------------ - -.. toctree:: - :maxdepth: 4 - - democrasite.webiscite.tests - Submodules ---------- diff --git a/docs/api/democrasite.webiscite.tests.conftest.rst b/docs/api/democrasite.webiscite.tests.conftest.rst deleted file mode 100644 index 35720a5..0000000 --- a/docs/api/democrasite.webiscite.tests.conftest.rst +++ /dev/null @@ -1,6 +0,0 @@ -democrasite.webiscite.tests.conftest module -=========================================== - -.. automodule:: democrasite.webiscite.tests.conftest - :members: - :show-inheritance: diff --git a/docs/api/democrasite.webiscite.tests.factories.rst b/docs/api/democrasite.webiscite.tests.factories.rst deleted file mode 100644 index a05261c..0000000 --- a/docs/api/democrasite.webiscite.tests.factories.rst +++ /dev/null @@ -1,6 +0,0 @@ -democrasite.webiscite.tests.factories module -============================================ - -.. automodule:: democrasite.webiscite.tests.factories - :members: - :show-inheritance: diff --git a/docs/api/democrasite.webiscite.tests.rst b/docs/api/democrasite.webiscite.tests.rst deleted file mode 100644 index d20b915..0000000 --- a/docs/api/democrasite.webiscite.tests.rst +++ /dev/null @@ -1,22 +0,0 @@ -democrasite.webiscite.tests package -=================================== - -.. automodule:: democrasite.webiscite.tests - :members: - :show-inheritance: - -Submodules ----------- - -.. toctree:: - :maxdepth: 4 - - democrasite.webiscite.tests.conftest - democrasite.webiscite.tests.factories - democrasite.webiscite.tests.test_constitution - democrasite.webiscite.tests.test_models - democrasite.webiscite.tests.test_tasks - democrasite.webiscite.tests.test_templates - democrasite.webiscite.tests.test_urls - democrasite.webiscite.tests.test_views - democrasite.webiscite.tests.test_webhooks diff --git a/docs/api/democrasite.webiscite.tests.test_constitution.rst b/docs/api/democrasite.webiscite.tests.test_constitution.rst deleted file mode 100644 index c0e68a9..0000000 --- a/docs/api/democrasite.webiscite.tests.test_constitution.rst +++ /dev/null @@ -1,6 +0,0 @@ -democrasite.webiscite.tests.test\_constitution module -===================================================== - -.. automodule:: democrasite.webiscite.tests.test_constitution - :members: - :show-inheritance: diff --git a/docs/api/democrasite.webiscite.tests.test_models.rst b/docs/api/democrasite.webiscite.tests.test_models.rst deleted file mode 100644 index a9d9782..0000000 --- a/docs/api/democrasite.webiscite.tests.test_models.rst +++ /dev/null @@ -1,6 +0,0 @@ -democrasite.webiscite.tests.test\_models module -=============================================== - -.. automodule:: democrasite.webiscite.tests.test_models - :members: - :show-inheritance: diff --git a/docs/api/democrasite.webiscite.tests.test_tasks.rst b/docs/api/democrasite.webiscite.tests.test_tasks.rst deleted file mode 100644 index bd8c20c..0000000 --- a/docs/api/democrasite.webiscite.tests.test_tasks.rst +++ /dev/null @@ -1,6 +0,0 @@ -democrasite.webiscite.tests.test\_tasks module -============================================== - -.. automodule:: democrasite.webiscite.tests.test_tasks - :members: - :show-inheritance: diff --git a/docs/api/democrasite.webiscite.tests.test_templates.rst b/docs/api/democrasite.webiscite.tests.test_templates.rst deleted file mode 100644 index 9cab33d..0000000 --- a/docs/api/democrasite.webiscite.tests.test_templates.rst +++ /dev/null @@ -1,6 +0,0 @@ -democrasite.webiscite.tests.test\_templates module -================================================== - -.. automodule:: democrasite.webiscite.tests.test_templates - :members: - :show-inheritance: diff --git a/docs/api/democrasite.webiscite.tests.test_urls.rst b/docs/api/democrasite.webiscite.tests.test_urls.rst deleted file mode 100644 index 4f56ef7..0000000 --- a/docs/api/democrasite.webiscite.tests.test_urls.rst +++ /dev/null @@ -1,6 +0,0 @@ -democrasite.webiscite.tests.test\_urls module -============================================= - -.. automodule:: democrasite.webiscite.tests.test_urls - :members: - :show-inheritance: diff --git a/docs/api/democrasite.webiscite.tests.test_views.rst b/docs/api/democrasite.webiscite.tests.test_views.rst deleted file mode 100644 index b423c1d..0000000 --- a/docs/api/democrasite.webiscite.tests.test_views.rst +++ /dev/null @@ -1,6 +0,0 @@ -democrasite.webiscite.tests.test\_views module -============================================== - -.. automodule:: democrasite.webiscite.tests.test_views - :members: - :show-inheritance: diff --git a/docs/api/democrasite.webiscite.tests.test_webhooks.rst b/docs/api/democrasite.webiscite.tests.test_webhooks.rst deleted file mode 100644 index 611f0d6..0000000 --- a/docs/api/democrasite.webiscite.tests.test_webhooks.rst +++ /dev/null @@ -1,6 +0,0 @@ -democrasite.webiscite.tests.test\_webhooks module -================================================= - -.. automodule:: democrasite.webiscite.tests.test_webhooks - :members: - :show-inheritance: diff --git a/docs/conf.py b/docs/conf.py index f3af2e5..19ae214 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,15 +10,10 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -import inspect import os import sys -from typing import cast import django -from django.db import models -from django.utils.encoding import force_str -from django.utils.html import strip_tags if os.getenv("READTHEDOCS", default="False") == "True": sys.path.insert(0, os.path.abspath("..")) diff --git a/docs/index.rst b/docs/index.rst index 5352c89..72e793c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,6 +10,8 @@ Democrasite Documentation :maxdepth: 2 :caption: Contents: + README + CONTRIBUTING howto users webiscite diff --git a/docs/webiscite.rst b/docs/webiscite.rst index 84f8692..da40588 100644 --- a/docs/webiscite.rst +++ b/docs/webiscite.rst @@ -1,34 +1,41 @@ .. _webiscite: +********* Webiscite -====================================================================== +********* "Webiscite" is an app which contains the core functionality of Democrasite: Bills, voting, and interfacing with GitHub. The name is a portmanteau of "web" -and "plebiscite," which is a referendum on a proposed law. +and "plebiscite," which is a public referendum on a proposed law. -Pull Request Process ----------------------------------------------------------------------- -When a pull request is created on `GitHub `_, -a `webhook `_ -makes a request to the GitHub webhook -:func:`view `. +Pull Requests +============= + +Pull Request Processing Pipeline +-------------------------------- + +When a pull request is created on `GitHub`_, a `webhook`_ makes a request to +the GitHub :func:`webhook view `. This method parses the data from the request and calls -:func:`process_pull ` -which itself calls :func:`pr_opened ` -in the even a new pull request was created. +:func:`~democrasite.webiscite.tasks.process_pull` +to handle the request. In the event a pull request was opened or reopened, +:func:`~democrasite.webiscite.tasks.pr_opened` is called. If the user who created the pull request has a democrasite account, a new -:class:`Bill ` +:class:`~democrasite.webiscite.models.Bill` is created with the information from the pull request and made visible on the homepage immediately. -A task is also queued up to execute once the voting period ends, at which point -:func:`submit_bill ` is called. The +A task to execute :func:`~democrasite.webiscite.tasks.submit_bill` +is also put in the celery queue to execute once the voting period ends. The function verifies that the pull request is open and unedited and then counts -the votes for and against that bill. +the votes for and against that Bill. + +If the votes for the Bill pass the threshold, the pull request is merged into +the master branch on Github and automatically deployed, officially making it +part of Democrasite. -If it passes the threshold, the bill is merged into the master branch on -Github, officially making it part of Democrasite. +.. _GitHub: https://github.com/mfosterw/cookiestocracy +.. _webhook: https://docs.github.com/en/developers/webhooks-and-events/webhooks/about-webhooks