Merge branch 'main' into mailman3-take-1

This commit is contained in:
Frank Wiles
2023-05-20 09:27:20 -05:00
committed by GitHub
81 changed files with 5470 additions and 1997 deletions

View File

@@ -1,4 +1,4 @@
COMPOSE_FILE := docker-compose-with-celery.yml
COMPOSE_FILE := docker-compose.yml
.PHONY: help
help:
@@ -12,7 +12,7 @@ help:
.PHONY: bootstrap
bootstrap: ## installs/updates all dependencies
@docker-compose --file $(COMPOSE_FILE) build --force-rm
@docker compose --file $(COMPOSE_FILE) build --force-rm
.PHONY: cibuild
cibuild: ## invoked by continuous integration servers to run tests
@@ -22,81 +22,79 @@ cibuild: ## invoked by continuous integration servers to run tests
.PHONY: console
console: ## opens a console
@docker-compose run --rm web bash
@docker compose run --rm web bash
.PHONY: server
server: ## starts app
@docker-compose --file docker-compose.yml run --rm web python manage.py migrate --noinput
@docker-compose up
@docker compose --file docker-compose.yml run --rm web python manage.py migrate --noinput
@docker compose up
.PHONY: setup
setup: ## sets up a project to be used for the first time
@docker-compose --file $(COMPOSE_FILE) build --force-rm
@docker-compose --file docker-compose.yml run --rm web python manage.py migrate --noinput
@docker compose --file $(COMPOSE_FILE) build --force-rm
@docker compose --file docker-compose.yml run --rm web python manage.py migrate --noinput
.PHONY: test_interrogate
test_interrogate:
@docker-compose run --rm web interrogate -vv --fail-under 100 --whitelist-regex "test_.*" .
@docker compose run --rm web interrogate -vv --fail-under 100 --whitelist-regex "test_.*" .
.PHONY: test_pytest
test_pytest:
@docker-compose run --rm web pytest -s
@docker compose run --rm web pytest -s
.PHONY: test
test: test_interrogate test_pytest
@docker-compose down
@docker compose down
.PHONY: update
update: ## updates a project to run at its current version
@docker-compose --file $(COMPOSE_FILE) rm --force celery
@docker-compose --file $(COMPOSE_FILE) rm --force celery-beat
@docker-compose --file $(COMPOSE_FILE) rm --force web
@docker-compose --file $(COMPOSE_FILE) pull
@docker-compose --file $(COMPOSE_FILE) build --force-rm
@docker-compose --file docker-compose.yml run --rm web python manage.py migrate --noinput
@docker compose --file $(COMPOSE_FILE) rm --force celery
@docker compose --file $(COMPOSE_FILE) rm --force celery-beat
@docker compose --file $(COMPOSE_FILE) rm --force web
@docker compose --file $(COMPOSE_FILE) pull
@docker compose --file $(COMPOSE_FILE) build --force-rm
@docker compose --file docker-compose.yml run --rm web python manage.py migrate --noinput
# ----
.PHONY: pip-compile
pip-compile: ## rebuilds our pip requirements
@docker-compose run --rm web pip-compile ./requirements.in --output-file ./requirements.txt
@docker compose run --rm web pip-compile ./requirements.in --output-file ./requirements.txt
.PHONY: build
build:
docker-compose pull
DOCKER_BUILDKIT=1 docker-compose build
docker compose pull
DOCKER_BUILDKIT=1 docker compose build
.PHONY: createsuperuser
createsuperuser:
docker-compose run --rm web /code/manage.py createsuperuser
docker compose run --rm web /code/manage.py createsuperuser
.PHONY: down
down:
docker-compose down
docker compose down
.PHONY: makemigrations
makemigrations:
@echo "Running makemigrations..."
docker-compose run --rm web /code/manage.py makemigrations
docker compose run --rm web /code/manage.py makemigrations
.PHONY: migrate
migrate:
@echo "Running migrations..."
docker-compose run --rm web /code/manage.py migrate --noinput
docker compose run --rm web /code/manage.py migrate --noinput
.PHONY: rebuild
rebuild:
@echo "Rebuilding local docker images..."
docker-compose kill
docker-compose rm -f web
DOCKER_BUILDKIT=1 docker-compose build --force-rm web
docker compose kill
docker compose rm -f web
DOCKER_BUILDKIT=1 docker compose build --force-rm web
.PHONY: shell
shell:
docker-compose run --rm web bash
docker compose run --rm web bash
.PHONY: up
up:
docker-compose up -d
docker compose up -d

View File

