Merge pull request #11 from kristoferssolo/development

Pre-release
This commit is contained in:
Kristofers Solo 2023-06-29 00:05:42 +00:00 committed by GitHub
commit 3c991fd025
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 1870 additions and 85 deletions

8
.github/workflows/ruff.yml vendored Normal file
View File

@ -0,0 +1,8 @@
name: Ruff
on: [push, pull_request]
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: chartboost/ruff-action@v1

View File

@ -1,23 +1,18 @@
name: Test name: Django Test
on: on: [push, pull_request]
- push
- pull_request
jobs: jobs:
test: test:
runs-on: ${{ matrix.os }} runs-on: ubuntu-latest
strategy:
matrix:
os: [ubuntu-latest]
python-version: ["3.10"]
steps: steps:
- uses: actions/checkout@v2 - name: Checkout code
- name: Set up Python ${{ matrix.python-version }} uses: actions/checkout@v3
uses: actions/setup-python@v2 - name: Set up Python
uses: actions/setup-python@v3
with: with:
python-version: ${{ matrix.python-version }} python-version: "3.10"
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install tox tox-gh-actions pip install -r requirements.txt
- name: Test with tox - name: Run tests
run: tox run: python src/manage.py test

6
.gitignore vendored
View File

@ -132,5 +132,7 @@ dmypy.json
config.json config.json
debug debug
/static/admin/ local-cdn/
/static/fontawesomefree/ config.json
node_modules/
package-lock.json

View File

