mirror of
https://github.com/kristoferssolo/FOSSDB.git
synced 2025-10-21 17:50:35 +00:00
commit
3c991fd025
8
.github/workflows/ruff.yml
vendored
Normal file
8
.github/workflows/ruff.yml
vendored
Normal 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
|
||||
27
.github/workflows/test.yml
vendored
27
.github/workflows/test.yml
vendored
@ -1,23 +1,18 @@
|
||||
name: Test
|
||||
on:
|
||||
- push
|
||||
- pull_request
|
||||
name: Django Test
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
python-version: ["3.10"]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
python-version: "3.10"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install tox tox-gh-actions
|
||||
- name: Test with tox
|
||||
run: tox
|
||||
pip install -r requirements.txt
|
||||
- name: Run tests
|
||||
run: python src/manage.py test
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -132,5 +132,7 @@ dmypy.json
|
||||
config.json
|
||||
debug
|
||||
|
||||
/static/admin/
|
||||
/static/fontawesomefree/
|
||||
local-cdn/
|
||||
config.json
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
||||
@ -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),
|
||||
]
|
||||
@ -1,7 +1,7 @@
|
||||
# FOSSDB
|
||||
|
||||

|
||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||

|
||||

|
||||
|
||||
|
||||
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
|
||||
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:
|
||||
- Browse projects by programming language, license, or search term
|
||||
- View project details, including programming languages, licenses, and descriptions
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
{
|
||||
"DEBUG": false,
|
||||
"SECRET_KEY": "",
|
||||
"ALLOWED_HOSTS": [
|
||||
"",
|
||||
"localhost"
|
||||
],
|
||||
"ALLOWED_HOSTS": [""],
|
||||
"DATABASE": {
|
||||
"ENGINE": "",
|
||||
"NAME": "",
|
||||
"USER": "",
|
||||
"PASSWORD": "",
|
||||
@ -1,12 +1,8 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=42.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.mypy]
|
||||
check_untyped_defs = true
|
||||
disallow_any_generics = true
|
||||
ignore_missing_imports = true
|
||||
mypy_path = "FOSSDB_web"
|
||||
mypy_path = "FOSSDB"
|
||||
no_implicit_optional = true
|
||||
no_implicit_reexport = true
|
||||
show_error_codes = true
|
||||
@ -15,3 +11,27 @@ warn_redundant_casts = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = 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
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
Django==4.1.7
|
||||
fontawesomefree==6.2.1
|
||||
Django==4.2.2
|
||||
Pillow==9.5.0
|
||||
fontawesomefree==6.4.0
|
||||
mysqlclient==2.1.1
|
||||
django-filter==23.2
|
||||
django-tailwind==3.6.0
|
||||
|
||||
@ -1,5 +1,2 @@
|
||||
flake8==6.0.0
|
||||
mypy==0.991
|
||||
pytest-cov==4.0.0
|
||||
pytest==7.2.0
|
||||
tox==3.27.1
|
||||
mypy==1.3.0
|
||||
ruff==0.0.272
|
||||
|
||||
@ -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``.
|
||||
|
||||
@ -11,6 +11,6 @@ import os
|
||||
|
||||
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()
|
||||
@ -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.
|
||||
|
||||
@ -15,17 +15,13 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / "subdir".
|
||||
BASE_PATH = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(BASE_PATH.joinpath("FOSSDB_web", "apps")))
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
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
|
||||
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
|
||||
|
||||
with open(BASE_PATH.joinpath("config.json"), "r", encoding="UTF-8") as config_file:
|
||||
with open(BASE_DIR.parent / "config.json", "r", encoding="UTF-8") as config_file:
|
||||
config = json.load(config_file)
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
@ -33,10 +29,20 @@ SECRET_KEY = config["SECRET_KEY"]
|
||||
|
||||
ALLOWED_HOSTS = config["ALLOWED_HOSTS"]
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = config["DEBUG"]
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"main",
|
||||
"account",
|
||||
"fossdb",
|
||||
"django_filters",
|
||||
"tailwind",
|
||||
"tokyonight_night",
|
||||
"crispy_forms",
|
||||
"fontawesomefree",
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
@ -55,12 +61,15 @@ MIDDLEWARE = [
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "FOSSDB_web.urls"
|
||||
ROOT_URLCONF = "FOSSDB.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [BASE_PATH.joinpath("templates")],
|
||||
"DIRS": [
|
||||
BASE_DIR / "templates",
|
||||
BASE_DIR / "**" / "templates",
|
||||
],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
@ -73,7 +82,9 @@ TEMPLATES = [
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "FOSSDB_web.wsgi.application"
|
||||
WSGI_APPLICATION = "FOSSDB.wsgi.application"
|
||||
|
||||
AUTH_USER_MODEL = "account.User"
|
||||
|
||||
|
||||
# Database
|
||||
@ -81,12 +92,12 @@ WSGI_APPLICATION = "FOSSDB_web.wsgi.application"
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.mysql",
|
||||
"ENGINE": f"django.db.backends.{config['DATABASE']['ENGINE']}",
|
||||
"NAME": config["DATABASE"]["NAME"],
|
||||
"USER": config["DATABASE"]["USER"],
|
||||
"PASSWORD": config["DATABASE"]["PASSWORD"],
|
||||
"HOST": config["DATABASE"]["HOST"],
|
||||
"PORT": config["DATABASE"]["PORT"]
|
||||
"PORT": config["DATABASE"]["PORT"],
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,7 +126,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = "Europe/Riga"
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
@ -126,10 +137,39 @@ USE_TZ = True
|
||||
# https://docs.djangoproject.com/en/4.0/howto/static-files/
|
||||
|
||||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = BASE_PATH.joinpath("static")
|
||||
MEDIA_ROOT = BASE_PATH.joinpath("media")
|
||||
STATICFILES_DIRS = [
|
||||
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
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
|
||||
|
||||
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
31
src/FOSSDB/urls.py
Normal 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)
|
||||
@ -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``.
|
||||
|
||||
@ -11,6 +11,6 @@ import os
|
||||
|
||||
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()
|
||||
18
src/apps/account/admin.py
Normal file
18
src/apps/account/admin.py
Normal 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
6
src/apps/account/apps.py
Normal 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
18
src/apps/account/forms.py
Normal 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",
|
||||
)
|
||||
47
src/apps/account/migrations/0001_initial.py
Normal file
47
src/apps/account/migrations/0001_initial.py
Normal 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()),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -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',
|
||||
),
|
||||
]
|
||||
65
src/apps/account/models.py
Normal file
65
src/apps/account/models.py
Normal 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()
|
||||
32
src/apps/account/templates/login.html
Normal file
32
src/apps/account/templates/login.html
Normal 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 %}
|
||||
10
src/apps/account/templates/profile.html
Normal file
10
src/apps/account/templates/profile.html
Normal 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 %}
|
||||
45
src/apps/account/templates/signup.html
Normal file
45
src/apps/account/templates/signup.html
Normal 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 %}
|
||||
0
src/apps/account/tests.py
Normal file
0
src/apps/account/tests.py
Normal file
9
src/apps/account/urls.py
Normal file
9
src/apps/account/urls.py
Normal 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
48
src/apps/account/views.py
Normal 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)
|
||||
0
src/apps/fossdb/__init__.py
Normal file
0
src/apps/fossdb/__init__.py
Normal file
48
src/apps/fossdb/admin.py
Normal file
48
src/apps/fossdb/admin.py
Normal 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
6
src/apps/fossdb/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FossdbConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "fossdb"
|
||||
28
src/apps/fossdb/filters.py
Normal file
28
src/apps/fossdb/filters.py
Normal 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
67
src/apps/fossdb/forms.py
Normal 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(),
|
||||
}
|
||||
117
src/apps/fossdb/migrations/0001_initial.py
Normal file
117
src/apps/fossdb/migrations/0001_initial.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
18
src/apps/fossdb/migrations/0002_alter_tag_icon.py
Normal file
18
src/apps/fossdb/migrations/0002_alter_tag_icon.py
Normal 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/'),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
29
src/apps/fossdb/migrations/0004_alter_tag_icon_star.py
Normal file
29
src/apps/fossdb/migrations/0004_alter_tag_icon_star.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
18
src/apps/fossdb/migrations/0005_operatingsystem_is_linux.py
Normal file
18
src/apps/fossdb/migrations/0005_operatingsystem_is_linux.py
Normal 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),
|
||||
),
|
||||
]
|
||||
0
src/apps/fossdb/migrations/__init__.py
Normal file
0
src/apps/fossdb/migrations/__init__.py
Normal file
144
src/apps/fossdb/models.py
Normal file
144
src/apps/fossdb/models.py
Normal 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)
|
||||
34
src/apps/fossdb/templates/create_view.html
Normal file
34
src/apps/fossdb/templates/create_view.html
Normal 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 %}
|
||||
55
src/apps/fossdb/templates/delete_view.html
Normal file
55
src/apps/fossdb/templates/delete_view.html
Normal 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 %}
|
||||
123
src/apps/fossdb/templates/detailed_view.html
Normal file
123
src/apps/fossdb/templates/detailed_view.html
Normal 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 %}
|
||||
15
src/apps/fossdb/templates/explore.html
Normal file
15
src/apps/fossdb/templates/explore.html
Normal 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
0
src/apps/fossdb/tests.py
Normal file
11
src/apps/fossdb/urls.py
Normal file
11
src/apps/fossdb/urls.py
Normal 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
108
src/apps/fossdb/views.py
Normal 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")
|
||||
0
src/apps/main/__init__.py
Normal file
0
src/apps/main/__init__.py
Normal file
0
src/apps/main/admin.py
Normal file
0
src/apps/main/admin.py
Normal file
@ -1,6 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FossdbConfig(AppConfig):
|
||||
class MainConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'fossdb'
|
||||
name = 'main'
|
||||
0
src/apps/main/migrations/__init__.py
Normal file
0
src/apps/main/migrations/__init__.py
Normal file
0
src/apps/main/models.py
Normal file
0
src/apps/main/models.py
Normal file
5
src/apps/main/templates/homepage.html
Normal file
5
src/apps/main/templates/homepage.html
Normal 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
0
src/apps/main/tests.py
Normal file
11
src/apps/main/urls.py
Normal file
11
src/apps/main/urls.py
Normal 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
21
src/apps/main/views.py
Normal 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"})
|
||||
0
src/apps/tokyonight_night/__init__.py
Normal file
0
src/apps/tokyonight_night/__init__.py
Normal file
5
src/apps/tokyonight_night/apps.py
Normal file
5
src/apps/tokyonight_night/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class Tokyonight_nightConfig(AppConfig):
|
||||
name = 'tokyonight_night'
|
||||
1
src/apps/tokyonight_night/static_src/.gitignore
vendored
Normal file
1
src/apps/tokyonight_night/static_src/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules
|
||||
29
src/apps/tokyonight_night/static_src/package.json
Normal file
29
src/apps/tokyonight_night/static_src/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
7
src/apps/tokyonight_night/static_src/postcss.config.js
Normal file
7
src/apps/tokyonight_night/static_src/postcss.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
"postcss-import": {},
|
||||
"postcss-simple-vars": {},
|
||||
"postcss-nested": {},
|
||||
},
|
||||
}
|
||||
30
src/apps/tokyonight_night/static_src/src/styles.css
Normal file
30
src/apps/tokyonight_night/static_src/src/styles.css
Normal 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%);
|
||||
}
|
||||
147
src/apps/tokyonight_night/static_src/tailwind.config.js
Normal file
147
src/apps/tokyonight_night/static_src/tailwind.config.js
Normal 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"),
|
||||
],
|
||||
}
|
||||
94
src/apps/tokyonight_night/templates/base.html
Normal file
94
src/apps/tokyonight_night/templates/base.html
Normal 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>
|
||||
@ -6,7 +6,7 @@ import sys
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "FOSSDB_web.settings")
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "FOSSDB.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
BIN
src/media/profile-pics/940e8e9e-77d4-42bb-84fd-04d332de403b.png
Normal file
BIN
src/media/profile-pics/940e8e9e-77d4-42bb-84fd-04d332de403b.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
src/media/profile-pics/default.jpg
Normal file
BIN
src/media/profile-pics/default.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
111
src/static/img/icons/logo.svg
Normal file
111
src/static/img/icons/logo.svg
Normal 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
52
src/static/js/buttons.js
Normal 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"
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user