@@ -2,11 +2,15 @@
## Overview
A Django based website that will power https://boost.org
A Django based website that will power a new Boost website
---
## Local Development Setup
This project will use Python 3.11, Docker, and Docker Compose.
This project will use Python 3.11, Docker, and Docker Compose.
Instructions to install those packages are included in [development_setup_notes.md](docs/development_setup_notes.md).
**NOTE**: All of these various `docker compose` commands, along with other helpful
developer utility commands, are codified in our `justfile` and can be ran with
@@ -14,7 +18,7 @@ less typing.
You will need to install `just`, by [following the documentation](https://just.systems/man/en/)
Copy file `env.template` to `.env` and adjust values to match your local environment:
**Environment Variables**: Copy file `env.template` to `.env` and adjust values to match your local environment. See [Environment Variables](docs/env_vars.md) for more information.
```shell
$ cp env.template .env
@@ -42,6 +46,8 @@ $ docker compose run --rm web python manage.py migrate
This will create the Docker image, install dependencies, start the services defined in `docker-compose.yml`, and start the webserver.
Access the site at http://localhost:8000
### Cleaning up
To shut down our database and any long running services, we shut everyone down using:
@@ -50,10 +56,6 @@ To shut down our database and any long running services, we shut everyone down u
$ docker compose down
```
## Environment Variables
See [Environment Variables](docs/env_vars.md) for more information on environment variables.
## Running the tests
To run the tests, execute:
@@ -88,21 +90,31 @@ For production, execute:
$ yarn build
```
## Generating Fake Data
---
### Versions and LibraryVersions
## Generating Local Data
First, make sure your `GITHUB_TOKEN` is set in you `.env` file and run `./manage.py update_libraries`. This takes a long time. See below.
### Sample (Fake) Data
Run `./manage.py generate_fake_versions`. This will create 50 active Versions, and associate Libraries to them.
To **remove all existing data** (except for superusers) from your local project's database (which is in its own Docker volume) and generate fresh sample data **that will not sync with GitHub**, run:
The data created is realistic-looking in that each Library will contain a M2M relationship to every Version newer than the oldest one it's included in. (So if a Library's earliest LibraryVersion is 1.56.0, then there will be a LibraryVersion object for that Library for each Version since 1.56.0 was released.)
```bash
./manage.py create_sample_data --all
```
This does not add VersionFile objects to the Versions.
For more information on the many, many options available for this command, see `create_sample_data` in [Management Commands](docs/commands.md).
### Libraries, Pull Requests, and Issues
### Live GitHub Libraries
There is not currently a way to generate fake Libraries, Issues, or Pull Requests. To generate those, use your GitHub token and run `./manage.py update_libraries` locally to pull in live GitHub data. This command takes a long time to run; you might consider editing `libraries/github.py` to add counters and breaks to shorten the runtime.
To **add real Boost libraries and sync all the data from GitHub**, run:
```bash
./manage.py update_libraries
```
This command can take a long time to run (about a half hour). For more information, see `update_libraries` in [Management Commands](docs/commands.md).
---
## Deploying
@@ -112,14 +124,35 @@ TDB
TDB
---
## Pre-commit
## Pre-commit Hooks
Pre-commit is configured for the following
We use [pre-commit hooks](https://pre-commit.com/) to check code for style, syntax, and other issues. They help to maintain consistent code quality and style across the project, and prevent issues from being introduced into the codebase.
* Black
* Ruff
* Djhtml for cleaning up django templates
* Rustywind for sorting tailwind classes
| Pre-commit Hook | Description |
| --------------- | ----------- |
| [Black](https://github.com/psf/black) | Formats Python code using the `black` code formatter |
| [Ruff](https://github.com/charliermarsh/ruff) | Wrapper around `flake8` and `isort`, among other linters |
| [Djhtml](https://github.com/rtts/djhtml) | Auto-formats Django templates |
| [Rustywind](https://github.com/avencera/rustywind) | Sorts and formats Tailwind CSS classes |
Add the hooks by executing `pre-commit install`
### Setup and Usage
| Description | Command |
| ---- | ------- |
| 1. Install the `pre-commit` package using `pip` | `pip install pre-commit` |
| 2. Install our list of pre-commit hooks locally | `pre-commit install` |
| 3. Run all hooks for changed files before commit | `pre-commit run` |
| 4. Run specific hook before commit | `pre-commit run {hook}` |
| 5. Run hooks for all files, even unchanged ones | `pre-commit run --all-files` |
| 6. Commit without running pre-commit hooks | `git commit -m "Your commit message" --no-verify` |
Example commands for running specific hooks:
| Hook | Example |
| --------------- | --------------- |
| Black | `pre-commit run black` |
| Ruff | `pre-commit run ruff` |
| Djhtml | `pre-commit run djhtml` |
| Rustywind | `pre-commit run rustywind` |

View File

@@ -14,10 +14,10 @@ app = Celery("config")
# should have a `CELERY_` prefix.
app.config_from_object("django.conf:settings", namespace="CELERY")
# Load task modules from all registered Django app configs.
# Load task modules from all registered Django apps.
app.autodiscover_tasks()
@app.task(bind=True)
def debug_task(self):
print("Request: {0!r}".format(self.request))
print(f"Request: {self.request!r}")

View File

@@ -88,7 +88,14 @@ INSTALLED_APPS += [
]
# Our Apps
INSTALLED_APPS += ["ak", "users", "versions", "libraries", "mailing_list"]
INSTALLED_APPS += [
"ak",
"users",
"versions",
"libraries",
"mailing_list",
"news",
]
AUTH_USER_MODEL = "users.User"
CSRF_COOKIE_HTTPONLY = True
@@ -271,8 +278,8 @@ CACHES = {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": f"redis://{REDIS_HOST}:6379/2",
"TIMEOUT": env(
"STATIC_CACHE_TIMEOUT", default=86400
), # Cache timeout in seconds: 1 day
"STATIC_CACHE_TIMEOUT", default="60"
), # Cache timeout in seconds: 1 minute
},
}

View File

@@ -28,11 +28,16 @@ from libraries.views import (
LibraryByCategory,
LibraryDetail,
LibraryListByVersion,
LibraryDetailByVersion,
LibraryListByVersionByCategory,
)
from libraries.api import LibrarySearchView
from mailing_list.views import MailingListView, MailingListDetailView
from news.views import (
EntryApproveView,
EntryCreateView,
EntryDetailView,
EntryListView,
)
from support.views import SupportView, ContactView
from versions.api import VersionViewSet
from versions.views import VersionList, VersionDetail
@@ -47,12 +52,6 @@ router.register(r"libraries", LibrarySearchView, basename="libraries")
urlpatterns = (
[
path("", HomepageView.as_view(), name="home"),
# scratch template for design scrums
path(
"scratch/",
TemplateView.as_view(template_name="scratch.html"),
name="scratch",
),
path("admin/", admin.site.urls),
path("accounts/", include("allauth.urls")),
path(
@@ -81,6 +80,18 @@ urlpatterns = (
),
path("health/", include("health_check.urls")),
path("forum/", include(machina_urls)),
# temp page for community until mailman is done.
path(
"community/",
TemplateView.as_view(template_name="community_temp.html"),
name="community",
),
# temp page for releases
path(
"releases/",
TemplateView.as_view(template_name="releases_temp.html"),
name="releases",
),
path(
"donate/",
TemplateView.as_view(template_name="donate/donate.html"),
@@ -103,6 +114,12 @@ urlpatterns = (
name="mailing-list-detail",
),
path("mailing-list/", MailingListView.as_view(), name="mailing-list"),
path("news/", EntryListView.as_view(), name="news"),
path("news/add/", EntryCreateView.as_view(), name="news-create"),
path("news/<slug:slug>/", EntryDetailView.as_view(), name="news-detail"),
path(
"news/<slug:slug>/approve/", EntryApproveView.as_view(), name="news-approve"
),
path(
"people/detail/",
TemplateView.as_view(template_name="boost/people_detail.html"),
@@ -153,16 +170,6 @@ urlpatterns = (
TemplateView.as_view(template_name="review/review_process.html"),
name="review-process",
),
path(
"news/detail/",
TemplateView.as_view(template_name="news/news_detail.html"),
name="news_detail",
),
path(
"news/",
TemplateView.as_view(template_name="news/news_list.html"),
name="news",
),
# support and contact views
path("support/", SupportView.as_view(), name="support"),
path(
@@ -184,7 +191,7 @@ urlpatterns = (
),
path(
"versions/<slug:version_slug>/<slug:slug>/",
LibraryDetailByVersion.as_view(),
LibraryDetail.as_view(),
name="library-detail-by-version",
),
path("versions/<slug:slug>/", VersionDetail.as_view(), name="version-detail"),

View File

@@ -8,6 +8,7 @@ from django.core.files import File as DjangoFile
# directories
pytest_plugins = [
"libraries.tests.fixtures",
"news.tests.fixtures",
"users.tests.fixtures",
"versions.tests.fixtures",
]

View File

@@ -50,6 +50,11 @@ def get_content_from_s3(key=None, bucket_name=None):
response = client.get_object(Bucket=bucket_name, Key=s3_key.lstrip("/"))
file_content = response["Body"].read()
content_type = response["ContentType"]
# Check if the file ends with '.js', if yes then set the content_type to 'application/javascript'
if s3_key.endswith(".js"):
content_type = "application/javascript"
logger.info(
"get_content_from_s3_success",
key=key,

View File

@@ -121,7 +121,11 @@ class StaticContentTemplateView(View):
content, content_type = result
# Store the result in cache
static_content_cache.set(cache_key, (content, content_type))
static_content_cache.set(
cache_key,
(content, content_type),
int(settings.CACHES["static_content"]["TIMEOUT"]),
)
response = HttpResponse(content, content_type=content_type)
logger.info(

View File

@@ -48,11 +48,7 @@ services:
mailman-core:
image: maxking/mailman-core
stop_grace_period: 30s
env_file:
- .env
depends_on:
- db
stop_grace_period: 5s
ports:
- "8001:8001" # API
- "8024:8024" # LMTP - incoming emails
@@ -60,6 +56,29 @@ services:
- ./mailman/core:/opt/mailman/
networks:
- backend
env_file:
- .env
depends_on:
- db
celery-worker:
build:
context: .
dockerfile: docker/Dockerfile
command:
[
"celery",
"-A",
"config",
"worker",
"--concurrency=10",
"--loglevel=debug"
]
env_file:
- .env
depends_on:
- db
- redis
mailman-web:
image: maxking/mailman-web
@@ -68,10 +87,14 @@ services:
- "DOCKER_DIR=/opt/mailman-docker"
- "PYTHON=python3"
- "WEB_PORT=8008"
env_file:
- .env
depends_on:
- redis
- db
networks:
- backend
volumes:
- .:/code
stop_signal: SIGKILL
ports:
- "8008:8008" # HTTP
- "8080:8080" # uwsgi
@@ -80,6 +103,29 @@ services:
- ./docker:/opt/mailman-docker
networks:
- backend
celery-beat:
build:
context: .
dockerfile: docker/Dockerfile
command:
[
"celery",
"-A",
"config",
"beat",
"--loglevel=debug"
]
env_file:
- .env
depends_on:
- db
- redis
networks:
- backend
volumes:
- .:/code
stop_signal: SIGKILL
networks:

View File

@@ -1,26 +1,26 @@
# Management Commands
# Management Commands
## `create_sample_data`
Running this command will populate the database with fake data for local development.
When run, it will create fake objects for these models:
When run, it will create fake objects for these models:
- User
- User
- Version
- Category
- Category
- Library
- LibraryVersion
- Authors for Libraries and Maintainers for LibraryVersions
- Issues and Pull Requests for Libraries
- Authors for Libraries and Maintainers for LibraryVersions
- Issues and Pull Requests for Libraries
The data generated is fake. Any links, information that looks like it comes from GitHub, email addresses, etc. is all fake. Some of it is made to look like realistic data.
The data generated is fake. Any links, information that looks like it comes from GitHub, email addresses, etc. is all fake. Some of it is made to look like realistic data.
The following options can be used with the command:
- `--all`: If True, run all methods including the drop command.
If you don't want to drop all records for the above models and create a new set of fresh data, you can pass these options to clear your database or and create new records.
If you don't want to drop all records for the above models and create a new set of fresh data, you can pass these options to clear your database or and create new records.
- `--drop`: If True, drop all records in the database.
- `--users`: If True, create fake users.
@@ -37,7 +37,7 @@ If you don't want to drop all records for the above models and create a new set
./manage.py create_sample_data --all
Output:
Output:
Dropping all records...
Dropping Non-Superusers...
@@ -68,11 +68,11 @@ Output:
...10 issues created for algorithm
### Example: Create new pull requests and issues for existing library objects
### Example: Create new pull requests and issues for existing library objects
./manage.py create_sample_data --prs --issues
./manage.py create_sample_data --prs --issues
Output:
Output:
Adding library pull requests...
...9 pull requests created for algorithm
@@ -82,30 +82,101 @@ Output:
...10 issues created for asio
## `generate_fake_versions`
## `generate_fake_versions`
Creates fake Version objects **only**, then creates LibraryVersion objects for each existing Library and the new Versions.
Creates fake Version objects **only**, then creates LibraryVersion objects for each existing Library and the new Versions.
### Example:
### Example:
./manage.py generate_fake_versions
Output:
Output:
Version 1.30.0 created succcessfully
---algorithm (1.30.0) created succcessfully
## `import_library_versions`
Connect Library objects to the Boost versions (AKA "release") that included them using information from the main Boost GitHub repo and the library repos. Functions of this command:
- Prints out any versions or libraries that were skipped at the end.
- Idempotent.
**Options**
Here are the options you can use:
- `--release`: Full or partial Boost version (release) number. If `release` is passed, the command will import all libraries for the versions that contain the passed-in release number. If not passed, the command will import libraries for all active versions.
- `--token`: Pass a GitHub API token. If not passed, will use the value in `settings.GITHUB_TOKEN`.
### Example:
./manage.py import_library_versions
Output:
Saved library version Log (boost-1.16.1).
Processing version boost-0.9.27...
Processing module python...
Saved library version Python (boost-0.9.27). Created? False
User stefan@seefeld.name added as a maintainer of Python (boost-0.9.27)
{"message": "User username added as a maintainer of Python (boost-0.9.27)", "logger": "libraries.github", "level": "info", "timestamp": "2023-05-17T21:24:39.046029Z"}
Updated maintainers for Python (boost-0.9.27).
Saved library version Python (boost-0.9.27).
Skipped disjoint_sets in boost-1.57.0: Could not find library in database by gitmodule name
Skipped signals in boost-1.57.0: Could not find library in database by gitmodule name
## `import_versions`
Import Boost version (AKA "release") information from the Boost GitHub repo. Functions of this command:
- **Retrieves Boost tags**: It collects all the Boost tags from the main Github repo, excluding beta releases and release candidates. For each tag, it gathers the associated data. If it's a full release, the data is in the tag; otherwise, the data is in the commit.
- **Updates local database**: For each tag, it creates or updates a Version instance in the local database.
- **Options for managing versions and library versions**: The command provides options to delete existing versions and library versions, and to create new library versions for the most recent Boost version.
- Idempotent.
**Options**
Here are the options you can use:
- `--delete-versions`: Deletes all existing Version instances in the database before importing new ones.
- `--delete-library-versions`: Deletes all existing LibraryVersion instances in the database before importing new ones.
- `--create-recent-library-versions`: Creates a LibraryVersion for each active Boost library and the most recent Boost version.
- `--skip-existing-versions`: If a Version exists in the database (by name), skip calling the GitHub API for more information on it.
- `--token`: Pass a GitHub API token. If not passed, will use the value in `settings.GITHUB_TOKEN`.
### Example:
./manage.py import_versions
Output:
Saved version boost-1.82.0. Created: True
Skipping boost-1.82.0.beta1, not a full release
Saved version boost-1.81.0. Created: True
Skipping boost-1.81.0.beta1, not a full release
tag_not_found
{"message": "tag_not_found", "tag_name": "boost-1.80.0", "repo_slug": "boost", "logger": "libraries.github", "level": "info", "timestamp": "2023-05-12T22:14:08.721270Z"}
...
Saved library version Math (boost-1.82.0). Created: True
Saved library version Xpressive (boost-1.82.0). Created: True
Saved library version Dynamic Bitset (boost-1.82.0). Created: True
Saved library version Multi-Index (boost-1.82.0). Created: True
...
## `update_libraries`
Runs the library update script, which cycles through the repos listed in the Boost library and syncs their information.
Runs the library update script, which cycles through the repos listed in the Boost library and syncs their information.
Synced information:
Synced information:
- Most library information comes from `meta/libraries.json` stored in each Boost library repo
- Library data and metadata from GitHub is saved to our database
- Categories are updated, if needed
- Library categories are updated, if need be.
- Issues and Pull Requests are synced
- Most library information comes from `meta/libraries.json` stored in each Boost library repo
- Library data and metadata from GitHub is saved to our database
- Categories are updated, if needed
- Library categories are updated, if need be.
- Issues and Pull Requests are synced
**NOTE**: Can take upwards of a half hour to run. If you are trying to populate tables for local development, `create_sample_data` is a better option if the GitHub integrations aren't important.
**NOTE**: Can take upwards of a half hour to run. If you are trying to populate tables for local development, `create_sample_data` is a better option if the GitHub integrations aren't important.

View File

@@ -0,0 +1,101 @@
## Development Setup Notes
The procedure to configure a development environment is mainly covered in the top-level README.md. This document will contain more details about installing prerequisites: Just, Python 3.11, Docker, and Docker Compose.
- [Windows](#Windows)
- [Ubuntu 22.04](#ubuntu-2204)
## Windows
(Tested on: Windows 2022 Server)
In Powershell, install WSL:
```
wsl --install
Restart-Computer
```
After rebooting, open Powershell (wait a minute, it may continue installing WSL). If the installation hasn't completed for some reason, `wsl --install` again. After WSL and an Ubuntu distribution have been installed, log in to WSL:
```
wsl
```
When running the Django website, everything should be done from a WSL session, not Powershell, DOS, or git-bash. Do not `git clone` the files in a DOS window, for example. However, once it's up and running, files may be edited elsewhere. The file path in explorer will be `\\wsl.localhost\Ubuntu\opt\github\cppalliance\temp-site`
Continue to the [Ubuntu 22.04 instructions](#ubuntu-2204) below. Return here before executing `docker compose`.
The docker daemon must be launched manually. Open a WSL window and keep it running. Otherwise there will be an error message "Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?"
```
sudo dockerd
```
Open another terminal:
```
wsl
```
Continue (as root) to the instructions in the top-level README.md file.
## Ubuntu 22.04
Check if python 3.11 is installed.
```
python3 --version
```
or install python 3.11:
```
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt-get update
sudo apt-get install -y python3.11
```
Install `makedeb` (as a standard user, not root).
```
bash -ci "$(wget -qO - 'https://shlink.makedeb.org/install')"
```
Install `just` (as a standard user, not root).
```
sudo mkdir -p /opt/justinstall
CURRENTUSER=$(whoami)
sudo chown $CURRENTUSER /opt/justinstall
chmod 777 /opt/justinstall
cd /opt/justinstall
git clone 'https://mpr.makedeb.org/just'
cd just
makedeb -si
```
Install docker and docker-compose.
```
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
"$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```
As root, clone the repository and switch to that directory.
```
sudo su -
mkdir -p /opt/github/cppalliance
cd /opt/github/cppalliance
git clone https://github.com/cppalliance/temp-site
cd temp-site
cp env.template .env
```
Continue (as the root user) to the instructions in the top-level README.md file. Or if using WSL, review the last few steps in that section again.
The advantage of running `docker compose` as root is the userid (0) will match the containers and the shared files.

View File

@@ -0,0 +1,167 @@
# Syncing Data about Boost Versions and Libraries with GitHub
## About
The data in our database generally originates from somewhere in the Boost GitHub ecosystem.
This page will explain to Django developers how data is synced from GitHub to our database.
- Most code is in `libraries/github.py` and `libraries/tasks.py`
## Release data
- Releases are also called "Versions."
- The model that saves Release/Version data is `versions/models.py::Version`
- We retrieve all the non-beta and non-release-candidate tags from the main Boost repo
Boost releases some tags as formal GitHub "releases," and these show up on the **Releases** tab.
Not all tags are official GitHub **Releases**, however, and this impacts where we get metadata about the tag.
To retrieve releases and tags, run:
```bash
./manage.py import_releases
```
This will:
- Delete existing Versions and LibraryVersions
- Retrieve tags and releases from the Boost GitHub repo
- Create new Versions for each tag and release that is not a beta or rc release
- Create a new LibraryVersion for each Library **but not for historical versions**
## Library data
- Once a month, the task `libraries/tasks/update_libraries()` runs.
- It cycles through all Boost libraries and updates data
- **It only handles the most recent version of Boost** and does not handle older versions yet.
- There are methods to download issues and PRs, but **the methods to download issues and PRs are not currently called**.
### Tasks or Questions
- [ ] A new GitHub API needs to be generated through the CPPAlliance GitHub organization, and be added as a kube secret
- [ ] `self.skip_modules`: This exists in both `GitHubAPIClient` and `LibraryUpdater` but it should probably only exist in `LibraryUpdater`, to keep `GitHubAPIClient` less tightly coupled to specific repos
- [ ] If we only want aggregate data for issues and PRs, do we need to save the data from them in models, or can we just save the aggregate data somewhere?
### Glossary
To make the code more readable to the Boost team, who will ultimately maintain the project, we tried to replicate their terminology as much as possible.
- Library: Boost “Libraries” correspond to GitHub repositories
- `.gitmodules`: The file in the main Boost project repo that contains the information on all the repos that are considered Boost libraries
- module and submodule: Other words for library that correspond more specifically to GitHub data
---
### How it Works
#### `LibraryUpdater`
_This is not a code walkthrough, but is a general overview of the objects and data that this class retrieves._
- The Celery task `libraries/tasks.py/update_libraries` runs `LibraryUpdater.update_libraries()`
- This class uses the `GitHubAPIClient` class to call the GitHub API
- It retrieves the list of libraries to update from the `.gitmodules` file in the [main Boost repo](https://github.com/boostorg/boost): [https://github.com/boostorg/boost/blob/master/.gitmodules](https://github.com/boostorg/boost/blob/master/.gitmodules)
- From that list, it makes sure to exclude any libraries in `self.skip_modules`. The modules in `self.skipped_submodules` are not imported into the database.
- For each remaining library:
- It uses the information from the `.gitmodules` file to call the GitHub API for that specific library
- It downloads the `meta/libraries.json` file for that library and parses that data
- It uses the parsed data to add or update the Library record in our database for that GitHub repo
- It adds the library to the most recent Version object to create a LibraryVersion record, if needed
- The library categories are updated
- The maintainers are updated and stub Users are added for them if needed.
- The authors are updated and stub Users are added for them if needed (updated second because maintainers are more likely to have email addresses, so matching is easier).
#### `GithubAPIClient`
- This class controls the requests to and responses from the GitHub API. Mostly a wrapper around `GhApi` that allows us to set some default values to make calling the methods easier, and allows us to retrieve some data that is very specific to the Boost repos
- Requires the environment variable `GITHUB_TOKEN` to be set
- Contains methods to retrieve the `.gitmodules` file, retrieve the `.libraries.json` file, general repo data, repo issues, repo PRs, and the git tree.
#### `GithubDataParser`
- Contains methods to parse the data we retrieve from GitHub into more useful formats
- Contains methods to parse the `.gitmodules` file and the `libraries.json` file, and to extract the author and maintainer names and email addresses, if present.
**Attributes**
| owner | GitHub repo owner | boostorg |
| --- | --- | --- |
| ref | GitHub branch or tag to use on that repo | heads/master |
| repo_slug | GitHub repo slug | default |
- `self.skip_modules`: This is the list of modules/libraries from `.gitmodules` that we do not download
---
### GitHub Data
- Each Boost Library has a GitHub repo.
- Most of the time, one library has one repo. Other times, one GitHub repo is shared among multiple libraries (the “Algorithm” library is an example).
- The most important file for each Boost library is `meta/libraries.json`
#### `.gitmodules`
This is the most important file in the main Boost repository. It contains the GitHub information for all Libraries included in that tagged Boost version, and is what we use to identify which Libraries to download into our database.
- `submodule`: Corresponds to the `key` in `libraries.json`
- Contains information for the top-level Library, but not other sub-libraries stored in the same repo
- `path`: the path to navigate to the Library repo from the main Boost repo
- `url`: the URL for the `.git` repo for the library, in relative terms (`../system.git`)
- `fetchRecurseSubmodules`: We dont use this field
- `branch`: We dont use this field
<img width="1381" alt="Screenshot 2023-05-08 at 12 32 32 PM" src="https://user-images.githubusercontent.com/2286304/236922229-af2a62e6-d91c-496d-b785-6c05e0a6c393.png">
#### `libraries.json`
This is the most important file in the GitHub repo for a library. It is where we retrieve all the metadata about the Library. It is the source of truth.
- `key`: The GitHub slug, and the slug we use for our Library object
- When the repo hosts a single Library, the `key` corresponds to the `submodule` in the main Boost repos `libraries.json` file. Example: `"key": "asio"`
- When the repo hosts multiple libraries, the **first** `key` corresponds to the `submodule`. Example: `"key": "algorithm"`. Then, the **following** keys in `libraries.json` will be prefixed with the original `key` before adding their own slug. Example: `"key": "algorithm/minimax"`
- `name`: What we save as the Library name
- `authors`: A list of names of original authors of the Librarys documentation.
- Data is very unlikely to change
- Data generally does not contain emails
- Stub users are creates for authors with fake email addresses and users will be able to claim those accounts.
- `description`: What we save as the `Library` description
- `category`: A list of category names. We use this to attach Categories to the Libraries.
- `maintainers`: A list of names and emails of current maintainers of this Library
- Data may change between versions
- Data generally contains emails
- Stub users are created for all maintainers. We use fake email addresses if an email address is not present
- We try to be smart — if the same name shows up as an author and a maintainer, we wont create two fake records. But its imperfect.
- `cxxstd`: C++ version in which this Library was added
Example with a single library:
<img width="1392" alt="Screenshot 2023-05-08 at 12 25 59 PM" src="https://user-images.githubusercontent.com/2286304/236922369-398aa9bf-060e-4e6e-9a37-20a68fb1d1d6.png">
Example with multiple libraries:
<img width="1369" alt="Screenshot 2023-05-08 at 12 25 30 PM" src="https://user-images.githubusercontent.com/2286304/236922503-4e633575-9f6b-47af-b6e1-05be8be2c4e4.png">
---
## General Maintenance Notes
### How to change the skipped libraries
- To **add a new skipped submodule**: add the name of the submodule to the list `self.skipped_modules` and make a PR. This will not remove the library from the database, but it will stop refreshing data for that library.
- To **remove a submodule that is currently being skipped**: remove the name of the submodule from `self.skipped_modules` and make a PR. The library will be added to the database the next time the update runs.
### How to delete Libraries
- Via the Admin. The Library update process does not delete any records.
### How to add new Categories
- They will be **automatically added** as part of the download process as soon as they are added to a library's `libraries.json` file.
### How to remove authors or maintainers
- Via the Admin.
- But if they are not also removed from the `libraries.json` file for the affected library, then they will be added back the next time the job runs.

View File

@@ -18,7 +18,8 @@ GITHUB_TOKEN="top-secret"
AWS_ACCESS_KEY_ID="changeme"
AWS_SECRET_ACCESS_KEY="changeme"
BUCKET_NAME="stage.boost.org"
BUCKET_NAME="boost.revsys.dev"
# Mailman database settings
DATABASE_URL="postgresql://postgres@db:5432/postgres"
@@ -28,3 +29,7 @@ HYPERKITTY_API_KEY="changeme!"
MAILMAN_ADMIN_USER=""
MAILMAN_ADMIN_EMAIL=""
SERVE_FROM_DOMAIN=localhost
CELERY_BROKER=redis://redis:6379/0
CELERY_BACKEND=redis://redis:6379/0

View File

@@ -1,7 +1,6 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url("https://fonts.googleapis.com/css2?family=Cairo:wght@200;300;400;500;600;700;800;900&display=swap");
@layer base {
[x-cloak] {
@@ -59,4 +58,24 @@
#authpages #footerSignup {
@apply hidden;
}
/* Dark mode scrollbar */
.dark ::-webkit-scrollbar {
@apply bg-gray-600;
}
.dark ::-webkit-scrollbar-track {
@apply bg-gray-700;
}
.dark ::-webkit-scrollbar-thumb {
@apply bg-gray-600 w-5 h-5;
}
.dark ::-webkit-scrollbar-button:start:decrement {
@apply bg-gray-700;
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="%23808080" stroke="%23808080" stroke-width="2" d="M6 15l6-6 6 6"/></svg>');
@apply object-center;
}
.dark ::-webkit-scrollbar-button:end:increment {
@apply bg-gray-700;
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="%23808080" stroke="%23808080" stroke-width="2" d="M7 9l5.25 5.25L18.5 9"/></svg>');
@apply object-center
}
}

View File

@@ -1,5 +1,5 @@
set dotenv-load := false
COMPOSE_FILE := "docker-compose-with-celery.yml"
COMPOSE_FILE := "docker-compose.yml"
ENV_FILE := ".env"
@_default:
@@ -19,13 +19,13 @@ bootstrap: ## installs/updates all dependencies
cp env.template {{ENV_FILE}}
fi
# docker-compose --file $(COMPOSE_FILE) build --force-rm
# docker compose --file $(COMPOSE_FILE) build --force-rm
rebuild:
docker-compose rm -f celery || true
docker-compose rm -f celery-beat || true
docker-compose rm -f web
docker-compose build --force-rm web
docker compose rm -f celery || true
docker compose rm -f celery-beat || true
docker compose rm -f web
docker compose build --force-rm web
@cibuild: ## invoked by continuous integration servers to run tests
python -m pytest
@@ -34,36 +34,44 @@ rebuild:
alias shell := console
@console: ## opens a console
docker-compose run --rm web bash
docker compose run --rm web bash
@server: ## starts app
docker-compose --file docker-compose.yml run --rm web python manage.py migrate --noinput
docker-compose up
docker compose --file docker-compose.yml run --rm web python manage.py migrate --noinput
docker compose up
@setup: ## sets up a project to be used for the first time
docker-compose --file $(COMPOSE_FILE) build --force-rm
docker-compose --file docker-compose.yml run --rm web python manage.py migrate --noinput
docker compose --file $(COMPOSE_FILE) build --force-rm
docker compose --file docker-compose.yml run --rm web python manage.py migrate --noinput
@test_pytest:
-docker-compose run --rm web pytest -s
-docker compose run --rm web pytest -s
@test:
just test_pytest
docker-compose down
docker compose down
@coverage:
docker-compose run --rm web pytest --cov=. --cov-report=html
docker compose run --rm web pytest --cov=. --cov-report=html
open htmlcov/index.html
@update: ## updates a project to run at its current version
docker-compose --file $(COMPOSE_FILE) rm --force celery
docker-compose --file $(COMPOSE_FILE) rm --force celery-beat
docker-compose --file $(COMPOSE_FILE) rm --force web
docker-compose --file $(COMPOSE_FILE) pull
docker-compose --file $(COMPOSE_FILE) build --force-rm
docker-compose --file docker-compose.yml run --rm web python manage.py migrate --noinput
docker compose --file $(COMPOSE_FILE) rm --force celery
docker compose --file $(COMPOSE_FILE) rm --force celery-beat
docker compose --file $(COMPOSE_FILE) rm --force web
docker compose --file $(COMPOSE_FILE) pull
docker compose --file $(COMPOSE_FILE) build --force-rm
docker compose --file docker-compose.yml run --rm web python manage.py migrate --noinput
@down: ## stops a project
docker compose down
# ----
@pip-compile: ## rebuilds our pip requirements
docker-compose run --rm web pip-compile ./requirements.in --output-file ./requirements.txt
# Compile new python dependencies
@pip-compile ARGS='': ## rebuilds our pip requirements
docker compose run --rm web pip-compile {{ ARGS }} ./requirements.in --output-file ./requirements.txt
# Upgrade existing Python dependencies to their latest versions
@pip-compile-upgrade:
just pip-compile --upgrade

View File

@@ -1,4 +1,4 @@
apiVersion: v1
description: boost.org website
description: boost.revsys.dev website
name: boost
version: v1.0.3

View File

@@ -119,6 +119,9 @@ Env:
secretKeyRef:
name: static-content
key: bucket_name
# Static content cache timeout
- name: STATIC_CACHE_TIMEOUT
value: "60"
# Volumes
Volumes:

View File

@@ -1,8 +1,18 @@
from django.forms import ModelForm
from .models import Library, Category
from django.forms import Form, ModelChoiceField, ModelForm
from versions.models import Version
from .models import Library
class LibraryForm(ModelForm):
class Meta:
model = Library
fields = ["categories"]
class VersionSelectionForm(Form):
version = ModelChoiceField(
queryset=Version.objects.all(),
label="Select a version",
empty_label="Choose a version...",
)

View File

@@ -1,9 +1,10 @@
import base64
import os
import re
from datetime import datetime
import requests
import structlog
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
@@ -11,6 +12,7 @@ from fastcore.xtras import obj2dict
from ghapi.all import GhApi, paged
from versions.models import Version
from .models import Category, Issue, Library, LibraryVersion, PullRequest
from .utils import generate_fake_email, parse_date
@@ -27,6 +29,7 @@ class GithubAPIClient:
owner: str = "boostorg",
ref: str = "heads/master",
repo_slug: str = "boost",
token: str = None,
) -> None:
"""
Initialize the GitHubAPIClient.
@@ -35,7 +38,7 @@ class GithubAPIClient:
:param ref: str, the Git reference
:param repo_slug: str, the repository slug
"""
self.api = self.initialize_api()
self.api = self.initialize_api(token=token)
self.owner = owner
self.ref = ref
self.repo_slug = repo_slug
@@ -83,43 +86,105 @@ class GithubAPIClient:
owner=self.owner, repo=repo_slug, file_sha=file_sha
)
def get_gitmodules(self, repo_slug: str = None) -> str:
def get_commit_by_sha(self, repo_slug: str = None, commit_sha: str = None) -> dict:
"""Get a commit by its SHA."""
if not repo_slug:
repo_slug = self.repo_slug
return self.api.git.get_commit(
owner=self.owner, repo=repo_slug, commit_sha=commit_sha
)
def get_first_tag(self, repo_slug: str = None):
"""
Get the .gitmodules file for the repo from the GitHub API.
Retrieves the earliest tag in the repo.
:param repo_slug: str, the repository slug
:return: str, the .gitmodules file
:return: tuple with GitHub tag object, commit date.
- See https://docs.github.com/en/rest/git/tags for tag object format.
"""
if not repo_slug:
repo_slug = self.repo_slug
ref = self.get_ref()
try:
per_page = 100
page = 1
all_tags = []
while True:
tags = self.api.repos.list_tags(
owner=self.owner, repo=repo_slug, per_page=per_page, page=page
)
all_tags.extend(tags)
if len(tags) < per_page: # End of results
break
page += 1 # Go to the next page
# Sort the tags by the commit date. The first tag will be the earliest.
# The Github API doesn't return the commit date with the tag, so we have to retrieve each
# one individually. This is slow, but it's the only way to get the commit date.
def get_tag_commit_date(tag):
"""Get the commit date for a tag.
For commit format, see
https://docs.github.com/en/rest/commits/commits."""
commit_sha = tag["commit"]["sha"]
commit = self.get_commit_by_sha(repo_slug, commit_sha)
return commit["committer"]["date"]
annotated_tags = [(tag, get_tag_commit_date(tag)) for tag in all_tags]
sorted_tags = sorted(annotated_tags, key=lambda x: x[1])
# Return the first (earliest) tag
return sorted_tags[0]
except Exception as e:
self.logger.exception("get_first_tag_and_date_failed", repo=repo_slug)
return None
def get_gitmodules(self, repo_slug: str = None, ref: dict = None) -> str:
"""
Get the .gitmodules file for the repo from the GitHub API.
:param repo_slug: str, the repository slug
:param ref: dict, the Git reference object (the commit hash). See https://docs.github.com/en/rest/git/refs
for expected format.
:return: str, the .gitmodules file from the repo
"""
if not repo_slug:
repo_slug = self.repo_slug
if not ref:
ref = self.get_ref()
tree_sha = ref["object"]["sha"]
tree = self.get_tree(tree_sha=tree_sha)
gitmodules = None
for item in tree["tree"]:
if item["path"] == ".gitmodules":
file_sha = item["sha"]
blob = self.get_blob(repo_slug=repo_slug, file_sha=file_sha)
return base64.b64decode(blob["content"])
def get_libraries_json(self, repo_slug: str):
def get_libraries_json(self, repo_slug: str, tag: str = "master"):
"""
Retrieve library metadata from 'meta/libraries.json'
Each Boost library will have a `meta` directory with a `libraries.json` file.
Example: https://github.com/boostorg/align/blob/5ad7df63cd792fbdb801d600b93cad1a432f0151/meta/libraries.json
"""
url = f"https://raw.githubusercontent.com/{self.owner}/{repo_slug}/develop/meta/libraries.json"
url = f"https://raw.githubusercontent.com/{self.owner}/{repo_slug}/{tag}/meta/libraries.json"
try:
response = requests.get(url)
return response.json()
except Exception:
response.raise_for_status()
# This usually happens because the library does not have a `meta/libraries.json` file
# in the requested tag. More likely to happen with older versions of libraries.
except requests.exceptions.HTTPError:
self.logger.exception(
"get_library_metadata_failed", repo=repo_slug, url=url
)
return None
else:
return response.json()
def get_ref(self, repo_slug: str = None, ref: str = None) -> dict:
"""
@@ -133,7 +198,7 @@ class GithubAPIClient:
repo_slug = self.repo_slug
if not ref:
ref = self.ref
return self.api.git.get_ref(owner=self.owner, repo=repo_slug, ref=ref)
return self.api.git.get_ref(owner=self.owner, repo=repo_slug, ref=f"tags/{ref}")
def get_repo(self, repo_slug: str = None) -> dict:
"""
@@ -147,7 +212,7 @@ class GithubAPIClient:
return self.api.repos.get(owner=self.owner, repo=repo_slug)
def get_repo_issues(
owner: str, repo_slug: str, state: str = "all", issues_only: bool = True
self, owner: str, repo_slug: str, state: str = "all", issues_only: bool = True
):
"""
Get all issues for a repo.
@@ -205,6 +270,41 @@ class GithubAPIClient:
return results
def get_tag_by_name(self, tag_name: str, repo_slug: str = None) -> dict:
"""Get a tag by name from the GitHub API."""
if not repo_slug:
repo_slug = self.repo_slug
try:
return self.api.repos.get_release_by_tag(
owner=self.owner, repo=repo_slug, tag=tag_name
)
except Exception as e:
logger.info("tag_not_found", tag_name=tag_name, repo_slug=repo_slug)
return
def get_tags(self, repo_slug: str = None) -> dict:
"""Get all the tags from the GitHub API."""
if not repo_slug:
repo_slug = self.repo_slug
per_page = 50
page = 1
tags = []
while True:
new_tags = self.api.repos.list_tags(
owner=self.owner, repo=repo_slug, per_page=per_page, page=page
)
tags.extend(new_tags)
# Check if we reached the last page
if len(new_tags) < per_page:
break
page += 1
return tags
def get_tree(self, repo_slug: str = None, tree_sha: str = None) -> dict:
"""
Get the tree from the GitHub API.
@@ -225,9 +325,21 @@ class GithubAPIClient:
class GithubDataParser:
def parse_commit(self, commit_data: dict) -> dict:
"""Parse the commit data from Github and return a dict of the data we want."""
published_at = commit_data["committer"]["date"]
description = commit_data.get("message", "")
github_url = commit_data["html_url"]
release_date = datetime.strptime(published_at, "%Y-%m-%dT%H:%M:%SZ").date()
return {
"release_date": release_date,
"description": description,
"github_url": github_url,
"data": obj2dict(commit_data),
}
def parse_gitmodules(self, gitmodules: str) -> dict:
"""
Parse the .gitmodules file.
"""Parse the .gitmodules file.
Expects the multiline contents of https://github.com/boostorg/boost/.gitmodules to be passed in
:param gitmodules: str, the .gitmodules file
@@ -255,9 +367,7 @@ class GithubDataParser:
return modules
def parse_libraries_json(self, libraries_json: dict) -> dict:
"""
Parse the individual library metadata from 'meta/libraries.json'
"""
"""Parse the individual library metadata from 'meta/libraries.json'."""
return {
"name": libraries_json["name"],
"key": libraries_json["key"],
@@ -268,8 +378,21 @@ class GithubDataParser:
"cxxstd": libraries_json.get("cxxstd"),
}
def parse_tag(self, tag_data: dict) -> dict:
"""Parse the tag data from Github and return a dict of the data we want."""
published_at = tag_data.get("published_at", "")
description = tag_data.get("body", "")
github_url = tag_data.get("html_url", "")
release_date = datetime.strptime(published_at, "%Y-%m-%dT%H:%M:%SZ").date()
return {
"release_date": release_date,
"description": description,
"github_url": github_url,
"data": obj2dict(tag_data),
}
def extract_contributor_data(self, contributor: str) -> dict:
"""Takes an author/maintainer string and returns a dict with their data"""
"""Takes an author/maintainer string and returns a dict with their data."""
data = {}
email = self.extract_email(contributor)
@@ -342,11 +465,13 @@ class LibraryUpdater:
and their `libraries.json` file metadata.
"""
def __init__(self, owner="boostorg"):
self.client = GithubAPIClient(owner=owner)
def __init__(self, client=None):
if client:
self.client = client
else:
self.client = GithubAPIClient()
self.api = self.client.initialize_api()
self.parser = GithubDataParser()
self.owner = owner
self.logger = structlog.get_logger()
# Modules we need to skip as they are not really Boost Libraries
@@ -414,8 +539,8 @@ class LibraryUpdater:
"update_all_libraries_metadata", library_count=len(library_data)
)
for library_data in library_data:
library = self.update_library(library_data)
for lib in library_data:
self.update_library(lib)
def update_library(self, library_data: dict) -> Library:
"""Update an individual library"""
@@ -446,6 +571,9 @@ class LibraryUpdater:
# Do authors second because maintainers are more likely to have emails to match
self.update_authors(obj, authors=library_data["authors"])
if created or not obj.first_github_tag_date:
self.update_first_github_tag_date(obj)
return obj
except Exception:
@@ -509,6 +637,17 @@ class LibraryUpdater:
return obj
def update_first_github_tag_date(self, obj):
"""
Update the date of the first tag for a library
"""
first_tag = self.client.get_first_tag(repo_slug=obj.github_repo)
if first_tag:
_, first_github_tag_date = first_tag
obj.first_github_tag_date = parse_date(first_github_tag_date)
obj.save()
self.logger.info("lib_first_release_updated", obj_id=obj.id)
def update_maintainers(self, obj, maintainers=None):
"""
Receives a list of strings from the libraries.json of a Boost library, and
@@ -545,7 +684,7 @@ class LibraryUpdater:
self.logger.info("updating_repo_issues")
issues_data = self.client.get_repo_issues(
self.owner, obj.github_repo, state="all", issues_only=True
self.client.owner, obj.github_repo, state="all", issues_only=True
)
for issue_dict in issues_data:

View File

@@ -0,0 +1,58 @@
import djclick as click
from libraries.github import GithubAPIClient, LibraryUpdater
from libraries.models import Library
@click.command()
@click.option(
"--library", is_flag=False, help="Name of library (example: Accumulators)"
)
@click.option(
"--limit",
is_flag=False,
type=int,
help="Number of libraries to update (example: 10)",
)
@click.option("--token", is_flag=False, help="Github API token")
def command(token, library, limit):
"""This command fetches and updates the date of the first GitHub tag for a given library, or for
all libraries if no library is specified.
It uses the GitHub API to fetch the tag data, and you may pass your own token.
The command updates the `first_github_tag_date` field of each library.
After the update, it prints out the library name and the date of the first tag.
If no tags were found for a library, it prints a warning message.
Arguments:
token: str, optional -- The GitHub API token for authentication. Default value is set in the
client class.
library: str, optional -- The name of the library for which to fetch the tag data. If not provided,
the command fetches data for all libraries. Case-insensitive.
limit: int, optional -- The number of libraries to update. If not provided, the command updates all
libraries. If this is not passed, this command can take a long time to run.
"""
client = GithubAPIClient(token=token)
updater = LibraryUpdater(client=client)
if library:
libraries = Library.objects.filter(name__iexact=library)
else:
libraries = Library.objects.filter(first_github_tag_date__isnull=True)
if limit:
libraries = libraries[: int(limit)]
for library in libraries:
updater.update_first_github_tag_date(library)
for library in libraries:
library.refresh_from_db()
if library.first_github_tag_date:
click.secho(
f"{library.name} - First tag: {library.first_github_tag_date.strftime('%m-%Y')}",
fg="green",
)
else:
click.secho(f"{library.name} - No tags found", fg="red")

View File

@@ -0,0 +1,143 @@
import djclick as click
from fastcore.net import HTTP422UnprocessableEntityError
from libraries.github import GithubAPIClient, GithubDataParser, LibraryUpdater
from libraries.models import Library, LibraryVersion
from versions.models import Version
@click.command()
@click.option("--token", is_flag=False, help="Github API token")
@click.option("--release", is_flag=False, help="Boost version number (example: 1.81.0)")
def command(release, token):
"""Cycles through all Versions in the database, and for each version gets the
corresponding tag's .gitmodules.
The command then goes to the same tag of the repo for each library in the
.gitmodules file and uses the information to create LibraryVersion instances, and
add maintainers to LibraryVersions.
Args:
token (str): Github API token, if you need to use something other than the setting.
release (str): Boost version number (example: 1.81.0). If a partial version number is
provided, the command process all versions that contain the partial version number
(example: "--version="1.7" would process 1.7.0, 1.7.1, 1.7.2, etc.)
"""
client = GithubAPIClient(token=token)
parser = GithubDataParser()
updater = LibraryUpdater(client=client)
skipped = []
if release is None:
versions = Version.objects.active()
else:
versions = Version.objects.filter(name__icontains=release)
for version in versions:
click.echo(f"Processing version {version.name}...")
# Get the .gitmodules for this version using the version name, which is also the git tag
ref = client.get_ref(ref=version.name)
try:
raw_gitmodules = client.get_gitmodules(ref=ref)
except HTTP422UnprocessableEntityError as e:
# Only happens for one version; uncertain why.
click.secho(f"Could not get gitmodules for {version.name}.", fg="red")
skipped.append({"version": version.name, "reason": str(e)})
continue
gitmodules = parser.parse_gitmodules(raw_gitmodules.decode("utf-8"))
for gitmodule in gitmodules:
library_name = gitmodule["module"]
click.echo(f"Processing module {library_name}...")
if library_name in updater.skip_modules:
click.echo(f"Skipping module {library_name}.")
continue
libraries_json = client.get_libraries_json(repo_slug=library_name)
# If the libraries.json file exists, we can use it to get the library info
if libraries_json:
libraries = (
libraries_json
if isinstance(libraries_json, list)
else [libraries_json]
)
parsed_libraries = [
parser.parse_libraries_json(lib) for lib in libraries
]
for lib_data in parsed_libraries:
library_version = handle_library_version(
version, lib_data["name"], lib_data["maintainers"], updater
)
if not library_version:
click.secho(
f"Could not save library version {lib_data['name']}.",
fg="red",
)
skipped.append(
{
"version": version.name,
"library": lib_data["name"],
"reason": "Could not save library version",
}
)
else:
# This can happen with older tags; the libraries.json file didn't always exist, so
# when it isn't present, we search for the library by the module name and try to save
# the LibraryVersion that way.
click.echo(
f"Could not get libraries.json for {library_name}; will try to save by gitmodule name."
)
library_version = handle_library_version(
version, library_name, [], updater
)
if not library_version:
click.secho(
f"Could not save library version {lib_data['name']}.", fg="red"
)
skipped.append(
{
"version": version.name,
"library": lib_data["name"],
"reason": "Could not save library version",
}
)
skipped_messages = [
f"Skipped {skipped_obj['library']} in {skipped_obj['version']}: {skipped_obj['reason']}"
if "library" in skipped_obj
else f"Skipped {skipped_obj['version']}: {skipped_obj['reason']}"
for skipped_obj in skipped
]
for message in skipped_messages:
click.secho(message, fg="red")
def handle_library_version(version, library_name, maintainers, updater):
"""Handles the creation and updating of a LibraryVersion instance."""
try:
library = Library.objects.get(name=library_name)
except Library.DoesNotExist:
click.secho(
f"Could not find library by gitmodule name; skipping {library_name}",
fg="red",
)
return
library_version, created = LibraryVersion.objects.get_or_create(
version=version, library=library
)
click.secho(
f"Saved library version {library_version}. Created? {created}", fg="green"
)
if created:
updater.update_maintainers(library_version, maintainers=maintainers)
click.secho(f"Updated maintainers for {library_version}.", fg="green")
return library_version

View File

@@ -0,0 +1,35 @@
# Generated by Django 4.2 on 2023-05-12 22:05
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("versions", "0007_version_data_version_github_url"),
("libraries", "0007_auto_20230323_1912"),
]
operations = [
migrations.AlterField(
model_name="libraryversion",
name="library",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="library_version",
to="libraries.library",
),
),
migrations.AlterField(
model_name="libraryversion",
name="version",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="library_version",
to="versions.version",
),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 4.2.1 on 2023-05-18 21:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("libraries", "0008_alter_libraryversion_library_and_more"),
]
operations = [
migrations.AddField(
model_name="library",
name="first_github_tag_date",
field=models.DateField(
blank=True,
help_text="The date of the first release, based on the date of the commit of the first GitHub tag.",
null=True,
),
),
]

View File

@@ -1,6 +1,7 @@
from urllib.parse import urlparse
from django.db import models
from django.utils.functional import cached_property
from django.utils.text import slugify
@@ -77,6 +78,11 @@ class Library(models.Model):
cpp_standard_minimum = models.CharField(max_length=50, blank=True, null=True)
active_development = models.BooleanField(default=True, db_index=True)
first_github_tag_date = models.DateField(
blank=True,
null=True,
help_text="The date of the first release, based on the date of the commit of the first GitHub tag.",
)
last_github_update = models.DateTimeField(blank=True, null=True, db_index=True)
categories = models.ManyToManyField(Category, related_name="libraries")
@@ -94,11 +100,28 @@ class Library(models.Model):
return self.name
def save(self, *args, **kwargs):
"""Override the save method to confirm the slug is set (or set it)"""
if not self.slug:
self.slug = slugify(self.name)
return super().save(*args, **kwargs)
def get_cpp_standard_minimum_display(self):
"""Returns the display name for the C++ standard, or the value if not found.
Source of values is
https://docs.cppalliance.org/user-guide/prev/library_metadata.html"""
display_names = {
"98": "C++98",
"03": "C++03",
"11": "C++11",
"14": "C++14",
"17": "C++17",
"20": "C++20",
}
return display_names.get(self.cpp_standard_minimum, self.cpp_standard_minimum)
def github_properties(self):
"""Returns the owner and repo name for the library"""
parts = urlparse(self.github_url)
path = parts.path.split("/")
@@ -110,26 +133,40 @@ class Library(models.Model):
"repo": repo,
}
@property
@cached_property
def github_owner(self):
"""Returns the name of the GitHub owner for the library"""
return self.github_properties()["owner"]
@property
@cached_property
def github_repo(self):
"""Returns the name of the GitHub repository for the library"""
return self.github_properties()["repo"]
@cached_property
def github_issues_url(self):
"""
Returns the URL to the GitHub issues page for the library
Does not check if the URL is valid.
"""
if not self.github_owner or not self.github_repo:
raise ValueError("Invalid GitHub owner or repository")
return f"https://github.com/{self.github_owner}/{self.github_repo}/issues"
class LibraryVersion(models.Model):
version = models.ForeignKey(
"versions.Version",
related_name="library_version",
on_delete=models.SET_NULL,
on_delete=models.CASCADE,
null=True,
)
library = models.ForeignKey(
"libraries.Library",
related_name="library_version",
on_delete=models.SET_NULL,
on_delete=models.CASCADE,
null=True,
)
maintainers = models.ManyToManyField("users.User", related_name="maintainers")

34
libraries/tasks.py Normal file
View File

@@ -0,0 +1,34 @@
import structlog
from celery.schedules import crontab
from config.celery import app
from libraries.github import LibraryUpdater
logger = structlog.getLogger(__name__)
@app.on_after_configure.connect
def setup_periodic_tasks(sender, **kwargs):
# Executes every 5th of the month at 7:30 a.m.
sender.add_periodic_task(
crontab(hour=7, minute=30, day_of_month=5),
update_libraries.s(),
)
@app.task
def update_libraries():
"""Update local libraries from GitHub Boost libraries.
Use the LibraryUpdater, which retrieves the active boost libraries from the
Boost GitHub repo, to update the models with the latest information on that
library (repo) along with its issues, pull requests, and related objects
from GitHub.
"""
updater = LibraryUpdater()
updater.update_libraries()
logger.info("libraries_tasks_update_libraries_finished")

View File

@@ -1,6 +1,21 @@
from ..forms import LibraryForm
from ..forms import LibraryForm, VersionSelectionForm
def test_library_form_success(tp, library, category):
form = LibraryForm(data={"categories": [category]})
assert form.is_valid() is True
def test_version_selection_form(library_version):
# Test with a valid version
valid_version = library_version.version
form = VersionSelectionForm(data={"version": valid_version.pk})
assert form.is_valid()
# Test with an invalid version
form = VersionSelectionForm(data={"version": 9999})
assert form.is_valid() is False
# Test with no version selected
form = VersionSelectionForm(data={"version": None})
assert form.is_valid() is False

View File

@@ -1,3 +1,4 @@
import datetime
from unittest.mock import MagicMock, patch
import pytest
@@ -5,11 +6,7 @@ import responses
from ghapi.all import GhApi
from model_bakery import baker
from libraries.github import (
GithubAPIClient,
GithubDataParser,
LibraryUpdater,
)
from libraries.github import GithubAPIClient, GithubDataParser, LibraryUpdater
from libraries.models import Category, Issue, Library, PullRequest
"""GithubAPIClient Tests"""
@@ -23,7 +20,7 @@ def github_api_client():
@pytest.fixture(scope="function")
def mock_api() -> GhApi:
"""Fixture that mocks the GitHub API."""
with patch("libraries.github_new.GhApi") as mock_api_class:
with patch("libraries.github.GhApi") as mock_api_class:
yield mock_api_class.return_value
@@ -104,11 +101,41 @@ def test_get_blob(github_api_client):
# )
@pytest.mark.skip(reason="Mocking the API is not working")
def test_get_first_tag(github_api_client, mock_api):
"""Test the get_first_tag method of GithubAPIClient."""
# Mock tags from the GitHub API
mock_tags = [
{"name": "tag2", "commit": {"sha": "2"}},
{"name": "tag1", "commit": {"sha": "1"}},
]
# Mock the commit data from the GitHub API
mock_commits = [
{"sha": "2", "committer": {"date": "2023-05-12T00:00:00Z"}},
{"sha": "1", "committer": {"date": "2023-05-11T00:00:00Z"}},
]
# Setup the mock API to return the mock tags and commits
github_api_client.api.repos.list_tags.side_effect = MagicMock(
return_value=mock_tags
)
github_api_client.api.git.get_commit.side_effect = MagicMock(
return_value=mock_commits
)
repo_slug = "sample_repo"
tag = github_api_client.get_first_tag(repo_slug=repo_slug)
# Assert that the earliest tag was returned
assert tag == (mock_tags[1], "2000-01-01T00:00:00Z")
@responses.activate
def test_get_libraries_json(github_api_client):
"""Test the get_libraries_json method of GitHubAPIClient."""
repo_slug = "sample_repo"
url = f"https://raw.githubusercontent.com/{github_api_client.owner}/{repo_slug}/develop/meta/libraries.json"
url = f"https://raw.githubusercontent.com/{github_api_client.owner}/{repo_slug}/master/meta/libraries.json"
sample_json = {"key": "math", "name": "Math"}
responses.add(
responses.GET,
@@ -190,6 +217,38 @@ def test_parse_libraries_json():
parser.parse_libraries_json(sample_libraries_json)
def test_parse_commit():
commit_data = {
"committer": {"date": "2023-05-10T00:00:00Z"},
"message": "This is a sample description for a commit",
"html_url": "http://example.com/commit/12345",
}
expected = {
"release_date": datetime.date(2023, 5, 10),
"description": commit_data["message"],
"github_url": "http://example.com/commit/12345",
"data": commit_data,
}
result = GithubDataParser().parse_commit(commit_data)
assert result == expected
def test_parse_tag():
tag_data = {
"published_at": "2023-05-10T00:00:00Z",
"body": "This is a sample description for a tag",
"html_url": "http://example.com/commit/12345",
}
expected = {
"release_date": datetime.date(2023, 5, 10),
"description": "This is a sample description for a tag",
"github_url": "http://example.com/commit/12345",
"data": tag_data,
}
result = GithubDataParser().parse_tag(tag_data)
assert result == expected
def test_extract_names():
sample = "Tester Testerson <tester -at- gmail.com>"
expected = ["Tester", "Testerson"]

View File

@@ -1,6 +1,16 @@
from model_bakery import baker
def test_get_cpp_standard_minimum_display(library):
library.cpp_standard_minimum = "11"
library.save()
assert library.get_cpp_standard_minimum_display() == "C++11"
library.cpp_standard_minimum = "42"
library.save()
assert library.get_cpp_standard_minimum_display() == "42"
def test_github_properties(library):
properties = library.github_properties()
assert properties["owner"] == "boostorg"
@@ -15,6 +25,12 @@ def test_github_repo(library):
assert library.github_repo == "multi_array"
def test_get_issues_link(library):
result = library.github_issues_url
expected = f"https://github.com/{library.github_owner}/{library.github_repo}/issues"
assert expected == result
def test_category_creation(category):
assert category.name is not None
@@ -46,9 +62,7 @@ def test_library_version_multiple_versions(library, library_version):
library_version__version=library_version.version
).exists()
other_version = baker.make("versions.Version", name="New Version")
new_library_version = baker.make(
"libraries.LibraryVersion", library=library, version=other_version
)
baker.make("libraries.LibraryVersion", library=library, version=other_version)
assert library.versions.count() == 2
assert library.versions.filter(
library_version__version=library_version.version

View File

@@ -1,12 +1,12 @@
import structlog
from django.http import Http404
from django.shortcuts import redirect
from django.shortcuts import get_object_or_404, redirect
from django.views.generic import DetailView, ListView
from django.views.generic.edit import FormMixin
from versions.models import Version
from .forms import LibraryForm
from .forms import LibraryForm, VersionSelectionForm
from .models import Category, Issue, Library, LibraryVersion, PullRequest
logger = structlog.get_logger()
@@ -52,9 +52,10 @@ class LibraryList(CategoryMixin, FormMixin, ListView):
return super().get(request)
class LibraryDetail(CategoryMixin, DetailView):
class LibraryDetail(CategoryMixin, FormMixin, DetailView):
"""Display a single Library in insolation"""
form_class = VersionSelectionForm
model = Library
template_name = "libraries/detail.html"
@@ -63,13 +64,22 @@ class LibraryDetail(CategoryMixin, DetailView):
context = super().get_context_data(**kwargs)
context["closed_prs_count"] = self.get_closed_prs_count(self.object)
context["open_issues_count"] = self.get_open_issues_count(self.object)
context["version"] = Version.objects.most_recent()
context["version"] = self.get_version()
context["maintainers"] = self.get_maintainers(context["version"])
context["versions"] = (
Version.objects.active()
.filter(library_version__library=self.object)
.distinct()
.order_by("-release_date")
)
return context
def get_object(self):
"""Get the current library object from the slug in the URL.
If present, use the version_slug to get the right LibraryVersion of the library.
Otherwise, default to the most recent version."""
slug = self.kwargs.get("slug")
version = Version.objects.most_recent()
version = self.get_version()
if not LibraryVersion.objects.filter(
version=version, library__slug=slug
@@ -83,16 +93,42 @@ class LibraryDetail(CategoryMixin, DetailView):
return obj
def get_closed_prs_count(self, obj):
"""Get the number of closed pull requests for the current library."""
return PullRequest.objects.filter(library=obj, is_open=True).count()
def get_maintainers(self, version):
"""Get the maintainers for the current LibraryVersion."""
obj = self.get_object()
library_version = LibraryVersion.objects.get(library=obj, version=version)
return library_version.maintainers.all()
def get_open_issues_count(self, obj):
"""Get the number of open issues for the current library."""
return Issue.objects.filter(library=obj, is_open=True).count()
def get_version(self):
"""Get the version of Boost for the library we're currently looking at."""
version_slug = self.kwargs.get("version_slug")
if version_slug:
return get_object_or_404(Version, slug=version_slug)
else:
return Version.objects.most_recent()
def post(self, request, *args, **kwargs):
"""User has submitted a form and will be redirected to the right LibraryVersion."""
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
version = form.cleaned_data["version"]
return redirect(
"library-detail-by-version",
version_slug=version.slug,
slug=self.object.slug,
)
else:
logger.info("library_list_invalid_version")
return super().get(request)
class LibraryByCategory(CategoryMixin, FormMixin, ListView):
"""List all of our libraries for the current version of Boost in a certain category"""
@@ -174,56 +210,6 @@ class LibraryListByVersion(CategoryMixin, FormMixin, ListView):
return super().get(request)
class LibraryDetailByVersion(CategoryMixin, DetailView):
"""Display a single Library for a specific Boost version"""
model = Library
template_name = "libraries/detail.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
object = self.get_object()
context["closed_prs_count"] = self.get_closed_prs_count(object)
context["open_issues_count"] = self.get_open_issues_count(object)
context["version_slug"] = self.kwargs.get("version_slug")
context["version"] = self.get_version(self.kwargs.get("version_slug"))
context["version_name"] = context["version"].name
context["maintainers"] = self.get_maintainers(context["version"])
return context
def get_object(self):
version_slug = self.kwargs.get("version_slug")
slug = self.kwargs.get("slug")
if not LibraryVersion.objects.filter(
version__slug=version_slug, library__slug=slug
).exists():
raise Http404("No library found matching the query")
try:
obj = self.get_queryset().get(slug=slug)
except self.model.DoesNotExist:
raise Http404("No library found matching the query")
return obj
def get_closed_prs_count(self, obj):
return PullRequest.objects.filter(library=obj, is_open=True).count()
def get_maintainers(self, version):
obj = self.get_object()
library_version = LibraryVersion.objects.get(library=obj, version=version)
return library_version.maintainers.all()
def get_open_issues_count(self, obj):
return Issue.objects.filter(library=obj, is_open=True).count()
def get_version(self, version_slug):
try:
return Version.objects.get(slug=version_slug)
except Version.DoesNotExist:
logger.info("libraries_by_version_detail_view_version_not_found")
raise Http404("No object found matching the query")
class LibraryListByVersionByCategory(CategoryMixin, FormMixin, ListView):
"""List all of our libraries in a certain category for a certain Boost version"""

0
news/__init__.py Normal file
View File

12
news/admin.py Normal file
View File

@@ -0,0 +1,12 @@
from django.contrib import admin
from .models import Entry
class EntryAdmin(admin.ModelAdmin):
list_display = ["title", "author", "created_at", "approved_at", "publish_at"]
readonly_fields = ["modified_at"]
prepopulated_fields = {"slug": ["title"]}
admin.site.register(Entry, EntryAdmin)

6
news/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class NewsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "news"

8
news/forms.py Normal file
View File

@@ -0,0 +1,8 @@
from django import forms
from .models import Entry
class EntryForm(forms.ModelForm):
class Meta:
model = Entry
fields = ["title", "description"]

View File

@@ -0,0 +1,142 @@
# Generated by Django 4.2 on 2023-05-12 18:34
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Entry",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("slug", models.SlugField()),
("title", models.CharField(max_length=255)),
("description", models.TextField(blank=True, default="")),
("external_url", models.URLField(blank=True, default="")),
("image", models.ImageField(blank=True, null=True, upload_to="news")),
("created_at", models.DateTimeField(default=django.utils.timezone.now)),
("publish_at", models.DateTimeField(default=django.utils.timezone.now)),
(
"author",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name_plural": "Entries",
},
),
migrations.CreateModel(
name="BlogPost",
fields=[
(
"entry_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="news.entry",
),
),
("body", models.TextField()),
("abstract", models.CharField(max_length=256)),
],
bases=("news.entry",),
),
migrations.CreateModel(
name="Link",
fields=[
(
"entry_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="news.entry",
),
),
],
bases=("news.entry",),
),
migrations.CreateModel(
name="Poll",
fields=[
(
"entry_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="news.entry",
),
),
],
bases=("news.entry",),
),
migrations.CreateModel(
name="Video",
fields=[
(
"entry_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="news.entry",
),
),
],
bases=("news.entry",),
),
migrations.CreateModel(
name="PollChoice",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("wording", models.CharField(max_length=200)),
("order", models.PositiveIntegerField()),
("votes", models.ManyToManyField(to=settings.AUTH_USER_MODEL)),
(
"poll",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="news.poll"
),
),
],
),
]