@ -1,21 +0,0 @@
"""OSSDB_web URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
urlpatterns = [
path("admin/", admin.site.urls),
]

View File

@ -1,7 +1,7 @@
# FOSSDB # FOSSDB
![Tests](https://github.com/kristoferssolo/FOSSDB-Web/actions/workflows/test.yml/badge.svg) ![Django Test](https://github.com/kristoferssolo/FOSSDB/actions/workflows/test.yml/badge.svg)
[![License](https://img.shields.io/badge/license-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.en.html) ![Ruff](https://github.com/kristoferssolo/FOSSDB/actions/workflows/ruff.yml/badge.svg)
FOSSDB is an open-source web application that helps users find, contribute, and collaborate on free and open-source software (FOSS) projects. FOSSDB is an open-source web application that helps users find, contribute, and collaborate on free and open-source software (FOSS) projects.
@ -33,7 +33,7 @@ python manage.py runserver
``` ```
## Usage ## Usage
After following the installation steps, you can access the application at [localhost:8000](http://localhost:8000). After following the installation steps, you can access the application at [https://localhost:8000](https://localhost:8000).
Here are some of the features: Here are some of the features:
- Browse projects by programming language, license, or search term - Browse projects by programming language, license, or search term
- View project details, including programming languages, licenses, and descriptions - View project details, including programming languages, licenses, and descriptions

View File

@ -1,10 +1,9 @@
{ {
"DEBUG": false,
"SECRET_KEY": "", "SECRET_KEY": "",
"ALLOWED_HOSTS": [ "ALLOWED_HOSTS": [""],
"",
"localhost"
],
"DATABASE": { "DATABASE": {
"ENGINE": "",
"NAME": "", "NAME": "",
"USER": "", "USER": "",
"PASSWORD": "", "PASSWORD": "",

View File

@ -1,12 +1,8 @@
[build-system]
requires = ["setuptools>=42.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.mypy] [tool.mypy]
check_untyped_defs = true check_untyped_defs = true
disallow_any_generics = true disallow_any_generics = true
ignore_missing_imports = true ignore_missing_imports = true
mypy_path = "FOSSDB_web" mypy_path = "FOSSDB"
no_implicit_optional = true no_implicit_optional = true
no_implicit_reexport = true no_implicit_reexport = true
show_error_codes = true show_error_codes = true
@ -15,3 +11,27 @@ warn_redundant_casts = true
warn_return_any = true warn_return_any = true
warn_unreachable = true warn_unreachable = true
warn_unused_configs = true warn_unused_configs = true
[tool.ruff]
line-length = 160
exclude = ["**/**/migrations"]
[tool.black]
line-length = 160
[tool.ruff.flake8-quotes]
docstring-quotes = "double"
[tool.djlint]
close_void_tags = true
format_attribute_template_tags = true
format_css = true
format_js = true
max_line_length = 120
[tool.djlint.css]
indent_size = 4
[tool.djlint.js]
indent_size = 4

View File

@ -1,3 +1,6 @@
Django==4.1.7 Django==4.2.2
fontawesomefree==6.2.1 Pillow==9.5.0
fontawesomefree==6.4.0
mysqlclient==2.1.1 mysqlclient==2.1.1
django-filter==23.2
django-tailwind==3.6.0

View File

@ -1,5 +1,2 @@
flake8==6.0.0 mypy==1.3.0
mypy==0.991 ruff==0.0.272
pytest-cov==4.0.0
pytest==7.2.0
tox==3.27.1

View File

@ -1,4 +0,0 @@
from setuptools import setup
if __name__ == "__main__":
setup()

View File

@ -1,5 +1,5 @@
""" """
ASGI config for FOSSDB_web project. ASGI config for FOSSDB project.
It exposes the ASGI callable as a module-level variable named ``application``. It exposes the ASGI callable as a module-level variable named ``application``.
@ -11,6 +11,6 @@ import os
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "FOSSDB_web.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "FOSSDB.settings")
application = get_asgi_application() application = get_asgi_application()

View File

@ -1,5 +1,5 @@
""" """
Django settings for FOSSDB_web project. Django settings for FOSSDB project.
Generated by "django-admin startproject" using Django 4.0.5. Generated by "django-admin startproject" using Django 4.0.5.
@ -15,17 +15,13 @@ import sys
from pathlib import Path from pathlib import Path
# Build paths inside the project like this: BASE_DIR / "subdir". # Build paths inside the project like this: BASE_DIR / "subdir".
BASE_PATH = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(BASE_PATH.joinpath("FOSSDB_web", "apps"))) sys.path.insert(0, str(BASE_DIR / "apps"))
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = BASE_PATH.joinpath("debug").is_file()
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
with open(BASE_DIR.parent / "config.json", "r", encoding="UTF-8") as config_file:
with open(BASE_PATH.joinpath("config.json"), "r", encoding="UTF-8") as config_file:
config = json.load(config_file) config = json.load(config_file)
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
@ -33,10 +29,20 @@ SECRET_KEY = config["SECRET_KEY"]
ALLOWED_HOSTS = config["ALLOWED_HOSTS"] ALLOWED_HOSTS = config["ALLOWED_HOSTS"]
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config["DEBUG"]
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
"main",
"account",
"fossdb",
"django_filters",
"tailwind",
"tokyonight_night",
"crispy_forms",
"fontawesomefree",
"django.contrib.admin", "django.contrib.admin",
"django.contrib.auth", "django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.contenttypes",
@ -55,12 +61,15 @@ MIDDLEWARE = [
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
] ]
ROOT_URLCONF = "FOSSDB_web.urls" ROOT_URLCONF = "FOSSDB.urls"
TEMPLATES = [ TEMPLATES = [
{ {
"BACKEND": "django.template.backends.django.DjangoTemplates", "BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_PATH.joinpath("templates")], "DIRS": [
BASE_DIR / "templates",
BASE_DIR / "**" / "templates",
],
"APP_DIRS": True, "APP_DIRS": True,
"OPTIONS": { "OPTIONS": {
"context_processors": [ "context_processors": [
@ -73,7 +82,9 @@ TEMPLATES = [
}, },
] ]
WSGI_APPLICATION = "FOSSDB_web.wsgi.application" WSGI_APPLICATION = "FOSSDB.wsgi.application"
AUTH_USER_MODEL = "account.User"
# Database # Database
@ -81,12 +92,12 @@ WSGI_APPLICATION = "FOSSDB_web.wsgi.application"
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.mysql", "ENGINE": f"django.db.backends.{config['DATABASE']['ENGINE']}",
"NAME": config["DATABASE"]["NAME"], "NAME": config["DATABASE"]["NAME"],
"USER": config["DATABASE"]["USER"], "USER": config["DATABASE"]["USER"],
"PASSWORD": config["DATABASE"]["PASSWORD"], "PASSWORD": config["DATABASE"]["PASSWORD"],
"HOST": config["DATABASE"]["HOST"], "HOST": config["DATABASE"]["HOST"],
"PORT": config["DATABASE"]["PORT"] "PORT": config["DATABASE"]["PORT"],
} }
} }
@ -115,7 +126,7 @@ AUTH_PASSWORD_VALIDATORS = [
LANGUAGE_CODE = "en-us" LANGUAGE_CODE = "en-us"
TIME_ZONE = "Europe/Riga" TIME_ZONE = "UTC"
USE_I18N = True USE_I18N = True
@ -126,10 +137,39 @@ USE_TZ = True
# https://docs.djangoproject.com/en/4.0/howto/static-files/ # https://docs.djangoproject.com/en/4.0/howto/static-files/
STATIC_URL = "/static/" STATIC_URL = "/static/"
STATIC_ROOT = BASE_PATH.joinpath("static") STATICFILES_DIRS = [
MEDIA_ROOT = BASE_PATH.joinpath("media") BASE_DIR / "static",
]
STATIC_ROOT = BASE_DIR.parent / "local-cdn" / "static"
MEDIA_URL = "media/"
MEDIAFILES_DIRS = [
BASE_DIR / "media",
]
MEDIA_ROOT = BASE_DIR.parent / "local-cdn" / "media"
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/"
LOGIN_URL = "/login/"
TAILWIND_APP_NAME = "tokyonight_night"
INTERNAL_IPS = [
"127.0.0.1",
]
# HTTPS settings
# SESSION_COOKIE_SECURE = True
# CSRF_COOKIE_SECURE = True
# SECURE_SSL_REDIRECT = True
# HSTS settings
# SECURE_HSTS_SECONDS = 31536000 # 1 year
# SECURE_HSTS_SECONDS = 1 # 1 year
# SECURE_HSTS_PRELOAD = True
# SECURE_HSTS_INCLUDE_SUBDOMAINS = True

31
src/FOSSDB/urls.py Normal file
View File

@ -0,0 +1,31 @@
"""FOSSDB URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path("", views.home, name="home")
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path("", Home.as_view(), name="home")
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path("blog/", include("blog.urls"))
"""
from django.conf import settings
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("fossdb.urls")),
path("", include("main.urls")),
path("", include("account.urls")),
path("", include("django.contrib.auth.urls")),
]
if settings.DEBUG:
from django.conf.urls.static import static
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -1,5 +1,5 @@
""" """
WSGI config for FOSSDB_web project. WSGI config for FOSSDB project.
It exposes the WSGI callable as a module-level variable named ``application``. It exposes the WSGI callable as a module-level variable named ``application``.
@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "FOSSDB_web.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "FOSSDB.settings")
application = get_wsgi_application() application = get_wsgi_application()

18
src/apps/account/admin.py Normal file
View File

@ -0,0 +1,18 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import Profile, User
class ProfileInline(admin.StackedInline):
model = Profile
can_delete = False
verbose_name_plural = "Profile"
class UserAdmin(BaseUserAdmin):
model = User
inlines = (ProfileInline,)
admin.site.register(User, UserAdmin)

6
src/apps/account/apps.py Normal file
View File

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

18
src/apps/account/forms.py Normal file
View File

@ -0,0 +1,18 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm
from .models import User
class SignUpForm(UserCreationForm):
email = forms.EmailField(required=False, help_text="Optional.")
class Meta:
model = User
fields = (
"username",
"email",
"password1",
"password2",
)

View File

@ -0,0 +1,47 @@
# Generated by Django 4.2.2 on 2023-06-27 16:35
import account.models
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')),
('profile_picture', models.ImageField(default='profile_pics/default.jpg', upload_to=account.models.get_profile_pic_path)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 4.2.2 on 2023-06-28 16:17
import account.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='user',
name='profile_picture',
field=models.ImageField(default='profile-pics/default.jpg', upload_to=account.models.get_profile_pic_path),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 4.2.2 on 2023-06-28 20:26
import account.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('account', '0002_alter_user_profile_picture'),
]
operations = [
migrations.RemoveField(
model_name='user',
name='profile_picture',
),
migrations.CreateModel(
name='Profile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('profile_picture', models.ImageField(default='profile-pics/default.jpg', upload_to=account.models.get_profile_pic_path)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-06-28 20:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('account', '0003_remove_user_profile_picture_profile'),
]
operations = [
migrations.RenameField(
model_name='profile',
old_name='profile_picture',
new_name='picture',
),
]

View File

@ -0,0 +1,65 @@
import uuid
from pathlib import Path
from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
def get_profile_pic_path(instance, filename) -> Path:
ext = filename.split(".")[-1]
filename = f"{instance.id}.{ext}"
return Path("profile_pics", filename)
class User(AbstractUser):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, verbose_name="ID")
@property
def full_name(self):
if not self.first_name and not self.last_name:
return ""
elif self.first_name and not self.last_name:
return self.first_name
elif not self.first_name and self.last_name:
return self.last_name
else:
return f"{self.first_name} {self.last_name}"
def __str__(self):
return self.username
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
picture = models.ImageField(upload_to=get_profile_pic_path, default="profile-pics/default.jpg")
def __str__(self):
return f"{self.user.username} Profile"
def save(self, *args, **kwags):
old_instance = None
if self.pk:
try:
old_instance = User.objects.get(pk=self.pk)
except ObjectDoesNotExist:
pass
super(Profile, self).save(*args, **kwags)
# Check if old instance exists and profile picture is different
if old_instance is not None:
if old_instance.profile_picture and self.picture and old_instance.profile_picture.url != self.picture.url:
old_instance.profile_picture.delete(save=False)
@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwags):
if created:
Profile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_profile(sender, instance, **kwags):
instance.profile.save()

View File

@ -0,0 +1,32 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block meta %}
<script src="{% static 'js/buttons.js' %}" defer></script>
{% endblock meta %}
{% block content %}
<form method="post"
class="flex flex-col items-center justify-center space-y-4 my-auto">
{% csrf_token %}
<input type="text"
placeholder="Username"
name="username"
value="{{ form.username.value|default:'' }}"
class="verify mt-1 block rounded-md border-2 border-slategray-200 hover:border-lightsteelblue-100 bg-gray-300 placeholder-lightsteelblue-100 focus:border-cadetblue-300" />
{% if form.username.errors %}<p class="text-indianred-100 text-xs italic">{{ form.username.errors }}</p>{% endif %}
<input type="password"
placeholder="Password"
name="password"
value="{{ form.username.value|default:'' }}"
class="verify mt-1 block rounded-md border-2 border-slategray-200 hover:border-lightsteelblue-100 bg-gray-300 placeholder-lightsteelblue-100 focus:border-cadetblue-300" />
{% if form.password.errors %}<p class="text-indianred-100 text-xs italic">{{ form.password.errors }}</p>{% endif %}
<button type="submit"
id="submit-button"
class="px-12 py-2 border border-transparent rounded-md text-lightsteelblue-100 bg-slategray-200 opacity-50 transform ease-in-out duration-500 cursor-default">
Login
</button>
<p>
Don't have an account? Create one <a class="underline text-skyblue-300" href="{% url 'signup' %}">Here</a>!
</p>
</form>
{% endblock %}

View File

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block meta %}{% endblock %}
{% block content %}
<h1>{{ user.username }}</h1>
<img src="{{ user.profile_picture.url }}"
alt="{{ user.username }}s profile picture" />
<p>{{ user.email }}</p>
{% endblock %}

View File

@ -0,0 +1,45 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block meta %}
<script src="{% static 'js/buttons.js' %}" defer></script>
{% endblock meta %}
{% block content %}
<form method="post"
id="form-field"
class="flex flex-col items-center justify-center space-y-4 my-auto">
{% csrf_token %}
<input type="text"
placeholder="Username"
name="username"
value="{{ form.username.value|default:'' }}"
class="verify mt-1 block rounded-md border-2 border-slategray-200 hover:border-lightsteelblue-100 bg-gray-300 placeholder-lightsteelblue-100 focus:border-cadetblue-300" />
{% if form.username.errors %}<p class="text-indianred-100 text-xs italic">{{ form.username.errors }}</p>{% endif %}
<input type="email"
placeholder="Email (optional)"
name="email"
value="{{ form.email.value|default:'' }}"
class="mt-1 block rounded-md border-2 border-slategray-200 hover:border-lightsteelblue-100 bg-gray-300 placeholder-lightsteelblue-100 focus:border-cadetblue-300" />
{% if form.email.errors %}<p class="text-indianred-100 text-xs italic">{{ form.email.errors }}</p>{% endif %}
<input type="password"
placeholder="Password"
name="password1"
value="{{ form.password1.value|default:'' }}"
class="verify mt-1 block rounded-md border-2 border-slategray-200 hover:border-lightsteelblue-100 bg-gray-300 placeholder-lightsteelblue-100 focus:border-cadetblue-300" />
{% if form.password1.errors %}<p class="text-indianred-100 text-xs italic">{{ form.password1.errors }}</p>{% endif %}
<input type="password"
placeholder="Confirm password"
name="password2"
value="{{ form.password2.value|default:'' }}"
class="verify mt-1 block rounded-md border-2 border-slategray-200 hover:border-lightsteelblue-100 bg-gray-300 placeholder-lightsteelblue-100 focus:border-cadetblue-300" />
{% if form.password2.errors %}<p class="text-indianred-100 text-xs italic">{{ form.password2.errors }}</p>{% endif %}
<button type="submit"
id="submit-button"
class="px-12 py-2 border border-transparent rounded-md text-lightsteelblue-100 bg-slategray-200 opacity-50 transform ease-in-out duration-500 cursor-default">
Sign Up
</button>
<p>
Have an account? Login <a class="underline text-skyblue-300" href="{% url 'login' %}">Here</a>!
</p>
</form>
{% endblock %}

View File

9
src/apps/account/urls.py Normal file
View File

@ -0,0 +1,9 @@
from django.urls import path
from . import views
urlpatterns = [
path("signup/", views.signup_view, name="signup"),
path("login/", views.login_view, name="login"),
path("<str:username>/", views.profile, name="profile"),
]

48
src/apps/account/views.py Normal file
View File

@ -0,0 +1,48 @@
from django.contrib.auth import authenticate, login
from django.contrib.auth.forms import AuthenticationForm
from django.shortcuts import get_object_or_404, redirect, render
from .forms import SignUpForm
from .models import User
def profile(request, username):
user = get_object_or_404(User, username=username)
context = {
"title": user.username + ("" if not user.full_name else f" ({user.full_name})"),
"user": user,
}
return render(request, "profile.html", context)
def signup_view(request):
form = SignUpForm(request.POST or None)
if request.method == "POST":
if form.is_valid():
user = form.save()
raw_password = form.cleaned_data.get("password1")
user = authenticate(username=user.username, password=raw_password)
login(request, user)
return redirect("homepage")
context = {
"title": "FOSSDB | SignUp",
"form": form,
}
return render(request, "signup.html", context)
def login_view(request):
form = AuthenticationForm(data=request.POST or None)
if request.method == "POST":
if form.is_valid():
user = form.get_user()
login(request, user)
return redirect("homepage")
context = {
"title": "FOSSDB | Login",
"form": form,
}
return render(request, "login.html", context)

View File

48
src/apps/fossdb/admin.py Normal file
View File

@ -0,0 +1,48 @@
from django.contrib import admin
from .models import (
HostingPlatform,
License,
OperatingSystem,
OperatingSystemVersion,
ProgrammingLanguage,
Project,
ProjectHostingPlatform,
ProjectProgrammingLanguage,
Tag,
)
class ProjectProgrammingLanguageInline(admin.TabularInline):
model = ProjectProgrammingLanguage
extra = 1
class ProjectHostingPlatformInline(admin.TabularInline):
model = ProjectHostingPlatform
extra = 1
class ProjectAdmin(admin.ModelAdmin):
inlines = [ProjectHostingPlatformInline, ProjectProgrammingLanguageInline]
list_display = (
"name",
"owner",
)
def _languages(self, object):
return " | ".join([i.programming_language.name for i in object.projectprogramminglanguage_set.all()])
models = (
HostingPlatform,
License,
ProgrammingLanguage,
Tag,
OperatingSystem,
OperatingSystemVersion,
)
for model in models:
admin.site.register(model)
admin.site.register(Project, ProjectAdmin)

6
src/apps/fossdb/apps.py Normal file
View File

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

View File

@ -0,0 +1,28 @@
import django_filters
from django.contrib.auth import get_user_model
from .models import Project
User = get_user_model()
class UserFilter(django_filters.FilterSet):
username = django_filters.CharFilter(lookup_expr="icontains")
class Meta:
model = User
fields = ("username",)
class ProjectFilter(django_filters.FilterSet):
owner = UserFilter()
name = django_filters.CharFilter(lookup_expr="icontains")
description = django_filters.CharFilter(lookup_expr="icontains")
class Meta:
model = Project
fields = (
"owner",
"name",
"description",
)

67
src/apps/fossdb/forms.py Normal file
View File

@ -0,0 +1,67 @@
from django import forms
from .models import HostingPlatform, ProgrammingLanguage, Project, ProjectHostingPlatform, ProjectProgrammingLanguage
class HostingPlatformForm(forms.ModelForm):
class Meta:
model = ProjectHostingPlatform
fields = (
"url",
"hosting_platform",
)
widgets = {
"hosting_platform": forms.Select(
choices=HostingPlatform.objects.all(),
)
}
class ProgrammingLanguageForm(forms.ModelForm):
class Meta:
model = ProjectProgrammingLanguage
fields = (
"programming_language",
"percentage",
)
widgets = {
"programming_language": forms.Select(
choices=ProgrammingLanguage.objects.all(),
),
"percentage": forms.NumberInput(
attrs={
"min": "0",
"max": "100",
}
),
}
class ProjectForm(forms.ModelForm):
class Meta:
model = Project
fields = (
"name",
"description",
"license",
"tag",
"operating_system",
)
widgets = {
"name": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": "Project name",
}
),
"description": forms.Textarea(
attrs={
"class": "form-control",
"placeholder": "Description",
}
),
"license": forms.CheckboxSelectMultiple(),
"tag": forms.CheckboxSelectMultiple(),
"operating_system": forms.CheckboxSelectMultiple(),
}

View File

@ -0,0 +1,117 @@
# Generated by Django 4.2.2 on 2023-06-27 16:35
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='HostingPlatform',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
],
),
migrations.CreateModel(
name='License',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('short_name', models.CharField(max_length=50, unique=True)),
('full_name', models.CharField(max_length=100, unique=True)),
('url', models.URLField(blank=True, default='')),
('text', models.TextField(blank=True, default='')),
],
),
migrations.CreateModel(
name='OperatingSystem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
('description', models.TextField(blank=True, default='')),
],
),
migrations.CreateModel(
name='OperatingSystemVersion',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('version', models.CharField(blank=True, default='', max_length=50)),
('codename', models.CharField(blank=True, default='', max_length=100)),
('is_lts', models.BooleanField(blank=True, default=False)),
('operating_system', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fossdb.operatingsystem')),
],
),
migrations.CreateModel(
name='ProgrammingLanguage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
],
),
migrations.CreateModel(
name='Project',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('description', models.TextField(blank=True, default='')),
('date_created', models.DateTimeField(auto_now_add=True)),
('license', models.ManyToManyField(blank=True, to='fossdb.license')),
('operating_system', models.ManyToManyField(blank=True, to='fossdb.operatingsystemversion')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Tag',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(db_index=True, max_length=100, unique=True)),
('description', models.TextField(blank=True, default='')),
('icon', models.ImageField(blank=True, upload_to='types/icons/')),
],
),
migrations.CreateModel(
name='ProjectProgrammingLanguage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('percentage', models.PositiveIntegerField()),
('programming_language', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fossdb.programminglanguage')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fossdb.project')),
],
),
migrations.CreateModel(
name='ProjectHostingPlatform',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('url', models.URLField(unique=True)),
('hosting_platform', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fossdb.hostingplatform')),
('project', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='fossdb.project')),
],
),
migrations.AddField(
model_name='project',
name='programming_language',
field=models.ManyToManyField(blank=True, through='fossdb.ProjectProgrammingLanguage', to='fossdb.programminglanguage'),
),
migrations.AddField(
model_name='project',
name='tag',
field=models.ManyToManyField(blank=True, to='fossdb.tag'),
),
migrations.AddConstraint(
model_name='project',
constraint=models.UniqueConstraint(fields=('owner', 'name'), name='unique_owner_name'),
),
migrations.AddConstraint(
model_name='operatingsystemversion',
constraint=models.UniqueConstraint(fields=('operating_system', 'version'), name='unique_os_version'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-06-27 16:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fossdb', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='tag',
name='icon',
field=models.ImageField(blank=True, null=True, upload_to='types/icons/'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-06-27 18:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fossdb', '0002_alter_tag_icon'),
]
operations = [
migrations.AlterField(
model_name='projectprogramminglanguage',
name='percentage',
field=models.PositiveIntegerField(blank=True, null=True),
),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 4.2.2 on 2023-06-28 16:17
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),
('fossdb', '0003_alter_projectprogramminglanguage_percentage'),
]
operations = [
migrations.AlterField(
model_name='tag',
name='icon',
field=models.ImageField(blank=True, null=True, upload_to='tags/icons/'),
),
migrations.CreateModel(
name='Star',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fossdb.project')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-06-28 22:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fossdb', '0004_alter_tag_icon_star'),
]
operations = [
migrations.AddField(
model_name='operatingsystem',
name='is_linux',
field=models.BooleanField(blank=True, default=False),
),
]

View File

144
src/apps/fossdb/models.py Normal file
View File

@ -0,0 +1,144 @@
import uuid
from django.conf import settings
from django.db import models
class License(models.Model):
short_name = models.CharField(max_length=50, unique=True)
full_name = models.CharField(max_length=100, unique=True)
url = models.URLField(blank=True, default="")
text = models.TextField(blank=True, default="")
def __str__(self):
return self.short_name
class OperatingSystem(models.Model):
name = models.CharField(max_length=100, unique=True)
description = models.TextField(blank=True, default="")
is_linux = models.BooleanField(blank=True, default=False)
def __str__(self):
return self.name
class OperatingSystemVersion(models.Model):
operating_system = models.ForeignKey(OperatingSystem, on_delete=models.CASCADE)
version = models.CharField(max_length=50, blank=True, default="")
codename = models.CharField(max_length=100, blank=True, default="")
is_lts = models.BooleanField(blank=True, default=False)
def __str__(self):
return f"{self.operating_system.name} {self.version} {'LTS' if self.is_lts else ''}"
class Meta:
constraints = (
models.UniqueConstraint(
fields=(
"operating_system",
"version",
),
name="unique_os_version",
),
)
class Tag(models.Model):
name = models.CharField(max_length=100, unique=True, db_index=True)
description = models.TextField(blank=True, default="")
icon = models.ImageField(upload_to="tags/icons/", null=True, blank=True)
def __str__(self):
return self.name
class ProgrammingLanguage(models.Model):
name = models.CharField(max_length=100, unique=True)
def __str__(self):
return self.name
class Project(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, verbose_name="ID")
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
description = models.TextField(blank=True, default="")
license = models.ManyToManyField(License, blank=True)
tag = models.ManyToManyField(Tag, blank=True)
operating_system = models.ManyToManyField(OperatingSystemVersion, blank=True)
programming_language = models.ManyToManyField(ProgrammingLanguage, through="ProjectProgrammingLanguage", blank=True)
date_created = models.DateTimeField(auto_now_add=True)
@property
def star_amount(self):
return self.star.count()
@property
def runs_on_macos(self):
return self.operating_system.filter(operating_system__name="macOS").exists()
@property
def runs_on_linux(self):
return self.operating_system.filter(operating_system__is_linux=True).exists()
@property
def runs_on_windows(self):
return self.operating_system.filter(operating_system__name="Windows").exists()
@property
def runs_on_ios(self):
return self.operating_system.filter(operating_system__name="iOS").exists()
@property
def runs_on_android(self):
return self.operating_system.filter(operating_system__name="Android").exists()
def get_absolute_url(self):
return f"/{self.owner}/{self.name}"
def __str__(self):
return f"{self.owner}/{self.name}"
class Meta:
constraints = (
models.UniqueConstraint(
fields=(
"owner",
"name",
),
name="unique_owner_name",
),
)
class ProjectProgrammingLanguage(models.Model):
project = models.ForeignKey(Project, on_delete=models.CASCADE)
programming_language = models.ForeignKey(ProgrammingLanguage, on_delete=models.CASCADE)
percentage = models.PositiveIntegerField(blank=True, null=True)
def __str__(self):
return f"{self.project.owner}/{self.project.name} | {self.programming_language} | {self.percentage}%"
class HostingPlatform(models.Model):
name = models.CharField(max_length=100, unique=True)
def __str__(self):
return self.name
class ProjectHostingPlatform(models.Model):
hosting_platform = models.ForeignKey(HostingPlatform, on_delete=models.CASCADE)
project = models.OneToOneField(Project, on_delete=models.CASCADE)
url = models.URLField(unique=True)
def __str__(self):
return self.url
class Star(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
project = models.ForeignKey(Project, on_delete=models.CASCADE)

View File

@ -0,0 +1,34 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block meta %}{% endblock %}
{% block content %}
<form method="post" id="project-form">
{% csrf_token %}
{{ form.as_p }}
{{ hosting_platform.management_form }}
{{ hosting_platform.as_table }}
<div id="language-formset">
{{ programming_language.management_form }}
{% for form in programming_language %}<div class="language-form">{{ form.as_table }}</div>{% endfor %}
</div>
<!-- This button will trigger the JS to append another language form -->
<button type="button" id="add-more">+</button>
<!-- Render the empty form, which you'll use as a template for new entries -->
<!-- Wrap it in a container so you can reference it by id and hide it -->
<div id="empty-form" style="display:none;">{{ empty_form.as_table }}</div>
<button type="submit">Submit</button>
</form>
<script>
document.querySelector("#add-more").addEventListener("click", function() {
var formIndex = document.querySelector("#id_language-TOTAL_FORMS").value;
var emptyFormDiv = document.querySelector("#empty-form");
var newFormHTML = emptyFormDiv.innerHTML.replace(/__prefix__/g, formIndex);
var newFormDiv = document.createElement("div");
newFormDiv.className = "language-form";
newFormDiv.innerHTML = newFormHTML;
document.querySelector("#language-formset").append(newFormDiv);
document.querySelector("#id_language-TOTAL_FORMS").value = parseInt(formIndex) + 1;
});
</script>
{% endblock %}

View File

@ -0,0 +1,55 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Delete {{ project }}{% endblock %}
{% block content %}
<form method="post"
id="delete-form"
class="flex flex-col items-center justify-center space-y-4 my-auto">
<p class="font-bold">To confirm, type "{{ project.owner.username }}/{{ project.name }}" in the box below</p>
{% csrf_token %}
<input type="text"
id="confirm-input"
class="mt-1 block rounded-md border-2 border-slategray-200 hover:border-lightsteelblue-100 bg-gray-300 placeholder-lightsteelblue-100 focus:border-cadetblue-300 text-center" />
<button id="submit-button"
class="px-12 py-2 border border-transparent rounded-md text-lightsteelblue-100 font-bold uppercase bg-slategray-200 opacity-50 transform ease-in-out duration-500 cursor-default"
type="submit">Delete</button>
</form>
<script type="text/javascript">
var confirm_string = "{{ project.owner.username }}/{{ project.name }}"
const USER_INPUT = document.getElementById("confirm-input")
const SUBMIT_BUTTON = document.getElementById("submit-button")
SUBMIT_BUTTON.disabled = true
USER_INPUT.addEventListener("input", () => {
if (confirm_string == USER_INPUT.value) {
SUBMIT_BUTTON.classList.remove(
"bg-slategray-200",
"text-lightsteelblue-100",
"opacity-50",
"cursor-default"
)
SUBMIT_BUTTON.classList.add(
"bg-indianred-100",
"text-gray-500",
"opacity-100",
"hover:opacity-60"
)
SUBMIT_BUTTON.disabled = false
} else {
SUBMIT_BUTTON.classList.remove(
"bg-indianred-100",
"text-gray-500",
"opacity-100",
"hover:opacity-60"
)
SUBMIT_BUTTON.classList.add(
"bg-slategray-200",
"text-lightsteelblue-100",
"opacity-50",
"cursor-default"
)
SUBMIT_BUTTON.disabled = true
}
})
</script>
{% endblock %}

View File

@ -0,0 +1,123 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ project.owner }}/{{ project.name }}{% endblock %}
{% block meta %}
<script src="{% static 'js/buttons.js' %}" defer></script>
{% endblock meta %}
{% block content %}
<div class="mx-auto font-condensed max-w-[60%] min-w-[32rem]">
<div class="p-8">
<h1 class="text-center text-4xl font-abel">{{ project.name }}</h1>
<h2 class="text-center text-2xl font-abel">
<a href="{% url 'profile' project.owner.username %}">By <span class="underline">{{ project.owner }}</span></a>
</h2>
<!-- os platform icons -->
<div class="my-4 flex justify-center items-center gap-x-4">
<a href=""><i class="fa-brands fa-linux fa-lg hover:text-lightsteelblue-200 transform duration-300 ease-in-out
{% if project.runs_on_linux %}
text-lightsteelblue-100
{% else %}
text-slategray-200
{% endif %}"></i></a>
<a href=""><i class="fa-brands fa-windows fa-lg hover:text-lightsteelblue-200 transform duration-300 ease-in-out
{% if project.runs_on_windows %}
text-lightsteelblue-100
{% else %}
text-slategray-200
{% endif %}"></i></a>
<a href=""><i class="fa-brands fa-apple fa-lg hover:text-lightsteelblue-200 transform duration-300 ease-in-out
{% if project.runs_on_macos %}
text-lightsteelblue-100
{% else %}
text-slategray-200
{% endif %}"></i></a>
<a href=""><i id="ios"
class="fa-brands fa-app-store-ios fa-lg hover:text-lightsteelblue-200 transform duration-300 ease-in-out
{% if project.runs_on_ios %}
text-lightsteelblue-100
{% else %}
text-slategray-200
{% endif %}"></i></a>
<a href=""><i id="android"
class="fa-brands fa-android fa-lg hover:text-lightsteelblue-200 transform duration-300 ease-in-out
{% if project.runs_on_android %}
text-lightsteelblue-100
{% else %}
text-slategray-200
{% endif %}"></i></a>
</div>
<!-- tags -->
<div class="my-8 flex flex-wrap justify-center items-start gap-2">
{% for tag in project.tag.all|dictsort:"name" %}
<a href="">
<span title="{{ tag.description }}"
class="bg-opacity-0 border rounded-xl border-slategray-200 px-3 text-xs min-w-16 hover:bg-steelblue-400 hover:bg-opacity-60 transform duration-300 ease-in-out">
{{ tag }}
</span>
</a>
{% endfor %}
</div>
<!-- programming languages -->
<div class="my-8 flex flex-wrap justify-center items-start gap-2">
{% for language in project.projectprogramminglanguage_set.all|dictsortreversed:"percentage" %}
<a href="">
<span title="{{ language.percentage }}%"
class="bg-opacity-0 border rounded-xl border-slategray-200 px-3 text-xs min-w-16 hover:bg-steelblue-400 hover:bg-opacity-60 transform duration-300 ease-in-out">
{{ language.programming_language }}
</span>
</a>
{% endfor %}
</div>
<!-- text -->
<p class="text-justify font-roboto my-12">{{ project.description }}</p>
<div class="">
<!-- hosting platform and licenses on one line -->
<div class="flex justify-between w-full mt-4 font-abel">
<!-- licenses -->
<div>
<button class="text-xl underline font-abel hover:text-lightsteelblue-200 transform duration-300 ease-in-out"
id="menu-button"
aria-haspopup="true"
aria-expanded="true">Licenses</button>
</div>
<!-- hosting platform -->
<div class="">
<p>
See project source code and read more <a class="underline text-skyblue-300 hover:text-cadetblue-300 transform duration-300 ease-in-out"
href="{{ project.projecthostingplatform.url }}"
target="_blank">here</a>!
</p>
</div>
</div>
<div class="w-full">
<div class="my-4 flex flex-wrap justify-center items-start gap-2"
role="menu"
id="dropdown-menu"
aria-orientation="vertical"
aria-labelledby="menu-button"
tabindex="-1"
style="display:none">
{% for license in project.license.all|dictsort:"short_name" %}
<a href="{{ license.url }}">
<span title="{{ license.full_name }}"
class="bg-opacity-0 border rounded-xl border-slategray-200 px-3 text-xs min-w-16 hover:bg-steelblue-400 hover:bg-opacity-60">
{{ license }}
</span>
</a>
{% endfor %}
</div>
</div>
</div>
</div>
{% if user == project.owner %}
<div class="flex justify-between mt-8 mx-16">
<button class="px-12 py-2 border border-transparent rounded-md text-gray-500 bg-skyblue-300 hover:opacity-60 transform ease-in-out duration-500">
<a href="{% url 'project-update' project.owner project.name %}">Update</a>
</button>
<button class="px-12 py-2 border border-transparent rounded-md text-gray-500 bg-burlywood hover:opacity-60 transform ease-in-out duration-500">
<a href="{% url 'project-delete' project.owner project.name %}">Delete</a>
</button>
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block meta %}{% endblock %}
{% block content %}
{% for project in projects %}
<div>
<a href="{{ project.get_absolute_url }}">{{ project }}</a>
</div>
<div>{{ project.description }}</div>
<hr />
{% empty %}
<p>No projects found</p>
{% endfor %}
{% endblock %}

0
src/apps/fossdb/tests.py Normal file
View File

11
src/apps/fossdb/urls.py Normal file
View File

@ -0,0 +1,11 @@
from django.urls import path
from . import views
urlpatterns = [
path("explore/", views.ProjectListView.as_view(), name="explore"),
path("contribute/", views.ProjectCreateView.as_view(), name="contribute"),
path("<str:owner>/<str:project_name>/", views.ProjectDetailView.as_view(), name="project-detail"),
path("<str:owner>/<str:project_name>/edit/", views.ProjectUpdateView.as_view(), name="project-update"),
path("<str:owner>/<str:project_name>/delete/", views.ProjectDeleteView.as_view(), name="project-delete"),
]

108
src/apps/fossdb/views.py Normal file
View File

@ -0,0 +1,108 @@
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.forms import inlineformset_factory
from django.shortcuts import redirect
from django.views.generic import CreateView, DeleteView, DetailView, UpdateView
from django_filters.views import FilterView
from .filters import ProjectFilter
from .forms import HostingPlatformForm, ProgrammingLanguageForm, ProjectForm
from .models import Project, ProjectProgrammingLanguage
ProgrammingLanguageInlineFormset = inlineformset_factory(
Project,
ProjectProgrammingLanguage,
form=ProgrammingLanguageForm,
extra=1,
)
class ProjectListView(FilterView):
model = Project
template_name = "explore.html"
filterset_class = ProjectFilter
context_object_name = "projects"
paginate_by = 100 # optional 10 projects a page
class ProjectCreateView(LoginRequiredMixin, CreateView):
model = Project
form_class = ProjectForm
template_name = "create_view.html"
login_url = "/login/"
redirect_field_name = "redirect_to"
def get_context_data(self, *args, **kwargs):
data = super().get_context_data(**kwargs)
data["hosting_platform"] = HostingPlatformForm(self.request.POST or None, prefix="hosting")
data["programming_language"] = ProgrammingLanguageInlineFormset(self.request.POST or None, prefix="language")
data["empty_form"] = ProgrammingLanguageInlineFormset(prefix="language_empty")
return data
def form_valid(self, form):
context = self.get_context_data()
form.instance.owner = self.request.user
hosting_platform = context["hosting_platform"]
programming_language = context["programming_language"]
self.object = form.save()
if hosting_platform.is_valid():
hosting_platform.instance.project = self.object
hosting_platform.save()
# TODO: allow adding multiple languages
if programming_language.is_valid():
for instance in programming_language.save(commit=False):
instance.project = self.object
instance.save()
programming_language.save_m2m()
if hosting_platform.is_valid() and programming_language.is_valid():
return super().form_valid(form)
else:
return self.render_to_response(self.get_context_data(form=form))
class ProjectDetailView(DetailView):
model = Project
template_name = "detailed_view.html"
context_object_name = "project"
slug_field = "name"
slug_url_kwarg = "project_name"
class ProjectUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = Project
template_name = "create_view.html"
form_class = ProjectForm
slug_field = "name"
slug_url_kwarg = "project_name"
login_url = "/login/"
redirect_field_name = "redirect_to"
def test_func(self):
return self.get_object().owner == self.request.user
def handle_no_permission(self):
return redirect("login")
def get_context_data(self, *args, **kwargs):
data = super(ProjectUpdateView, self).get_context_data(**kwargs)
data["hosting_platform"] = HostingPlatformForm(self.request.POST or None, instance=self.object.projecthostingplatform, prefix="hosting")
data["programming_language"] = ProgrammingLanguageInlineFormset(self.request.POST or None, instance=self.object, prefix="language")
data["empty_form"] = ProgrammingLanguageInlineFormset(prefix="language_empty")
return data
class ProjectDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
model = Project
template_name = "delete_view.html"
slug_field = "name"
slug_url_kwarg = "project_name"
login_url = "/login/"
redirect_field_name = "redirect_to"
success_url = "/"
def test_func(self):
return self.get_object().owner == self.request.user
def handle_no_permission(self):
return redirect("login")

View File

0
src/apps/main/admin.py Normal file
View File

View File

@ -1,6 +1,6 @@
from django.apps import AppConfig from django.apps import AppConfig
class FossdbConfig(AppConfig): class MainConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'fossdb' name = 'main'

View File

0
src/apps/main/models.py Normal file
View File

View File

@ -0,0 +1,5 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block meta %}{% endblock %}
{% block content %}{% endblock %}

0
src/apps/main/tests.py Normal file
View File

11
src/apps/main/urls.py Normal file
View File

@ -0,0 +1,11 @@
from django.urls import path
from . import views
urlpatterns = [
path("", views.homepage, name="homepage"),
path("contribute/", views.contribute, name="contribute"),
path("news/", views.news, name="news"),
path("dashboard/", views.dashboard, name="dashboard"),
path("help/", views.help, name="help"),
]

21
src/apps/main/views.py Normal file
View File

@ -0,0 +1,21 @@
from django.shortcuts import render
def homepage(request):
return render(request, "homepage.html", {"title": "FOSSDB"})
def contribute(request):
return render(request, "contribute.html", {"title": "FOSSDB | Contribute"})
def news(request):
return render(request, "news.html", {"title": "FOSSDB | News"})
def dashboard(request):
return render(request, "dashboard.html", {"title": "FOSSDB | Dashboard"})
def help(request):
return render(request, "help.html", {"title": "FOSSDB | Help"})

View File

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class Tokyonight_nightConfig(AppConfig):
name = 'tokyonight_night'

View File

@ -0,0 +1 @@
node_modules

View File

@ -0,0 +1,29 @@
{
"name": "tokyonight_night",
"version": "3.6.0",
"description": "",
"scripts": {
"start": "npm run dev",
"build": "npm run build:clean && npm run build:tailwind",
"build:clean": "rimraf ../static/css/dist",
"build:tailwind": "cross-env NODE_ENV=production tailwindcss --postcss -i ./src/styles.css -o ../static/css/dist/styles.css --minify",
"dev": "cross-env NODE_ENV=development tailwindcss --postcss -i ./src/styles.css -o ../static/css/dist/styles.css -w",
"tailwindcss": "node ./node_modules/tailwindcss/lib/cli.js"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/line-clamp": "^0.4.4",
"@tailwindcss/typography": "^0.5.9",
"cross-env": "^7.0.3",
"postcss": "^8.4.24",
"postcss-import": "^15.1.0",
"postcss-nested": "^6.0.1",
"postcss-simple-vars": "^7.0.1",
"rimraf": "^5.0.1",
"tailwindcss": "^3.3.2"
}
}

View File

@ -0,0 +1,7 @@
module.exports = {
plugins: {
"postcss-import": {},
"postcss-simple-vars": {},
"postcss-nested": {},
},
}

View File

@ -0,0 +1,30 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url("https://fonts.googleapis.com/css2?family=Abel&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Rationale&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Roboto+Condensed:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap");
body {
@apply leading-[normal] m-0;
}
*,
::before,
::after {
border-width: 0;
}
.border-gradient-horizontal {
position: relative;
}
.border-gradient-horizontal::before {
content: "";
position: absolute;
top: 0%;
width: 100%;
height: 0.25rem;
background: linear-gradient(to right, transparent, #27a1b9, transparent);
transform: translateY(-50%);
}

View File

@ -0,0 +1,147 @@
/**
* This is a minimal config.
*
* If you need the full config, get it from here:
* https://unpkg.com/browse/tailwindcss@latest/stubs/defaultConfig.stub.js
*/
module.exports = {
content: [
/**
* HTML. Paths to Django template files that will contain Tailwind CSS classes.
*/
/* Templates within theme app (<tailwind_app_name>/templates), e.g. base.html. */
"../templates/**/*.html",
/*
* Main templates directory of the project (BASE_DIR/templates).
* Adjust the following line to match your project structure.
*/
"../../templates/**/*.html",
/*
* Templates in other django apps (BASE_DIR/<any_app_name>/templates).
* Adjust the following line to match your project structure.
*/
"../../**/templates/**/*.html",
/**
* JS: If you use Tailwind CSS in JavaScript, uncomment the following lines and make sure
* patterns match your project structure.
*/
/* JS 1: Ignore any JavaScript in node_modules folder. */
// '!../../**/node_modules',
/* JS 2: Process all JavaScript files in the project. */
// '../../**/*.js',
/**
* Python: If you use Tailwind CSS classes in Python, uncomment the following line
* and make sure the pattern below matches your project structure.
*/
// '../../**/*.py'
],
theme: {
colors: {
darkslateblue: "#3d59a1",
cornflowerblue: "#7aa2f7",
gray: {
100: "#37222c",
200: "#1f2231",
300: "#1a1b26",
400: "#16161e",
500: "#15161e",
},
darkslategray: {
100: "#2c5a66",
200: "#414868",
300: "#3b4261",
400: "#283457",
500: "#292e42",
600: "#20303b",
},
skyblue: {
100: "#89ddff",
200: "#2ac3de",
300: "#0db9d7",
},
paleturquoise: "#b4f9f8",
steelblue: {
100: "#6183bb",
200: "#536c9e",
300: "#565f89",
400: "#394b70",
},
cadetblue: {
100: "#41a6b5",
200: "#449dab",
300: "#27a1b9",
},
lightskyblue: "#7dcfff",
lightsteelblue: {
100: "#c0caf5",
200: "#a9b1d6",
},
indianred: {
100: "#db4b4b",
200: "#b2555b",
300: "#914c54",
},
sienna: "#713137",
slategray: {
100: "#737aa2",
200: "#545c7e",
},
mediumturquoise: "#73daca",
lightgreen: "#9ece6a",
teal: "#266d6a",
burlywood: "#e0af68",
mediumaquamarine: "#1abc9c",
lightcoral: "#f7768e",
mediumpurple: {
100: "#bb9af7",
200: "#9d7cd8",
},
sandybrown: "#ff9e64",
deeppink: "#ff007c",
},
fontFamily: {
rationale: [
"Rationale",
"Roboto",
"Helvetica",
"Arial",
"sans-serif",
],
abel: ["Abel", "Roboto", "Helvetica", "Arial", "sans-serif"],
condensed: [
"Roboto Condensed",
"Roboto",
"Helvetica",
"Arial",
"sans-serif",
],
roboto: ["Roboto", "Helvetica", "Arial", "sans-serif"],
},
fontSize: {
base: "1rem",
xl: "1.25rem",
"2xl": "1.5rem",
"3xl": "2rem",
"4xl": "4rem",
xs: "0.75rem",
},
extend: {},
},
plugins: [
/**
* '@tailwindcss/forms' is the forms plugin that provides a minimal styling
* for forms. If you don't like it or have own styling for forms,
* comment the line below to disable '@tailwindcss/forms'.
*/
require("@tailwindcss/forms"),
require("@tailwindcss/typography"),
require("@tailwindcss/line-clamp"),
require("@tailwindcss/aspect-ratio"),
],
}

View File

@ -0,0 +1,94 @@
{% load static tailwind_tags %}
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<link rel="icon" href="{% static 'img/icons/logo.svg' %}" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Abel&family=Rationale&family=Roboto+Condensed:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Abel&family=Roboto+Condensed:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
rel="stylesheet" />
{% tailwind_css %}
<link href="{% static 'fontawesomefree/css/all.min.css' %}"
rel="stylesheet" />
{% block meta %}{% endblock %}
<title>
{% block title %}{% endblock %}
</title>
</head>
<body class="text-lightsteelblue-100 bg-gray-300 flex flex-col min-h-screen font-roboto">
<header class="flex justify-between items-center px-6 py-2 font-abel text-xl">
<div class="flex justify-between">
<!-- logo -->
<div class="flex items-center text-4xl font-abel">
<img class=""
width="40"
src="{% static 'img/icons/logo.svg' %}"
alt="logo" />
<a class="hover:text-skyblue-300 transform duration-300 ease-in-out"
href="{% url 'homepage' %}">foss<span class="text-skyblue-300">db</span></a>
</div>
<!-- search -->
<div class="relative items-center flex">
<input type="text"
placeholder="Search projects..."
class="py-2 mx-4 placeholder-slategray-100 bg-gray-300 border-0 border-b-2 border-b-slategray-100 hover:border-b-lightsteelblue-100 transform duration-200 ease-in-out focus:outline-none focus:border-b-skyblue-300 focus:border-b-2 max-w-[10rem]" />
</div>
<!-- navbar -->
<nav class="uppercase flex gap-x-6 items-center">
<a href="{% url 'explore' %}"
class="hover:text-skyblue-300 transform duration-200 ease-in-out">explore</a>
<a href="{% url 'contribute' %}"
class="hover:text-skyblue-300 transform duration-200 ease-in-out">contribute</a>
<a href="{% url 'news' %}"
class="hover:text-skyblue-300 transform duration-200 ease-in-out">news</a>
<a href="{% url 'dashboard' %}"
class="hover:text-skyblue-300 transform duration-200 ease-in-out">dashboard</a>
<a href="{% url 'help' %}"
class="hover:text-skyblue-300 transform duration-200 ease-in-out">help</a>
</nav>
</div>
<div class="flex items-center justify-between">
<a href="" class=""><i class="fa-solid fa-globe fa-lg"></i></a>
{% if user.is_authenticated %}
<div class="flex items-center justify-between">
<p class="mx-4">{{ user.username }}</p>
<a href="{% url 'profile' user.username %}">
<img src="{{ user.profile.picture.url }}"
class="w-[2rem]"
alt="{{ user.username }}'s profile picture" />
</a>
</div>
{% else %}
<a href="{% url 'login' %}"
class="hover:text-skyblue-300 transition duration-300 ease-in-out"><i class="fa-solid fa-user fa-lg ml-4 "></i></a>
{% endif %}
</div>
</header>
<main class="flex flex-col flex-grow">
{% block content %}{% endblock %}
</main>
<footer class="w-full h-32 flex items-center text-slategray-200 bg-gray-500 mt-32 font-condensed border-gradient-horizontal">
<div class="p-8">
<div class="space-y-4 text-left">
<p>
FOSSDB is a passion project of <a class="underline"
href="https://github.com/kristoferssolo"
target="_blank">@kristoferssolo</a> and a dedicated community of reporters.
</p>
<!-- TODO: finish these sentences -->
<p>This site uses data from GitHub as well as data...</p>
<p>This site has no affiliation with GitHub or any other hosting platform.</p>
</div>
<div></div>
</div>
</footer>
</body>
</html>

View File

@ -6,7 +6,7 @@ import sys
def main(): def main():
"""Run administrative tasks.""" """Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "FOSSDB_web.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "FOSSDB.settings")
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="396.43799"
height="512"
viewBox="0 0 104.89088 135.46667"
version="1.1"
id="svg38941"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="penguin.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview38943"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.77504209"
inkscape:cx="239.98697"
inkscape:cy="355.46457"
inkscape:window-width="1916"
inkscape:window-height="1055"
inkscape:window-x="1920"
inkscape:window-y="21"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs38938" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:#cee2fb;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.395717;stroke-miterlimit:10000;stroke-dasharray:none;paint-order:markers stroke fill"
d="M 13.333187,84.618973 9.6005212,109.78868 0,83.420191 0.71185222,50.927976 Z"
id="path11984"
sodipodi:nodetypes="ccccc"
inkscape:export-filename="512p_blank.png"
inkscape:export-xdpi="143.58"
inkscape:export-ydpi="143.58" />
<path
style="fill:#e88353;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.395717;stroke-miterlimit:10000;stroke-dasharray:none;paint-order:markers stroke fill"
d="m 104.89093,16.970784 c 0,0 -5.839523,-1.121858 -29.013232,1.872563 0,0 -2.667291,-0.564322 -2.806952,1.847489 -0.139692,2.41181 0.626942,6.876194 0.626942,6.876194 6.116392,-3.715341 21.495581,-7.841451 31.193242,-10.596246 z"
id="path1175"
sodipodi:nodetypes="cccccc" />
<path
style="fill:#1a2e3c;fill-opacity:1;stroke:none;stroke-width:0.395717;stroke-miterlimit:10000;stroke-dasharray:none;paint-order:markers stroke fill"
d="m 104.74514,16.095951 c -0.0863,-0.115567 -2.98575,-3.725719 -2.98575,-3.725719 L 75.937254,7.7660864 46.654615,21.674733 73.309503,48.53419 73.819537,20.985821 c 0,0 0.02677,-2.002222 2.058161,-2.142474 2.031464,-0.14026 28.867452,-2.747396 28.867452,-2.747396 z"
id="path1177"
sodipodi:nodetypes="scccccscs" />
<path
style="fill:#4a5a69;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.395717;stroke-miterlimit:10000;stroke-dasharray:none;paint-order:markers stroke fill"
d="m 62.215647,0 0.07792,2.7613939 6.228984,8.5265221 7.414701,-3.5218296 z"
id="path4968"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#4a5a69;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.395717;stroke-miterlimit:10000;stroke-dasharray:none;paint-order:markers stroke fill"
d="M 101.75939,12.370232 75.937254,7.7660864 98.372199,8.1171024 Z"
id="path7241"
sodipodi:nodetypes="cccc" />
<path
style="fill:#f5f8fa;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.395717;stroke-miterlimit:10000;stroke-dasharray:none;paint-order:markers stroke fill"
d="m 61.647025,1.9546891 6.875528,9.3332269 c 0,0 -21.835797,10.60734 -21.867938,10.386817 C 46.622459,21.454209 42.694361,5.6132912 42.694361,5.6132912 Z"
id="path7243"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#1a2f3c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.395717;stroke-miterlimit:10000;stroke-dasharray:none;paint-order:markers stroke fill"
d="m 62.215647,0 c 0,0 0.15568,2.829599 0.07792,2.7613939 -0.07777,-0.0682 -0.646544,-0.8067048 -0.646544,-0.8067048 L 42.694361,5.6132912 46.654615,21.674733 24.979029,23.851689 42.750713,75.525429 40.671899,93.308335 20.697937,132.67748 6.2053796,132.68196 6.1452704,85.951869 0.71185072,50.927976 40.99639,0.48868237 Z"
id="path9102"
sodipodi:nodetypes="cscccccccccccc" />
<path
style="fill:#f3f8fc;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.395717;stroke-miterlimit:10000;stroke-dasharray:none;paint-order:markers stroke fill"
d="M 24.979029,23.851689 46.654615,21.674733 82.792262,58.089726 67.359703,59.575448 Z"
id="path9650"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#dbebfc;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.395717;stroke-miterlimit:10000;stroke-dasharray:none;paint-order:markers stroke fill"
d="m 24.979029,23.851689 17.771684,51.67374 -2.078814,17.782906 -18.774983,36.905825 26.941956,5.25251 18.520831,-75.891222 z"
id="path10267"
sodipodi:nodetypes="ccccccc" />
<path
style="fill:#75a7d2;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.395717;stroke-miterlimit:10000;stroke-dasharray:none;paint-order:markers stroke fill"
d="m 67.359703,59.575448 c 0.202792,-0.06638 15.432559,-1.485722 15.432559,-1.485722 L 70.770838,122.55849 49.210822,135.46667 h -0.37195 z"
id="path11022"
sodipodi:nodetypes="cccccc" />
<path
style="fill:#102631;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.395717;stroke-miterlimit:10000;stroke-dasharray:none;paint-order:markers stroke fill"
d="m 70.770838,122.55849 c 0.504669,0.0643 13.01434,3.35272 13.01434,3.35272 L 82.792262,58.089726 Z"
id="path13291"
sodipodi:nodetypes="cccc" />
<path
style="fill:#ab5e44;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.395717;stroke-miterlimit:10000;stroke-dasharray:none;paint-order:markers stroke fill"
d="M 73.697688,27.56703 104.89093,16.970784 75.140145,21.877162 74.212154,21.328889 73.819537,20.985821 Z"
id="path22968"
sodipodi:nodetypes="cccccc" />
<path
style="fill:#0c2333;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.395717;stroke-miterlimit:10000;stroke-dasharray:none;paint-order:markers stroke fill"
d="M 56.448797,7.5849631 56.579933,11.307183 54.226569,11.532052 52.689192,11.311476 51.599122,10.28935 51.83251,8.4196571 53.177576,7.220503 Z"
id="path24475"
sodipodi:nodetypes="cccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

