Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: whitesmith/hawkpost
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.3.0
Choose a base ref
...
head repository: whitesmith/hawkpost
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref

Commits on Oct 13, 2019

  1. Copy the full SHA
    8e96f87 View commit details

Commits on Oct 19, 2019

  1. Copy the full SHA
    5c27a06 View commit details
  2. Copy the full SHA
    bc366a2 View commit details
  3. Copy the full SHA
    142872e View commit details

Commits on Oct 29, 2019

  1. Try and trigger CircleCI

    pjaneiro committed Oct 29, 2019
    Copy the full SHA
    bd24e94 View commit details
  2. Merge pull request #294 from whitesmith/fix-vulnerable-dependencies

    Fix vulnerable dependencies
    pjaneiro authored Oct 29, 2019

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    cb0d1b0 View commit details
  3. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    03acaf6 View commit details
  4. Merge pull request #295 from whitesmith/253-increase-test-coverage

    Increase test coverage
    pjaneiro authored Oct 29, 2019

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    11fe1f2 View commit details

Commits on Feb 12, 2020

  1. Bump django from 1.11.25 to 1.11.28

    Bumps [django](https://github.com/django/django) from 1.11.25 to 1.11.28.
    - [Release notes](https://github.com/django/django/releases)
    - [Commits](django/django@1.11.25...1.11.28)
    
    Signed-off-by: dependabot[bot] <support@github.com>
    dependabot[bot] authored Feb 12, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    9a5e25a View commit details
  2. Merge pull request #298 from whitesmith/dependabot/pip/django-1.11.28

    Bump django from 1.11.25 to 1.11.28
    dethos authored Feb 12, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    f2f6142 View commit details

Commits on Mar 10, 2020

  1. Copy the full SHA
    6369cd5 View commit details

Commits on Jun 5, 2020

  1. Bump django from 1.11.28 to 1.11.29

    Bumps [django](https://github.com/django/django) from 1.11.28 to 1.11.29.
    - [Release notes](https://github.com/django/django/releases)
    - [Commits](django/django@1.11.28...1.11.29)
    
    Signed-off-by: dependabot[bot] <support@github.com>
    dependabot[bot] authored Jun 5, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    35a8e7a View commit details

Commits on Jun 6, 2020

  1. Merge pull request #301 from whitesmith/dependabot/pip/django-1.11.29

    Bump django from 1.11.28 to 1.11.29
    dethos authored Jun 6, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    4f65c8e View commit details

Commits on Jun 27, 2020

  1. Copy the full SHA
    5d4f017 View commit details

Commits on Jul 21, 2020

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    deb19a5 View commit details
  2. Merge pull request #302 from whitesmith/update-old-libs

    Update older dependencies and remove "mail_debug"
    dethos authored Jul 21, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    278067e View commit details
  3. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    68b534a View commit details
  4. Merge pull request #300 from whitesmith/update-broken-link

    Fix broken link to pipenv documentation
    dethos authored Jul 21, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    ed6d384 View commit details

Commits on Oct 2, 2020

  1. Scan the code for security issues

    Leverage this new Github feature (CodeQL) to scan the code base for security issues.
    dethos authored Oct 2, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    ba989c1 View commit details
  2. Merge pull request #303 from whitesmith/dethos-patch-1

    Scan the code for security issues
    dethos authored Oct 2, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    3cf7a77 View commit details

Commits on Nov 20, 2020

  1. Copy the full SHA
    2918e14 View commit details
  2. Merge pull request #306 from whitesmith/allow-delete-social

    Allow accounts without password to be deleted
    dethos authored Nov 20, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    0f6ec2f View commit details

Commits on Dec 4, 2020

  1. Add missing migration

    pdcribeiro committed Dec 4, 2020
    Copy the full SHA
    30bd546 View commit details
  2. Copy the full SHA
    0ec03b5 View commit details

Commits on Dec 9, 2020

  1. Copy the full SHA
    cd02b31 View commit details
  2. Copy the full SHA
    2c50eec View commit details
  3. Copy the full SHA
    4115e35 View commit details
  4. Fix typo in template

    pdcribeiro committed Dec 9, 2020
    Copy the full SHA
    348a372 View commit details
  5. Merge pull request #308 from whitesmith/pdcribeiro-patch

    Handle box not found
    dethos authored Dec 9, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    7cea93e View commit details
  6. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    5fb5d43 View commit details
  7. Merge pull request #309 from whitesmith/fix-keyerror-fingerprint

    Handle missing 'fingerprint' when key is imported
    dethos authored Dec 9, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    1e602b6 View commit details
  8. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    e6aa151 View commit details
  9. Merge pull request #310 from whitesmith/bump-versions

    Bump raven and django-axes versions
    dethos authored Dec 9, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    e246310 View commit details
  10. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    c711f34 View commit details
  11. Merge pull request #311 from whitesmith/connect-social

    Connect social account when email exists
    dethos authored Dec 9, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    3a7210b View commit details

Commits on Dec 15, 2020

  1. Copy the full SHA
    521b241 View commit details
  2. Fix failing tests

    pdcribeiro committed Dec 15, 2020
    Copy the full SHA
    200efff View commit details

Commits on Dec 17, 2020

  1. Merge pull request #312 from whitesmith/fix-axes-settings

    Fix django-axes proxy settings
    dethos authored Dec 17, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    bd907d0 View commit details

Commits on Mar 2, 2021

  1. Copy the full SHA
    434b8ad View commit details
  2. Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    dethos Gonçalo Valério
    Copy the full SHA
    0530d76 View commit details

Commits on Mar 3, 2021

  1. Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    dethos Gonçalo Valério
    Copy the full SHA
    a4e4832 View commit details
  2. Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    dethos Gonçalo Valério
    Copy the full SHA
    af1e679 View commit details
  3. Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    dethos Gonçalo Valério
    Copy the full SHA
    b8a3c4a View commit details

Commits on Mar 8, 2021

  1. Merge pull request #317 from whitesmith/fix-ci

    Fix the CI
    dethos authored Mar 8, 2021

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    0121229 View commit details
  2. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    237d7bc View commit details

Commits on Mar 9, 2021

  1. Merge pull request #315 from whitesmith/ignore-file-not-found

    Ignore errors when removing GPG homedir
    dethos authored Mar 9, 2021

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    4b805dd View commit details

Commits on Mar 14, 2021

  1. Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    dethos Gonçalo Valério
    Copy the full SHA
    515f309 View commit details

Commits on Mar 18, 2021

  1. Bump django from 2.2.13 to 2.2.18

    Bumps [django](https://github.com/django/django) from 2.2.13 to 2.2.18.
    - [Release notes](https://github.com/django/django/releases)
    - [Commits](django/django@2.2.13...2.2.18)
    
    Signed-off-by: dependabot[bot] <support@github.com>
    dependabot[bot] authored Mar 18, 2021

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    5d4d486 View commit details
  2. Merge pull request #321 from whitesmith/dependabot/pip/django-2.2.18

    Bump django from 2.2.13 to 2.2.18
    dethos authored Mar 18, 2021

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    b7d2e4e View commit details
  3. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    2a3c838 View commit details
Showing with 1,599 additions and 3,956 deletions.
  1. +0 −60 .circleci/config.yml
  2. +14 −0 .github/dependabot.yml
  3. +55 −0 .github/workflows/automated-tests.yml
  4. +71 −0 .github/workflows/codeql-analysis.yml
  5. +0 −4 .pyup.yml
  6. +1 −1 Dockerfile.dev
  7. +0 −149 Gulpfile.js
  8. +15 −17 Pipfile
  9. +537 −219 Pipfile.lock
  10. +21 −20 README.md
  11. +3 −2 boxes/forms.py
  12. +18 −0 boxes/migrations/0009_box_verified_only.py
  13. +12 −5 boxes/models.py
  14. +16 −2 boxes/static/javascripts/openpgp.min.js
  15. +1 −1 boxes/tasks.py
  16. +32 −12 boxes/templates/boxes/box_list.html
  17. +1 −1 boxes/templates/boxes/box_submit.html
  18. +1 −1 boxes/templates/boxes/success.html
  19. +26 −0 boxes/templates/boxes/verified_only.html
  20. +129 −53 boxes/tests.py
  21. +6 −6 boxes/urls.py
  22. +16 −9 boxes/views.py
  23. +9 −14 docker-compose.yml
  24. +6 −4 hawkpost/middleware.py
  25. +32 −11 hawkpost/settings/common.py
  26. +5 −7 hawkpost/settings/development.py
  27. +4 −2 hawkpost/settings/production.py
  28. +0 −4 hawkpost/static/javascripts/jquery-2.2.2.min.js
  29. +2 −0 hawkpost/static/javascripts/jquery.min.js
  30. +2 −2 hawkpost/templates/account/auth_modals.html
  31. +1 −1 hawkpost/templates/account/verified_email_required.html
  32. +2 −2 hawkpost/templates/layout/base.html
  33. +1 −1 hawkpost/templates/layout/navbar.html
  34. +22 −12 hawkpost/urls.py
  35. +14 −0 humans/adapter.py
  36. +8 −4 humans/admin.py
  37. +1 −1 humans/apps.py
  38. +9 −3 humans/forms.py
  39. +18 −0 humans/migrations/0013_auto_20201204_1807.py
  40. +20 −10 humans/models.py
  41. +4 −7 humans/tasks.py
  42. +1 −1 humans/templates/humans/update_user_form.html
  43. +2 −0 humans/templates/humans/user_confirm_delete.html
  44. +116 −14 humans/tests.py
  45. +3 −3 humans/urls.py
  46. +10 −7 humans/utils.py
  47. +14 −14 humans/views.py
  48. BIN locale/pt_PT/LC_MESSAGES/django.mo
  49. +281 −250 locale/pt_PT/LC_MESSAGES/django.po
  50. +0 −33 package.json
  51. +2 −3 pages/templates/pages/help.html
  52. +1 −1 pages/templates/pages/index.html
  53. +30 −1 pages/tests.py
  54. +4 −4 pages/urls.py
  55. BIN stylesheets/Ayuthaya.ttf
  56. BIN stylesheets/Exo-SemiBold.ttf
  57. +0 −380 stylesheets/_base.scss
  58. +0 −209 stylesheets/_boxes.scss
  59. +0 −99 stylesheets/_buttons.scss
  60. +0 −301 stylesheets/_details.scss
  61. +0 −72 stylesheets/_faq.scss
  62. +0 −314 stylesheets/_forms.scss
  63. +0 −66 stylesheets/_global.scss
  64. +0 −175 stylesheets/_landing.scss
  65. +0 −42 stylesheets/_messages.scss
  66. +0 −89 stylesheets/_mixins.scss
  67. +0 −218 stylesheets/_mobileNavigation.scss
  68. +0 −161 stylesheets/_modal.scss
  69. +0 −60 stylesheets/_modalxs.scss
  70. +0 −80 stylesheets/_navigation.scss
  71. +0 −117 stylesheets/_newbox.scss
  72. +0 −19 stylesheets/style.scss
  73. +0 −1 stylesheets/vendor/_flexboxgrid.min.css.scss
  74. +0 −4 stylesheets/vendor/_font-awesome.min.css.scss
  75. +0 −568 stylesheets/vendor/_jquery.datetimepicker.css.scss
  76. +0 −3 stylesheets/vendor/vendor.scss
60 changes: 0 additions & 60 deletions .circleci/config.yml

This file was deleted.

14 changes: 14 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
reviewers:
- "dethos"
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
reviewers:
- "dethos"
55 changes: 55 additions & 0 deletions .github/workflows/automated-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Automated Tests

on:
push:
branches: ["master"]
pull_request:
branches: ["master"]

env:
DB_HOST: localhost
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
USERNAME: root

jobs:
unittest:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9]

services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: hawkpost_dev
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7
ports:
- 6379:6379

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install Dependencies
run: |
python -m pip install --upgrade pip pipenv
pipenv install --dev
- name: Collect Static assets
run: |
pipenv run python manage.py collectstatic --no-input
- name: Run Tests
run: |
pipenv run python manage.py test
71 changes: 71 additions & 0 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: "CodeQL"

on:
push:
branches: [master]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '0 18 * * 0'

jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['python', 'javascript']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection

steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2

# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main

# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1

# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl

# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language

#- run: |
# make bootstrap
# make release

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
4 changes: 0 additions & 4 deletions .pyup.yml

This file was deleted.

2 changes: 1 addition & 1 deletion Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3
FROM python:3.9

ENV HOME /home/user
RUN useradd --create-home --home-dir $HOME user \
149 changes: 0 additions & 149 deletions Gulpfile.js

This file was deleted.

32 changes: 15 additions & 17 deletions Pipfile
Original file line number Diff line number Diff line change
@@ -4,25 +4,23 @@ verify_ssl = true
name = "pypi"

[dev-packages]
django-debug-toolbar = "==1.9.1"
django-extensions = "*"
hypothesis = {extras = ["django"]}
coverage = "*"
django-debug-toolbar = "==3.8.1"
django-extensions = "==3.2.1"

[packages]
django-dotenv = "==1.4.1"
"psycopg2" = "==2.7.3.2"
django-allauth = "==0.34.0"
django-dotenv = "==1.4.2"
"psycopg2-binary" = "==2.9.5"
django-allauth = {version = "==64.2.1", extras=["socialaccount"]}
gnupg = "==2.3.1"
whitenoise = "==3.3.0"
celery = "==4.1.1"
redis = "==2.10.5"
gunicorn = "==19.7.1"
raven = "==6.1.0"
django-timezone-field = "==2.0"
django-braces = "==1.11.0"
django-axes = "==2.3.3"
Django = "==1.11.16"
whitenoise = "==5.3.0"
gunicorn = "==23.0.0"
raven = "==6.4.0"
django-timezone-field = "==4.2.3"
django-braces = "==1.15.0"
django-axes = "==5.40.1"
Django = "==4.2.15"
celery = {version = "==5.2.7", extras = ["redis"]}
requests = "==2.32.3"

[requires]
python_version = "3.6"
python_version = "3.9"
756 changes: 537 additions & 219 deletions Pipfile.lock

Large diffs are not rendered by default.

41 changes: 21 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
Hawkpost
========
# Hawkpost

Hawkpost lets you create unique links that you can share with the person that desires to send you important information but doesn't know how to deal with PGP.

You can deploy your own server using the code from this repository or use the official server (that is running an exact copy of this repo) at [https://hawkpost.co](https://hawkpost.co).

## NOTICE

**Hawkpost is currently in maintenance mode. This means that bug fixes will be merged and vulnerabilities in the codebase and its dependencies will be patched; however, improvements and new features will not be handled or included.**

**This status might change in the future, but for the moment it will stay as is.**

## Rationale

@@ -17,44 +21,43 @@ The way it works is like this:
1. The server then signs (**experimental**) the encrypted content.
1. Finally the server forwards it to your e-mail address.


# Setting up a development environment

In this section you can find the steps to setup a minimal development environment on your machine.

Base requirements:

* Python 3
* Redis
* PostgreSQL
- Python 3
- Redis
- PostgreSQL

## On Linux

On a **Debian** based operating system execute the following steps, after cloning the repository:

* Make sure you have `pipenv` installed. You can check [this page for more information](https://docs.pipenv.org/install/#installing-pipenv)
- Make sure you have `pipenv` installed. You can check [this page for more information](https://pipenv.readthedocs.io/en/latest/install/#installing-pipenv)

* Install the dependencies

```
$ pipenv install
```

* Create the local postgreSQL database with your user and no password
- Create the local postgreSQL database with your user and no password

* Migrate the database
- Migrate the database

```
$ pipenv run python manage.py migrate
```

* Generate stylesheet with gulp (installation instructions for gulp can be found [here](https://gulpjs.com/))
- Generate stylesheet with gulp (installation instructions for gulp can be found [here](https://gulpjs.com/))

```
$ gulp build
```

* Now you should be able to launch the server and its workers
- Now you should be able to launch the server and its workers

```
$ pipenv run python manage.py runserver
@@ -99,7 +102,6 @@ DB_HOST=db
DB_USER=hawkpost
DB_PASSWORD=hawkpost
REDIS_URL=redis://redis:6379/0
EMAIL_HOST=mail_debug
```

**Don't forget to set the remaining variables** as well.
@@ -115,9 +117,9 @@ $ docker-compose up -d db redis
# (using `--rm` to remove the temporary container afterwards)
$ docker-compose run --rm web pipenv run python manage.py migrate

# Run the web, celery and mail_debug containers
# Run the web and celery containers
# (`docker-compose up` would log db and redis as well)
$ docker-compose up web celery mail_debug
$ docker-compose up web celery
```

These commands
@@ -126,9 +128,8 @@ These commands
we're not bothered by their logs while working on the application.
1. **Perform the migrations** using a temporary `web` container; it is removed
afterwards.
1. **Run the `web`, `celery` and `mail_debug` containers** attached to the
console. `mail_debug` is optional since it is only used when debugging the
e-mails being sent.
1. **Run the `web` and `celery`** attached to the
console.

The `web` container will reload on code changes.

@@ -156,13 +157,13 @@ network feature may require additional steps.

# Running the test suite

To execute our current test suite, you just need to execute the following command after settinng up your local development environment:
To execute our current test suite, you just need to execute the following command after setting up your local development environment:

> $ pipenv run python manage.py test
> \$ pipenv run python manage.py test
In case you are using our docker setup the command should be:

> $ docker-compose run --rm web pipenv run python manage.py test
> \$ docker-compose run --rm web pipenv run python manage.py test
# Credits

5 changes: 3 additions & 2 deletions boxes/forms.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.forms import ModelForm, Form, CharField, Textarea, BooleanField
from .models import Box
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _

from sys import getsizeof

@@ -24,7 +24,8 @@ class Meta:
"name",
"description",
"expires_at",
"max_messages"]
"max_messages",
"verified_only"]

def clean_expires_at(self):
# Validate the expiration date
18 changes: 18 additions & 0 deletions boxes/migrations/0009_box_verified_only.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 2.2.13 on 2021-03-15 19:50

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('boxes', '0008_auto_20170519_1431'),
]

operations = [
migrations.AddField(
model_name='box',
name='verified_only',
field=models.BooleanField(default=False, verbose_name='Restrict to verified users'),
),
]
17 changes: 12 additions & 5 deletions boxes/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.db import models
from django.conf import settings
from django.core.validators import MinValueValidator
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
import uuid


@@ -26,7 +26,9 @@ class Box(models.Model):
verbose_name=_('Unique ID'))

owner = models.ForeignKey(settings.AUTH_USER_MODEL,
related_name='own_boxes', verbose_name=_('Owner'))
on_delete=models.CASCADE,
related_name='own_boxes',
verbose_name=_('Owner'))

recipients = models.ManyToManyField(settings.AUTH_USER_MODEL,
related_name='boxes',
@@ -49,6 +51,9 @@ class Box(models.Model):
last_sent_at = models.DateTimeField(null=True,
verbose_name=_('Last sent at'))

verified_only = models.BooleanField(default=False,
verbose_name=_('Restrict to verified users'))

class Meta:
verbose_name = _('Box')
verbose_name_plural = _('Boxes')
@@ -84,8 +89,9 @@ class Membership(models.Model):
verbose_name=_('Created at'))
updated_at = models.DateTimeField(auto_now=True,
verbose_name=_('Updated at'))
box = models.ForeignKey("Box")
user = models.ForeignKey(settings.AUTH_USER_MODEL)
box = models.ForeignKey("Box", on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)

class Meta:
verbose_name = _('Membership')
@@ -107,7 +113,8 @@ class Message(models.Model):
(FAILED, _('Failed'))
)

box = models.ForeignKey("Box", related_name='messages')
box = models.ForeignKey(
"Box", on_delete=models.CASCADE, related_name='messages')
status = models.IntegerField(choices=STATUSES, default=ONQUEUE,
verbose_name=_('Status'))
created_at = models.DateTimeField(auto_now_add=True,
18 changes: 16 additions & 2 deletions boxes/static/javascripts/openpgp.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion boxes/tasks.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@
from django.core.mail import EmailMultiAlternatives
from django.conf import settings
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _

from .models import Message

44 changes: 32 additions & 12 deletions boxes/templates/boxes/box_list.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% extends "layout/base.html" %}
{% load i18n %}
{% load staticfiles %}
{% load static %}

{% block content %}
<div class="section-definer-grey">
@@ -18,7 +18,7 @@ <h2 class="text-lightest title">{% trans "MANAGE BOXES" %}</h2>
<div class="row no-padding">
<div class="center-xs col-xs-12 flex-d">
<li class="full-width"><a href="{% url "boxes_list" %}?display=Open" class="box__nav__item {% if display_status == 'Open' %}active{% endif %}">{% trans "Open" %}</a></li>
<li class="full-width" ><a href="{% url "boxes_list" %}?display=Done"class="box__nav__item {% if display_status == 'Done' %}active{% endif %}">{% trans "Done" %}</a></li>
<li class="full-width" ><a href="{% url "boxes_list" %}?display=Done" class="box__nav__item {% if display_status == 'Done' %}active{% endif %}">{% trans "Done" %}</a></li>
<li class="full-width"><a href="{% url "boxes_list" %}?display=Expired" class="box__nav__item {% if display_status == 'Expired' %}active{% endif %}">{% trans "Expired" %}</a></li>
<li class="full-width" ><a href="{% url "boxes_list" %}?display=Closed" class="box__nav__item {% if display_status == 'Closed' %}active{% endif %}">{% trans "Closed" %}</a></li>
</div>
@@ -48,6 +48,10 @@ <h2 class="text-lightest title">{% trans "MANAGE BOXES" %}</h2>
{% endif %}

<div class="extra-info">
<p class="no-margin-bottom mt-small">
<span class="xsmallmedium text-darkest">{% trans "Restricted to verified users:" %}</span>
<span class="xsmalltext text-darkest">{% if box.verified_only %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}</span>
</p>
<p class="no-margin-bottom mt-small">
<span class="xsmallmedium text-darkest">{% trans "Messages:" %}</span>
<span class="xsmalltext text-darkest">{{box.messages.count}}/{{box.max_messages}}</span>
@@ -129,17 +133,12 @@ <h3 class="subtitle center-xs text-lightest">{% trans "CREATE NEW BOX" %}</h3>
<form action="{% url 'boxes_create' %}" method="post" class="" id="box-create-form-js">
{% csrf_token %}
<label class="input-anim smalltext">
<p>
<span id="name-errors-js">{{ form.name.errors }}</span>
{{ form.name.label_tag }}
{{ form.name }}
</p>
<div class="row middle-xs">
<div class="col-xs-9 col-xxs-12 col-sm-8">
<p class="multiline-input">
<span id="description-errors-js">{{ form.description.errors }}</span>
{{ form.description.label_tag }}
{{ form.description }}
<p>
<span id="name-errors-js">{{ form.name.errors }}</span>
{{ form.name.label_tag }}
{{ form.name }}
</p>
</div>
<div class="col-xs-3 col-xxs-12 col-sm-4">
@@ -150,6 +149,24 @@ <h3 class="subtitle center-xs text-lightest">{% trans "CREATE NEW BOX" %}</h3>
</p>
</div>
</div>
<div class="row middle-xs">
<div class="col-xs-9 col-xxs-12 col-sm-8">
<p class="multiline-input">
<span id="description-errors-js">{{ form.description.errors }}</span>
{{ form.description.label_tag }}
{{ form.description }}
</p>
</div>
<div class="col-xs-3 col-xxs-12 col-sm-4">
<div class="start-xs middle-xs">
<p>
<span id="verified_only-errors-js">{{ form.verified_only.errors }}</span>
{{ form.verified_only.label_tag }}
</p>
{{ form.verified_only }}
</div>
</div>
</div>
<div class="row bottom-xs padding-b-m">
<div class="col-xs-9 col-xxs-12 col-sm-8">
<p>
@@ -160,7 +177,10 @@ <h3 class="subtitle center-xs text-lightest">{% trans "CREATE NEW BOX" %}</h3>
</div>
<div class="col-xs-3 col-xxs-12 col-sm-4">
<div class="start-xs middle-xs">
<p><span id="never_expires-errors-js">{{ form.never_expires.errors }}</span>{{ form.never_expires.label_tag }}</p>
<p>
<span id="never_expires-errors-js">{{ form.never_expires.errors }}</span>
{{ form.never_expires.label_tag }}
</p>
{{ form.never_expires }}
</div>
</div>
2 changes: 1 addition & 1 deletion boxes/templates/boxes/box_submit.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% extends "layout/base.html" %}
{% load i18n %}
{% load staticfiles %}
{% load static %}

{% block header %}
{% include "layout/messages.html" %}
2 changes: 1 addition & 1 deletion boxes/templates/boxes/success.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% extends "layout/base.html" %}
{% load i18n %}
{% load staticfiles %}
{% load static %}

{% block header %}
{% include "layout/messages.html" %}
26 changes: 26 additions & 0 deletions boxes/templates/boxes/verified_only.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{% extends "layout/base.html" %}
{% load i18n %}

{% block head_title %}{% trans "Verify Your Identity" %}{% endblock %}

{% block content %}
<div class="section-definer-dark">
<div class="section-definer section-resized-extra">
<section class="text-lightest">
<div class="row center-xs center-sm">
<div class="col-offset-xs-1 col-xs-10 col-offset-sm-2 col-xs-8">
<h1 class="title text-upper text-lightest">
{% trans "Verify Your Identity" %}
</h1>
<p>
{% trans "To access this page you need to verify your identity. Please login to your Hawkpost account or sign up if you don't have one." %}
</p>
<p>
{% trans "Note that if you've signed up using your email address, you need to verify it to get access. To do this, simply visit the link provided in the email that was sent to you upon registration." %}
</p>
</div>
</div>
</section>
</div>
</div>
{% endblock %}
182 changes: 129 additions & 53 deletions boxes/tests.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.test import TestCase
from django.utils import timezone
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.core import mail
from humans.models import User
from datetime import timedelta
@@ -13,41 +13,46 @@
import random
import string

future_datetime = timezone.now() + timedelta(days=1)
future_datetime_string = future_datetime.strftime("%m/%d/%Y %H:%M")

def create_boxes(user):
not_expired = timezone.now() + timedelta(hours=3)
expired = timezone.now() - timedelta(hours=3)
user.own_boxes.create(
name="open", expires_at=not_expired, status=Box.OPEN)
user.own_boxes.create(
name="closed", expires_at=not_expired, status=Box.CLOSED)
user.own_boxes.create(
name="sent", expires_at=not_expired, status=Box.DONE)
user.own_boxes.create(
name="expired", expires_at=expired, status=Box.EXPIRED)

def create_and_login_user(client, add_key=True):
user = create_user(add_key)
client.force_login(user)
return user

def create_open_box(user):
expires_at = timezone.now() + timedelta(hours=3)
return user.own_boxes.create(name="open",
expires_at=expires_at,
status=Box.OPEN)

def create_user(add_key=True):
username = "".join(random.choice(string.ascii_uppercase) for _ in range(5))
user_data = {"username": username, "email": f"{username}@example.com"}
if add_key:
user_data["public_key"] = VALID_KEY
user_data["fingerprint"] = VALID_KEY_FINGERPRINT
return User.objects.create_user(**user_data)

def create_and_login_user(client):
username = ''.join(random.choice(string.ascii_uppercase) for _ in range(5))
user = User.objects.create_user(username=username,
email="{}@example.com".format(username))
client.force_login(user)
return user

def create_box(user, status="open", verified_only=False):
statuses = {
"open": Box.OPEN,
"done": Box.DONE,
"closed": Box.CLOSED,
"expired": Box.EXPIRED
}
past_datetime = timezone.now() - timedelta(days=1)
return user.own_boxes.create(
name="test_box",
status=statuses[status],
expires_at=past_datetime if status == "expired" else future_datetime,
verified_only=verified_only)


class BoxFormTests(TestCase):

def test_invalid_expiration_date(self):
"""
Expiration must be submitted provided and with valid format
If one of this the above statments are not true form must be
If one of this the above statements are not true form must be
invalid
"""
data = {
@@ -66,7 +71,7 @@ def test_valid_expiration_date(self):
data = {
"name": "some name",
"description": "some text",
"expires_at": "12/12/2020 23:00",
"expires_at": future_datetime_string,
"max_messages": 1
}
form = CreateBoxForm(data)
@@ -92,20 +97,20 @@ def test_empty_name(self):
data = {
"name": "",
"description": "some text",
"expires_at": "12/12/2020 23:00",
"expires_at": future_datetime_string,
"max_messages": 1
}
form = CreateBoxForm(data)
self.assertEqual(form.is_valid(), False)

def test_empty_desription(self):
def test_empty_description(self):
"""
Description is optional
"""
data = {
"name": "some name",
"description": "",
"expires_at": "12/12/2020 23:00",
"expires_at": future_datetime_string,
"max_messages": 1
}
form = CreateBoxForm(data)
@@ -118,7 +123,7 @@ def test_invalid_max_messages(self):
data = {
"name": "some name",
"description": "",
"expires_at": "12/12/2020 23:00",
"expires_at": future_datetime_string,
"max_messages": 0
}
form = CreateBoxForm(data)
@@ -179,17 +184,27 @@ def test_open_boxes_are_default_list(self):
Base page shows list of open boxes
"""
user = create_and_login_user(self.client)
create_boxes(user)
create_box(user)
response = self.client.get(reverse("boxes_list"))
for box in response.context["object_list"]:
self.assertEqual(box.status, Box.OPEN)

def test_verified_only_boxes_list(self):
"""
Base page shows verified_only boxes
"""
user = create_and_login_user(self.client)
create_box(user, verified_only=True)
response = self.client.get(reverse("boxes_list"))
for box in response.context["object_list"]:
self.assertTrue(box.verified_only)

def test_expired_boxes_list(self):
"""
With expired query param expired boxes are shown
"""
user = create_and_login_user(self.client)
create_boxes(user)
create_box(user, status="expired")
response = self.client.get(reverse("boxes_list"),
{'display': 'Expired'})
for box in response.context["object_list"]:
@@ -200,7 +215,7 @@ def test_closed_boxes_list(self):
With closed query param closed boxes are shown
"""
user = create_and_login_user(self.client)
create_boxes(user)
create_box(user, status="closed")
response = self.client.get(reverse("boxes_list"),
{'display': 'Closed'})
for box in response.context["object_list"]:
@@ -211,7 +226,7 @@ def test_sent_boxes_list(self):
With sent query param sent boxes are shown
"""
user = create_and_login_user(self.client)
create_boxes(user)
create_box(user, status="done")
response = self.client.get(reverse("boxes_list"),
{'display': 'Done'})
for box in response.context["object_list"]:
@@ -221,39 +236,103 @@ def test_sent_boxes_list(self):
class BoxSubmitViewTests(TestCase):

def test_valid_owner_key(self):
user = create_and_login_user(self.client)
user.public_key = VALID_KEY
user.fingerprint = VALID_KEY_FINGERPRINT
user.save()
box = create_open_box(user)
user = create_user()
box = create_box(user)
response = self.client.get(reverse("boxes_show", args=(box.uuid,)))
self.assertEqual(response.template_name[0], 'boxes/box_submit.html')

def test_revoked_owner_key(self):
user = create_and_login_user(self.client)
user = create_user(add_key=False)
user.public_key = REVOKED_KEY
user.fingerprint = REVOKED_KEY_FINGERPRINT
user.save()
box = create_open_box(user)
box = create_box(user)
response = self.client.get(reverse("boxes_show", args=(box.uuid,)))
self.assertEqual(response.template_name, 'boxes/closed.html')

def test_expired_owner_key(self):
user = create_and_login_user(self.client)
user = create_user(add_key=False)
user.public_key = EXPIRED_KEY
user.fingerprint = EXPIRED_KEY_FINGERPRINT
user.save()
box = create_open_box(user)
box = create_box(user)
response = self.client.get(reverse("boxes_show", args=(box.uuid,)))
self.assertEqual(response.template_name, 'boxes/closed.html')

def test_no_owner_key(self):
user = create_and_login_user(self.client)
box = create_open_box(user)
user.save()
user = create_user(add_key=False)
box = create_box(user)
response = self.client.get(reverse("boxes_show", args=(box.uuid,)))
self.assertEqual(response.template_name, 'boxes/closed.html')

def test_box_not_found(self):
user = create_user()
box = create_box(user)
box.delete()
response = self.client.get(reverse("boxes_show", args=(box.uuid,)))
self.assertEqual(response.status_code, 404)

def test_anonymous_access_to_verified_only_box(self):
user = create_user()
box = create_box(user, verified_only=True)
response = self.client.get(reverse("boxes_show", args=(box.uuid,)))
self.assertEqual(response.status_code, 401)

def test_authenticated_access_to_verified_only_box(self):
user = create_user()
box = create_box(user, verified_only=True)
create_and_login_user(self.client)
response = self.client.get(reverse("boxes_show", args=(box.uuid,)))
self.assertEqual(response.status_code, 200)


class BoxCreateViewTests(TestCase):

def test_create_new_box(self):
user = create_and_login_user(self.client)
response = self.client.post(reverse("boxes_create"), {
"name": "test",
"never_expires": True,
"max_messages": 1})
self.assertEqual(response.status_code, 200)
self.assertEqual(user.boxes.count(), 1)
self.assertEqual(user.boxes.first().name, "test")


class BoxDeleteViewTests(TestCase):

def test_delete_existing_box(self):
user = create_and_login_user(self.client)
box = create_box(user)
response = self.client.post(reverse("boxes_delete", args=(box.id,)))
self.assertEqual(response.status_code, 302)
self.assertEqual(user.own_boxes.count(), 0)

def test_cannot_delete_box_that_are_not_open(self):
user = create_and_login_user(self.client)
box = create_box(user, status="closed")
self.assertEqual(user.own_boxes.count(), 1)
response = self.client.post(reverse("boxes_delete", args=(box.id,)))
self.assertEqual(response.status_code, 302)
self.assertEqual(user.own_boxes.count(), 0)


class BoxCloseView(TestCase):

def test_close_an_open_box(self):
user = create_and_login_user(self.client)
box = create_box(user)
response = self.client.post(reverse("boxes_close", args=(box.id,)))
self.assertEqual(response.status_code, 302)
self.assertEqual(user.own_boxes.filter(status=Box.CLOSED).count(), 1)

def test_close_box_that_is_not_open(self):
user = create_and_login_user(self.client)
box = create_box(user, status="done")
response = self.client.post(reverse("boxes_close", args=(box.id,)))
self.assertEqual(response.status_code, 302)
self.assertEqual(user.own_boxes.filter(status=Box.CLOSED).count(), 0)


class MailTaskTests(TestCase):

@@ -262,9 +341,8 @@ def test_email_sending(self):
With a valid message_id an email is sent and the Message status
is changed to sent
"""
user = create_and_login_user(self.client)
create_boxes(user)
initial_box = user.own_boxes.all()[0]
user = create_user()
initial_box = create_box(user)
message = initial_box.messages.create()
process_email(message.id, {"message": ENCRYPTED_MESSAGE})
after_box = user.own_boxes.get(id=initial_box.id)
@@ -276,13 +354,11 @@ def test_email_sending(self):

def test_email_sending_with_reply_to(self):
mail.outbox = []
user = create_and_login_user(self.client)
create_boxes(user)
initial_box = user.own_boxes.all()[0]
user = create_user()
initial_box = create_box(user)
message = initial_box.messages.create()
process_email(
message.id, {"message": ENCRYPTED_MESSAGE}, sent_by=user.email
)
message.id, {"message": ENCRYPTED_MESSAGE}, sent_by=user.email)
message.refresh_from_db()
self.assertEqual(len(mail.outbox), 1)
self.assertIn(user.email, mail.outbox[0].reply_to)
12 changes: 6 additions & 6 deletions boxes/urls.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from django.conf.urls import url
from django.urls import path, re_path
from .views import (BoxListView, BoxCreateView, BoxDeleteView, BoxSubmitView,
BoxCloseView)

urlpatterns = [
url(r'^$', BoxListView.as_view(), name="boxes_list"),
url(r'^create$', BoxCreateView.as_view(), name="boxes_create"),
url(r'^(?P<pk>[0-9]+)/delete$', BoxDeleteView.as_view(), name="boxes_delete"),
url(r'^(?P<pk>[0-9]+)/closed$', BoxCloseView.as_view(), name="boxes_close"),
url(r'^(?P<box_uuid>[0-9a-z-]+)$', BoxSubmitView.as_view(), name="boxes_show"),
path('', BoxListView.as_view(), name="boxes_list"),
path('create', BoxCreateView.as_view(), name="boxes_create"),
path('<int:pk>/delete', BoxDeleteView.as_view(), name="boxes_delete"),
path('<int:pk>/closed', BoxCloseView.as_view(), name="boxes_close"),
re_path(r'^(?P<box_uuid>[0-9a-z-]+)$', BoxSubmitView.as_view(), name="boxes_show"),
]
25 changes: 16 additions & 9 deletions boxes/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from django.views.generic import ListView, CreateView, DeleteView, UpdateView
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse_lazy
from django.http import Http404, HttpResponseRedirect
from django.urls import reverse_lazy
from django.core.exceptions import ObjectDoesNotExist
from django.contrib import messages
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from humans.views import LoginRequiredMixin
from django.utils.translation import gettext_lazy as _
from humans.views import AuthMixin, LoginRequiredMixin
from humans.utils import key_state
from django.conf import settings
from .forms import CreateBoxForm, SubmitBoxForm
@@ -47,7 +47,7 @@ class BoxCreateView(JSONResponseMixin, LoginRequiredMixin, CreateView):
def dispatch(self, request, *args, **kwargs):
# Check if user can create boxes:
user = request.user
if user.is_authenticated() and not user.has_setup_complete():
if user.is_authenticated and not user.has_setup_complete():
url = reverse_lazy("humans_update") + "?setup=1"
return self.render_to_response({"location": url})

@@ -120,7 +120,7 @@ def post(self, request, *args, **kwargs):
return HttpResponseRedirect(success_url)


class BoxSubmitView(UpdateView):
class BoxSubmitView(AuthMixin, UpdateView):
template_name = "boxes/box_submit.html"
form_class = SubmitBoxForm
model = Box
@@ -139,8 +139,8 @@ def get_object(self, queryset=None):
try:
q = queryset.select_related('owner').prefetch_related('recipients')
return q.get(uuid=self.kwargs.get("box_uuid"))
except ValueError:
raise ObjectDoesNotExist(_('Not Found. Double check your URL'))
except (ValueError, Box.DoesNotExist):
raise Http404(_('Not Found. Double check your URL'))

def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
@@ -160,6 +160,13 @@ def dispatch(self, request, *args, **kwargs):
context={"box": self.object},
template="boxes/closed.html",
using=self.template_engine)
elif self.object.verified_only and request.user.is_anonymous:
return self.response_class(
request=request,
context=super().get_context_data(),
template="boxes/verified_only.html",
using=self.template_engine,
status=401)
else:
return super().dispatch(request, *args, **kwargs)

@@ -175,7 +182,7 @@ def post(self, request, *args, **kwargs):
self.object.save()

add_user_email = cleaned_data.get("add_reply_to", False)
if request.user.is_authenticated() and add_user_email:
if request.user.is_authenticated and add_user_email:
user_email = request.user.email
else:
user_email = None
23 changes: 9 additions & 14 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version: '2'
version: "2"

services:
web:
@@ -7,6 +7,9 @@ services:
service: app
ports:
- "80:8000"
depends_on:
- db
- redis

celery:
extends:
@@ -15,20 +18,12 @@ services:
command: pipenv run celery -A hawkpost worker --beat -l info
volumes:
- "./gpg_home:/home/user/.gnupg"

mail_debug:
extends:
file: common.yml
service: app
command: pipenv run python manage.py mail_debug --settings hawkpost.settings.development --use-settings
ports:
- "1025:1025"
environment:
EMAIL_HOST: 0.0.0.0
EMAIL_PORT: 1025
depends_on:
- db
- redis

db:
image: postgres:9
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
@@ -37,6 +32,6 @@ services:
- POSTGRES_DB=hawkpost_dev

redis:
image: redis:3-alpine
image: redis:7-alpine
ports:
- "6379:6379"
10 changes: 6 additions & 4 deletions hawkpost/middleware.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from django.utils import timezone, translation
from django.utils.deprecation import MiddlewareMixin


class TimezoneMiddleware():
class TimezoneMiddleware(MiddlewareMixin):
def process_request(self, request):
if request.user.is_authenticated():
if request.user.is_authenticated:
timezone.activate(request.user.timezone)
else:
timezone.deactivate()

class LanguageMiddleware():

class LanguageMiddleware(MiddlewareMixin):
def process_request(self, request):
if request.user.is_authenticated():
if request.user.is_authenticated:
translation.activate(request.user.language)
43 changes: 32 additions & 11 deletions hawkpost/settings/common.py
Original file line number Diff line number Diff line change
@@ -9,8 +9,9 @@
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.9/ref/settings/
"""
from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from celery.schedules import crontab
import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
@@ -47,20 +48,22 @@
'boxes',
'pages',
]
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'

MIDDLEWARE_CLASSES = [
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'hawkpost.middleware.TimezoneMiddleware',
'hawkpost.middleware.LanguageMiddleware',
'axes.middleware.AxesMiddleware',
'allauth.account.middleware.AccountMiddleware'
]

ROOT_URLCONF = 'hawkpost.urls'
@@ -106,6 +109,7 @@
# Authentication settings

AUTHENTICATION_BACKENDS = (
'axes.backends.AxesBackend',
# Needed to login by username in Django admin, regardless of `allauth`
'django.contrib.auth.backends.ModelBackend',
# `allauth` specific authentication methods, such as login by e-mail
@@ -123,15 +127,14 @@

LANGUAGE_CODE = 'en-us'
LANGUAGES = [
('en-us', _('English')),
('pt-pt', _('Portuguese'))
('en-us', _('English')),
('pt-pt', _('Portuguese'))
]

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True

@@ -147,7 +150,14 @@

STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": 'whitenoise.storage.CompressedStaticFilesStorage',
},
}

# Media Files

@@ -178,17 +188,28 @@
}
}

SOCIALACCOUNT_ADAPTER = 'humans.adapter.SocialAccountAdapter'

# Authentication Limits Config (AXES)
AXES_LOGIN_FAILURE_LIMIT = 5
AXES_FAILURE_LIMIT = 5
AXES_COOLOFF_TIME = 1 # hour
AXES_USERNAME_FORM_FIELD = 'login'
AXES_DISABLE_SUCCESS_ACCESS_LOG = True

# Email Settings
DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL")

# Celerey Settings
CELERY_BROKER_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/0")
CELERY_BEAT_SCHEDULE = {
"update_public_keys": {
"task": "humans.tasks.update_public_keys",
"schedule": crontab(minute=0, hour=4), # Every day at 4 AM UTC
},
"validate_public_keys": {
"task": "humans.tasks.validate_public_keys",
"schedule": crontab(minute=30, hour=5), # Every day at 5:30 AM UTC
}
}

# SITE DOMAIN
SITE_DOMAIN = os.environ.get("SITE_DOMAIN")
@@ -205,4 +226,4 @@
SUPPORT_NAME = os.environ.get("SUPPORT_NAME")
SUPPORT_EMAIL = os.environ.get("SUPPORT_EMAIL")
INSTANCE_DESCRIPTION = os.environ.get("INSTANCE_DESCRIPTION", "")
VERSION = "1.3.0"
VERSION = "1.4.0"
12 changes: 5 additions & 7 deletions hawkpost/settings/development.py
Original file line number Diff line number Diff line change
@@ -18,25 +18,23 @@

# If the DB_HOST was specified it is overriding the default connection
if 'DB_HOST' in os.environ:
DATABASES['default']['HOST'] = os.environ.get("DB_HOST")
DATABASES['default']['HOST'] = os.environ.get("DB_HOST", "localhost")
DATABASES['default']['PORT'] = os.environ.get("DB_PORT", 5432)
DATABASES['default']['USER'] = os.environ.get("DB_USER")
DATABASES['default']['USER'] = os.environ.get("DB_USER", "postgres")
DATABASES['default']['NAME'] = os.environ.get("DB_NAME", "hawkpost_dev")

if 'DB_PASSWORD' in os.environ:
DATABASES['default']['PASSWORD'] = os.environ.get("DB_PASSWORD")
DATABASES['default']['PASSWORD'] = os.environ.get("DB_PASSWORD", "postgres")

# Development Applications
INSTALLED_APPS += (
'debug_toolbar',
'django_extensions',
)

MIDDLEWARE_CLASSES.insert(0, 'debug_toolbar.middleware.DebugToolbarMiddleware')

EMAIL_HOST = os.environ.get("EMAIL_HOST", "127.0.0.1")
EMAIL_PORT = os.environ.get("EMAIL_PORT", 1025)
MIDDLEWARE.insert(0, 'debug_toolbar.middleware.DebugToolbarMiddleware')

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

DEBUG_TOOLBAR_PANELS = [
'debug_toolbar.panels.versions.VersionsPanel',
6 changes: 4 additions & 2 deletions hawkpost/settings/production.py
Original file line number Diff line number Diff line change
@@ -62,5 +62,7 @@
}

# Axes Behind proxy
AXES_BEHIND_REVERSE_PROXY = True
AXES_NUM_PROXIES = 1
AXES_META_PRECEDENCE_ORDER = [
'HTTP_X_FORWARDED_FOR',
'REMOTE_ADDR',
]
4 changes: 0 additions & 4 deletions hawkpost/static/javascripts/jquery-2.2.2.min.js

This file was deleted.

2 changes: 2 additions & 0 deletions hawkpost/static/javascripts/jquery.min.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions hawkpost/templates/account/auth_modals.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{% load i18n %}
{% load account socialaccount %}
{% load staticfiles %}
{% load static %}

<div class="md-modal-xs md-effect-1 " id="modal-login">
<div class="md-content-xs ">
<button type="button" class="md-close btn-none"><i class="fa fa-times fa-lg" aria-hidden="true"></i></button>
<div class="row-s start-xs">
<form class="login smalltext text-lightest" id="login-form-js" method="POST" action="{% url 'account_login' %}">
<form class="login smalltext text-lightest" id="login-form-js" method="POST" action="{% url 'account_login' %}?next={{ request.path_info }}">
<div class="col-xs-12">
<h3 class="title text-lightest">{% trans "LOGIN" %}</h3>
<p>{% blocktrans %}If you have not created an account yet, then please <a class=" md-trigger text-blue" href="#modal-signup" data-modal="modal-signup"><span>sign up</span></a>.{% endblocktrans %}</p>
2 changes: 1 addition & 1 deletion hawkpost/templates/account/verified_email_required.html
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@
<section class="text-lightest">
<div class="row center-xs center-sm">
<div class="col-offset-xs-1 col-xs-10 col-offset-sm-2 col-xs-8">
<h1 class="title text-lightest">{{% trans "VERIFY YOUR E-MAIL ADDRESS" %}</h1>
<h1 class="title text-lightest">{% trans "VERIFY YOUR E-MAIL ADDRESS" %}</h1>

{% url 'account_email' as email_url %}

4 changes: 2 additions & 2 deletions hawkpost/templates/layout/base.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% load i18n %}
{% load staticfiles %}
{% load static %}

<!DOCTYPE html>
<html>
@@ -12,7 +12,7 @@
<link rel="stylesheet" type="text/css" href="{% static 'css/style.css' %}">
<link rel="icon" href="{% static 'images/favicon.ico' %}">
{% endblock styles %}
<script type="text/javascript" src="{% static 'javascripts/jquery-2.2.2.min.js' %}"></script>
<script type="text/javascript" src="{% static 'javascripts/jquery.min.js' %}"></script>
</head>

<body>
2 changes: 1 addition & 1 deletion hawkpost/templates/layout/navbar.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% load i18n %}
{% load staticfiles %}
{% load static %}

<div class="nav-big smalltext">
<nav class="navbar">
34 changes: 22 additions & 12 deletions hawkpost/urls.py
Original file line number Diff line number Diff line change
@@ -14,28 +14,38 @@
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls import url, include
from django.urls import include, path, re_path
from django.contrib import admin
from django.conf.urls.i18n import i18n_patterns
from axes.decorators import watch_login
from allauth.account.views import login
from django.utils.decorators import method_decorator

from allauth.account.views import LoginView
from axes.decorators import axes_dispatch
from axes.decorators import axes_form_invalid

from humans.forms import LoginForm

LoginView.dispatch = method_decorator(axes_dispatch)(LoginView.dispatch)
LoginView.form_invalid = method_decorator(
axes_form_invalid)(LoginView.form_invalid)

urlpatterns = [
url(r'^admin/login/$', watch_login(admin.site.login)),
url(r'^admin/', admin.site.urls),
url(r'^users/login/$', watch_login(login)),
url(r'^users/', include('allauth.urls')),
url(r'^users/', include('humans.urls')),
url(r'^box/', include('boxes.urls')),
url(r'^', include('pages.urls'))
path('admin/login/', admin.site.login),
re_path(r'^admin/', admin.site.urls),
path('users/login/', LoginView.as_view(form_class=LoginForm),
name='account_login'),
path('users/', include('allauth.urls')),
path('users/', include('humans.urls')),
path('box/', include('boxes.urls')),
path('', include('pages.urls'))
]

urlpatterns += i18n_patterns(
url(r'^', include('pages.urls')),
path('', include('pages.urls')),
)

if settings.DEBUG:
import debug_toolbar
urlpatterns = [
url(r'^__debug__/', include(debug_toolbar.urls)),
path('__debug__/', include(debug_toolbar.urls)),
] + urlpatterns
14 changes: 14 additions & 0 deletions humans/adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from allauth.account.models import EmailAddress
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter

class SocialAccountAdapter(DefaultSocialAccountAdapter):
def pre_social_login(self, request, sociallogin):
if sociallogin.is_existing:
return

try:
email = EmailAddress.objects.get(email=sociallogin.user.email)
except EmailAddress.DoesNotExist:
return

sociallogin.connect(request, email.user)
12 changes: 8 additions & 4 deletions humans/admin.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@
from django.contrib.auth.admin import UserAdmin as DefaultUserAdmin
from django.contrib import messages
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from .models import User, Notification, KeyChangeRecord
from .tasks import enqueue_email_notifications

@@ -35,7 +35,7 @@ class NotificationAdmin(admin.ModelAdmin):
fields = ["subject", "body", "send_to"]
search_fields = ['subject', 'body']

actions = ["send_notification", "delete_selected"]
actions = ["send_notification"]

def delete_model(self, request, obj):
if obj.sent_at:
@@ -44,11 +44,17 @@ def delete_model(self, request, obj):
else:
obj.delete()

@admin.action(
description=_('Delete selected notifications')
)
def delete_selected(self, request, queryset):
queryset.filter(sent_at=None).delete()
msg = _('Removed all unsent notifications in selection')
messages.success(request, msg)

@admin.action(
description=_('Send selected notifications')
)
def send_notification(self, request, queryset):
queryset = queryset.filter(sent_at=None)
for notification in queryset:
@@ -58,8 +64,6 @@ def send_notification(self, request, queryset):
queryset.update(sent_at=timezone.now())
messages.success(request, _('All notifications enqueued for sending'))

send_notification.short_description = _('Send selected notifications')
delete_selected.short_description = _('Delete selected notifications')


@admin.register(KeyChangeRecord)
2 changes: 1 addition & 1 deletion humans/apps.py
Original file line number Diff line number Diff line change
@@ -2,4 +2,4 @@


class AuthConfig(AppConfig):
name = 'auth'
name = 'humans'
12 changes: 9 additions & 3 deletions humans/forms.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.forms import ModelForm
from django import forms
from django.contrib.auth.password_validation import validate_password
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from allauth.account.forms import LoginForm as BaseLoginForm
from allauth.account.forms import SignupForm as BaseSignupForm
from .models import User
@@ -30,8 +30,8 @@ class Meta:
}

current_password = forms.CharField(label=_('Current password'),
required=False,
widget=forms.PasswordInput)
required=False,
widget=forms.PasswordInput)
new_password1 = forms.CharField(label=_('New password'),
required=False,
widget=forms.PasswordInput)
@@ -136,6 +136,12 @@ def __init__(self, *args, **kwargs):
self.fields['password'].widget.attrs["placeholder"] = ""
self.fields['password'].widget.attrs["autocomplete"] = "off"

def user_credentials(self):
credentials = super().user_credentials()
credentials['login'] = credentials.get(
'email') or credentials.get('username')
return credentials


class SignupForm(BaseSignupForm):
def __init__(self, *args, **kwargs):
18 changes: 18 additions & 0 deletions humans/migrations/0013_auto_20201204_1807.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 2.2.13 on 2020-12-04 18:07

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('humans', '0012_remove_user_server_signed'),
]

operations = [
migrations.AlterField(
model_name='user',
name='last_name',
field=models.CharField(blank=True, max_length=150, verbose_name='last name'),
),
]
30 changes: 20 additions & 10 deletions humans/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.db import models
from django.contrib.auth.models import AbstractUser, Group
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from timezone_field import TimeZoneField
from django.db import transaction

@@ -14,12 +14,17 @@ class User(AbstractUser):
('pt-pt', _('Portuguese')),
)

organization = models.CharField(null=True, blank=True, max_length=80, verbose_name=_('Organization'))
public_key = models.TextField(blank=True, null=True, verbose_name=_('Public key'))
fingerprint = models.CharField(null=True, blank=True, max_length=50, verbose_name=_('Fingerprint'))
keyserver_url = models.URLField(null=True, blank=True, verbose_name=_('Key server URL'))
organization = models.CharField(
null=True, blank=True, max_length=80, verbose_name=_('Organization'))
public_key = models.TextField(
blank=True, null=True, verbose_name=_('Public key'))
fingerprint = models.CharField(
null=True, blank=True, max_length=50, verbose_name=_('Fingerprint'))
keyserver_url = models.URLField(
null=True, blank=True, verbose_name=_('Key server URL'))
timezone = TimeZoneField(default='UTC', verbose_name=_('Timezone'))
language = models.CharField(default="en-us", max_length=16, choices=LANGUAGE_CHOICES , verbose_name=_('Language'))
language = models.CharField(
default="en-us", max_length=16, choices=LANGUAGE_CHOICES, verbose_name=_('Language'))

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -61,14 +66,18 @@ class Notification(models.Model):
by an Administrator. Just once.
"""

subject = models.CharField(null=False, blank=False, max_length=150, verbose_name=_('Subject'))
subject = models.CharField(
null=False, blank=False, max_length=150, verbose_name=_('Subject'))
body = models.TextField(null=False, blank=False, verbose_name=_('Body'))

created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('Updated at'))
created_at = models.DateTimeField(
auto_now_add=True, verbose_name=_('Created at'))
updated_at = models.DateTimeField(
auto_now=True, verbose_name=_('Updated at'))

sent_at = models.DateTimeField(null=True, verbose_name=_('Sent at'))
send_to = models.ForeignKey(Group, null=True, blank=True, verbose_name=_('Send to'))
send_to = models.ForeignKey(
Group, null=True, blank=True, on_delete=models.CASCADE, verbose_name=_('Send to'))

class Meta:
verbose_name = _('Notification')
@@ -86,6 +95,7 @@ class KeyChangeRecord(models.Model):
This allows the user to be aware of any suspicious activity
"""
user = models.ForeignKey(User,
on_delete=models.CASCADE,
related_name='keychanges',
verbose_name=_('User'))
prev_fingerprint = models.CharField(null=True,
11 changes: 4 additions & 7 deletions humans/tasks.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
from celery.task.schedules import crontab
from celery.decorators import periodic_task
from celery import shared_task
from celery.utils.log import get_task_logger
from django.core.mail import EmailMultiAlternatives
from django.conf import settings
from django.db.models import Q
from django.template.loader import render_to_string
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from .models import User, Notification
from .utils import key_state
import requests
@@ -31,8 +29,7 @@ def send_email(user, subject, template):
email.send()


# Every day at 4 AM UTC
@periodic_task(run_every=(crontab(minute=0, hour=4)), ignore_result=True)
@shared_task(ignore_result=True)
def update_public_keys():
users = User.objects.exclude(
Q(keyserver_url__isnull=True) | Q(keyserver_url__exact=''))
@@ -76,8 +73,8 @@ def update_public_keys():

logger.info(_('Finished Updating user keys'))

# Every day at 5h30 AM UTC
@periodic_task(run_every=(crontab(minute=30, hour=5)), ignore_result=True)

@shared_task(ignore_result=True)
def validate_public_keys():
users = User.objects.exclude(
Q(public_key__isnull=True) | Q(public_key__exact=''))
2 changes: 1 addition & 1 deletion humans/templates/humans/update_user_form.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% extends "layout/base.html" %}
{% load staticfiles %}
{% load static %}
{% load i18n %}

{% block content %}
2 changes: 2 additions & 0 deletions humans/templates/humans/user_confirm_delete.html
Original file line number Diff line number Diff line change
@@ -17,11 +17,13 @@ <h1 class="title text-light">{% trans "DELETE ACCOUNT" %}</h1>
<div class="col-xs-12 col-offset-sm-2 col-sm-8 col-offset-md-3 col-md-6">
<form class="form__wrap" method="post" action="{% url "humans_delete" %}">
{% csrf_token %}
{% if request.user.has_usable_password %}
<div class="text-left input-anim">
<label for="current-password" class="">{% trans "Insert your current password:" %}</label>
<input id="current-password" type="password" name="current_password" placeholder="{% trans "Current password" %}" class="padding-inside mt-normal" autocomplete="off"></input>
</div>
<br>
{% endif %}
<div>
<input type="submit" class="btn-danger" value="{% trans "Yes, I want to delete everything" %}"></input>
</div>
130 changes: 116 additions & 14 deletions humans/tests.py
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@
from django.utils import timezone
from django.test import TestCase
from django.core import mail
from django.urls import reverse
from boxes.tests import create_and_login_user
from hawkpost import celery_app
from .models import Notification, User
@@ -16,6 +17,22 @@
from shutil import rmtree
import gnupg
import tempfile
import random
import string
from unittest import mock
from collections import namedtuple


DEFAULT_USER_DATA = {
"first_name": "some name",
"last_name": "some last name",
"company": "some company",
"fingerprint": VALID_KEY_FINGERPRINT,
"timezone": "UTC",
"language": "en-us",
"public_key": VALID_KEY
}


def create_notification(sent=False, group=None):
sent_at = timezone.now() if sent else None
@@ -24,32 +41,34 @@ def create_notification(sent=False, group=None):
sent_at=sent_at,
send_to=group)


@with_gpg_obj
def create_expiring_key(days_to_expire, gpg):
days_to_expire = str(days_to_expire) + "d"
# Example values for expire_date: “2009-12-31”, “365d”, “3m”, “6w”, “5y”, “seconds=<epoch>”, 0
input_data = gpg.gen_key_input(key_type="RSA",
key_length=1024,
expire_date=days_to_expire,
passphrase="secret")
key_length=1024,
expire_date=days_to_expire,
passphrase="secret")
key_id = gpg.gen_key(input_data)
# retrieve the key
key_ascii = gpg.export_keys(key_id)
# remove the keyring
return key_ascii


def create_and_login_user(client):
username = ''.join(random.choice(string.ascii_uppercase) for _ in range(5))
user = User.objects.create_user(username=username,
email="{}@example.com".format(username))
client.force_login(user)
return user


class UpdateUserFormTests(TestCase):

def setUp(self):
self.default_data = {
"first_name": "some name",
"last_name": "some last name",
"company": "some company",
"fingerprint": VALID_KEY_FINGERPRINT,
"timezone": "UTC",
"language": "en-us",
"public_key": VALID_KEY
}
self.default_data = DEFAULT_USER_DATA

def test_empty_fingerprint(self):
data = copy(self.default_data)
@@ -64,7 +83,10 @@ def test_fingerprint_plus_public_key(self):
form = UpdateUserInfoForm(data)
self.assertEqual(form.is_valid(), True)

def test_fingerprint_plus_keyserver_url(self):
@mock.patch("humans.forms.requests.get")
def test_fingerprint_plus_keyserver_url(self, get_mock):
Response = namedtuple("Response", "status_code,text")
get_mock.return_value = Response(200, VALID_KEY)
data = copy(self.default_data)
data["keyserver_url"] = VALID_KEYSERVER_URL
form = UpdateUserInfoForm(data)
@@ -145,7 +167,7 @@ def test_change_password(self):
Tests if the password is actually changed
"""
data = {
'current_password':'123123',
'current_password': '123123',
'new_password1': 'abcABCD123',
'new_password2': 'abcABCD123',
'timezone': 'UTC',
@@ -278,3 +300,83 @@ def test_ip_address_and_user_agent_are_recorded_when_available(self):
keychangerecord = self.user.keychanges.last()
self.assertEqual(keychangerecord.ip_address, '127.0.0.1')
self.assertEqual(keychangerecord.agent, 'test_agent')


class UpdateSettingsTests(TestCase):

def test_unauthenticated_get_request(self):
response = self.client.get(reverse("humans_update"))
self.assertEqual(response.status_code, 302)

def test_unauthenticated_post_request(self):
response = self.client.post(reverse("humans_update"))
self.assertEqual(response.status_code, 302)

def test_authenticated_get_request(self):
create_and_login_user(self.client)
response = self.client.get(reverse("humans_update"))
self.assertEqual(response.status_code, 200)

def test_update_user_name(self):
user = create_and_login_user(self.client)
response = self.client.post(reverse("humans_update"),
DEFAULT_USER_DATA,
headers={"user-agent": "testagent"})
self.assertEqual(response.status_code, 302)
user.refresh_from_db()
self.assertEqual(user.first_name, "some name")


class DeleteUserTests(TestCase):

def test_unauthenticated_get_request(self):
response = self.client.get(reverse("humans_delete"))
self.assertEqual(response.status_code, 302)

def test_authenticated_get_request(self):
create_and_login_user(self.client)
response = self.client.get(reverse("humans_delete"))
self.assertEqual(response.status_code, 200)

def test_delete_without_password(self):
user = create_and_login_user(self.client)
user.set_password("somepassword")
user.save()
self.client.force_login(user)
response = self.client.post(reverse("humans_delete"))
self.assertEqual(response.status_code, 200)
self.assertTrue(User.objects.all().exists())
self.assertIn(
"In order to delete the account you must provide the current password.",
[str(msg) for msg in response.context["messages"]])

def test_delete_with_wrong_password(self):
user = create_and_login_user(self.client)
user.set_password("somepassword")
user.save()
self.client.force_login(user)
response = self.client.post(reverse("humans_delete"), {
"current_password": "wrong"})
self.assertEqual(response.status_code, 200)
self.assertIn(
"In order to delete the account you must provide the current password.",
[str(msg) for msg in response.context["messages"]])
self.assertTrue(User.objects.all().exists())

def test_delete_with_correct_password(self):
password = "somepassword"
user = create_and_login_user(self.client)
user.set_password(password)
user.save()
self.client.force_login(user)
response = self.client.post(reverse("humans_delete"), {
"current_password": password})
self.assertEqual(response.status_code, 302)
self.assertFalse(User.objects.all().exists())

def test_delete_passwordless_account(self):
user = create_and_login_user(self.client)
self.client.force_login(user)
response = self.client.post(reverse("humans_delete"))
self.assertEqual(response.status_code, 302)
self.assertFalse(User.objects.all().exists())
6 changes: 3 additions & 3 deletions humans/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.conf.urls import url
from django.urls import path
from .views import UpdateSettingsView, DeleteUserView

urlpatterns = [
url(r'^settings$', UpdateSettingsView.as_view(), name="humans_update"),
url(r'^delete$', DeleteUserView.as_view(), name="humans_delete"),
path('settings', UpdateSettingsView.as_view(), name="humans_update"),
path('delete', DeleteUserView.as_view(), name="humans_delete"),
]
17 changes: 10 additions & 7 deletions humans/utils.py
Original file line number Diff line number Diff line change
@@ -17,21 +17,24 @@ def inner(key):
gpg_obj.encoding = 'utf-8'
ret = func(key, gpg_obj)
# remove the keyring
rmtree(temp_dir)
rmtree(temp_dir, ignore_errors=True)

return ret
return inner


@with_gpg_obj
def key_state(key, gpg):
INVALID = (None, "invalid", -1)
if not key:
return None, "invalid", -1
return INVALID
results = gpg.import_keys(key).results
if not results:
return INVALID
# Key data is present in the last element of the list
if not results or not results[-1]["fingerprint"]:
return None, "invalid", -1

key_fingerprint = results[-1]["fingerprint"]
key_fingerprint = results[-1].get("fingerprint")
if not key_fingerprint:
return INVALID

# Since the keyring is exclusive for this import
# only the imported key exists in it.
@@ -53,5 +56,5 @@ def key_state(key, gpg):

def request_ip_address(request):
"""Takes a Request Object and returns the caller IP address"""
x_forward_for = request.META.get('HTTP_X_FORWARDED_FOR', None)
x_forward_for = request.headers.get('x-forwarded-for', None)
return x_forward_for if x_forward_for else request.META.get('REMOTE_ADDR')
28 changes: 14 additions & 14 deletions humans/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from django.contrib.auth.mixins import LoginRequiredMixin as LoginRequired
from django.contrib.auth import logout, authenticate
from django.contrib.auth import logout
from django.contrib.auth import update_session_auth_hash
from django.views.generic import FormView, DeleteView
from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.contrib import messages
from django.conf import settings
from .forms import UpdateUserInfoForm, LoginForm, SignupForm
from .models import User
from .utils import request_ip_address
@@ -19,7 +18,7 @@ class LoginRequiredMixin(LoginRequired):
class AuthMixin():
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if not self.request.user.is_authenticated():
if not self.request.user.is_authenticated:
context["login_form"] = LoginForm()
context["signup_form"] = SignupForm()
return context
@@ -50,7 +49,7 @@ def get_form_kwargs(self):

def form_valid(self, form):
ip = request_ip_address(self.request)
agent = self.request.META.get('HTTP_USER_AGENT')
agent = self.request.headers.get('user-agent')
form.save(ip=ip, agent=agent)
if form.change_password:
update_session_auth_hash(self.request, form.instance)
@@ -69,17 +68,18 @@ class DeleteUserView(LoginRequiredMixin, DeleteView):
def get_object(self, queryset=None):
return self.request.user

def delete(self, request, *args, **kwargs):
current_pw = request.POST.get("current_password", "")
if authenticate(username=request.user.username, password=current_pw):
response = super().delete(request, *args, **kwargs)
logout(request)
messages.success(request,
def form_valid(self, form):
current_pw = self.request.POST.get("current_password", "")
user = self.request.user
if not user.has_usable_password() or user.check_password(current_pw):
response = super().form_valid(form)
logout(self.request)
messages.success(self.request,
_('Account deleted successfully.'
' We hope you comeback soon.'))
return response
else:
messages.error(request,
messages.error(self.request,
_('In order to delete the account you must provide'
' the current password.'))
return self.get(request, *args, **kwargs)
return self.get(self.request)
Binary file modified locale/pt_PT/LC_MESSAGES/django.mo
Binary file not shown.
531 changes: 281 additions & 250 deletions locale/pt_PT/LC_MESSAGES/django.po

Large diffs are not rendered by default.

33 changes: 0 additions & 33 deletions package.json

This file was deleted.

5 changes: 2 additions & 3 deletions pages/templates/pages/help.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% extends "layout/base.html" %}
{% load i18n %}
{% load staticfiles %}
{% load static %}

{% block content %}
<div class="section-definer-dark">
@@ -33,8 +33,7 @@
{% if support_email %}
<li>{% trans "Ask the instance administrator through email" %}: {{support_email}}</li>
{% endif %}
<li>{% trans "Chat with the development team on IRC. The channel is #hawkpost on freenode" %}</li>
<li>{% blocktrans %}<a href="https://github.com/whitesmith/hawkpost/issues" class="testelinks">Open an issue on the project board with your question</a> {% endblocktrans %}</li>
<li>{% blocktrans %}<a href="https://github.com/whitesmith/hawkpost/discussions" class="testelinks">Open a new discussion on the project code repository with your question</a> {% endblocktrans %}</li>
</ul>
</div>
<div class="col-xs-12 col-sm-11">
2 changes: 1 addition & 1 deletion pages/templates/pages/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% extends "layout/base.html" %}
{% load i18n %}
{% load staticfiles %}
{% load static %}
{% block content %}

<div class="section-definer-dark">
31 changes: 30 additions & 1 deletion pages/tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
from django.test import TestCase
from django.urls import reverse
from django.conf import settings

# Create your tests here.

class TestPages(TestCase):
"""Some very redimentary tests to make sure pages are being delivered."""

def test_help_page_rendered(self):
response = self.client.get(reverse("pages_help"))
self.assertEqual(response.status_code, 200)
self.assertIn("pages/help.html", [t.name for t in response.templates])
self.assertEqual(
response.context["support_email"], settings.SUPPORT_EMAIL)

def test_about_page_rendered(self):
response = self.client.get(reverse("pages_about"))
self.assertEqual(response.status_code, 200)
self.assertIn("pages/about.html", [t.name for t in response.templates])
self.assertEqual(
response.context["admin_name"], settings.SUPPORT_NAME)
self.assertEqual(
response.context["admin_email"], settings.SUPPORT_EMAIL)
self.assertEqual(
response.context["description"], settings.INSTANCE_DESCRIPTION)
self.assertEqual(
response.context["version"], settings.VERSION)

def test_home_page_rendered(self):
response = self.client.get(reverse("pages_index"))
self.assertEqual(response.status_code, 200)
self.assertIn("pages/index.html", [t.name for t in response.templates])
8 changes: 4 additions & 4 deletions pages/urls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from django.conf.urls import url
from django.urls import path
from .views import HomeView, AboutView, HelpView

urlpatterns = [
url(r'^$', HomeView.as_view(), name="pages_index"),
url(r'^about$', AboutView.as_view(), name="pages_about"),
url(r'^help$', HelpView.as_view(), name="pages_help")
path('', HomeView.as_view(), name="pages_index"),
path('about', AboutView.as_view(), name="pages_about"),
path('help', HelpView.as_view(), name="pages_help")
]
Binary file removed stylesheets/Ayuthaya.ttf
Binary file not shown.
Binary file removed stylesheets/Exo-SemiBold.ttf
Binary file not shown.
380 changes: 0 additions & 380 deletions stylesheets/_base.scss

This file was deleted.

209 changes: 0 additions & 209 deletions stylesheets/_boxes.scss

This file was deleted.

99 changes: 0 additions & 99 deletions stylesheets/_buttons.scss

This file was deleted.

301 changes: 0 additions & 301 deletions stylesheets/_details.scss

This file was deleted.

72 changes: 0 additions & 72 deletions stylesheets/_faq.scss

This file was deleted.

Loading