View File

@@ -0,0 +1,37 @@
# Generated by Django 4.2 on 2023-05-17 16:41
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("news", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="entry",
name="approved_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="entry",
name="moderator",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="moderated_entries_set",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="entry",
name="modified_at",
field=models.DateTimeField(auto_now=True),
),
]

View File

132
news/models.py Normal file
View File

@@ -0,0 +1,132 @@
from django.contrib.auth import get_user_model
from django.db import models
from django.urls import reverse
from django.utils.text import slugify
from django.utils.timezone import now
User = get_user_model()
class EntryManager(models.Manager):
def get_queryset(self):
return (
super()
.get_queryset()
.annotate(
approved=models.Q(moderator__isnull=False, approved_at__lte=now())
)
.annotate(published=models.Q(publish_at__lte=now(), approved=True))
)
class Entry(models.Model):
"""A news entry.
Please note that this is a concrete class with its own DB table. Children
of this class have their own table with their own attributes, plus a 1-1
relationship with their parent.
"""
class AlreadyApprovedError(Exception):
"""The entry cannot be approved again."""
slug = models.SlugField()
title = models.CharField(max_length=255)
description = models.TextField(blank=True, default="")
author = models.ForeignKey(User, on_delete=models.CASCADE)
moderator = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="moderated_entries_set",
)
external_url = models.URLField(blank=True, default="")
image = models.ImageField(upload_to="news", null=True, blank=True)
created_at = models.DateTimeField(default=now)
approved_at = models.DateTimeField(null=True, blank=True)
modified_at = models.DateTimeField(auto_now=True)
publish_at = models.DateTimeField(default=now)
objects = EntryManager()
class Meta:
verbose_name_plural = "Entries"
def __str__(self):
return f"{self.title} by {self.author}"
@property
def is_approved(self):
return (
self.moderator is not None
and self.approved_at is not None
and self.approved_at <= now()
)
@property
def is_published(self):
return self.is_approved and self.publish_at <= now()
def approve(self, user):
"""Mark this entry as approved by the given `user`."""
if self.is_approved:
raise self.AlreadyApprovedError()
self.moderator = user
self.approved_at = now()
self.save(update_fields=["moderator", "approved_at", "modified_at"])
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
return super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse("news-detail", args=[self.slug])
def can_view(self, user):
return (
self.is_published
or user == self.author
or (user is not None and user.has_perm("news.view_entry"))
)
def can_approve(self, user):
return user is not None and user.has_perm("news.change_entry")
def can_edit(self, user):
return (not self.is_approved and user == self.author) or (
user is not None and user.has_perm("news.change_entry")
)
def can_delete(self, user):
return user is not None and user.has_perm("news.delete_entry")
class BlogPost(Entry):
body = models.TextField()
abstract = models.CharField(max_length=256)
# Possible extra fields: RSS feed? banner? keywords?
class Link(Entry):
pass
class Video(Entry):
pass
# Possible extra fields: length? quality?
class Poll(Entry):
pass
# Possible extra fields: voting expiration date?
class PollChoice(models.Model):
poll = models.ForeignKey(Poll, on_delete=models.CASCADE)
wording = models.CharField(max_length=200)
order = models.PositiveIntegerField()
votes = models.ManyToManyField(User)

0
news/tests/__init__.py Normal file
View File

55
news/tests/fixtures.py Normal file
View File

@@ -0,0 +1,55 @@
import datetime
import pytest
from django.contrib.auth.models import Permission
from django.utils.timezone import now
from model_bakery import baker
@pytest.fixture
def make_entry(db):
def _make_it(approved=True, published=True, **kwargs):
past = now() - datetime.timedelta(hours=1)
future = now() + datetime.timedelta(days=1)
if approved:
approved_at = past
moderator = baker.make("users.User")
else:
approved_at = None
moderator = None
if published:
publish_at = past
else:
publish_at = future
kwargs.setdefault("approved_at", approved_at)
kwargs.setdefault("moderator", moderator)
kwargs.setdefault("publish_at", publish_at)
entry = baker.make("Entry", **kwargs)
entry.author.set_password("password")
entry.author.save()
return entry
return _make_it
@pytest.fixture
def moderator_user(db):
# we could use `tp.make_user` but we need this fix to be released
# https://github.com/revsys/django-test-plus/issues/199
user = baker.make("users.User")
user.user_permissions.add(
*Permission.objects.filter(
content_type__app_label="news", content_type__model="entry"
)
)
user.set_password("password")
user.save()
return user
@pytest.fixture
def regular_user(db):
user = baker.make("users.User")
user.set_password("password")
user.save()
return user

60
news/tests/test_forms.py Normal file
View File

@@ -0,0 +1,60 @@
import datetime
from django.utils.timezone import now
from model_bakery import baker
from ..forms import EntryForm
from ..models import Entry
def test_form_fields():
form = EntryForm()
assert sorted(form.fields.keys()) == ["description", "title"]
def test_form_model_creates_entry(make_entry):
title = "The Title"
description = "Some description"
user = baker.make("users.User")
assert Entry.objects.filter(title=title).count() == 0
before = now()
form = EntryForm(instance=None, data={"title": title, "description": description})
assert form.is_valid()
form.instance.author = user
result = form.save()
after = now()
assert isinstance(result, Entry)
assert result.pk is not None
assert before <= result.created_at <= after
assert before <= result.modified_at <= after
assert before <= result.publish_at <= after
assert result.approved_at is None
assert result.title == title
assert result.description == description
assert result.author == user
assert result.moderator is None
assert Entry.objects.get(pk=result.pk) == result
def test_form_model_modifies_entry(make_entry):
# Guard against runs that are too fast and this modified_at would not change
past = now() - datetime.timedelta(minutes=1)
news = make_entry(title="Old title", modified_at=past)
form = EntryForm(instance=news, data={"title": "New title"})
assert form.is_valid()
result = form.save()
# Modified fields
assert result.title == "New title"
assert result.modified_at > past
# Unchanged fields
assert result.pk == news.pk
assert result.created_at == news.created_at
assert result.approved_at == news.approved_at
assert result.publish_at == news.publish_at
assert result.description == news.description
assert result.author == news.author
assert result.moderator == news.moderator

270
news/tests/test_models.py Normal file
View File

@@ -0,0 +1,270 @@
import datetime
import pytest
from django.contrib.auth.models import Permission
from django.utils.timezone import now
from model_bakery import baker
from ..models import Entry, Poll
def test_entry_str():
entry = baker.make("Entry")
assert str(entry) == f"{entry.title} by {entry.author}"
def test_entry_generate_slug():
author = baker.make("users.User")
entry = Entry.objects.create(title="😀 Foo Bar Baz!@! +", author=author)
assert entry.slug == "foo-bar-baz"
def test_entry_slug_not_overwriten():
author = baker.make("users.User")
entry = Entry.objects.create(title="Foo!", author=author, slug="different")
assert entry.slug == "different"
def test_entry_approved(make_entry):
entry = make_entry(moderator=baker.make("users.User"), approved_at=now())
assert entry.is_approved is True
def test_entry_not_approved(make_entry):
entry = make_entry(moderator=None, approved_at=now())
assert entry.is_approved is False
entry = make_entry(moderator=baker.make("users.User"), approved_at=None)
assert entry.is_approved is False
future = now() + datetime.timedelta(minutes=1)
entry = make_entry(moderator=baker.make("users.User"), approved_at=future)
assert entry.is_approved is False
def test_entry_published(make_entry):
entry = make_entry(approved=True, publish_at=now())
assert entry.is_published is True
def test_entry_not_published(make_entry):
entry = make_entry(approved=False, publish_at=now())
assert entry.is_published is False
future = now() + datetime.timedelta(minutes=1)
entry = make_entry(approved=True, publish_at=future)
assert entry.is_published is False
def test_entry_absolute_url():
entry = baker.make("Entry", slug="the-slug")
assert entry.get_absolute_url() == "/news/the-slug/"
def test_approve_entry(make_entry):
future = now() + datetime.timedelta(hours=1)
entry = make_entry(approved=False, publish_at=future)
assert not entry.is_approved
assert not entry.is_published
user = baker.make("users.User")
before = now()
entry.approve(user)
after = now()
entry.refresh_from_db()
assert entry.moderator == user
# Avoid mocking `now()`, yet still ensure that the approval timestamp
# ocurred between `before` and `after`
assert entry.approved_at <= after
assert entry.approved_at >= before
assert entry.is_approved
assert not entry.is_published
def test_approve_already_approved_entry(make_entry):
entry = make_entry(approved=True)
assert entry.is_approved
with pytest.raises(Entry.AlreadyApprovedError):
entry.approve(baker.make("users.User"))
def test_entry_permissions_author(make_entry):
entry = make_entry(approved=False)
author = entry.author
assert entry.can_view(author) is True
assert entry.can_edit(author) is True
assert entry.can_delete(author) is False
assert entry.can_approve(author) is False
entry.approve(baker.make("users.User"))
assert entry.can_view(author) is True
assert entry.can_edit(author) is False
assert entry.can_delete(author) is False
assert entry.can_approve(author) is False
def test_not_approved_entry_permissions_other_users(make_entry):
entry = make_entry(approved=False)
assert entry.can_view(None) is False
assert entry.can_edit(None) is False
assert entry.can_delete(None) is False
assert entry.can_approve(None) is False
regular_user = baker.make("users.User")
assert entry.can_view(regular_user) is False
assert entry.can_edit(regular_user) is False
assert entry.can_delete(regular_user) is False
assert entry.can_approve(regular_user) is False
superuser = baker.make("users.User", is_superuser=True)
assert entry.can_view(superuser) is True
assert entry.can_edit(superuser) is True
assert entry.can_delete(superuser) is True
assert entry.can_approve(superuser) is True
user_with_add_perm = baker.make("users.User")
user_with_add_perm.user_permissions.add(
Permission.objects.get(codename="add_entry")
)
assert entry.can_view(user_with_add_perm) is False
assert entry.can_edit(user_with_add_perm) is False
assert entry.can_delete(user_with_add_perm) is False
assert entry.can_approve(user_with_add_perm) is False
user_with_change_perm = baker.make("users.User")
user_with_change_perm.user_permissions.add(
Permission.objects.get(codename="change_entry")
)
assert entry.can_view(user_with_change_perm) is False
assert entry.can_edit(user_with_change_perm) is True
assert entry.can_delete(user_with_change_perm) is False
assert entry.can_approve(user_with_change_perm) is True
user_with_delete_perm = baker.make("users.User")
user_with_delete_perm.user_permissions.add(
Permission.objects.get(codename="delete_entry")
)
assert entry.can_view(user_with_delete_perm) is False
assert entry.can_edit(user_with_delete_perm) is False
assert entry.can_delete(user_with_delete_perm) is True
assert entry.can_approve(user_with_delete_perm) is False
user_with_view_perm = baker.make("users.User")
user_with_view_perm.user_permissions.add(
Permission.objects.get(codename="view_entry")
)
assert entry.can_view(user_with_view_perm) is True
assert entry.can_edit(user_with_view_perm) is False
assert entry.can_delete(user_with_view_perm) is False
assert entry.can_approve(user_with_view_perm) is False
def test_approved_entry_permissions_other_users(make_entry):
entry = make_entry(approved=True)
assert entry.can_view(None) is True
assert entry.can_edit(None) is False
assert entry.can_delete(None) is False
assert entry.can_approve(None) is False
regular_user = baker.make("users.User")
assert entry.can_view(regular_user) is True
assert entry.can_edit(regular_user) is False
assert entry.can_delete(regular_user) is False
assert entry.can_approve(regular_user) is False
superuser = baker.make("users.User", is_superuser=True)
assert entry.can_view(superuser) is True
assert entry.can_edit(superuser) is True
assert entry.can_delete(superuser) is True
assert entry.can_approve(superuser) is True
user_with_add_perm = baker.make("users.User")
user_with_add_perm.user_permissions.add(
Permission.objects.get(codename="add_entry")
)
assert entry.can_view(user_with_add_perm) is True
assert entry.can_edit(user_with_add_perm) is False
assert entry.can_delete(user_with_add_perm) is False
assert entry.can_approve(user_with_add_perm) is False
user_with_change_perm = baker.make("users.User")
user_with_change_perm.user_permissions.add(
Permission.objects.get(codename="change_entry")
)
assert entry.can_view(user_with_change_perm) is True
assert entry.can_edit(user_with_change_perm) is True
assert entry.can_delete(user_with_change_perm) is False
assert entry.can_approve(user_with_change_perm) is True
user_with_delete_perm = baker.make("users.User")
user_with_delete_perm.user_permissions.add(
Permission.objects.get(codename="delete_entry")
)
assert entry.can_view(user_with_delete_perm) is True
assert entry.can_edit(user_with_delete_perm) is False
assert entry.can_delete(user_with_delete_perm) is True
assert entry.can_approve(user_with_delete_perm) is False
user_with_view_perm = baker.make("users.User")
user_with_view_perm.user_permissions.add(
Permission.objects.get(codename="view_entry")
)
assert entry.can_view(user_with_view_perm) is True
assert entry.can_edit(user_with_view_perm) is False
assert entry.can_delete(user_with_view_perm) is False
assert entry.can_approve(user_with_view_perm) is False
def test_entry_manager_custom_queryset(make_entry):
entry_published = make_entry(approved=True, published=True)
entry_approved = make_entry(approved=True, published=False)
entry_not_approved = make_entry(approved=False)
entry_not_published = make_entry(approved=False, published=False)
assert list(Entry.objects.filter(approved=True).order_by("id")) == [
entry_published,
entry_approved,
]
assert list(Entry.objects.filter(approved=False).order_by("id")) == [
entry_not_approved,
entry_not_published,
]
assert list(Entry.objects.filter(published=True).order_by("id")) == [
entry_published
]
assert list(Entry.objects.filter(published=False).order_by("id")) == [
entry_approved,
entry_not_approved,
entry_not_published,
]
def test_blogpost():
blogpost = baker.make("BlogPost")
assert isinstance(blogpost, Entry)
assert Entry.objects.get(id=blogpost.id).blogpost == blogpost
def test_link():
link = baker.make("Link")
assert isinstance(link, Entry)
assert Entry.objects.get(id=link.id).link == link
def test_video():
video = baker.make("Video")
assert isinstance(video, Entry)
assert Entry.objects.get(id=video.id).video == video
def test_poll():
poll = baker.make("Poll")
assert isinstance(poll, Entry)
assert Entry.objects.get(id=poll.id).poll == poll
def test_poll_choice():
choice = baker.make("PollChoice")
assert isinstance(choice.poll, Poll)

244
news/tests/test_views.py Normal file
View File