52
src/static/js/buttons.js Normal file
View File

@ -0,0 +1,52 @@
window.addEventListener("DOMContentLoaded", () => {
const FORM_VERIFY = document.getElementsByClassName("verify")
const SUBMIT_BUTTON = document.getElementById("submit-button")
SUBMIT_BUTTON.disabled = true
Array.from(FORM_VERIFY).forEach((input) => {
input.addEventListener("input", () => {
const ALL_FILLED = Array.from(FORM_VERIFY).every(
(input) => input.value.trim() !== ""
)
if (ALL_FILLED) {
SUBMIT_BUTTON.classList.remove(
"bg-slategray-200",
"text-lightsteelblue-100",
"opacity-50",
"cursor-default"
)
SUBMIT_BUTTON.classList.add(
"bg-skyblue-300",
"text-gray-500",
"opacity-100",
"hover:opacity-60"
)
SUBMIT_BUTTON.disabled = false
} else {
SUBMIT_BUTTON.classList.remove(
"bg-skyblue-300",
"text-gray-500",
"opacity-100",
"hover:opacity-60"
)
SUBMIT_BUTTON.classList.add(
"bg-slategray-200",
"text-lightsteelblue-100",
"opacity-50",
"cursor-default"
)
SUBMIT_BUTTON.disabled = true
}
})
})
})
window.addEventListener("DOMContentLoaded", () => {
document
.getElementById("menu-button")
.addEventListener("click", function() {
document.getElementById("dropdown-menu").style.display =
this.ariaExpanded === "true" ? "none" : "flex"
this.ariaExpanded = this.ariaExpanded === "true" ? "false" : "true"
})
})