@@ -0,0 +1,244 @@
import datetime
from django.utils.timezone import now
from model_bakery import baker
from ..forms import EntryForm
from ..models import Entry
def test_entry_list(tp, make_entry, regular_user, authenticated=False):
"""List published news for non authenticated users."""
not_approved_news = make_entry(approved=False, title="needs moderation")
yesterday_news = make_entry(
approved=True, title="old news", publish_at=now() - datetime.timedelta(days=1)
)
today_news = make_entry(
approved=True, title="current news", publish_at=now().today()
)
tomorrow_news = make_entry(
approved=True,
title="future news",
publish_at=now() + datetime.timedelta(days=1),
)
if authenticated:
tp.login(regular_user)
response = tp.get("news")
tp.response_200(response)
expected = [today_news, yesterday_news]
assert list(response.context.get("entry_list", [])) == expected
content = str(response.content)
for n in expected:
assert n.get_absolute_url() in content
assert n.title in content
assert not_approved_news.get_absolute_url() not in content
assert not_approved_news.title not in content
assert tomorrow_news.get_absolute_url() not in content
assert tomorrow_news.title not in content
# If user is not authenticated, the Create News link should not be shown
assert (tp.reverse("news-create") in content) == authenticated
def test_entry_list_authenticated(tp, make_entry, regular_user):
test_entry_list(tp, make_entry, regular_user, authenticated=True)
def test_news_detail(tp, make_entry):
"""Browse details for a given news entry."""
a_past_date = now() - datetime.timedelta(hours=10)
news = make_entry(approved=True, publish_at=a_past_date)
url = tp.reverse("news-detail", news.slug)
response = tp.get(url)
tp.response_200(response)
content = str(response.content)
assert news.title in content
assert news.description in content
assert tp.reverse("news-approve", news.slug) not in content
# no next nor prev links
assert "newer entries" not in content.lower()
assert "older entries" not in content.lower()
# create an older news
older_date = a_past_date - datetime.timedelta(hours=1)
older = make_entry(approved=True, publish_at=older_date)
response = tp.get(url)
tp.response_200(response)
content = str(response.content)
assert "newer entries" not in content.lower()
assert "older entries" in content.lower()
assert older.get_absolute_url() in content
# create a newer news, but still older than now so it's shown
newer_date = a_past_date + datetime.timedelta(hours=1)
assert newer_date < now()
newer = make_entry(approved=True, publish_at=newer_date)
response = tp.get(url)
tp.response_200(response)
content = str(response.content)
assert "newer entries" in content.lower()
assert "older entries" in content.lower()
assert newer.get_absolute_url() in content
def test_news_detail_404(tp):
"""No news is good news."""
url = tp.reverse("news-detail", "not-there")
response = tp.get(url)
tp.response_404(response)
def test_news_detail_404_if_not_published(tp, make_entry, regular_user):
"""Details for a news entry are available if published or authored."""
news = make_entry(published=False)
response = tp.get(news.get_absolute_url())
tp.response_404(response)
# even if logged in, a regular user can not access the unpublished news
with tp.login(regular_user):
response = tp.get(news.get_absolute_url())
tp.response_404(response)
# but the entry author can access it even if unpublished
with tp.login(news.author):
response = tp.get(news.get_absolute_url())
tp.response_200(response)
def test_news_detail_actions_author(tp, make_entry):
"""News entry is updatable by authors (if not approved)."""
news = make_entry(approved=False) # not approved entry
with tp.login(news.author):
response = tp.get(news.get_absolute_url())
tp.response_200(response)
content = str(response.content)
assert tp.reverse("news-approve", news.slug) not in content
news.approve(baker.make("users.User"))
with tp.login(news.author):
response = tp.get(news.get_absolute_url())
tp.response_200(response)
content = str(response.content)
assert tp.reverse("news-approve", news.slug) not in content
def test_news_detail_actions_moderator(tp, make_entry, moderator_user):
"""Moderators can update, delete and approve a news entry."""
news = make_entry(approved=False) # approved entry
with tp.login(moderator_user):
response = tp.get(news.get_absolute_url())
tp.response_200(response)
content = str(response.content)
assert tp.reverse("news-approve", news.slug) in content
news.approve(baker.make("users.User"))
with tp.login(moderator_user):
response = tp.get(news.get_absolute_url())
tp.response_200(response)
content = str(response.content)
assert tp.reverse("news-approve", news.slug) not in content
def test_news_create_get(tp, regular_user):
url_name = "news-create"
# assertLoginRequired expects a non resolved URL, that is an URL name
# see https://github.com/revsys/django-test-plus/issues/202
tp.assertLoginRequired(url_name)
with tp.login(regular_user):
# assertGoodView expects a resolved URL
# see https://github.com/revsys/django-test-plus/issues/202
url = tp.reverse(url_name)
response = tp.assertGoodView(url, test_query_count=3, verbose=True)
form = tp.get_context("form")
assert isinstance(form, EntryForm)
for field in form:
tp.assertResponseContains(str(field), response)
def test_news_create_post(tp, regular_user):
url = tp.reverse("news-create")
data = {
"title": "Lorem Ipsum",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing.",
}
before = now()
with tp.login(regular_user):
response = tp.post(url, data=data, follow=True)
after = now()
entries = Entry.objects.filter(title=data["title"])
assert len(entries) == 1
entry = entries.get()
assert entry.slug == "lorem-ipsum"
assert entry.description == data["description"]
assert entry.author == regular_user
assert not entry.is_approved
assert not entry.is_published
# Avoid mocking `now()`, yet still ensure that the timestamps are
# between `before` and `after`
assert before <= entry.created_at <= after
assert before <= entry.modified_at <= after
tp.assertRedirects(response, entry.get_absolute_url())
def test_news_approve_get_method_not_allowed(
tp, make_entry, regular_user, moderator_user
):
entry = make_entry(approved=False)
# login is required
url_params = ("news-approve", entry.slug)
tp.assertLoginRequired(*url_params)
# regular users would get a 403
with tp.login(regular_user):
response = tp.get(*url_params)
tp.response_403(response)
# moderators users would get a 405 for GET
with tp.login(moderator_user):
response = tp.get(*url_params)
tp.response_405(response)
def test_news_approve_post(tp, make_entry, regular_user, moderator_user):
entry = make_entry(approved=False)
url_params = ("news-approve", entry.slug)
# regular users would still get a 403 on POST
with tp.login(regular_user):
response = tp.post(*url_params)
tp.response_403(response)
# moderators users can POST to the view to approve an entry
with tp.login(moderator_user):
before = now()
response = tp.post(*url_params)
after = now()
tp.assertRedirects(response, entry.get_absolute_url())
entry.refresh_from_db()
assert entry.is_approved is True
assert entry.moderator == moderator_user
assert before <= entry.approved_at <= after
assert before <= entry.modified_at <= after

83
news/views.py Normal file
View File

@@ -0,0 +1,83 @@
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.http import Http404, HttpResponseRedirect
from django.utils.translation import gettext as _
from django.views.generic import (
CreateView,
DetailView,
ListView,
View,
)
from django.views.generic.detail import SingleObjectMixin
from .models import Entry
from .forms import EntryForm
def get_published_or_none(sibling_getter):
"""Helper method to get next/prev published sibling of a given entry."""
try:
result = sibling_getter(published=True)
except Entry.DoesNotExist:
result = None
return result
class EntryListView(ListView):
model = Entry
template_name = "news/list.html"
ordering = ["-publish_at"]
paginate_by = 10
def get_queryset(self):
return super().get_queryset().filter(published=True)
class EntryDetailView(DetailView):
model = Entry
template_name = "news/detail.html"
def get_object(self, *args, **kwargs):
# Published news are available to anyone, otherwise to authors only
result = super().get_object(*args, **kwargs)
if not result.can_view(self.request.user):
raise Http404()
return result
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["next"] = get_published_or_none(self.object.get_next_by_publish_at)
context["prev"] = get_published_or_none(self.object.get_previous_by_publish_at)
context["user_can_approve"] = self.object.can_approve(self.request.user)
return context
class EntryCreateView(LoginRequiredMixin, CreateView):
model = Entry
form_class = EntryForm
template_name = "news/form.html"
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
class EntryApproveView(
LoginRequiredMixin, UserPassesTestMixin, SingleObjectMixin, View
):
model = Entry
http_method_names = ["post"]
def test_func(self):
entry = self.get_object()
return entry.can_approve(self.request.user)
def post(self, request, *args, **kwargs):
entry = self.get_object()
try:
entry.approve(user=self.request.user)
except Entry.AlreadyApprovedError:
messages.error(request, _("The entry was already approved."))
else:
messages.success(request, _("The entry was successfully approved."))
return HttpResponseRedirect(entry.get_absolute_url())

View File

@@ -1,8 +1,8 @@
{
"name": "boost.org",
"name": "boost.revsys.dev",
"version": "1.0.0",
"main": "index.js",
"repository": "git@github.com:revsys/boost.org.git",
"repository": "git@github.com:cppalliance/temp-site.git",
"author": "Greg Newman <greg@gregnewman.org>",
"license": "MIT",
"scripts": {

View File

@@ -28,7 +28,7 @@ python-json-logger
structlog
# Celery
celery==5.2.2
celery==5.2.7
redis==4.5.4
# Testing

View File

@@ -10,23 +10,21 @@ appdirs==1.4.4
# via fs
asgiref==3.6.0
# via django
asttokens==2.0.5
asttokens==2.2.1
# via stack-data
async-timeout==4.0.2
# via redis
attrs==21.1.0
# via pytest
backcall==0.2.0
# via ipython
billiard==3.6.4.0
# via celery
black==22.3
# via -r ./requirements.in
boto3==1.17.68
boto3==1.26.135
# via
# -r ./requirements.in
# django-bakery
botocore==1.20.68
botocore==1.29.135
# via
# boto3
# s3transfer
@@ -36,15 +34,15 @@ bump2version==1.0.1
# via bumpversion
bumpversion==0.6.0
# via -r ./requirements.in
celery==5.2.2
celery==5.2.7
# via -r ./requirements.in
certifi==2022.6.15
certifi==2023.5.7
# via
# minio
# requests
cffi==1.15.1
# via cryptography
charset-normalizer==2.1.0
charset-normalizer==3.1.0
# via requests
click==8.1.3
# via
@@ -55,15 +53,15 @@ click==8.1.3
# click-repl
# django-click
# pip-tools
click-didyoumean==0.0.3
click-didyoumean==0.3.0
# via celery
click-plugins==1.1.1
# via celery
click-repl==0.2.0
# via celery
coverage==5.5
coverage[toml]==7.2.5
# via pytest-cov
cryptography==39.0.1
cryptography==40.0.2
# via
# -r ./requirements.in
# pyjwt
@@ -71,45 +69,47 @@ decorator==5.1.1
# via ipython
defusedxml==0.7.1
# via python3-openid
dj-database-url==0.5.0
dj-database-url==2.0.0
# via environs
dj-email-url==1.0.2
dj-email-url==1.0.6
# via environs
django==4.2
django==4.2.1
# via
# -r ./requirements.in
# dj-database-url
# django-allauth
# django-db-geventpool
# django-extensions
# django-haystack
# django-health-check
# django-js-asset
# django-machina
# django-redis
# django-rest-auth
# django-storages
# djangorestframework
# model-bakery
django-admin-env-notice==0.4
django-admin-env-notice==1.0
# via -r ./requirements.in
django-allauth==0.53.1
# via -r ./requirements.in
django-bakery==0.12.7
django-bakery==0.13.2
# via -r ./requirements.in
django-cache-url==3.2.3
django-cache-url==3.4.4
# via environs
django-click==2.3.0
# via -r ./requirements.in
django-db-geventpool==4.0.0
django-db-geventpool==4.0.1
# via -r ./requirements.in
django-extensions==3.1.3
django-extensions==3.2.1
# via -r ./requirements.in
django-haystack==3.2.1
# via
# -r ./requirements.in
# django-machina
django-health-check==3.16.4
django-health-check==3.17.0
# via -r ./requirements.in
django-js-asset==1.2.2
django-js-asset==2.0.0
# via django-mptt
django-machina==1.2.0
# via -r ./requirements.in
@@ -117,17 +117,17 @@ django-mptt==0.14.0
# via
# -r ./requirements.in
# django-machina
django-redis==5.0.0
django-redis==5.2.0
# via -r ./requirements.in
django-rest-auth==0.9.5
# via -r ./requirements.in
django-storages==1.13.2
# via -r ./requirements.in
django-test-plus==1.4.0
django-test-plus==2.2.1
# via -r ./requirements.in
django-tracer==0.9.3
# via -r ./requirements.in
django-widget-tweaks==1.4.9
django-widget-tweaks==1.4.12
# via
# -r ./requirements.in
# django-machina
@@ -135,19 +135,19 @@ djangorestframework==3.14.0
# via
# -r ./requirements.in
# django-rest-auth
environs[django]==9.3.2
environs[django]==9.5.0
# via -r ./requirements.in
executing==0.8.3
executing==1.2.0
# via stack-data
faker==9.8.2
faker==18.9.0
# via -r ./requirements.in
fastcore==1.5.5
fastcore==1.5.29
# via ghapi
fs==2.4.13
fs==2.4.16
# via django-bakery
gevent==22.10.2
# via -r ./requirements.in
ghapi==0.1.23
ghapi==1.0.3
# via -r ./requirements.in
greenlet==2.0.1
# via
@@ -155,41 +155,42 @@ greenlet==2.0.1
# gevent
gunicorn==20.1.0
# via -r ./requirements.in
idna==3.3
idna==3.4
# via requests
iniconfig==1.1.1
iniconfig==2.0.0
# via pytest
ipython==8.4.0
ipython==8.13.2
# via -r ./requirements.in
jedi==0.18.1
jedi==0.18.2
# via ipython
jmespath==0.10.0
jmespath==1.0.1
# via
# boto3
# botocore
kombu==5.2.4
# via celery
markdown2==2.4.1
markdown2==2.4.8
# via django-machina
marshmallow==3.11.1
marshmallow==3.19.0
# via environs
matplotlib-inline==0.1.3
matplotlib-inline==0.1.6
# via ipython
minio==7.1.14
# via -r ./requirements.in
mistletoe==0.8.2
mistletoe==1.0.1
# via -r ./requirements.in
model-bakery==1.11
model-bakery==1.11.0
# via -r ./requirements.in
mypy-extensions==0.4.3
mypy-extensions==1.0.0
# via black
oauthlib==3.2.2
# via requests-oauthlib
packaging==23.0
packaging==23.1
# via
# build
# fastcore
# ghapi
# marshmallow
# pytest
parso==0.8.3
# via jedi
@@ -205,11 +206,11 @@ pillow==9.4.0
# django-machina
pip-tools==6.13.0
# via -r ./requirements.in
platformdirs==3.2.0
platformdirs==3.5.1
# via black
pluggy==0.13.1
pluggy==1.0.0
# via pytest
prompt-toolkit==3.0.18
prompt-toolkit==3.0.38
# via
# click-repl
# ipython
@@ -223,40 +224,41 @@ pure-eval==0.2.2
# via stack-data
pycparser==2.21
# via cffi
pygments==2.12.0
pygments==2.15.1
# via ipython
pyjwt[crypto]==2.6.0
pyjwt[crypto]==2.7.0
# via django-allauth
pyproject-hooks==1.0.0
# via build
pytest==7.2.2
pytest==7.3.1
# via
# -r ./requirements.in
# pytest-cov
# pytest-django
pytest-cov==2.11.1
pytest-cov==4.0.0
# via -r ./requirements.in
pytest-django==4.2.0
pytest-django==4.5.2
# via -r ./requirements.in
python-dateutil==2.8.1
python-dateutil==2.8.2
# via
# botocore
# faker
python-dotenv==0.17.1
python-dotenv==1.0.0
# via environs
python-frontmatter==1.0.0
# via -r ./requirements.in
python-json-logger==2.0.1
python-json-logger==2.0.7
# via -r ./requirements.in
python3-openid==3.2.0
# via django-allauth
pytz==2021.1
pytz==2023.3
# via
# celery
# djangorestframework
# fs
pyyaml==6.0
# via python-frontmatter
# via
# python-frontmatter
# responses
redis==4.5.4
# via
# -r ./requirements.in
@@ -269,9 +271,9 @@ requests==2.28.2
# responses
requests-oauthlib==1.3.1
# via django-allauth
responses==0.22.0
responses==0.23.1
# via -r ./requirements.in
s3transfer==0.4.2
s3transfer==0.6.1
# via boto3
six==1.16.0
# via
@@ -280,23 +282,21 @@ six==1.16.0
# django-rest-auth
# fs
# python-dateutil
sqlparse==0.4.1
sqlparse==0.4.4
# via django
stack-data==0.2.0
stack-data==0.6.2
# via ipython
structlog==21.1.0
structlog==23.1.0
# via -r ./requirements.in
text-unidecode==1.3
# via faker
toml==0.10.2
# via responses
traitlets==5.2.1.post0
traitlets==5.9.0
# via
# ipython
# matplotlib-inline
types-toml==0.10.8.1
types-pyyaml==6.0.12.9
# via responses
urllib3==1.26.4
typing-extensions==4.5.0
# via dj-database-url
urllib3==1.26.15
# via
# botocore
# minio
@@ -306,17 +306,17 @@ vine==5.0.0
# via
# celery
# kombu
wcwidth==0.2.5
wcwidth==0.2.6
# via prompt-toolkit
wheel==0.38.1
wheel==0.40.0
# via
# -r ./requirements.in
# pip-tools
whitenoise==5.2.0
whitenoise==6.4.0
# via -r ./requirements.in
zope-event==4.5.0
zope-event==4.6
# via gevent
zope-interface==5.4.0
zope-interface==6.0
# via gevent
# The following packages are considered to be unsafe in a requirements file:

View File

@@ -1,4 +1,8 @@
[
{
"site_path": "/doc/",
"s3_path": "/site-docs/develop/"
},
{
"site_path": "/doc/user-guide/",
"s3_path": "/site-docs/develop/user-guide/"

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@@ -1,7 +1,7 @@
{% extends "admin/base.html" %}
{% load i18n %}
{% block title %}{{ title }} | {% trans 'boost.org admin' %}{% endblock %}
{% block title %}{{ title }} | {% trans 'boost.revsys.dev admin' %}{% endblock %}
{% block extrastyle %}{{ block.super }}
{% if ENVIRONMENT_NAME and ENVIRONMENT_COLOR %}
@@ -27,7 +27,7 @@
{% endblock %}
{% block branding %}
<h1 id="site-name">{% trans 'boost.org administration' %}</h1>
<h1 id="site-name">{% trans 'boost.revsys.dev administration' %}</h1>
{% endblock %}
{% block nav-global %}{% endblock %}
{% block nav-global %}{% endblock %}

View File

@@ -3,13 +3,25 @@
<html>
<head>
<title>{% block title %}{% endblock %}</title>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-1N51ZC252K"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-1N51ZC252K');
</script>
<title>{% block title %}Boost{% endblock %}</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{% block description %}{% endblock %}">
<meta name="keywords" content="{% block keywords %}{% endblock %}">
<meta name="author" content="{% block author %}{% endblock %}">
<link rel="shortcut icon" href="{% static 'img/Boost_Symbol_Transparent.svg' %}" type="image/x-icon">
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="{% static 'css/styles.css' %}" rel="stylesheet">
<!-- Google fonts for Cairo Medium and SemiBold -->
<link rel="preconnect" href="https://fonts.googleapis.com">
@@ -17,6 +29,8 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.css" />
<script src="//unpkg.com/alpinejs" defer></script>
<script src="https://unpkg.com/htmx.org@1.8.5" integrity="sha384-7aHh9lqPYGYZ7sTHvzP1t3BAfLhYSTy9ArHdP3Xsr9/3TlGurYgcPBoFmXX2TX/w" crossorigin="anonymous"></script>
<!-- TODO bring this local if we like it -->
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
<!-- detect dark or light mode -->
<script src="{% static 'js/DetectMode.js' %}"></script>
{% block extra_head %}{% endblock %}
@@ -39,8 +53,8 @@
if (m !== 'dark' && m !== 'light') return;
mode = m;
}"
class="h-screen dark:bg-black bg-stone/60 font-cairo" {% block body_id %}{% endblock %}>
<div class="md:mx-auto lg:container">
class="h-screen bg-gray-200 dark:bg-black font-cairo" {% block body_id %}{% endblock %}>
<div class="container md:mx-auto md:mt-32">
{% include "includes/_header.html" %}
@@ -51,7 +65,7 @@
{% comment %}body block is for use in forums{% endcomment %}
{% block forum_body %}{% endblock %}
<div class="md:px-11">
<div class="md:px-6 min-vh-110">
{% block content_wrapper %}
{% block subnav %}{% endblock %}
{% block content %}{% endblock %}

View File

@@ -0,0 +1,46 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Community" %}{% endblock %}
{% block content %}
<div class="py-0 px-3 mb-3 md:py-6 md:px-0">
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<div class="p-6 text-white bg-white rounded-lg shadow-lg dark:text-white text-slate dark:bg-charcoal dark:bg-neutral-700">
<h5 class="text-2xl font-bold leading-tight text-orange"><a href="https://lists.boost.org/mailman/listinfo.cgi/boost">Mailing List</a></h5>
<ul class="pt-4">
<li>Users</li>
<li>Developers</li>
<li>Announcements</li>
</ul>
</div>
<div class="p-6 text-white bg-white rounded-lg shadow-lg dark:text-white text-slate dark:bg-charcoal dark:bg-neutral-700">
<h5 class="text-2xl font-bold leading-tight text-orange"><a href="https://lists.boost.org/Archives/boost/">Discussion Forums</a></h5>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. </p>
</div>
<div class="p-6 text-white bg-white rounded-lg shadow-lg dark:text-white text-slate dark:bg-charcoal dark:bg-neutral-700">
<h5 class="text-2xl font-bold leading-tight text-orange"><a href="https://cppalliance.org/slack/">C++ Slack Workspace</a></h5>
<p>This is a multi-channel Slack workspace environment which gathers users of C++ from around the world into the same place so we can all learn from each other. It is like IRC but with modern clients that support multimedia, notifications, and smart devices.</p>
</div>
<div class="p-6 text-white bg-white rounded-lg shadow-lg dark:text-white text-slate dark:bg-charcoal dark:bg-neutral-700">
<h5 class="text-2xl font-bold leading-tight text-orange">Users</h5>
<p>See everyone involved with Boost. (Under construction)</p>
</div>
<div class="p-6 text-white bg-white rounded-lg shadow-lg dark:text-white text-slate dark:bg-charcoal dark:bg-neutral-700">
<h5 class="text-2xl font-bold leading-tight text-orange"><a href="https://twitter.com/boost_libraries">Twitter</a></h5>
<p>Follow us, like, and retweet announcements and informative updates about Boost and C++.</p>
</div>
<div class="p-6 text-white bg-white rounded-lg shadow-lg dark:text-white text-slate dark:bg-charcoal dark:bg-neutral-700">
<h5 class="text-2xl font-bold leading-tight text-orange"><a href="https://isocpp.org/">ISO C++</a></h5>
<p>News, Status & Discussion about Standard C++</p>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,7 +1,9 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Learn" %}{% endblock %}
{% comment %}
This is a temporary landing page for docs
{% endcomment %}
@@ -12,12 +14,11 @@ This is a temporary landing page for docs
<div class="mb-4 space-y-4 md:flex md:space-y-0 md:space-x-4">
<div
class="block relative w-full text-white bg-white rounded-lg shadow-lg md:w-1/2 dark:bg-charcoal dark:bg-neutral-700">
<a href="#" class="absolute top-5 right-10 py-3 px-4 text-white rounded shadow-md bg-orange">New Here?</a>
{# FIXME #}
<a href="/doc/user-guide/">
<img
class="overflow-y-hidden w-full rounded-t-lg max-h-[470px]"
src="{% static 'img/fpo/guide.jpg' %}"
src="{% static 'img/fpo/user_guide.png' %}"
alt="" />
</a>
<div class="py-3 px-6 dark:text-white text-slate">
@@ -27,18 +28,11 @@ This is a temporary landing page for docs
</h5>
<p class="py-1 border-b border-gray-700 text-green">How to use Boost in your programs</p>
<ul class="flex flex-wrap mt-2 text-sm">
<li class="w-1/2"><a href="#">Get the Release</a></li>
<li class="w-1/2"><a href="#">Including Headers</a></li>
<li class="w-1/2"><a href="#">Using Bjam</a></li>
<li class="w-1/2"><a href="#">Running Tests</a></li>
<li class="w-1/2"><a href="#">Reporting Issues</a></li>
<li class="w-1/2"><a href="#">Library Documentation</a></li>
<li class="w-1/2"><a href="#">Build the Libraries</a></li>
<li class="w-1/2"><a href="#">Linking Your Program</a></li>
<li class="w-1/2"><a href="#">Using CMake</a></li>
<li class="w-1/2"><a href="#">Using Bjam</a></li>
<li class="w-1/2"><a href="#">Requesting Features</a></li>
<li class="w-1/2"><a href="#">Toolsets</a></li>
<li class="w-1/2"><a href="/doc/user-guide/intro.html">Introduction to Boost</a></li>
<li class="w-1/2"><a href="/doc/user-guide/getting-started.html">Getting Started</a></li>
<li class="w-1/2"><a href="/doc/user-guide/boost-history.html">History of Boost</a></li>
<li class="w-1/2"><a href="/doc/user-guide/library-naming.html">Library Organization</a></li>
<li class="w-1/2"><a href="/doc/user-guide/header-organization-compilation.html">Header Organization</a></li>
</ul>
</div>
</div>
@@ -49,7 +43,7 @@ This is a temporary landing page for docs
<a href="/doc/contributor-guide/">
<img
class="overflow-y-hidden w-full rounded-t-lg max-h-[470px]"
src="{% static 'img/fpo/man-tree.jpeg' %}"
src="{% static 'img/fpo/contributor_guide.png' %}"
alt="" />
</a>
<div class="py-3 px-6 dark:text-white text-slate">
@@ -57,20 +51,14 @@ This is a temporary landing page for docs
class="text-xl font-bold leading-tight text-orange">
Contributor Guide
</h5>
<p class="py-1 border-b border-gray-300 text-white/60">This is how you can help</p>
<p class="py-1 border-b border-gray-700 text-green">This is how you can help</p>
<ul class="flex flex-wrap mt-2 text-sm">
<li class="w-1/2"><a href="#">Get the Release</a></li>
<li class="w-1/2"><a href="#">Including Headers</a></li>
<li class="w-1/2"><a href="#">Using Bjam</a></li>
<li class="w-1/2"><a href="#">Running Tests</a></li>
<li class="w-1/2"><a href="#">Reporting Issues</a></li>
<li class="w-1/2"><a href="#">Library Documentation</a></li>
<li class="w-1/2"><a href="#">Build the Libraries</a></li>
<li class="w-1/2"><a href="#">Linking Your Program</a></li>
<li class="w-1/2"><a href="#">Using CMake</a></li>
<li class="w-1/2"><a href="#">Using Bjam</a></li>
<li class="w-1/2"><a href="#">Requesting Features</a></li>
<li class="w-1/2"><a href="#">Toolsets</a></li>
<li class="w-1/2"><a href="/doc/contributor-guide/intro.html">Becoming a Contributor</a></li>
<li class="w-1/2"><a href="/doc/contributor-guide/license-requirements.html">License Requirements</a></li>
<li class="w-1/2"><a href="/doc/contributor-guide/portability-requirements.html">Portability Requirements</a></li>
<li class="w-1/2"><a href="/doc/contributor-guide/organization-requirements.html">Organization Requirements</a></li>
<li class="w-1/2"><a href="/doc/contributor-guide/library-design-guidelines.html">Library Design Guidelines</a></li>
<li class="w-1/2"><a href="/doc/contributor-guide/antora.html">Antora Guide</a></li>
</ul>
</div>
</div>
@@ -80,13 +68,13 @@ This is a temporary landing page for docs
<div
class="flex flex-col text-white bg-white rounded-lg shadow-lg md:flex-row dark:bg-charcoal dark:bg-neutral-700">
{# FIXME #}
<a href="/doc/formal-reviews/">
<a href="/doc/formal-reviews/" class="block md:w-1/2">
<img
class="object-cover w-full h-96 rounded-t-lg md:w-48 md:h-auto md:rounded-none md:rounded-l-lg"
src="{% static 'img/fpo/clipboardman.jpeg' %}"
class="object-cover overflow-x-hidden w-auto h-full rounded-t-lg md:rounded-none md:rounded-l-lg"
src="{% static 'img/fpo/formal_reviews.png' %}"
alt="" />
</a>
<div class="flex flex-col justify-start p-6 dark:text-white text-slate">
<div class="flex flex-col justify-start p-6 md:w-1/2 dark:text-white text-slate">
<h5
class="text-xl font-bold leading-tight">
Boost Formal Reviews
@@ -95,18 +83,14 @@ This is a temporary landing page for docs
How libraries become part of the collection.
</p>
<ul class="flex flex-wrap">
<li class="w-1/2"><a href="#">Proposing</a></li>
<li class="w-1/2"><a href="#">Scheduling</a></li>
<li class="w-1/2"><a href="#">The Manager</a></li>
<li class="w-1/2"><a href="#">The Review Process</a></li>
<li class="w-1/2"><a href="#">Submitting a Review</a></li>
<li class="w-1/2"><a href="/doc/formal-reviews/index.html">Introduction</a></li>
</ul>
</div>
</div>
<div
class="flex flex-col bg-white rounded-lg shadow-lg md:flex-row dark:bg-charcoal dark:bg-neutral-700">
<div class="flex flex-col justify-start p-6 w-full dark:text-white text-slate">
<div class="flex flex-col justify-start p-6 md:w-1/2 dark:text-white text-slate">
<h5
class="text-xl font-bold leading-tight">
Release Process
@@ -115,19 +99,14 @@ This is a temporary landing page for docs
"The trains always run on time"
</p>
<ul class="flex flex-wrap">
<li class="w-1/2"><a href="#">Release Calendar</a></li>
<li class="w-1/2"><a href="#">Process Steps</a></li>
<li class="w-1/2"><a href="#">Betas and RCs</a></li>
<li class="w-1/2"><a href="#">Testing the release</a></li>
<li class="w-1/2"><a href="#">What's in the Archive</a></li>
<li class="w-1/2"><a href="#">Library Debuts</a></li>
<li class="w-1/2"><a href="/doc/release-process/index.html">Introduction</a></li>
</ul>
</div>
{# FIXME #}
<a href="/doc/release-process/">
<a href="/doc/release-process/" class="block order-first md:order-last md:w-1/2">
<img
class="object-cover w-full h-96 rounded-t-lg md:w-48 md:h-auto md:rounded-none md:rounded-r-lg"
src="{% static 'img/fpo/construction.png' %}"
class="object-cover object-right overflow-x-hidden w-auto h-full rounded-t-lg md:rounded-none md:rounded-r-lg"
src="{% static 'img/fpo/release_process.png' %}"
alt="" />
</a>
</div>

View File

@@ -4,7 +4,7 @@
{% block content %}
<!-- Homepage Hero Section -->
<main class="px-4 my-16 mx-auto sm:mt-24 md:max-w-7xl">
<main class="px-4 my-16 mx-auto sm:mt-24">
<div class="md:flex">
<div class="order-1 mt-6 w-full text-center md:order-2 md:mt-3 md:w-1/2">
<div id="scene03"></div>
@@ -15,10 +15,10 @@
</h1>
<p class="mx-auto mt-3 max-w-md text-base text-lg md:mt-5 md:mb-11 md:max-w-3xl md:text-xl">Experience C++ libraries created by experts to be reliable, skillfully designed, and well-tested.</p>
<div class="justify-center mt-5 space-y-3 max-w-md md:flex md:justify-between md:mt-8 md:space-y-0">
<a href="#" class="flex justify-center items-center py-3 px-8 text-base font-medium text-white rounded-md border md:py-4 md:px-4 md:text-lg border-orange bg-orange dark:bg-charcoal dark:text-orange">
<a href="{% url 'releases' %}" class="flex justify-center items-center py-3 px-8 text-base font-medium text-white rounded-md border md:py-4 md:px-4 md:text-lg border-orange bg-orange dark:bg-charcoal dark:text-orange">
<i class="pr-2 text-white fas fa-arrow-circle-down dark:text-orange"></i> Download Latest
</a>
<a href="#" class="flex justify-center items-center py-3 px-8 text-base font-medium bg-gray-300 rounded-md border md:py-4 md:px-10 md:text-lg border-steel text-slate dark:bg-charcoal dark:text-orange">
<a href="{% url 'releases' %}" class="flex justify-center items-center py-3 px-8 text-base font-medium bg-gray-300 rounded-md border md:py-4 md:px-10 md:text-lg border-steel text-slate dark:bg-charcoal dark:text-orange">
Version Details <i class="pl-2 fas fa-chevron-right text-slate dark:text-orange"></i>
</a>
</div>

View File

@@ -1,51 +1,15 @@
{% load static %}
<footer class="py-5 px-4 my-5 mx-auto max-w-full sm:mt-24 md:py-4 md:my-6">
{% if not request.user.is_authenticated %}
<div class="md:flex" id="footerSignup">
<div class="grid gap-4 content-center text-left md:w-1/2">
<div>
<h1 class="block pt-6 text-4xl font-extrabold tracking-tight sm:text-5xl md:text-6xl xl:inline">
Join the growing community
</h1>
</div>
<div class="grid grid-cols-1 gap-4 my-16 md:grid-cols-2">
<div><img class="float-left mt-1 mr-3 w-auto h-8" src="{% static 'img/icons/icon_Cup-C++.svg' %}" alt=""> Best collections of C++ libraries</div>
<div><img class="float-left mt-1 mr-3 w-auto h-8" src="{% static 'img/icons/icon_graduation-cap.svg' %}" alt=""> Supports research and education for C++</div>
<div><img class="float-left mt-1 mr-3 w-auto h-8" src="{% static 'img/icons/icon_down-arrow-stack.svg' %}" alt=""> Accessible with minimal restriction</div>
<div><img class="float-left mt-1 mr-3 w-auto h-8" src="{% static 'img/icons/icon_three-people.svg' %}" alt=""> Collaborative community</div>
</div>
<div class="hidden mt-5 max-w-md sm:flex md:mt-8">
<div class="rounded-md shadow">
<a href="{% url 'account_signup' %}" class="flex justify-center items-center py-3 px-8 w-full text-base font-medium text-white rounded-md border md:py-3 md:px-8 md:text-lg border-orange bg-orange dark:text-orange dark:bg-charcoal">Sign Up <i class="pl-2 text-white fas fa-chevron-right dark:text-orange"></i> </a>
</div>
</div>
</div>
<div class="text-center md:w-1/2">
<div id="scene01"></div>
</div>
</div>
{% endif %}
<footer class="py-5 px-4 my-5 mx-auto max-w-full sm:mt-24 md:pt-4 md:pb-11 md:mt-6 md:mb-11">
<div class="items-center px-5 pt-4 mt-16 md:flex">
<div class="mr-11">
<img class="hidden mb-1 w-auto dark:inline-block h-[40px]"
src="{% static 'img/Boost_Brandmark_WhiteBoost_Transparent.svg' %}"
alt="Boost">
<img class="inline-block mb-1 w-auto dark:hidden h-[40px]"
src="{% static 'img/Boost_Brandmark_BlackBoost_Transparent.svg' %}"
alt="Boost">
</div>
<div class="justify-between pt-3 pb-3 w-4/5 md:space-x-3 xl:space-x-10">
<a class="block my-2 md:inline" href="/">&#169; 2022 Boost.org</a>
<a class="block my-2 md:inline" href="{% url 'contact' %}">Contact</a>
<a class="block my-2 md:inline" href="">Privacy Policy</a>
<a class="block my-2 md:inline" href="">Terms of Use</a>
</div>
<div class="pt-3 pb-3 space-x-3 w-1/5 md:space-x-5 md:text-right">
<a href="https://twitter.com/boost_libraries" target="_blank"><i class="fab fa-twitter text-slate dark:text-white/50 dark:hover:text-orange hover:text-orange"></i></a>
<a href="https://github.com/boostorg" target="_blank"><i class="fab fa-github text-slate dark:text-white/50 dark:hover:text-orange hover:text-orange"></i></a>
<div class="items-center px-5 pt-4 mt-16 text-center">
<div class="justify-between pt-3 pb-3 md:space-x-3 xl:space-x-6">
<span>&#169; The C Plus Plus Alliance, Inc.</span>
<a class="block my-2 md:inline hover:text-orange" href="{% url 'contact' %}">Contact</a>
<a class="block my-2 md:inline hover:text-orange" href="">Privacy Policy</a>
<a class="block my-2 md:inline hover:text-orange" href="">Terms of Use</a>
<a href="https://twitter.com/boost_libraries" target="_blank" class="hover:text-orange">Twitter</a>
<a href="https://github.com/boostorg" target="_blank" class="hover:text-orange">GitHub</a>
</div>
</div>
</footer>

View File

@@ -2,32 +2,32 @@
{% load account socialaccount %}
<div class="relative">
<div class="relative pb-11 md:pb-6">
<div>
<div class="hidden items-center py-5 border-b-2 border-gray-300 md:flex darK:border-charcoal">
<div class="w-[200px]">
<div class="relative z-50 bg-gray-200 md:fixed md:top-0 md:right-0 md:left-0 dark:bg-black">
<div class="relative mx-auto max-w-7xl">
<div class="container mx-auto">
<div class="hidden items-center pt-2 pb-2 border-b-2 border-gray-300 md:flex dark:border-charcoal">
<div class="w-[130px]">
<a href="{% url 'home' %}">
<img class="hidden -mb-1 w-auto dark:inline-block h-[50px]"
<img class="hidden -mb-1 w-auto dark:inline-block h-[32px]"
src="{% static 'img/Boost_Brandmark_WhiteBoost_Transparent.svg' %}"
alt="Boost">
<img class="inline-block -mb-1 w-auto dark:hidden h-[50px]"
<img class="inline-block -mb-1 w-auto dark:hidden h-[32px]"
src="{% static 'img/Boost_Brandmark_BlackBoost_Transparent.svg' %}"
alt="Boost">
</a>
</div>
<div class="flex pt-5 w-full">
<nav class="relative items-center pl-6 space-x-10 w-3/4 text-lg text-left">
<div class="flex pt-2 w-full">
<nav class="relative items-center pl-6 space-x-4 w-full text-lg text-left md:space-x-8 lg:space-x-10">
<a href="{% url 'news' %}" class="font-semibold dark:font-medium dark:text-white text-slate dark:hover:text-orange hover:text-orange">News</a>
<a href="{% url 'docs' %}" class="font-semibold dark:font-medium dark:text-white text-slate dark:hover:text-orange hover:text-orange">Learn</a>
<a href="/forum/" class="font-semibold dark:font-medium dark:text-white text-slate dark:hover:text-orange hover:text-orange">Community</a>
<a href="{% url 'community' %}" class="font-semibold dark:font-medium dark:text-white text-slate dark:hover:text-orange hover:text-orange">Community</a>
<a href="/libraries/" class="font-semibold dark:font-medium dark:text-white text-slate dark:hover:text-orange hover:text-orange">Libraries</a>
<a href="#" class="font-semibold dark:font-medium dark:text-white text-slate dark:hover:text-orange hover:text-orange">Releases</a>
<a href="{% url 'releases' %}" class="font-semibold dark:font-medium dark:text-white text-slate dark:hover:text-orange hover:text-orange">Releases</a>
</nav>
<nav class="items-center space-x-5 w-1/4 text-right" x-data="{ 'searchOpen': false }">
<nav class="float-right items-center space-x-3 w-44 text-right lg:space-x-5" x-data="{ 'searchOpen': false }">
<span class="relative">
<i class="inline-flex -mt-1 cursor-pointer text-slate fas fa-search dark:text-white/50 dark:hover:text-orange hover:text-orange" @click="searchOpen = !searchOpen"></i>
<i class="inline -mt-1 cursor-pointer text-slate fas fa-search dark:text-white/50 dark:hover:text-orange hover:text-orange" @click="searchOpen = !searchOpen"></i>
<div x-show="searchOpen"
@click.away="searchOpen = false"
x-transition:enter="transition ease-out duration-100"
@@ -36,7 +36,7 @@
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95"
class="absolute -top-2 -right-2 z-10"
class="absolute -right-2 z-10 -top-[2px]"
x-ref="search-form"
x-description="Search"
role="menu"
@@ -54,16 +54,11 @@
required="required"
type="text"
>
<span class="flex absolute inset-y-0 right-3 items-center pl-2">
<button type="submit">
<i class="fas fa-search text-charcoal dark:text-white/40 dark:hover:text-orange hover:text-orange"></i>
</button>
</span>
</form>
</div>
</span>
<a href="{% url 'support' %}" class="inline-flex"><i class="text-slate fas fa-question-circle dark:text-white/50 dark:hover:text-orange hover:text-orange"></i></a>
<a href="{% url 'support' %}" class="inline"><i class="text-slate fas fa-question-circle dark:text-white/50 dark:hover:text-orange hover:text-orange"></i></a>
<!-- theme switcher -->
<span x-data="{ 'modeOpen': false }" class="relative">
@@ -100,16 +95,16 @@
>
<a
@click="mode='light'; setColorMode('light'); modeOpen = false;"
:class="{'font-bold': mode === 'light', 'font-thin': mode !== 'light' }"
:class="{'font-bold': mode === 'light', 'font-medium': mode !== 'light' }"
class="block py-2 text-xs cursor-pointer dark:text-white text-charcoal dark:hover:text-orange hover:text-orange"
>
<i class="inline-block mr-1 fas fa-sun text-gold"></i>
<i class="inline-block mr-1 font-semibold fas fa-sun text-gold"></i>
Light Mode
</a>
<a
@click="mode = 'dark'; setColorMode('dark'); modeOpen = false;"
:class="{'font-bold': mode === 'dark', 'font-thin': mode !== 'dark' }"
:class="{'font-bold': mode === 'dark', 'font-medium': mode !== 'dark' }"
class="block py-2 text-xs cursor-pointer dark:text-white text-charcoal dark:hover:text-orange hover:text-orange"
>
<i class="inline-block mr-1 fas fa-moon text-sky-600"></i>
@@ -120,12 +115,12 @@
<span x-data="{ 'userOpen': false }" class="relative">
{% if not user.is_authenticated %}
<a href="{% url 'account_signup' %}" class="font-medium dark:text-white text-charcoal dark:hover:text-orange hover:text-orange">Join</a>
<a href="{% url 'account_signup' %}" class="inline font-medium dark:text-white text-charcoal dark:hover:text-orange hover:text-orange">Join</a>
{% else %}
{% if user.image %}
<img src="{{ user.image.url }}" alt="user" class="inline-flex rounded-sm cursor-pointer w-[30px]" @click="userOpen = !userOpen" />
<img src="{{ user.image.url }}" alt="user" class="inline -mt-1 rounded-sm cursor-pointer w-[30px]" @click="userOpen = !userOpen" />
{% else %}
<i class="inline-flex mr-2 text-5xl fas fa-user text-charcoal dark:text-white/60"></i>
<i class="inline mr-2 cursor-pointer fas fa-user text-charcoal dark:text-white/60" @click="userOpen = !userOpen"></i>
{% endif %}
{% endif %}
@@ -147,8 +142,8 @@
tabindex="-1"
style="display: none;"
>
<a href="#" class="block py-2 text-xs dark:text-white text-charcoal dark:hover:text-orange hover:text-orange">My Profile</a>
<a href="{% url 'account_logout' %}" class="block py-2 text-xs dark:text-white text-charcoal dark:hover:text-orange hover:text-orange">Log Out</a>
<a href="#" class="block py-2 text-xs font-medium dark:text-white text-charcoal dark:hover:text-orange hover:text-orange">My Profile</a>
<a href="{% url 'account_logout' %}" class="block py-2 text-xs font-medium dark:text-white text-charcoal dark:hover:text-orange hover:text-orange">Log Out</a>
</div>
{% endif %}
</span>
@@ -204,25 +199,19 @@
</div>
<div x-show="isOpen" class="absolute inset-x-0 top-10 z-50 h-screen bg-charcoal">
<div class="px-2 pt-2 pb-3 text-2xl">
<a href="/versions/" class="block py-2 px-3 text-white dark:hover:text-orange hover:text-orange">Versions</a>
<a href="{% url 'news' %}" class="block py-2 px-3 text-white dark:hover:text-orange hover:text-orange">News</a>
<a href="{% url 'docs' %}" class="block py-2 px-3 text-white dark:hover:text-orange hover:text-orange">Learn</a>
<a href="{% url 'community' %}" class="block py-2 px-3 text-white dark:hover:text-orange hover:text-orange">Community</a>
<a href="/libraries/" class="block py-2 px-3 text-white dark:hover:text-orange hover:text-orange">Libraries</a>
<a href="{% url 'review-process' %}" class="block py-2 px-3 text-white dark:hover:text-orange hover:text-orange">Review Process</a>
<a href="/forum/" class="block py-2 px-3 text-white dark:hover:text-orange hover:text-orange">Forums</a>
<a href="{% url 'news' %}" class="block py-2 px-3 text-white dark:hover:text-orange hover:text-orange">News</a>
<a href="{% url 'donate' %}" class="block py-2 px-3 text-white dark:hover:text-orange hover:text-orange">Donate</a>
<a href="{% url 'releases' %}" class="block py-2 px-3 text-white dark:hover:text-orange hover:text-orange">Releases</a>
</div>
<div class="absolute left-0 bottom-10 px-2 pt-2 pb-3 text-sm">
<a href="{% url 'boost-about' %}" class="block py-2 px-3 text-gray-700 text-thin">About</a>
<a href="{% url 'support' %}" class="block py-2 px-3 text-gray-700 text-thin">Support</a>
<a href="{% url 'resources' %}" class="block py-2 px-3 text-gray-700 text-thin">Resources</a>
<a href="{% url 'account_signup' %}" class="block py-2 px-3 text-gray-700 text-thin">Sign Up</a>
<a href="#" class="block py-2 px-3 text-gray-700 text-thin">Log In </a>

View File

@@ -21,7 +21,7 @@
<p class="mb-3 text-gray-700 dark:text-gray-300">{{ library.description }}</p>
</div>
<div class="text-sm flex py-3 bottom-0 absolute w-[90%]">
<div class="w-1/6"><span class="py-1 px-2 text-sm font-bold text-gray-400 rounded-full border bg-green/20 border-green/40 dark:bg-green/10">{{ library.cpp_standard_minimum }}</span></div>
<div class="w-1/6"><span class="py-0 px-2 text-sm font-bold text-gray-600 rounded-full border dark:text-gray-300 bg-green/40 border-green/60 dark:bg-green/40">{{ library.cpp_standard_minimum }}</span></div>
<div class="w-1/6 tracking-wider text-charcoal dark:text-white/60">2yrs</div>
<div class="w-4/6 text-right dark:text-gray-400 text-slate">
{% for c in library.categories.all %}

View File

@@ -3,177 +3,279 @@
{% load static %}
{% block content %}
<!-- Breadcrumb used on filtered views -->
<div class="p-3 md:p-0">
<a class="text-orange" href="{% if version_slug %}{% url 'libraries-by-version' version_slug %}{% else %}{% url 'libraries' %}{% endif %}">Libraries{% if version_name %} ({{ version.name }}){% endif %}</a> > <a class="text-orange" href="{% url 'libraries' %}">Specific</a> > <span class="capitalize">{{ object.name }}</span>
<!-- Breadcrumb -->
<div class="p-3 space-x-2 text-sm divide-x divide-gray-300 md:p-0">
<a class="hover:text-orange" href="/"><i class="fas fa-home"></i></a>
<a class="pl-2 hover:text-orange" href="{% if version_slug %}{% url 'libraries-by-version' version_slug %}{% else %}{% url 'libraries' %}{% endif %}">Libraries{% if version_name %} ({{ version.name }}){% endif %}</a>
<span class="pl-2 capitalize">{{ object.name }}</span>
</div>
<!-- end breadcrumb -->
<div class="flex mt-4">
<div class="hidden md:block md:w-1/4">
<ul>
<li class="py-2"><a href="#overview">Overview</a></li>
<li class="py-2"><a href="#reviews">Reviews</a></li>
<li class="py-2"><a href="#discussion">Forum Discussion</a></li>
</ul>
</div>
<div class="px-3 mx-3 md:px-0 md:mx-0 md:w-3/4">
<div class="px-3 mx-3 w-full md:px-0 md:mx-0">
<!-- Overview -->
<div class="pb-16 mb-16 border-b border-slate">
<div class="pb-16 mb-16">
<!-- Form to select a version of this library -->
<form action="{% url 'library-detail' slug=object.slug %}" method="post" class="float-right">
{% csrf_token %}
<div>
<label for="id_version" hidden="true">Versions:</label>
<select onchange="this.form.submit()"
name="version"
class="block py-1 pr-8 pl-5 mb-3 w-full text-sm bg-white rounded-md border border-gray-300 cursor-pointer sm:inline-block md:mb-0 md:w-auto dark:bg-black text-sky-600 dark:text-orange dark:border-slate"
id="id_version"
>
<option>Filter by version</option>
{% for v in versions %}
<option value="{{ v.pk }}" {% if version == v %}selected="selected"{% endif %}>{{ v.display_name }}</option>
{% endfor %}
</select>
</div>
</form>
<div class="pb-6">
<h3 class="text-4xl capitalize" id="overview">{{ object.name }}</h3>
</div>
<p>{{ object.description }}</p>
<div class="mt-4 space-y-3 max-w-md text-sm uppercase md:flex md:mt-11 md:space-y-0 md:space-x-3">
<a href="#" class="inline-block py-3 px-6 rounded-md border text-orange border-slate">Documentation</a>
<a href="{{ object.github_url }}" class="inline-block py-3 px-6 rounded-md border text-orange border-slate">Github</a>
<h3 class="text-4xl capitalize text-orange" id="overview">
Boost.{{ object.name }}
</h3>
</div>
<div class="justify-between py-6 mt-4 md:flex md:mt-11">
<div>
<h3 class="text-2xl">Categories</h3>
{% for c in object.categories.all %}
<a href="{% url 'libraries-by-category' category=c.slug %}" class="text-orange">{{ c.name }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
</div>
<div class="p-4 md:flex md:space-x-3">
<div class="pr-3 space-y-2 w-full bg-white rounded-lg md:w-1/3">
<a class="block items-center py-1 px-2 rounded cursor-pointer hover:bg-gray-100" href="https://boost.org/libs/{{ object.slug }}">
<i class="float-right mt-3 fas fa-folder"></i>
Documentation
<span class="block text-xs text-sky-600">boost.org/libs/{{ object.slug }}</span>
</a>
<a class="block items-center py-1 px-2 rounded cursor-pointer hover:bg-gray-100" href="{{ object.github_issues_url }}">
<i class="float-right mt-3 fas fa-bug"></i>
GitHub Issues
<span class="block text-xs text-sky-600">{{ object.github_issues_url|cut:"https://" }}</span>
</a>
<a class="block items-center py-1 px-2 rounded cursor-pointer hover:bg-gray-100" href="{{ object.github_url }}">
<i class="float-right mt-3 fab fa-github"></i>
Source Code
<span class="block text-xs text-sky-600">{{ object.github_url|cut:"https://" }}</span>
</a>
<a class="inline-block py-1 px-2 rounded cursor-pointer hover:bg-gray-100">{% if object.first_github_tag_date%}Since {{ object.first_github_tag_date|date:"Y" }}{% endif %}</a>
<a class="inline-block float-right py-1 px-2 rounded cursor-pointer hover:bg-gray-100">{{ object.get_cpp_standard_minimum_display }}</a>
<span class="block py-1 px-2 rounded cursor-pointer hover:bg-gray-100">Categories:
{% for category in object.categories.all %}
<a class="inline text-sky-600" href="{% url 'libraries-by-category' category=category.slug %}">{{ category.name }}</a>
{% endfor %}
</span>
<div>
<h3 class="text-2xl">Minimum C++ Version</h3>
{{ object.cpp_standard_minimum }}
<p class="pt-4 pl-2 mt-4 text-sm border-t border-gray-200">{{ object.description }}</p>
</ul>
</div>
<div class="flex space-x-3 w-full bg-gray-300 rounded-lg md:w-2/3">
<div class="relative w-1/2">
<div id="chart1" class="absolute bottom-10 w-full text-center">Commits per Month Graph</div>
</div>
<div>
<h3 class="text-2xl">Active Development</h3>
<div class="text-orange">{% if object.active_development %}Yes{% else %}No{% endif %}</div>
<p>Last Update: {{ object.last_github_update|date:"F j, Y"}}</p>
</div>
</div>
<div class="py-6 mt-4 md:mt-11">
<h3 class="mb-4 text-2xl">Level of Activity</h3>
<div class="h-6 rounded bg-charcoal">
<!-- TODO: Change the width to use a percent of activity -->
<div class="w-3/4 h-6 bg-gradient-to-r rounded from-green to-orange"></div>
</div>
</div>
<div class="py-6 my-4 md:flex md:justify-between md:my-11">
<div class="py-6">
<h3 class="mb-1 text-2xl">Closed Pull Requests</h3>
{{ closed_prs_count }}
</div>
<div class="py-6">
<h3 class="mb-1 text-2xl">Open Issues</h3>
{{ open_issues_count }}
</div>
<div class="py-6">
<h3 class="mb-1 text-2xl">Commits Per Release</h3>
<p>ZZ</p>
</div>
</div>
<div class="md:flex md:py-11 md:space-x-11">
<div class="py-6">
<h3 class="mb-4 text-2xl">Authors</h3>
<div class="space-y-3">
{% for author in object.authors.all %}
<div>
{% if author.image %}
<img src="{{ author.image.url }}" alt="user" class="inline mr-2 rounded w-[47px]" />
{% else %}
<i class="mr-2 text-5xl fas fa-user text-white/60"></i>
{% endif %}
{{ author.get_display_name }}
</div>
{% endfor %}
</div>
</div>
<div class="py-6">
<h3 class="mb-4 text-2xl">Maintainers</h3>
<div class="space-y-3">
{% for maintainer in maintainers.all %}
<div>
{% if maintainer.image %}
<img src="{{ maintainer.image.url }}" alt="user" class="inline mr-2 rounded w-[47px]" />
{% else %}
<i class="mr-2 text-5xl fas fa-user text-white/60"></i>
{% endif %}
{{ maintainer.get_display_name }}
</div>
{% endfor %}
<div class="relative w-1/2">
<div id="chart2" class="absolute bottom-10 w-full text-center">Commits per Year Graph</div>
</div>
</div>
</div>
<!-- Avatars -->
<div class="p-5 my-6 w-full">
Profile photos here
</div>
<div class="p-4 my-4 bg-white rounded-lg">
<h3>Introduction</h3>
<p>Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec id elit non mi porta gravida at eget metus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.</p>
<p>Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Nulla vitae elit libero, a pharetra augue. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas faucibus mollis interdum.</p>
<p>Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Donec id elit non mi porta gravida at eget metus. Donec ullamcorper nulla non metus auctor fringilla. Maecenas faucibus mollis interdum. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.</p>
<p>Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Cras mattis consectetur purus sit amet fermentum. Donec sed odio dui.</p>
</div>
</div>
<!-- end overview -->
<!-- Review -->
<div class="pb-16 mb-16 border-b border-slate">
<h3 class="text-4xl" id="reviews">Review</h3>
<p>Date of Review: 10/31/20</p>
<div class="justify-between mt-11 md:flex">
<div>
<h3 class="text-2xl">ACCEPT Reviews:</h3>
<p>15 <a href="#" class="text-orange">View List</a></p>
<h3 class="mt-5 text-2xl">REJECT Reviews:</h3>
<p>3 <a href="#" class="text-orange">View List</a></p>
<div class="mt-11"><a href="#" class="py-2 px-4 rounded-md border text-orange border-slate">Review Results</a></div>
</div>
<div class="py-11 md:py-0">
<h3 class="mb-4 text-2xl">Review Manager</h3>
<div class="space-y-3">
<div><img src="{% static 'img/fpo/user.png' %}" alt="user" class="inline mr-5" /> Glen Fernandes</div>
</div>
</div>
<div class="py-3 md:py-0">
<h3 class="mb-4 text-2xl">Reviewers</h3>
<div class="space-y-3">
<div><img src="{% static 'img/fpo/user.png' %}" alt="user" class="inline mr-5" /> Glen Fernandes</div>
<div><img src="{% static 'img/fpo/user.png' %}" alt="user" class="inline mr-5" /> Glen Fernandes</div>
</div>
</div>
</div>
</div>
<!-- end review -->
<!-- Discussions -->
<div class="pb-6">
<h3 class="mb-11 text-4xl" id="discussion">Discussions</h3>
<div class="py-5 px-3 my-4 rounded md:px-11 md:my-0 bg-charcoal">
<div class="flex justify-between mb-6">
<span class="inline py-1 px-3 w-auto text-lg uppercase rounded text-green bg-green/10">Forum Discussions</span>
</div>
<div class="space-y-8 divide-y divide-slate">
<div class="pt-6">
<h3 class="text-xl">Topic of discussion headline from the Forum</h3>
<div class="mt-3"><img src="{% static 'img/fpo/user.png' %}" alt="user" class="inline mr-3" /> Name of Author</div>
</div>
<div class="pt-6">
<h3 class="text-xl">Topic of discussion headline from the Forum</h3>
<div class="mt-3"><img src="{% static 'img/fpo/user.png' %}" alt="user" class="inline mr-3" /> Name of Author</div>
</div>
<div class="pt-6">
<h3 class="text-xl">Topic of discussion headline from the Forum</h3>
<div class="mt-3"><img src="{% static 'img/fpo/user.png' %}" alt="user" class="inline mr-3" /> Name of Author</div>
</div>
<div class="pt-6">
<h3 class="text-xl">Topic of discussion headline from the Forum</h3>
<div class="mt-3"><img src="{% static 'img/fpo/user.png' %}" alt="user" class="inline mr-3" /> Name of Author</div>
</div>
</div>
</div>
</div>
<!-- end discussions -->
</div>
</div>
{% endblock %}
{% block footer_js %}
{#<script>#}
{# var options = {#}
{# series: [{#}
{# name: 'Commits',#}
{# data: [256, 255, 251, 260, 267, 230, 254, 242, 274, 265, 243, 234]#}
{# }],#}
{# chart: {#}
{# height: 350,#}
{# type: 'bar',#}
{# },#}
{# plotOptions: {#}
{# bar: {#}
{# borderRadius: 2,#}
{# dataLabels: {#}
{# position: 'top', // top, center, bottom#}
{# },#}
{# }#}
{# },#}
{# dataLabels: {#}
{# enabled: true,#}
{# formatter: function (val) {#}
{# //return val + "%";#}
{# return val;#}
{# },#}
{# offsetY: -20,#}
{# style: {#}
{# fontSize: '12px',#}
{# colors: ["#304758"]#}
{# }#}
{# },#}
{##}
{# xaxis: {#}
{# categories: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],#}
{# position: 'top',#}
{# axisBorder: {#}
{# show: false#}
{# },#}
{# axisTicks: {#}
{# show: false#}
{# },#}
{# crosshairs: {#}
{# fill: {#}
{# type: 'gradient',#}
{# gradient: {#}
{# colorFrom: '#D8E3F0',#}
{# colorTo: '#BED1E6',#}
{# stops: [0, 100],#}
{# opacityFrom: 0.4,#}
{# opacityTo: 0.5,#}
{# }#}
{# }#}
{# },#}
{# tooltip: {#}
{# enabled: true,#}
{# }#}
{# },#}
{# yaxis: {#}
{# axisBorder: {#}
{# show: true#}
{# },#}
{# axisTicks: {#}
{# show: true,#}
{# },#}
{# labels: {#}
{# show: true,#}
{# formatter: function (val) {#}
{# //return val + "%";#}
{# return val;#}
{# }#}
{# }#}
{##}
{# },#}
{# title: {#}
{# text: 'Commits per Month',#}
{# floating: true,#}
{# offsetY: 330,#}
{# align: 'center',#}
{# style: {#}
{# color: '#444'#}
{# }#}
{# }#}
{# };#}
{##}
{##}
{##}
{# var options2 = {#}
{# series: [{#}
{# name: 'Commits',#}
{# data: [1367, 981, 400, 2235, 1888]#}
{# }],#}
{# chart: {#}
{# height: 350,#}
{# type: 'bar',#}
{# },#}
{# plotOptions: {#}
{# bar: {#}
{# borderRadius: 2,#}
{# dataLabels: {#}
{# position: 'top', // top, center, bottom#}
{# },#}
{# }#}
{# },#}
{# dataLabels: {#}
{# enabled: true,#}
{# formatter: function (val) {#}
{# //return val + "%";#}
{# return val;#}
{# },#}
{# offsetY: -20,#}
{# style: {#}
{# fontSize: '12px',#}
{# colors: ["#304758"]#}
{# }#}
{# },#}
{##}
{# xaxis: {#}
{# categories: ["2019", "2020", "2021", "2022", "2023"],#}
{# position: 'top',#}
{# axisBorder: {#}
{# show: false#}
{# },#}
{# axisTicks: {#}
{# show: false#}
{# },#}
{# crosshairs: {#}
{# fill: {#}
{# type: 'gradient',#}
{# gradient: {#}
{# colorFrom: '#D8E3F0',#}
{# colorTo: '#BED1E6',#}
{# stops: [0, 100],#}
{# opacityFrom: 0.4,#}
{# opacityTo: 0.5,#}
{# }#}
{# }#}
{# },#}
{# tooltip: {#}
{# enabled: true,#}
{# }#}
{# },#}
{# yaxis: {#}
{# axisBorder: {#}
{# show: true#}
{# },#}
{# axisTicks: {#}
{# show: true,#}
{# },#}
{# labels: {#}
{# show: true,#}
{# formatter: function (val) {#}
{# //return val + "%";#}
{# return val;#}
{# }#}
{# }#}
{##}
{# },#}
{# title: {#}
{# text: 'Commits per Year',#}
{# floating: true,#}
{# offsetY: 330,#}
{# align: 'center',#}
{# style: {#}
{# color: '#444'#}
{# }#}
{# }#}
{# };#}
{##}
{# var chart = new ApexCharts(document.querySelector("#chart1"), options);#}
{# chart.render();#}
{##}
{# var chart2 = new ApexCharts(document.querySelector("#chart2"), options2);#}
{# chart2.render();#}
{#</script>#}
{% endblock %}

View File

@@ -1,30 +1,46 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Libraries" %}{% endblock %}
{% block content %}
<!-- Breadcrumb used on filtered views -->
<div class="p-3 md:p-0">
<div class="p-3 space-x-2 text-sm divide-x divide-gray-300 md:p-0">
{% if version_slug %}
<a class="text-orange" href="{% url 'libraries-by-version' version_slug %}">Libraries ({{ version_name }})</a>
{% else %}
<a class="text-orange" href="{% url 'libraries' %}">Libraries</a>
<a class="hover:text-orange" href="/"><i class="fas fa-home"></i></a>
<a class="pl-2 hover:text-orange" href="{% url 'libraries-by-version' version_slug %}">Libraries ({{ version_name }})</a>
{% endif %}
{% if category %} > Categorized >
<a class="text-orange" href="
{% if version_slug %}
{% url 'libraries-by-version-by-category' version_slug=version_slug category=category.slug%}
{% else %}
{% url 'libraries-by-category' category=category.slug %}
{% endif %}">{{ category.name }}</a>
<a class="pl-2 hover:text-orange" href="
{% if version_slug %}
{% url 'libraries-by-version-by-category' version_slug=version_slug category=category.slug%}
{% else %}
{% url 'libraries-by-category' category=category.slug %}
{% endif %}">{{ category.name }}</a>
{% endif %}
</div>
<!-- end breadcrumb -->
<div class="py-0 px-3 mb-3 md:flex md:py-6 md:px-0" x-data="{'showSearch': false}" x-on:keydown.escape="showSearch=false">
<div class="px-3 md:px-0 md:w-1/2">
<h2 class="my-5 text-4xl">Libraries</h2>
</div>
<div class="mt-3 space-y-3 w-full md:mt-0 md:w-1/2 md:text-right">
<div class="py-0 px-3 mb-3 text-right md:px-0 md:pt-6" x-data="{'showSearch': false}" x-on:keydown.escape="showSearch=false">
<form action="" method="post" class="float-right">
{% csrf_token %}
<div>
<label for="id_version" hidden="true">Versions:</label>
<select onchange="this.form.submit()"
name="version"
class="block py-1 pr-8 pl-5 mb-3 w-full text-sm bg-white rounded-md border border-gray-300 cursor-pointer sm:inline-block md:mb-0 md:w-auto dark:bg-black text-sky-600 dark:text-orange dark:border-slate"
id="id_version"
>
<option>Filter by version</option>
{% for v in versions %}
<option value="{{ v.pk }}" {% if version == v %}selected="selected"{% endif %}>{{ v.name }}</option>
{% endfor %}
</select>
</div>
</form>
<div class="mt-3 space-x-3 md:flex md:mt-0">
<div class="relative md:mb-6">
<span class="flex absolute inset-y-0 right-3 items-center pl-2">
<button type="submit" class="p-1 focus:outline-none focus:shadow-outline">
@@ -32,7 +48,7 @@
</button>
</span>
<input @click="showSearch = true; $nextTick(() => { setTimeout(() => { document.getElementById('q').focus(); }, 300);});"
type="search" name="q" class="py-2 px-3 w-full text-sm bg-white rounded-md border-gray-300 md:w-1/3 text-sky-600 dark:text-orange dark:border-slate dark:bg-charcoal focus:text-charcoal" type="text" value="" placeholder="Search Library" />
type="search" name="q" class="py-2 px-3 w-full text-sm bg-white rounded-md border-gray-300 md:w-1/3 min-w-[300px] text-sky-600 dark:text-orange dark:border-slate dark:bg-charcoal focus:text-charcoal" type="text" value="" placeholder="Search" />
</div>
<!-- Form to select a category -->

View File

@@ -0,0 +1,63 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "News" %}{% endblock %}
{% block content %}
<!-- Breadcrumb used on filtered views -->
<div class="p-3 md:p-0">
<a class="text-orange" href="{% url "news" %}">News</a> > {{ entry.title }}
</div>
<!-- end breadcrumb -->
<div class="py-0 px-3 mb-3 md:py-6 md:px-0">
<div class="py-16 md:mx-auto md:w-3/4">
<h1 class="text-4xl">{{ entry.title }}</h1>
<!-- ACTIONS -->
<p>
{% if not entry.is_approved %}
{% if user_can_approve %}
<form method="POST" action="{% url 'news-approve' entry.slug %}">
{% csrf_token %}
<button type="submit" name="approve">{% translate "Approve" %}</button>
</form>
{% else %}
<strong>{% translate "Pending Moderation" %}</strong>
{% endif %}
{% endif %}
</p>
<!-- END ACTIONS -->
<p class="mt-0 text-sm font-light">{{ entry.publish_at|date:"M jS, Y" }}</p>
<p>{{ entry.description|linebreaks }}</p>
</div>
<div class="flex py-8 my-5 space-x-4 border-t border-b md:mx-auto md:w-3/4 border-slate">
<span class="inline-block">Share:</span>
<img class="inline-block" src="{% static 'img/icons/icon_Facebook-logo.svg' %}" alt="Facebook" />
<img class="inline-block" src="{% static 'img/icons/icon_Twitter-logo.svg' %}" alt="Twitter" />
<img class="inline-block" src="{% static 'img/icons/icon_Linkedin-logo.svg' %}" alt="Linkedin" />
<img class="inline-block" src="{% static 'img/icons/icon_email.svg' %}" alt="Email" />
</div>
{% if next or prev %}
<div class="block flex my-16 md:mx-auto md:w-3/4">
{% if next %}
<div class="w-1/2 text-left">
<a href="{{ next.get_absolute_url }}" class="py-2 px-4 text-sm font-medium uppercase rounded-md border border-slate text-orange">< Newer Entries</a>
</div>
{% endif %}
{% if prev %}
<div class="w-1/2 text-right">
<a href="{{ prev.get_absolute_url }}" class="py-2 px-4 text-sm font-medium uppercase rounded-md border border-slate text-orange">Older Entries ></a>
</div>
{% endif %}
</div>
{% endif %}
</div>
{% endblock %}

11
templates/news/form.html Normal file
View File

@@ -0,0 +1,11 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h1>{% if entry.pk %}{% translate "Update News" %}{% else %}{% translate "Create News" %}{% endif %}</h1>
<form method="POST" action="{% if entry.pk %}{% url "news-update" entry.slug %}{% else %}{% url "news-create" %}{% endif %}">
{% csrf_token %}
{{ form.as_div }}
<button type="submit" name="update">{% if entry.pk %}{% translate "Update" %}{% else %}{% translate "Create" %}{% endif %}</button>
</form>
{% endblock %}

69
templates/news/list.html Normal file
View File

@@ -0,0 +1,69 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "News" %}{% endblock %}
{% block content %}
<div class="py-0 px-3 mb-3 md:py-6 md:px-0">
<div class="md:w-full">
<h1 class="text-4xl">Latest Stories</h1>
<p class="mt-0 text-xl">
Keep up with current information from Boost and our community. (Under construction)
</p>
<div class="divide-y divide-gray-300">
{% if entry_list %} {% comment %}New functionality{% endcomment %}
{% for entry in entry_list %}
<div class="flex py-4">
<div class="py-5 w-1/6">
<h5>{{ entry.publish_at|date:"M jS, Y" }}</h5>
</div>
<div class="w-5/6 text-xl">
<p><a href="{{ entry.get_absolute_url }}">{{ entry.title }}</a></p>
<div class="space-x-8 uppercase">
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="flex py-4">
<div class="py-5 w-1/6">
<h5>May 10th, 2023</h5>
</div>
<div class="w-5/6 text-xl">
<p>
Boost website renovation Beta Phase started.
</p>
<div class="space-x-8 uppercase">
</div>
</div>
</div>
<div class="flex py-4">
<div class="py-5 w-1/6">
<h5>Oct 9th, 2021</h5>
</div>
<div class="w-5/6 text-xl">
<p>
Start of website project.
</p>
<div class="space-x-8 uppercase">
</div>
</div>
</div>
{% endif %}
</div>
{% if entry_list and user.is_authenticated %}
<a href="{% url 'news-create' %}">{% translate "Create News" %}</a>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -1,64 +0,0 @@
{% extends "base.html" %}
{% load static %}
{% block content %}
<!-- Breadcrumb used on filtered views -->
<div class="p-3 md:p-0">
<a class="text-orange" href="{% url "news" %}">News</a> > Boost has moved download to JFrog Artifactory
</div>
<!-- end breadcrumb -->
<div class="py-0 px-3 mb-3 md:py-6 md:px-0">
<div class="py-16 md:mx-auto md:w-3/4">
<h1 class="text-4xl">
Boost has moved downloads to JFrog Artifactory
</h1>
<p class="mt-0 text-sm font-light">
April 29th, 2022 18:00 GMT
</p>
<p>
The service that Boost uses to serve up its releases, Bintray.com is being retired by JFrog on the 1st of
May. Fortunately for Boost, they have a new service, called JFrog.Arifactory, which we have transitioned to.
</p>
<p>
For the users of Boost, the only difference is that there is a new URL to download releases and snapshots.
</p>
<p>
Instead of: <a href="#" class="text-orange">https://dl.bintray.com/boostorg/release/</a> you should use
<a href="#" class="text-orange">https://boostorg.jfrog.io/artifactory/main/release/</a> to retrieve boost
releases.
</p>
<p>
Note: The pre-1.64 Boost releases are still available via Sourceforge.
</p>
<p>
Thank you to JFrog for all your past and current support.
</p>
</div>
<div class="flex py-8 my-5 space-x-4 border-t border-b md:mx-auto md:w-3/4 border-slate">
<span class="inline-block">Share:</span>
<img class="inline-block" src="{% static 'img/icons/icon_Facebook-logo.svg' %}" alt="Facebook" />
<img class="inline-block" src="{% static 'img/icons/icon_Twitter-logo.svg' %}" alt="Twitter" />
<img class="inline-block" src="{% static 'img/icons/icon_Linkedin-logo.svg' %}" alt="Linkedin" />
<img class="inline-block" src="{% static 'img/icons/icon_email.svg' %}" alt="Email" />
</div>
<div class="block flex my-16 md:mx-auto md:w-3/4">
<div class="w-1/2 text-left">
<a href="#" class="py-2 px-4 text-sm font-medium uppercase rounded-md border border-slate text-orange">< Newer Entries</a>
</div>
<div class="w-1/2 text-right">
<a href="#" class="py-2 px-4 text-sm font-medium uppercase rounded-md border border-slate text-orange">Older Entries ></a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,110 +0,0 @@
{% extends "base.html" %}
{% load static %}
{% block content %}
<div class="py-0 px-3 mb-3 md:py-6 md:px-0">
<div class="py-16 md:w-full">
<h1 class="text-4xl text-center">Boost News</h1>
<p class="mt-0 text-center">
Keep up with the latest exciting news from Boost and our community!
</p>
<div class="flex py-11 mt-11 w-full border-t border-slate">
<div class="py-5 w-1/4">
<h3>Version 1.78.0</h3>
<p class="p-0">December 8th, 2021 03:45 GMT</p>
</div>
<div class="w-3/4">
<p>
Updated Libraries: Asio, Assert, Atomic, Beast, Core, Describe, DLL, Filesystem, Geometry, JSON, Lambda2,
Log, Math, MultiIndex, Multiprecision, Nowide, PFR, Predef, Regex, System, Utility, Variant2.
</p>
<div class="space-x-8 uppercase">
<span><a href="#" class="pr-8 border-r text-orange border-slate">Release notes</a></span>
<span><a href="#" class="pr-8 border-r text-orange border-slate">Download</a></span>
<span><a href="#" class="text-orange">Version Details</a></span>
</div>
</div>
</div>
<div class="flex py-11 mt-11 w-full border-t border-slate">
<div class="py-5 w-1/4">
<h3>Version 1.78.0</h3>
<p class="p-0">December 8th, 2021 03:45 GMT</p>
</div>
<div class="w-3/4">
<p>
Updated Libraries: Asio, Assert, Atomic, Beast, Core, Describe, DLL, Filesystem, Geometry, JSON, Lambda2,
Log, Math, MultiIndex, Multiprecision, Nowide, PFR, Predef, Regex, System, Utility, Variant2.
</p>
<div class="space-x-8 uppercase">
<span><a href="#" class="pr-8 border-r text-orange border-slate">Release notes</a></span>
<span><a href="#" class="pr-8 border-r text-orange border-slate">Download</a></span>
<span><a href="#" class="text-orange">Version Details</a></span>
</div>
</div>
</div>
<div class="flex py-11 mt-11 w-full border-t border-slate">
<div class="py-5 w-1/4">
<h3>Version 1.78.0</h3>
<p class="p-0">December 8th, 2021 03:45 GMT</p>
</div>
<div class="w-3/4">
<p>
Updated Libraries: Asio, Assert, Atomic, Beast, Core, Describe, DLL, Filesystem, Geometry, JSON, Lambda2,
Log, Math, MultiIndex, Multiprecision, Nowide, PFR, Predef, Regex, System, Utility, Variant2.
</p>
<div class="space-x-8 uppercase">
<span><a href="#" class="pr-8 border-r text-orange border-slate">Release notes</a></span>
<span><a href="#" class="pr-8 border-r text-orange border-slate">Download</a></span>
<span><a href="#" class="text-orange">Version Details</a></span>
</div>
</div>
</div>
<div class="flex py-11 mt-11 w-full border-t border-slate">
<div class="py-5 w-1/4">
<h3>Version 1.78.0</h3>
<p class="p-0">December 8th, 2021 03:45 GMT</p>
</div>
<div class="w-3/4">
<p>
Updated Libraries: Asio, Assert, Atomic, Beast, Core, Describe, DLL, Filesystem, Geometry, JSON, Lambda2,
Log, Math, MultiIndex, Multiprecision, Nowide, PFR, Predef, Regex, System, Utility, Variant2.
</p>
<div class="space-x-8 uppercase">
<span><a href="#" class="pr-8 border-r text-orange border-slate">Release notes</a></span>
<span><a href="#" class="pr-8 border-r text-orange border-slate">Download</a></span>
<span><a href="#" class="text-orange">Version Details</a></span>
</div>
</div>
</div>
<div class="flex py-11 mt-11 w-full border-t border-slate">
<div class="py-5 w-1/4">
<h3>Version 1.78.0</h3>
<p class="p-0">December 8th, 2021 03:45 GMT</p>
</div>
<div class="w-3/4">
<p>
Updated Libraries: Asio, Assert, Atomic, Beast, Core, Describe, DLL, Filesystem, Geometry, JSON, Lambda2,
Log, Math, MultiIndex, Multiprecision, Nowide, PFR, Predef, Regex, System, Utility, Variant2.
</p>
<div class="space-x-8 uppercase">
<span><a href="#" class="pr-8 border-r text-orange border-slate">Release notes</a></span>
<span><a href="#" class="pr-8 border-r text-orange border-slate">Download</a></span>
<span><a href="#" class="text-orange">Version Details</a></span>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,8 +1,13 @@
<div id="messages" class="w-full text-center">
<div id="messages" class="w-full text-center" x-data="{show: true}">
{% if messages %}
{% for message in messages %}
<div class="w-2/3 mx-auto text-left items-center text-white rounded text-base px-3 py-2 {% if 'error' in message.tags %}bg-red-500{% else %}bg-green/50{% endif %} fade show">
<button type="button" class="float-right" data-dismiss="alert" aria-hidden="true">&times;</button>
<div x-show="show" class="w-2/3 mx-auto text-left items-center text-slate dark:text-white rounded text-base px-3 py-2 {% if 'error' in message.tags %}bg-red-500{% else %}bg-green/70{% endif %} fade show">
<button type="button"
class="float-right"
data-dismiss="alert"
aria-hidden="true"
x-on:click="show = ! show"
><i class="fas fa-times-circle"></i></button>
{{ message }}
</div>
{% endfor %}

1345
templates/releases_temp.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,126 +0,0 @@
{% extends "base.html" %}
{% load static %}
{% block content %}
<!-- Homepage Hero Section -->
<main class="px-4 my-4 mx-auto md:max-w-7xl">
<div class="mb-4 space-y-4 md:flex md:space-y-0 md:space-x-4">
<div
class="block relative w-full text-white rounded-lg shadow-lg md:w-1/2 bg-charcoal dark:bg-neutral-700">
<a href="#" class="absolute top-5 right-10 py-3 px-4 text-white rounded shadow-md bg-orange">New Here?</a>
<a href="#!">
<img
class="overflow-y-hidden w-full rounded-t-lg max-h-[470px]"
src="{% static 'img/fpo/guide.jpg' %}"
alt="" />
</a>
<div class="py-3 px-6 text-white">
<h5
class="text-xl font-bold leading-tight text-orange">
User Guide
</h5>
<p class="py-1 border-b border-gray-700 text-green">How to use Boost in your programs</p>
<ul class="flex flex-wrap mt-2 text-sm">
<li class="w-1/2"><a href="#">Get the Release</a></li>
<li class="w-1/2"><a href="#">Including Headers</a></li>
<li class="w-1/2"><a href="#">Using Bjam</a></li>
<li class="w-1/2"><a href="#">Running Tests</a></li>
<li class="w-1/2"><a href="#">Reporting Issues</a></li>
<li class="w-1/2"><a href="#">Library Documentation</a></li>
<li class="w-1/2"><a href="#">Build the Libraries</a></li>
<li class="w-1/2"><a href="#">Linking Your Program</a></li>
<li class="w-1/2"><a href="#">Using CMake</a></li>
<li class="w-1/2"><a href="#">Using Bjam</a></li>
<li class="w-1/2"><a href="#">Requesting Features</a></li>
<li class="w-1/2"><a href="#">Toolsets</a></li>
</ul>
</div>
</div>
<div
class="block relative w-full rounded-lg shadow-lg md:w-1/2 bg-charcoal dark:bg-neutral-700">
<a href="#!">
<img
class="overflow-y-hidden w-full rounded-t-lg max-h-[470px]"
src="{% static 'img/fpo/man-tree.jpeg' %}"
alt="" />
</a>
<div class="py-3 px-6 text-white">
<h5
class="text-xl font-bold leading-tight text-orange">
Contributor Guide
</h5>
<p class="py-1 border-b border-gray-300 text-white/60">This is how you can help</p>
<ul class="flex flex-wrap mt-2 text-sm">
<li class="w-1/2"><a href="#">Get the Release</a></li>
<li class="w-1/2"><a href="#">Including Headers</a></li>
<li class="w-1/2"><a href="#">Using Bjam</a></li>
<li class="w-1/2"><a href="#">Running Tests</a></li>
<li class="w-1/2"><a href="#">Reporting Issues</a></li>
<li class="w-1/2"><a href="#">Library Documentation</a></li>
<li class="w-1/2"><a href="#">Build the Libraries</a></li>
<li class="w-1/2"><a href="#">Linking Your Program</a></li>
<li class="w-1/2"><a href="#">Using CMake</a></li>
<li class="w-1/2"><a href="#">Using Bjam</a></li>
<li class="w-1/2"><a href="#">Requesting Features</a></li>
<li class="w-1/2"><a href="#">Toolsets</a></li>
</ul>
</div>
</div>
</div>
<div class="mb-16 space-y-4">
<div
class="flex flex-col text-white rounded-lg shadow-lg md:flex-row bg-charcoal dark:bg-neutral-700">
<img
class="object-cover w-full h-96 rounded-t-lg md:w-48 md:h-auto md:rounded-none md:rounded-l-lg"
src="{% static 'img/fpo/clipboardman.jpeg' %}"
alt="" />
<div class="flex flex-col justify-start p-6">
<h5
class="text-xl font-bold leading-tight">
Boost Formal Reviews
</h5>
<p class="py-1 mb-4 text-base text-neutral-600 dark:text-neutral-200">
How libraries become part of the collection.
</p>
<ul class="flex flex-wrap">
<li class="w-1/2"><a href="#">Proposing</a></li>
<li class="w-1/2"><a href="#">Scheduling</a></li>
<li class="w-1/2"><a href="#">The Manager</a></li>
<li class="w-1/2"><a href="#">The Review Process</a></li>
<li class="w-1/2"><a href="#">Submitting a Review</a></li>
</ul>
</div>
</div>
<div
class="flex flex-col rounded-lg shadow-lg md:flex-row bg-charcoal dark:bg-neutral-700">
<div class="flex flex-col justify-start p-6 w-full">
<h5
class="text-xl font-bold leading-tight">
Release Process
</h5>
<p class="py-1 mb-4 text-base text-neutral-600 dark:text-neutral-200">
"The trains always run on time"
</p>
<ul class="flex flex-wrap">
<li class="w-1/2"><a href="#">Release Calendar</a></li>
<li class="w-1/2"><a href="#">Process Steps</a></li>
<li class="w-1/2"><a href="#">Betas and RCs</a></li>
<li class="w-1/2"><a href="#">Testing the release</a></li>
<li class="w-1/2"><a href="#">What's in the Archive</a></li>
<li class="w-1/2"><a href="#">Library Debuts</a></li>
</ul>
</div>
<img
class="object-cover w-full h-96 rounded-t-lg md:w-48 md:h-auto md:rounded-none md:rounded-r-lg"
src="{% static 'img/fpo/construction.png' %}"
alt="" />
</div>
</div>
</main>
<!-- End Homepage Hero Section -->
{% endblock %}

View File

@@ -17,7 +17,7 @@
<form method="post">
{% csrf_token %}
<button type="submit" class="py-3 px-4 uppercase rounded border border-orange text-orange">{% trans "Continue" %}</button>
<button type="submit" class="py-3 px-4 text-white uppercase rounded border border-orange bg-orange">{% trans "Continue" %}</button>
</form>
</div>
{% endblock %}

View File

@@ -1,7 +1,9 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Contact Us" %}{% endblock %}
{% block content %}
<div class="py-0 px-3 mb-3 md:py-6 md:px-0">

View File

@@ -1,7 +1,9 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Getting Started" %}{% endblock %}
{% block subnav %}
<div class="py-8 px-4 space-y-4 text-sm uppercase border-b md:py-2 md:px-0 md:space-x-10 border-slate">
<a href="{% url 'support' %}" class="block md:inline">Support</a>

View File

@@ -1,7 +1,9 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Support" %}{% endblock %}
{% block subnav %}
<div class="py-8 px-4 space-y-4 text-sm uppercase border-b md:py-2 md:px-0 md:space-x-10 border-slate">
<a href="{% url 'support' %}" class="block md:inline md:py-1 md:border-b text-orange md:border-orange">Support</a>

View File

View File

View File

@@ -0,0 +1,99 @@
import djclick as click
from libraries.github import GithubAPIClient, GithubDataParser
from libraries.models import Library, LibraryVersion
from versions.models import Version
@click.command()
@click.option("--delete-versions", is_flag=True, help="Delete all existing versions")
@click.option(
"--skip-existing-versions",
is_flag=True,
help="Skip versions that already exist in our database",
)
@click.option(
"--delete-library-versions",
is_flag=True,
help="Delete all existing library versions",
)
@click.option(
"--create-recent-library-versions",
is_flag=True,
help="Create library-versions for the most recent Boost version and each active Boost library",
)
@click.option("--token", is_flag=False, help="Github API token")
def command(
delete_versions,
skip_existing_versions,
delete_library_versions,
create_recent_library_versions,
token,
):
"""Imports Boost release information from Github and updates the local database.
The function retrieves Boost tags from the main Github repo, excluding beta releases and release candidates.
For each tag, it fetches the associated data based on whether it's a full release (data in the tag) or not (data in the commit).
It then creates or updates a Version instance in the local database for each tag.
Depending on the options provided, it can also delete existing versions and library versions, and create new library versions for the most recent Boost version.
Args:
delete_versions (bool): If True, deletes all existing Version instances before importing.
skip-existing-versions (bool): If True, skips versions that already exist in the database.
delete_library_versions (bool): If True, deletes all existing LibraryVersion instances before importing.
create_recent_library_versions (bool): If True, creates a LibraryVersion for each active Boost library and the most recent Boost version.
token (str): Github API token, if you need to use something other than the setting.
"""
# Delete Versions and LibraryVersions based on options
if delete_versions:
Version.objects.all().delete()
click.echo("Deleted all existing versions.")
if delete_library_versions:
LibraryVersion.objects.all().delete()
click.echo("Deleted all existing library versions.")
# Get all Boost tags from Github
client = GithubAPIClient(token=token)
tags = client.get_tags()
for tag in tags:
name = tag["name"]
# If we already have this version, skip importing it
if skip_existing_versions and Version.objects.filter(name=name).exists():
click.echo(f"Skipping {name}, already exists in database")
continue
# Skip beta releases, release candidates, etc.
if any(["beta" in name.lower(), "-rc" in name.lower()]):
click.echo(f"Skipping {name}, not a full release")
continue
tag_data = client.get_tag_by_name(name)
version_data = None
parser = GithubDataParser()
if tag_data:
# This is a tag and a release, so the metadata is in the tag itself
version_data = parser.parse_tag(tag_data)
else:
# This is a tag, but not a release, so the metadata is in the commit
commit_data = client.get_commit_by_sha(commit_sha=tag["commit"]["sha"])
version_data = parser.parse_commit(commit_data)
if not version_data:
click.echo(f"Skipping {name}, no version data found")
continue
version, _ = Version.objects.update_or_create(name=name, defaults=version_data)
click.echo(f"Saved version {version.name}. Created: {_}")
if create_recent_library_versions:
# Associate existing Libraries with the most recent LibraryVersion
version = Version.objects.most_recent()
for library in Library.objects.all():
library_version, _ = LibraryVersion.objects.get_or_create(
library=library, version=version
)
click.echo(f"Saved library version {library_version}. Created: {_}")

View File

@@ -0,0 +1,28 @@
# Generated by Django 4.2 on 2023-05-10 21:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("versions", "0006_version_slug"),
]
operations = [
migrations.AddField(
model_name="version",
name="data",
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name="version",
name="github_url",
field=models.URLField(
blank=True,
help_text="The URL of the Boost version's GitHub repository.",
max_length=500,
null=True,
),
),
]

View File

@@ -1,5 +1,6 @@
import hashlib
from django.db import models
from django.utils.functional import cached_property
from django.utils.text import slugify
from .managers import VersionManager, VersionFileManager
@@ -16,6 +17,13 @@ class Version(models.Model):
default=True,
help_text="Control whether or not this version is available on the website",
)
github_url = models.URLField(
max_length=500,
blank=True,
null=True,
help_text="The URL of the Boost version's GitHub repository.",
)
data = models.JSONField(default=dict)
objects = VersionManager()
@@ -33,6 +41,10 @@ class Version(models.Model):
name = self.name.replace(".", " ")
return slugify(name)[:50]
@cached_property
def display_name(self):
return self.name.replace("boost-", "")
class VersionFile(models.Model):
Unix = "Unix"

View File

@@ -22,5 +22,16 @@ def test_version_get_slug(db):
assert version.get_slug() == "sample-library"
def test_version_display_bname(version):
version.name = "boost-1.81.0"
version.save()
assert version.display_name == "1.81.0"
version.name = "1.79.0"
version.save()
del version.display_name
assert version.display_name == "1.79.0"
def test_version_file_creation(full_version_one):
assert full_version_one.files.count() == 3